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:
Erik 2026-07-27 00:43:26 +02:00
parent 7818494116
commit a9a822f206
28 changed files with 2707 additions and 345 deletions

View file

@ -0,0 +1,797 @@
using System.Net;
using System.Numerics;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Spells;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
using AcDream.Runtime.Session;
using AcDream.Runtime.World;
namespace AcDream.Runtime.Tests.Support;
internal sealed class NoWindowGameRuntimeHost : IDisposable
{
private readonly FixtureOperations _operations;
private readonly GameplayOperations _gameplay;
private readonly ImmediateResetHost _resetHost = new();
private readonly IDisposable _hostLease;
private readonly IDisposable _traceSubscription;
private EventRoute? _events;
private CommandRoute? _commands;
private bool _disposed;
public NoWindowGameRuntimeHost(
string user = "runtime-user",
string password = "runtime-password",
uint characterId = 0x50000001u,
string characterName = "Runtime")
{
_operations = new FixtureOperations(
characterId,
characterName);
_gameplay = new GameplayOperations();
Runtime = new GameRuntime(new GameRuntimeDependencies(
_gameplay,
_gameplay,
_gameplay,
_gameplay,
SessionOperations: _operations,
CombatTime: () => _gameplay.Now));
_gameplay.Bind(Runtime);
_hostLease = Runtime.AcquireHostLease("no-window test host");
Trace = new RuntimeTraceRecorder();
_traceSubscription = Runtime.Subscribe(Trace);
Session = new LiveSessionHost(
Runtime.Session,
new LiveSessionHostBindings(
new LiveSessionRoutingFactories(
CreateEventRoute,
CreateCommandRoute),
(generation) =>
Runtime.ResetGeneration(generation, _resetHost),
new LiveSessionSelectionBindings(
id => Runtime.PlayerIdentity.ServerGuid = id,
_ => { },
Runtime.CommunicationOwner.Chat.SetLocalPlayerGuid,
_ => { },
_ => { },
Runtime.ActionOwner.Combat.Clear),
new LiveSessionEnteredWorldBindings(
_ => { },
() => { },
() => { },
_ => { },
() => { }),
(host, port, connectingUser) =>
_operations.RecordConnecting(
host,
port,
connectingUser),
_operations.RecordConnected),
new LiveSessionConnectOptions(
true,
"127.0.0.1",
9000,
user,
password));
}
public GameRuntime Runtime { get; }
public LiveSessionHost Session { get; }
public RuntimeTraceRecorder Trace { get; }
public IReadOnlyList<string> LifecycleTrace => _operations.Trace;
public IReadOnlyList<string> GameplayTrace => _gameplay.Trace;
public int ProjectionRetirementCount =>
_resetHost.ProjectionRetirementCount;
public int ProjectionDrainCount => _resetHost.ProjectionDrainCount;
public int ProjectionCompletionCount =>
_resetHost.ProjectionCompletionCount;
public RuntimeStateCheckpoint CaptureCheckpoint()
{
RuntimeStateCheckpoint checkpoint =
Runtime.CaptureCheckpoint();
Trace.AddCheckpoint(
Runtime.EntityObjects.Events.NextStamp(),
checkpoint);
return checkpoint;
}
public RuntimeSessionStartResult Start()
{
RuntimeLifecycleState previous = Runtime.Lifecycle.State;
RuntimeSessionStartResult result =
Session.Start(Runtime.Generation);
Runtime.EventSink.EmitLifecycle(
previous,
Runtime.Lifecycle.State);
return result;
}
public RuntimeSessionStartResult Reconnect()
{
RuntimeLifecycleState previous = Runtime.Lifecycle.State;
RuntimeSessionStartResult result =
Session.Reconnect(Runtime.Generation);
Runtime.EventSink.EmitLifecycle(
previous,
Runtime.Lifecycle.State);
return result;
}
public RuntimeTeardownAcknowledgement Stop()
{
RuntimeLifecycleState previous = Runtime.Lifecycle.State;
RuntimeTeardownAcknowledgement result =
Session.Stop(Runtime.Generation);
Runtime.EventSink.EmitLifecycle(
previous,
Runtime.Lifecycle.State);
return result;
}
public bool Deliver(Action<GameRuntime> delivery) =>
Deliver(Runtime.Generation, delivery);
public bool Deliver(
RuntimeGenerationToken expectedGeneration,
Action<GameRuntime> delivery)
{
ArgumentNullException.ThrowIfNull(delivery);
EventRoute? route = _events;
if (route is null
|| !route.IsActive
|| route.Generation != expectedGeneration
|| expectedGeneration != Runtime.Generation)
{
return false;
}
delivery(Runtime);
return true;
}
public bool Execute(Action<GameRuntime> command) =>
Execute(Runtime.Generation, command);
public bool Execute(
RuntimeGenerationToken expectedGeneration,
Action<GameRuntime> command)
{
ArgumentNullException.ThrowIfNull(command);
CommandRoute? route = _commands;
if (route is null
|| !route.IsActive
|| route.Generation != expectedGeneration
|| expectedGeneration != Runtime.Generation)
{
return false;
}
command(Runtime);
return true;
}
public bool Move()
{
return Execute(runtime =>
{
Require(
runtime.MovementOwner.Execute(
RuntimeMovementCommand.ToggleRunLock),
"movement command was rejected");
_gameplay.Trace.Add("movement:run");
runtime.EventSink.EmitMovement(
runtime.MovementOwner.Snapshot);
runtime.EventSink.EmitCommand(
RuntimeCommandDomain.Movement,
(int)RuntimeMovementCommand.ToggleRunLock,
RuntimeCommandStatus.Accepted);
});
}
public bool Use(uint serverGuid)
{
return Execute(runtime =>
{
ItemUseRequestReservation reservation =
runtime.ActionOwner.Transactions
.BeginUseRequestReservation();
RuntimeInteractionDispatchResult result =
runtime.ActionOwner.Transactions.TryDispatchUse(
serverGuid,
ownedByPlayer: false,
useable: true,
reservation,
_gameplay,
out _);
Require(
result == RuntimeInteractionDispatchResult.Dispatched,
$"use command was rejected: {result}");
runtime.ActionOwner.Transactions.CompleteUse(0u);
runtime.EventSink.EmitCommand(
RuntimeCommandDomain.Selection,
(int)RuntimeSelectionCommand.UseSelected,
RuntimeCommandStatus.Accepted,
serverGuid);
});
}
public bool Attack()
{
return Execute(runtime =>
{
runtime.ActionOwner.Combat.SetCombatMode(CombatMode.Melee);
runtime.ActionOwner.CombatAttack.SetDesiredPower(0.5f);
runtime.ActionOwner.CombatAttack.PressAttack(
AttackHeight.Medium);
_gameplay.Now += 0.5d;
runtime.ActionOwner.CombatAttack.ReleaseAttack();
runtime.EventSink.EmitCommand(
RuntimeCommandDomain.Combat,
(int)RuntimeCombatCommand.ToggleMode,
RuntimeCommandStatus.Accepted);
});
}
public bool CastFixtureSpell()
{
return Execute(runtime =>
{
runtime.CharacterOwner.InstallSpellMetadata(
FixtureSpellMetadata());
runtime.CharacterOwner.Spellbook.OnSpellLearned(1u);
CastRequestResult result =
runtime.ActionOwner.SpellCast.Cast(1u);
Require(
result == CastRequestResult.Sent,
$"spell command was rejected: {result}");
runtime.EventSink.EmitCommand(
RuntimeCommandDomain.Magic,
operation: 1,
RuntimeCommandStatus.Accepted,
primaryObjectId: 1u);
});
}
public bool AdvanceProjectile(
uint serverGuid = 0x70000021u,
ushort incarnation = 1)
{
return Execute(runtime =>
{
RuntimeEntityRecord record =
runtime.EntityObjects.RegisterEntity(
ProjectileSpawn(serverGuid, incarnation)).Canonical
?? throw new InvalidOperationException(
"projectile registration produced no canonical record");
PhysicsBody body =
runtime.EntityObjects.Physics.GetOrCreatePhysicsBody(
record,
static canonical =>
{
var created = new PhysicsBody
{
Position = new Vector3(10f, 20f, 5f),
Orientation = Quaternion.Identity,
InWorld = true,
TransientState =
TransientStateFlags.Active,
};
created.SnapToCell(
canonical.FullCellId,
created.Position,
created.Position);
return created;
});
_ = runtime.EntityObjects.Physics.BindProjectile(
record,
body,
new ProjectileCollisionSphere(
Vector3.Zero,
0.25f));
runtime.EntityObjects.Physics.AcknowledgeSpatialProjection(
record,
spatial: true);
var updater = new RuntimeProjectilePhysicsUpdater(
runtime.EntityObjects.Physics);
Require(
updater.TryBegin(
record,
quantum: 0.05f,
record.ObjectClockEpoch,
externalOwnerValid: null,
out RuntimeProjectilePhysicsCommit commit),
"projectile update did not begin");
_ = updater.Complete(
commit,
liveCenterX: 1,
liveCenterY: 1,
_ =>
{
_gameplay.Trace.Add("projectile:ack");
return true;
});
});
}
public bool CompletePortal(
uint destinationCell = 0x8A020164u,
ushort sequence = 1)
{
return Execute(runtime =>
{
RuntimeWorldTransitState transit = runtime.TransitOwner;
Require(
transit.TryQueueTeleportStart(sequence),
"portal start was rejected");
Require(
transit.ActivateQueuedTeleport(),
"portal activation was rejected");
Require(
transit.OfferTeleportDestination(
new RuntimeTeleportDestination(
runtime.PlayerIdentity.ServerGuid,
InstanceSequence: 1,
PositionSequence: 1,
TeleportSequence: sequence,
ForcePositionSequence: 1,
Position: new Position(
destinationCell,
Vector3.Zero,
Quaternion.Identity)),
teleportTimestampAdvanced: true),
"portal destination was rejected");
Require(
transit.TryBeginPortalReveal(
sequence,
destinationCell,
out long revealGeneration),
"portal reveal was rejected");
EmitPortal(runtime);
Require(
transit.TryRegisterHostProjection(
revealGeneration,
destinationCell,
out RuntimeWorldHostProjectionToken projection),
"portal projection registration was rejected");
Acknowledge(
transit,
projection,
RuntimeWorldHostAcknowledgementStage
.ProjectionRegistered);
bool indoor = (destinationCell & 0xFFFFu) >= 0x0100u;
Require(
transit.AcknowledgeDestinationReadiness(
new RuntimeDestinationReadiness(
revealGeneration,
destinationCell,
indoor,
IsUnhydratable: false,
RequiredRenderRadius: indoor ? 0 : 1,
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: true)),
"portal readiness was rejected");
EmitPortal(runtime);
Require(
transit.AcknowledgePortalMaterialized(
revealGeneration,
sequence,
destinationCell),
"portal materialization was rejected");
EmitPortal(runtime);
DrainPortalAcknowledgements(transit, projection);
Require(
transit.RequireDestinationReservationRelease(
projection),
"portal reservation release was rejected");
DrainPortalAcknowledgements(transit, projection);
Require(
transit.AcknowledgeWorldViewportVisible(
revealGeneration),
"portal viewport acknowledgement was rejected");
EmitPortal(runtime);
Require(
transit.Complete(revealGeneration),
"portal completion was rejected");
EmitPortal(runtime);
DrainPortalAcknowledgements(transit, projection);
});
}
public void Advance(double deltaSeconds)
{
_ = Runtime.Clock.Advance(deltaSeconds);
Runtime.Session.Tick();
}
public void Dispose()
{
if (_disposed)
return;
if (Runtime.Session.CurrentSession is not null)
_ = Stop();
_traceSubscription.Dispose();
_hostLease.Dispose();
Runtime.Dispose();
_disposed = true;
}
private ILiveSessionEventRouting CreateEventRoute(WorldSession session)
{
var route = new EventRoute(Runtime.Generation, _operations.Trace);
_events = route;
return route;
}
private static void Acknowledge(
RuntimeWorldTransitState transit,
RuntimeWorldHostProjectionToken projection,
RuntimeWorldHostAcknowledgementStage stage) =>
Require(
transit.AcknowledgeHostProjection(
new RuntimeWorldHostAcknowledgement(
projection,
stage)),
$"portal host acknowledgement was rejected: {stage}");
private static void DrainPortalAcknowledgements(
RuntimeWorldTransitState transit,
RuntimeWorldHostProjectionToken projection)
{
while (transit.TryGetHostProjection(
projection,
out RuntimeWorldHostProjectionSnapshot pending))
{
RuntimeWorldHostAcknowledgementStage stages =
pending.PendingAcknowledgements;
if ((stages & RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected) != 0)
{
Acknowledge(
transit,
projection,
RuntimeWorldHostAcknowledgementStage
.SimulationReleaseProjected);
continue;
}
if ((stages & RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased) != 0)
{
Acknowledge(
transit,
projection,
RuntimeWorldHostAcknowledgementStage
.DestinationReservationReleased);
continue;
}
if ((stages & RuntimeWorldHostAcknowledgementStage
.TerminalProjected) != 0)
{
Acknowledge(
transit,
projection,
RuntimeWorldHostAcknowledgementStage
.TerminalProjected);
continue;
}
break;
}
}
private static void EmitPortal(GameRuntime runtime) =>
runtime.EventSink.EmitPortal(runtime.TransitOwner.Snapshot);
private static SpellTable FixtureSpellMetadata()
{
const string header =
"Spell ID,Name,Flags [Hex],IsUntargetted,TargetMask [Hex]";
const string row = "1,Runtime Fixture,0x0,true,0x0";
return SpellTable.LoadFromReader(
new System.IO.StringReader($"{header}\n{row}"));
}
private static WorldSession.EntitySpawn ProjectileSpawn(
uint guid,
ushort incarnation)
{
var position = new CreateObject.ServerPosition(
0x01010001u,
10f,
20f,
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
| PhysicsStateFlags.Missile),
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: new Vector3(10f, 0f, 0f),
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 projectile",
null,
null,
null,
PhysicsState: physics.RawState,
InstanceSequence: incarnation,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
private static void Require(bool condition, string message)
{
if (!condition)
throw new InvalidOperationException(message);
}
private ILiveSessionCommandRouting CreateCommandRoute(
WorldSession session)
{
var route = new CommandRoute(Runtime.Generation, _operations.Trace);
_commands = route;
return route;
}
private sealed class ImmediateResetHost : IRuntimeGenerationResetHost
{
public int ProjectionRetirementCount { get; private set; }
public int ProjectionDrainCount { get; private set; }
public int ProjectionCompletionCount { get; private set; }
public void RetireEntityProjection(RuntimeEntityRecord entity) =>
ProjectionRetirementCount++;
public void DrainEntityProjectionBoundary() =>
ProjectionDrainCount++;
public void CompleteEntityProjectionRetirement() =>
ProjectionCompletionCount++;
}
private sealed class EventRoute(
RuntimeGenerationToken generation,
List<string> trace) : ILiveSessionEventRouting
{
public RuntimeGenerationToken Generation { get; } = generation;
public bool IsActive { get; private set; }
public void Attach()
{
IsActive = true;
trace.Add($"events+:{Generation.Value}");
}
public void Dispose()
{
IsActive = false;
trace.Add($"events-:{Generation.Value}");
}
}
private sealed class CommandRoute(
RuntimeGenerationToken generation,
List<string> trace) : ILiveSessionCommandRouting
{
public RuntimeGenerationToken Generation { get; } = generation;
public bool IsActive { get; private set; }
public void Activate()
{
IsActive = true;
trace.Add($"commands+:{Generation.Value}");
}
public void Dispose()
{
IsActive = false;
trace.Add($"commands-:{Generation.Value}");
}
}
private sealed class FixtureOperations(
uint characterId,
string characterName) : ILiveSessionOperations
{
public List<string> Trace { get; } = [];
public IPEndPoint ResolveEndpoint(string host, int port)
{
Trace.Add($"resolve:{host}:{port}");
return new IPEndPoint(IPAddress.Loopback, port);
}
public WorldSession CreateSession(IPEndPoint endpoint)
{
Trace.Add("session+");
return new WorldSession(endpoint, new FixtureTransport());
}
public void Connect(
WorldSession session,
string user,
string password) =>
Trace.Add($"connect:{user}");
public CharacterList.Parsed GetCharacters(WorldSession session)
{
Trace.Add("characters");
return new CharacterList.Parsed(
0u,
[new CharacterList.Character(
characterId,
characterName,
0u)],
[],
11,
"NoWindow",
true,
true);
}
public void EnterWorld(
WorldSession session,
int activeCharacterIndex) =>
Trace.Add($"enter:{activeCharacterIndex}");
public void Tick(WorldSession session) => Trace.Add("tick");
public void DisposeSession(WorldSession session)
{
Trace.Add("session-");
session.Dispose();
}
public void RecordConnecting(
string host,
int port,
string user) =>
Trace.Add($"connecting:{host}:{port}:{user}");
public void RecordConnected() => Trace.Add("connected");
}
private sealed class FixtureTransport : 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 GameplayOperations :
IRuntimeInteractionTransport,
IRuntimeCombatAttackOperations,
IRuntimeCombatTargetOperations,
IRuntimeCombatModeOperations,
IRuntimeSpellCastOperations
{
private GameRuntime? _runtime;
private uint _sequence;
public double Now { get; set; } = 10d;
public List<string> Trace { get; } = [];
public void Bind(GameRuntime runtime) =>
_runtime = runtime
?? throw new ArgumentNullException(nameof(runtime));
public bool CanStartAttack() => IsInWorld;
public void PrepareAttackRequest() =>
Trace.Add("attack:prepare");
public bool SendAttack(AttackHeight height, float power)
{
Trace.Add($"attack:{height}:{power:0.0}");
return true;
}
public void SendCancelAttack() { }
public bool IsDualWield => false;
public bool PlayerReadyForAttack => true;
public bool AutoRepeatAttack => false;
public bool AutoTarget => false;
public uint? SelectClosestTarget() => null;
public bool IsInWorld => _runtime?.Session.IsInWorld == true;
public IReadOnlyList<ClientObject> GetOrderedEquipment() => [];
public void NotifyExplicitCombatModeRequest() { }
public void SendChangeCombatMode(CombatMode mode) =>
Trace.Add($"combat:{mode}");
public uint LocalPlayerId =>
_runtime?.PlayerIdentity.ServerGuid ?? 0u;
public bool CanSend => IsInWorld;
public bool HasRequiredComponents(uint spellId) => true;
public bool IsTargetCompatible(
uint targetId,
SpellMetadata spell,
bool showMessage) => true;
public void StopCompletely() =>
Trace.Add("cast:stop");
public void SendUntargeted(uint spellId) =>
Trace.Add($"cast:{spellId}");
public void SendTargeted(uint targetId, uint spellId) =>
Trace.Add($"cast:{targetId}:{spellId}");
public void DisplayMessage(string message) =>
Trace.Add($"message:{message}");
public void IncrementBusy()
{
_runtime?.InventoryOwner.Transactions.IncrementBusyCount();
Trace.Add("cast:busy");
}
public bool TrySendUse(uint serverGuid, out uint sequence)
{
sequence = ++_sequence;
Trace.Add($"use:{serverGuid:X8}");
return true;
}
public bool TrySendPickup(
uint itemGuid,
uint destinationContainerId,
int placement,
out uint sequence)
{
sequence = ++_sequence;
Trace.Add(
$"pickup:{itemGuid:X8}:{destinationContainerId:X8}:{placement}");
return true;
}
}
}