feat(runtime): define borrowed views commands and ordered events
Establish the J1 presentation-independent contract with instance-scoped clocks and generations, immutable borrowed views, typed generation-gated commands, normalized ordered diagnostics, and teardown acknowledgements. Route graphical startup plus press-time selection, movement, and combat through focused App adapters over the exact existing owners without adding a queue or mirrored world. Validated by the Release solution build, 13 Runtime tests, 3,838 App tests with three existing skips, and the complete 8,424-test Release suite with five existing skips. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
afebbe3eca
commit
854d9e9cd1
21 changed files with 2594 additions and 44 deletions
|
|
@ -1,5 +1,5 @@
|
|||
using AcDream.App.Composition;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.Runtime;
|
||||
|
||||
namespace AcDream.App.Tests.Composition;
|
||||
|
||||
|
|
@ -11,19 +11,25 @@ public sealed class SessionStartCompositionTests
|
|||
var messages = new List<string>();
|
||||
|
||||
SessionStartCompositionPhase.Report(
|
||||
new LiveSessionStartResult(
|
||||
LiveSessionStartStatus.MissingCredentials),
|
||||
new RuntimeSessionStartResult(
|
||||
RuntimeSessionStartStatus.MissingCredentials,
|
||||
default),
|
||||
messages.Add);
|
||||
SessionStartCompositionPhase.Report(
|
||||
new LiveSessionStartResult(
|
||||
LiveSessionStartStatus.Failed,
|
||||
new RuntimeSessionStartResult(
|
||||
RuntimeSessionStartStatus.Failed,
|
||||
default,
|
||||
Error: new InvalidOperationException("boom")),
|
||||
messages.Add);
|
||||
SessionStartCompositionPhase.Report(
|
||||
new LiveSessionStartResult(LiveSessionStartStatus.Disabled),
|
||||
new RuntimeSessionStartResult(
|
||||
RuntimeSessionStartStatus.Disabled,
|
||||
default),
|
||||
messages.Add);
|
||||
SessionStartCompositionPhase.Report(
|
||||
new LiveSessionStartResult(LiveSessionStartStatus.Connected),
|
||||
new RuntimeSessionStartResult(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
default),
|
||||
messages.Add);
|
||||
|
||||
Assert.Equal(2, messages.Count);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.App.Combat;
|
||||
using AcDream.App.Diagnostics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.Tests.Input;
|
||||
|
|
@ -86,6 +87,7 @@ public sealed class GameplayInputCommandControllerTests
|
|||
TargetMode = new FakeTargetMode(Calls);
|
||||
Camera = new FakeCamera(Calls);
|
||||
Combat = new FakeCombat(Calls);
|
||||
Runtime = new FakeRuntimeView();
|
||||
Window = new FakeWindow(Calls);
|
||||
Controller = new GameplayInputCommandController(
|
||||
Retained,
|
||||
|
|
@ -94,6 +96,7 @@ public sealed class GameplayInputCommandControllerTests
|
|||
Player,
|
||||
TargetMode,
|
||||
Camera,
|
||||
Runtime,
|
||||
Combat,
|
||||
Window);
|
||||
}
|
||||
|
|
@ -106,6 +109,7 @@ public sealed class GameplayInputCommandControllerTests
|
|||
public FakeTargetMode TargetMode { get; }
|
||||
public FakeCamera Camera { get; }
|
||||
public FakeCombat Combat { get; }
|
||||
public FakeRuntimeView Runtime { get; }
|
||||
public FakeWindow Window { get; }
|
||||
public GameplayInputCommandController Controller { get; }
|
||||
}
|
||||
|
|
@ -168,9 +172,32 @@ public sealed class GameplayInputCommandControllerTests
|
|||
public void ExitFlyMode() => calls.Add("exit-fly");
|
||||
}
|
||||
|
||||
private sealed class FakeCombat(List<string> calls) : ILiveCombatModeCommand
|
||||
private sealed class FakeCombat(List<string> calls) : IRuntimeCombatCommands
|
||||
{
|
||||
public void Toggle() => calls.Add("combat");
|
||||
public RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
RuntimeCombatCommand command)
|
||||
{
|
||||
Assert.Equal(RuntimeCombatCommand.ToggleMode, command);
|
||||
calls.Add("combat");
|
||||
return new RuntimeCommandResult(
|
||||
RuntimeCommandStatus.Accepted,
|
||||
expectedGeneration);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeRuntimeView : IGameRuntimeView
|
||||
{
|
||||
public RuntimeGenerationToken Generation => new(7);
|
||||
public RuntimeLifecycleSnapshot Lifecycle => throw new NotSupportedException();
|
||||
public IGameRuntimeClock Clock => throw new NotSupportedException();
|
||||
public IRuntimeEntityView Entities => throw new NotSupportedException();
|
||||
public IRuntimeInventoryView Inventory => throw new NotSupportedException();
|
||||
public IRuntimeChatView Chat => throw new NotSupportedException();
|
||||
public IRuntimeMovementView Movement => throw new NotSupportedException();
|
||||
public IRuntimePortalView Portal => throw new NotSupportedException();
|
||||
public RuntimeStateCheckpoint CaptureCheckpoint() =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private sealed class FakeWindow(List<string> calls) : IGameplayWindowCommands
|
||||
|
|
|
|||
|
|
@ -125,10 +125,10 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"SessionStartComposition.cs"));
|
||||
AssertAppearsInOrder(
|
||||
startPhase,
|
||||
"frame.SessionHost.Start(_dependencies.Options)",
|
||||
"frame.GameRuntime.Session.Start(frame.GameRuntime.Generation)",
|
||||
"switch (result.Status)",
|
||||
"case LiveSessionStartStatus.MissingCredentials:",
|
||||
"case LiveSessionStartStatus.Failed:");
|
||||
"case RuntimeSessionStartStatus.MissingCredentials:",
|
||||
"case RuntimeSessionStartStatus.Failed:");
|
||||
int start = body.IndexOf(
|
||||
".Start(frameRoots));",
|
||||
StringComparison.Ordinal);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,613 @@
|
|||
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.World;
|
||||
using AcDream.Runtime;
|
||||
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);
|
||||
|
||||
LiveEntityRegistrationResult registration =
|
||||
harness.Entities.RegisterLiveEntity(Spawn(
|
||||
Harness.TargetGuid,
|
||||
instance: 3,
|
||||
cell: 0x12340001u));
|
||||
Assert.NotNull(registration.Record);
|
||||
|
||||
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(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);
|
||||
}
|
||||
|
||||
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();
|
||||
Entities = new LiveEntityRuntime(
|
||||
new GpuWorldState(),
|
||||
new NoopEntityResources());
|
||||
Objects = new ClientObjectTable();
|
||||
Chat = new ChatLog();
|
||||
Selection = new SelectionState();
|
||||
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,
|
||||
() => 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();
|
||||
_session = new LiveSessionController(new SessionOperations());
|
||||
Host = CreateHost(_session, Commands, Identity);
|
||||
Runtime = new CurrentGameRuntimeAdapter(
|
||||
Options,
|
||||
_session,
|
||||
Host,
|
||||
Identity,
|
||||
Entities,
|
||||
Objects,
|
||||
Chat,
|
||||
new LocalPlayerControllerSlot(),
|
||||
WorldReveal,
|
||||
Clock,
|
||||
Selection,
|
||||
selectionController,
|
||||
GameplayInput,
|
||||
Combat);
|
||||
}
|
||||
|
||||
public RuntimeOptions Options { get; }
|
||||
public LocalPlayerIdentityState Identity { get; }
|
||||
public LiveEntityRuntime Entities { get; }
|
||||
public ClientObjectTable Objects { get; }
|
||||
public ChatLog Chat { get; }
|
||||
public SelectionState Selection { get; }
|
||||
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 LiveSessionHost Host { get; }
|
||||
public CurrentGameRuntimeAdapter Runtime { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Runtime.Dispose();
|
||||
_session.Dispose();
|
||||
_items.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,
|
||||
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),
|
||||
reset,
|
||||
new LiveSessionSelectionBindings(
|
||||
id => identity.ServerGuid = id,
|
||||
_ => { },
|
||||
_ => { },
|
||||
_ => { },
|
||||
_ => { },
|
||||
() => { }),
|
||||
new LiveSessionEnteredWorldBindings(
|
||||
_ => { },
|
||||
() => { },
|
||||
() => { },
|
||||
_ => { },
|
||||
() => { }),
|
||||
(_, _, _) => { },
|
||||
() => { }));
|
||||
}
|
||||
|
||||
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 : ILiveSessionOperations
|
||||
{
|
||||
public IPEndPoint ResolveEndpoint(string host, int port) =>
|
||||
new(IPAddress.Loopback, port);
|
||||
|
||||
public WorldSession CreateSession(IPEndPoint endpoint) =>
|
||||
new(endpoint, new TestTransport());
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private sealed class TestTransport : IWorldSessionTransport
|
||||
{
|
||||
public void Send(ReadOnlySpan<byte> datagram)
|
||||
{
|
||||
}
|
||||
|
||||
public void Send(IPEndPoint remote, ReadOnlySpan<byte> datagram)
|
||||
{
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue