refactor(app): complete session startup composition

Move the live-session reset and routing graph, combat and diagnostic command targets, and the sole gameplay input subscriber into Phase 7 before frame publication. Add exact retryable ownership for late bindings so partial startup cannot strand session or component teardown edges.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 18:49:31 +02:00
parent 7fa60971e2
commit 826f9ea9b5
22 changed files with 924 additions and 412 deletions

View file

@ -32,9 +32,11 @@ What does NOT go here:
canonical runtime/world, selection/radar, landblock publishers, and canonical runtime/world, selection/radar, landblock publishers, and
portal/sky/particle resources. portal/sky/particle resources.
I.7 now owns streaming/session/hydration/local-player/teleport startup and I.7 now owns streaming/session/hydration/local-player/teleport startup and
every late edge explicitly. `GameWindow.OnUpdate` and `GameWindow.OnRender` every late edge explicitly. I.8a now owns the focused session factory,
are typed orchestration handoffs and the class is 2,479 raw lines. I.8I.9 command targets, and sole gameplay input subscriber before frame publication.
plus the remaining shutdown/closeout `GameWindow.OnUpdate` and `GameWindow.OnRender` are typed orchestration
handoffs and the class is 2,149 raw lines. I.8bI.9 plus the remaining
shutdown/closeout
cleanup is active in cleanup is active in
[`docs/architecture/code-structure.md`](architecture/code-structure.md). [`docs/architecture/code-structure.md`](architecture/code-structure.md).
This is the behavior-preserving prerequisite before new M4 feature bodies. This is the behavior-preserving prerequisite before new M4 feature bodies.

View file

@ -734,11 +734,13 @@ owners without callbacks to the window. The old item-use and selection wrapper
methods are deleted. I.7 now owns the complete streaming/session/hydration/ methods are deleted. I.7 now owns the complete streaming/session/hydration/
local-player/teleport construction phase and every late edge through named local-player/teleport construction phase and every late edge through named
exact-owner tokens; the former 432-line inline body and spawn-claim memo are exact-owner tokens; the former 432-line inline body and spawn-claim memo are
gone. `GameWindow` is 2,479 raw lines after I.7. I.8L remain active, with gone. I.8a moves the exact live-session reset/router graph, combat and
atomic frame-root publication and terminal session start next. The App gate diagnostic command targets, and sole gameplay input subscriber into Phase 7
passes 3,420 tests / 3 intentional skips and the complete Release suite passes before frame publication. `GameWindow` is 2,149 raw lines after I.8a. I.8bL
7,792 tests / 5 intentional skips. The clean solution build retains only the remain active, with atomic frame-root publication next. The App gate passes
17 test-project warnings tracked by #228; the I.7 behavior, architecture, and 3,424 tests / 3 intentional skips and the complete Release suite passes 7,796
tests / 5 intentional skips. The clean solution build retains only the 17
test-project warnings tracked by #228; the I.8a behavior, architecture, and
adversarial reviews are clean. adversarial reviews are clean.
### 4.4 Exit criteria ### 4.4 Exit criteria

View file

@ -104,10 +104,11 @@ controllers, selection/radar, landblock publishers, and portal/sky/particle
resources have transactional phase ownership and exact-owner late bindings. resources have transactional phase ownership and exact-owner late bindings.
I.7 adds the complete streaming/session/hydration/local-player/teleport phase, I.7 adds the complete streaming/session/hydration/local-player/teleport phase,
including explicit reversible ownership for every late edge. `GameWindow` is including explicit reversible ownership for every late edge. `GameWindow` is
now 2,479 raw lines. I.8I.9, shutdown, canonical soak snapshots, and closeout now 2,149 raw lines after I.8a, which composes the session factory, command
checkpoints JL remain. The App gate passes 3,420 tests / 3 intentional skips, targets, and sole gameplay input subscriber before frame publication. I.8bI.9,
the complete Release suite passes 7,792 tests / 5 skips, and all three I.7 shutdown, canonical soak snapshots, and closeout checkpoints JL remain. The
corrected-diff reviews are clean. App gate passes 3,424 tests / 3 intentional skips, the complete Release suite
passes 7,796 tests / 5 skips, and the I.8a corrected-diff review is clean.
This is a behavior-preserving structural program. Severe regressions still get This is a behavior-preserving structural program. Severe regressions still get
root-cause fixes in separate commits; ordinary feature work resumes with M4 root-cause fixes in separate commits; ordinary feature work resumes with M4

View file

@ -59,11 +59,12 @@ runtime/world, presentation/effect controllers, selection/radar, landblock
publishers, and portal/sky/particle resources have transactional phase publishers, and portal/sky/particle resources have transactional phase
ownership and exact-owner late bindings. I.7 now owns the complete streaming, ownership and exact-owner late bindings. I.7 now owns the complete streaming,
session, hydration, local-player, combat, and teleport startup graph, including session, hydration, local-player, combat, and teleport startup graph, including
named reversible late edges. `GameWindow` is 2,479 raw lines. I.8I.9 and named reversible late edges. I.8a adds the focused live-session runtime factory
checkpoints JL remain active. Issue #232 tracks process-residency variance in and attaches command/input owners before frame publication. `GameWindow` is
the soak without loosening its leak threshold. The App gate passes 3,420 tests 2,149 raw lines. I.8bI.9 and checkpoints JL remain active. Issue #232 tracks
/ 3 skips, the complete Release suite passes 7,792 tests / 5 skips, and all process-residency variance in the soak without loosening its leak threshold.
three I.7 corrected-diff reviews are clean. The App gate passes 3,424 tests / 3 skips, the complete Release suite passes
7,796 tests / 5 skips, and the I.8a corrected-diff review is clean.
Carried: Carried:
#153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus #153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus

View file

@ -293,6 +293,10 @@ ledger only after the complete pipeline is live.
frame-root publication and terminal session start is the active cut. Its frame-root publication and terminal session start is the active cut. Its
detailed session-binding, atomic-root, and terminal-start plan is detailed session-binding, atomic-root, and terminal-start plan is
[`2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md`](2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md). [`2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md`](2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md).
I.8a is complete: the session factory, command targets, and sole gameplay
input subscriber now belong to Phase 7 and publish before any frame root;
`GameWindow.cs` is 2,149 raw lines. I.8b atomic frame-root composition is the
active cut.
Checkpoint I remains active until I.9 closes the complete nine-phase pipeline. Checkpoint I remains active until I.9 closes the complete nine-phase pipeline.
## 6. Automated acceptance ## 6. Automated acceptance

View file

@ -1,6 +1,6 @@
# GameWindow Slice 8 — Checkpoint I.8 Frame Roots and Session Start # GameWindow Slice 8 — Checkpoint I.8 Frame Roots and Session Start
**Status:** Active **Status:** Active — I.8a complete; I.8b frame-root composition next
**Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md` **Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`
@ -27,6 +27,15 @@ unchanged, so no divergence-register row is added.
## 2. I.8a — complete session/player composition ## 2. I.8a — complete session/player composition
**Completed 2026-07-22.** `LiveSessionRuntimeFactory` now owns the exact
reset/router/command graph without a window reference. Phase 7 publishes the
session host, exact-owner combat and diagnostic command bindings, and the sole
gameplay input subscriber before Phase 8 can expose a frame root. Desired
components have a focused canonical snapshot owner. The component-lifecycle
handoff into the earlier live-presentation phase is an exact, retryable
adoption lease, so any later Phase-7 fault rolls it back without stranding or
replaying teardown.
### 2.1 Focused session runtime factory ### 2.1 Focused session runtime factory
Add `LiveSessionRuntimeFactory` under `AcDream.App/Net`. It owns the existing Add `LiveSessionRuntimeFactory` under `AcDream.App/Net`. It owns the existing

View file

@ -29,8 +29,8 @@ audit.
- [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots - [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots
explicit single ownership and transfer seams. explicit single ownership and transfer seams.
- [ ] I — group `OnLoad` into small ordered, fakeable composition phases with - [ ] I — group `OnLoad` into small ordered, fakeable composition phases with
transactional partial-acquisition rollback. I.1I.7 are complete; I.8 transactional partial-acquisition rollback. I.1I.7 and I.8a are complete;
frame-root publication and terminal session start is active. Detailed plan: I.8b atomic frame-root publication is active. Detailed plan:
[`2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`](2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md). [`2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`](2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md).
- [ ] J — move the exact retryable shutdown manifest to a focused lifetime - [ ] J — move the exact retryable shutdown manifest to a focused lifetime
owner and prove all partial-load/reentrant/retry paths. owner and prove all partial-load/reentrant/retry paths.

View file

@ -109,6 +109,24 @@ internal sealed class LiveCombatModeCommandSlot : ILiveCombatModeCommand
} }
} }
public IDisposable BindOwned(ILiveCombatModeCommand target)
{
ArgumentNullException.ThrowIfNull(target);
lock (_gate)
{
ObjectDisposedException.ThrowIf(_deactivated, this);
if (_target is not null)
{
throw new InvalidOperationException(
"Live combat-mode commands are already bound.");
}
_target = target;
}
return new Binding(this, target);
}
public void Deactivate() public void Deactivate()
{ {
lock (_gate) lock (_gate)
@ -126,6 +144,23 @@ internal sealed class LiveCombatModeCommandSlot : ILiveCombatModeCommand
_target?.Toggle(); _target?.Toggle();
} }
} }
private sealed class Binding : IDisposable
{
private LiveCombatModeCommandSlot? _owner;
private readonly ILiveCombatModeCommand _expected;
public Binding(
LiveCombatModeCommandSlot owner,
ILiveCombatModeCommand expected)
{
_owner = owner;
_expected = expected;
}
public void Dispose() =>
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
}
} }
/// <summary> /// <summary>

View file

@ -406,6 +406,20 @@ internal sealed class ShortcutSnapshotState
} }
} }
internal sealed class DesiredComponentSnapshotState
{
private IReadOnlyList<(uint Id, uint Amount)> _items =
Array.Empty<(uint Id, uint Amount)>();
public IReadOnlyList<(uint Id, uint Amount)> Items
{
get => _items;
set => _items = value ?? Array.Empty<(uint Id, uint Amount)>();
}
public void Clear() => _items = Array.Empty<(uint Id, uint Amount)>();
}
/// <summary> /// <summary>
/// Probe scripts are mounted in Phase 5; reveal/resource facts become valid in /// Probe scripts are mounted in Phase 5; reveal/resource facts become valid in
/// Phase 7. Calls remain inert and diagnostic until that exact owner binds. /// Phase 7. Calls remain inert and diagnostic until that exact owner binds.

View file

@ -10,7 +10,9 @@ namespace AcDream.App.Composition;
/// </summary> /// </summary>
internal sealed class LivePresentationRuntimeBindings : IDisposable internal sealed class LivePresentationRuntimeBindings : IDisposable
{ {
private readonly List<(string Name, IDisposable Binding)> _bindings = []; private sealed record Entry(string Name, IDisposable Binding);
private readonly List<Entry> _bindings = [];
private bool _deactivationStarted; private bool _deactivationStarted;
public void Adopt(string name, IDisposable binding) public void Adopt(string name, IDisposable binding)
@ -18,7 +20,17 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable
ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentNullException.ThrowIfNull(binding); ArgumentNullException.ThrowIfNull(binding);
ObjectDisposedException.ThrowIf(_deactivationStarted, this); ObjectDisposedException.ThrowIf(_deactivationStarted, this);
_bindings.Add((name, binding)); _bindings.Add(new Entry(name, binding));
}
public IDisposable AdoptOwned(string name, IDisposable binding)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentNullException.ThrowIfNull(binding);
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
var entry = new Entry(name, binding);
_bindings.Add(entry);
return new Adoption(this, entry);
} }
public void AdoptRelease(string name, Action release) => public void AdoptRelease(string name, Action release) =>
@ -56,16 +68,16 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable
List<Exception>? failures = null; List<Exception>? failures = null;
for (int i = _bindings.Count - 1; i >= 0; i--) for (int i = _bindings.Count - 1; i >= 0; i--)
{ {
(string name, IDisposable binding) = _bindings[i]; Entry entry = _bindings[i];
try try
{ {
binding.Dispose(); entry.Binding.Dispose();
_bindings.RemoveAt(i); _bindings.RemoveAt(i);
} }
catch (Exception failure) catch (Exception failure)
{ {
(failures ??= []).Add(new InvalidOperationException( (failures ??= []).Add(new InvalidOperationException(
$"Live-presentation binding '{name}' did not detach.", $"Live-presentation binding '{entry.Name}' did not detach.",
failure)); failure));
} }
} }
@ -78,6 +90,38 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable
} }
} }
private void ReleaseAdoption(Entry expected)
{
int index = _bindings.IndexOf(expected);
if (index < 0)
return;
expected.Binding.Dispose();
_bindings.RemoveAt(index);
}
private sealed class Adoption : IDisposable
{
private LivePresentationRuntimeBindings? _owner;
private readonly Entry _expected;
public Adoption(
LivePresentationRuntimeBindings owner,
Entry expected)
{
_owner = owner;
_expected = expected;
}
public void Dispose()
{
LivePresentationRuntimeBindings? owner = _owner;
if (owner is null)
return;
owner.ReleaseAdoption(_expected);
_owner = null;
}
}
private sealed class DelegateBinding : IDisposable private sealed class DelegateBinding : IDisposable
{ {
private Action? _release; private Action? _release;

View file

@ -1,6 +1,7 @@
using AcDream.App.Combat; using AcDream.App.Combat;
using AcDream.App.Input; using AcDream.App.Input;
using AcDream.App.Interaction; using AcDream.App.Interaction;
using AcDream.App.Diagnostics;
using AcDream.App.Net; using AcDream.App.Net;
using AcDream.App.Physics; using AcDream.App.Physics;
using AcDream.App.Rendering; using AcDream.App.Rendering;
@ -12,11 +13,15 @@ using AcDream.App.Update;
using AcDream.App.World; using AcDream.App.World;
using AcDream.Core.Combat; using AcDream.Core.Combat;
using AcDream.Core.Items; using AcDream.Core.Items;
using AcDream.Core.Chat;
using AcDream.Core.Physics; using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion; using AcDream.Core.Physics.Motion;
using AcDream.Core.Plugins; using AcDream.Core.Plugins;
using AcDream.Core.Rendering; using AcDream.Core.Rendering;
using AcDream.Core.Selection; using AcDream.Core.Selection;
using AcDream.Core.Player;
using AcDream.Core.Social;
using AcDream.Core.Spells;
using AcDream.Core.World; using AcDream.Core.World;
using Silk.NET.Windowing; using Silk.NET.Windowing;
@ -28,6 +33,11 @@ internal sealed record SessionPlayerDependencies(
object DatLock, object DatLock,
RuntimeSettingsController Settings, RuntimeSettingsController Settings,
SettingsDevToolsResult SettingsDevTools, SettingsDevToolsResult SettingsDevTools,
WorldEnvironmentController WorldEnvironment,
WorldSceneDebugState WorldSceneDebugState,
RuntimeDiagnosticCommandSlot RuntimeDiagnosticCommands,
LiveCombatModeCommandSlot CombatModeCommands,
HostQuiescenceGate HostQuiescence,
PhysicsEngine PhysicsEngine, PhysicsEngine PhysicsEngine,
PhysicsDataCache PhysicsDataCache, PhysicsDataCache PhysicsDataCache,
WorldGameState WorldGameState, WorldGameState WorldGameState,
@ -55,6 +65,9 @@ internal sealed record SessionPlayerDependencies(
LocalPlayerSkillState PlayerSkills, LocalPlayerSkillState PlayerSkills,
ViewportAspectState ViewportAspect, ViewportAspectState ViewportAspect,
PlayerApproachCompletionState PlayerApproachCompletions, PlayerApproachCompletionState PlayerApproachCompletions,
PlayerCharacterOptionsState CharacterOptions,
ShortcutSnapshotState Shortcuts,
DesiredComponentSnapshotState DesiredComponents,
PointerPositionState PointerPosition, PointerPositionState PointerPosition,
DispatcherMovementInputSource MovementInput, DispatcherMovementInputSource MovementInput,
IInputCaptureSource InputCapture, IInputCaptureSource InputCapture,
@ -66,6 +79,14 @@ internal sealed record SessionPlayerDependencies(
CombatAttackOperationsSlot CombatAttackOperations, CombatAttackOperationsSlot CombatAttackOperations,
CombatState Combat, CombatState Combat,
CombatFeedbackSlot CombatFeedback, CombatFeedbackSlot CombatFeedback,
ChatLog Chat,
LocalPlayerState LocalPlayer,
Spellbook Spellbook,
ItemManaState ItemMana,
FriendsState Friends,
SquelchState Squelch,
TurbineChatState TurbineChat,
ExternalContainerState ExternalContainers,
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback, TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
Action<string> Log); Action<string> Log);
@ -91,6 +112,8 @@ internal sealed record SessionPlayerResult(
PlayerModeController PlayerMode, PlayerModeController PlayerMode,
PlayerModeAutoEntry PlayerModeAutoEntry, PlayerModeAutoEntry PlayerModeAutoEntry,
LocalPlayerTeleportController LocalTeleport, LocalPlayerTeleportController LocalTeleport,
LiveSessionHost SessionHost,
GameplayInputActionRouter? GameplayActions,
SessionPlayerRuntimeBindings RuntimeBindings); SessionPlayerRuntimeBindings RuntimeBindings);
internal interface IGameWindowSessionPlayerPublication internal interface IGameWindowSessionPlayerPublication
@ -115,6 +138,9 @@ internal enum SessionPlayerCompositionPoint
PlayerModeBound, PlayerModeBound,
PortalTransferred, PortalTransferred,
TeleportBound, TeleportBound,
SessionHostCreated,
CommandsBound,
GameplayActionsAttached,
ResultPublished, ResultPublished,
} }
@ -694,9 +720,10 @@ internal sealed class SessionPlayerCompositionPhase
IDisposable componentLifecycleBinding = IDisposable componentLifecycleBinding =
live.ComponentLifecycle.BindOwned(teardown); live.ComponentLifecycle.BindOwned(teardown);
IDisposable componentLifecycleAdoption;
try try
{ {
live.RuntimeBindings.Adopt( componentLifecycleAdoption = live.RuntimeBindings.AdoptOwned(
"live runtime component teardown", "live runtime component teardown",
componentLifecycleBinding); componentLifecycleBinding);
} }
@ -705,6 +732,153 @@ internal sealed class SessionPlayerCompositionPhase
componentLifecycleBinding.Dispose(); componentLifecycleBinding.Dispose();
throw; throw;
} }
var componentLifecycleLease = scope.Own(
"live runtime component teardown adoption",
componentLifecycleAdoption,
static value => value.Dispose());
AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? vitals =
d.SettingsDevTools.DevTools?.Vitals
?? interaction.RetainedUi?.Vitals;
AcDream.UI.Abstractions.Panels.Chat.ChatVM? retailChat =
interaction.RetainedUi?.Chat;
var sessionRuntimeFactory = new LiveSessionRuntimeFactory(
new LiveSessionPlayerRuntime(
d.PlayerIdentity,
d.PlayerController,
d.PlayerSkills,
d.WorldOrigin,
d.CharacterOptions,
d.Shortcuts,
d.DesiredComponents),
new LiveSessionDomainRuntime(
d.Objects,
d.LocalPlayer,
d.Spellbook,
d.Combat,
d.ItemMana,
d.Friends,
d.Squelch,
d.TurbineChat,
d.ExternalContainers,
d.Chat),
new LiveSessionUiRuntime(
interaction.RetainedUi?.Runtime,
vitals,
retailChat,
interaction.RetainedUi?.CharacterSheet,
interaction.RetainedUi?.Magic,
live.PaperdollPresenter),
new LiveSessionInteractionRuntime(
d.Settings,
gameplayInput,
playerMode,
playerModeAutoEntry,
interaction.ItemInteraction,
interaction.CombatAttack,
live.SelectionInteractions),
new LiveSessionWorldRuntime(
content.Dats,
live.WorldState,
live.LiveEntities,
sessionEvents,
d.WorldEnvironment,
d.TeleportSink,
spawnClaimClassifier,
live.EquippedChildren,
live.SelectionScene,
d.ParticleVisibility,
d.InboundEntityEvents,
liveness,
networkUpdates,
hydration,
live.RemoteTeleport,
live.EntityEffects,
content.AnimationHookFrames,
live.Presentation,
d.RemoteMovementObservations),
d.Log);
LiveSessionHost sessionHost = sessionRuntimeFactory.Create(liveSession);
Fault(SessionPlayerCompositionPoint.SessionHostCreated);
Action<string>? debugToast = d.SettingsDevTools.DevTools is { } devOwner
? message => devOwner.Debug.AddToast(message)
: null;
var combatCommand = new LiveCombatModeCommandController(
new LiveSessionCombatModeAuthority(sessionHost),
new LocalPlayerCombatEquipmentSource(
d.Objects,
d.PlayerIdentity),
d.Combat,
new ItemInteractionCombatModeIntentSink(
interaction.ItemInteraction),
d.Log,
debugToast,
text => d.Chat.OnSystemMessage(text, 0x1Au));
bindings.Adopt(
"live combat-mode commands",
d.CombatModeCommands.BindOwned(combatCommand));
var nearbyDiagnostics = new NearbyWorldDiagnosticDumper(
new RuntimeNearbyWorldDiagnosticSource(
d.PlayerMode,
d.PlayerController,
host.CameraController,
d.WorldOrigin,
live.WorldState,
d.PhysicsEngine),
d.Log);
var runtimeDiagnostics = new RuntimeDiagnosticCommandController(
d.WorldEnvironment,
d.WorldSceneDebugState,
host.CameraPointerInput,
nearbyDiagnostics,
debugToast);
bindings.Adopt(
"runtime diagnostic commands",
d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics));
Fault(SessionPlayerCompositionPoint.CommandsBound);
CompositionAcquisitionScope.CompositionAcquisitionLease<
GameplayInputActionRouter>? gameplayActionsLease = null;
if (host.InputDispatcher is { } dispatcher)
{
CameraPointerInputController pointer = host.CameraPointerInput
?? throw new InvalidOperationException(
"Gameplay action routing requires the composed pointer owner.");
var commands = new GameplayInputCommandController(
new RetainedGameplayWindowCommands(
interaction.RetainedUi?.Runtime),
new DevToolsGameplayCommands(
d.SettingsDevTools.DevTools?.Presenter),
runtimeDiagnostics,
new PlayerModeGameplayCommands(
d.PlayerMode,
playerMode),
new ItemTargetModeCommands(interaction.ItemInteraction),
new GameplayCameraModeCommands(host.CameraController),
combatCommand,
new GameplayWindowCommands(d.Window.Close));
var targets = new RuntimeGameplayInputPriorityTargets(
gameplayInput,
pointer,
interaction.RetainedUi?.Runtime,
live.SelectionInteractions,
commands);
GameplayInputActionRouter gameplayActions =
GameplayInputActionRouter.Create(
dispatcher,
d.Combat,
targets,
d.HostQuiescence,
d.Log);
gameplayActionsLease = scope.Own(
"gameplay input actions",
gameplayActions,
static value => value.Dispose());
gameplayActions.Attach();
}
Fault(SessionPlayerCompositionPoint.GameplayActionsAttached);
var bindingsLease = scope.Own( var bindingsLease = scope.Own(
"session/player runtime bindings", "session/player runtime bindings",
@ -733,12 +907,16 @@ internal sealed class SessionPlayerCompositionPhase
playerMode, playerMode,
playerModeAutoEntry, playerModeAutoEntry,
teleportLease.Resource, teleportLease.Resource,
sessionHost,
gameplayActionsLease?.Resource,
bindings); bindings);
_publication.PublishSessionPlayer(result); _publication.PublishSessionPlayer(result);
streamerLease.Transfer(); streamerLease.Transfer();
liveSessionLease.Transfer(); liveSessionLease.Transfer();
teleportLease.Transfer(); teleportLease.Transfer();
gameplayActionsLease?.Transfer();
componentLifecycleLease.Transfer();
bindingsLease.Transfer(); bindingsLease.Transfer();
Fault(SessionPlayerCompositionPoint.ResultPublished); Fault(SessionPlayerCompositionPoint.ResultPublished);
return result; return result;

View file

@ -57,6 +57,24 @@ internal sealed class RuntimeDiagnosticCommandSlot : IRuntimeDiagnosticCommands
} }
} }
public IDisposable BindOwned(IRuntimeDiagnosticCommands target)
{
ArgumentNullException.ThrowIfNull(target);
lock (_gate)
{
ObjectDisposedException.ThrowIf(_deactivated, this);
if (_target is not null)
{
throw new InvalidOperationException(
"Runtime diagnostic commands are already bound.");
}
_target = target;
}
return new Binding(this, target);
}
public void Deactivate() public void Deactivate()
{ {
lock (_gate) lock (_gate)
@ -103,6 +121,23 @@ internal sealed class RuntimeDiagnosticCommandSlot : IRuntimeDiagnosticCommands
_target?.ToggleCollisionWireframes(); _target?.ToggleCollisionWireframes();
} }
} }
private sealed class Binding : IDisposable
{
private RuntimeDiagnosticCommandSlot? _owner;
private readonly IRuntimeDiagnosticCommands _expected;
public Binding(
RuntimeDiagnosticCommandSlot owner,
IRuntimeDiagnosticCommands expected)
{
_owner = owner;
_expected = expected;
}
public void Dispose() =>
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
}
} }
internal interface INearbyWorldDiagnosticSource internal interface INearbyWorldDiagnosticSource

View file

@ -0,0 +1,386 @@
using System.Diagnostics;
using AcDream.App.Combat;
using AcDream.App.Composition;
using AcDream.App.Input;
using AcDream.App.Interaction;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Selection;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Settings;
using AcDream.App.Spells;
using AcDream.App.Streaming;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.App.World;
using AcDream.Core.Chat;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Player;
using AcDream.Core.Social;
using AcDream.Core.Spells;
using AcDream.Core.World;
using AcDream.Content;
using AcDream.UI.Abstractions.Panels.Chat;
using AcDream.UI.Abstractions.Panels.Vitals;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Net;
internal sealed record LiveSessionPlayerRuntime(
LocalPlayerIdentityState Identity,
LocalPlayerControllerSlot Controller,
LocalPlayerSkillState Skills,
LiveWorldOriginState WorldOrigin,
PlayerCharacterOptionsState CharacterOptions,
ShortcutSnapshotState Shortcuts,
DesiredComponentSnapshotState DesiredComponents);
internal sealed record LiveSessionDomainRuntime(
ClientObjectTable Objects,
LocalPlayerState LocalPlayer,
Spellbook Spellbook,
CombatState Combat,
ItemManaState ItemMana,
FriendsState Friends,
SquelchState Squelch,
TurbineChatState TurbineChat,
ExternalContainerState ExternalContainers,
ChatLog Chat);
internal sealed record LiveSessionUiRuntime(
RetailUiRuntime? RetailUi,
VitalsVM? Vitals,
ChatVM? RetailChat,
CharacterSheetProvider? CharacterSheet,
MagicRuntime? Magic,
PaperdollFramePresenter? Paperdoll);
internal sealed record LiveSessionInteractionRuntime(
RuntimeSettingsController Settings,
GameplayInputFrameController GameplayInput,
PlayerModeController PlayerMode,
PlayerModeAutoEntry PlayerModeAutoEntry,
ItemInteractionController ItemInteraction,
CombatAttackController CombatAttack,
SelectionInteractionController SelectionInteractions);
internal sealed record LiveSessionWorldRuntime(
IDatReaderWriter Dats,
GpuWorldState WorldState,
LiveEntityRuntime LiveEntities,
LiveEntitySessionController EntitySession,
WorldEnvironmentController Environment,
DeferredLocalPlayerTeleportNetworkSink Teleport,
DatSpawnClaimHydrationClassifier SpawnClaims,
EquippedChildRenderController EquippedChildren,
RetailSelectionScene SelectionScene,
ParticleVisibilityController ParticleVisibility,
RetailInboundEventDispatcher InboundEvents,
LiveEntityLivenessController Liveness,
LiveEntityNetworkUpdateController NetworkUpdates,
LiveEntityHydrationController Hydration,
RemoteTeleportController RemoteTeleport,
EntityEffectController EntityEffects,
AnimationHookFrameQueue AnimationHookFrames,
LiveEntityPresentationController Presentation,
RemoteMovementObservationTracker RemoteMovementObservations);
/// <summary>
/// Builds the exact per-generation route/reset graph for the canonical live
/// session. It owns no mirrored session state and retains no window callback.
/// </summary>
internal sealed class LiveSessionRuntimeFactory
{
private readonly LiveSessionPlayerRuntime _player;
private readonly LiveSessionDomainRuntime _domain;
private readonly LiveSessionUiRuntime _ui;
private readonly LiveSessionInteractionRuntime _interaction;
private readonly LiveSessionWorldRuntime _world;
private readonly Action<string> _log;
public LiveSessionRuntimeFactory(
LiveSessionPlayerRuntime player,
LiveSessionDomainRuntime domain,
LiveSessionUiRuntime ui,
LiveSessionInteractionRuntime interaction,
LiveSessionWorldRuntime world,
Action<string> log)
{
_player = player ?? throw new ArgumentNullException(nameof(player));
_domain = domain ?? throw new ArgumentNullException(nameof(domain));
_ui = ui ?? throw new ArgumentNullException(nameof(ui));
_interaction = interaction
?? throw new ArgumentNullException(nameof(interaction));
_world = world ?? throw new ArgumentNullException(nameof(world));
_log = log ?? throw new ArgumentNullException(nameof(log));
}
public LiveSessionHost Create(LiveSessionController controller)
{
ArgumentNullException.ThrowIfNull(controller);
return new LiveSessionHost(controller, new LiveSessionHostBindings(
Routing: new(
CreateEventRouter,
session => new LiveSessionCommandRouter(
CreateCommandBindings(session))),
Reset: CreateResetBindings(),
Selection: new(
SetPlayerIdentity: id => _player.Identity.ServerGuid = id,
SetVitalsIdentity: id => _ui.Vitals?.SetLocalPlayerGuid(id),
SetChatIdentity: _domain.Chat.SetLocalPlayerGuid,
MarkPersistent: _world.WorldState.MarkPersistent,
SetVanishProbeIdentity: id => EntityVanishProbe.PlayerGuid = id,
ClearCombat: _domain.Combat.Clear),
EnteredWorld: new(
SetActiveCharacter: _interaction.Settings.SetActiveCharacter,
RestoreLayout: () => _ui.RetailUi?.RestoreLayout(),
SyncToolbar: () => _ui.RetailUi?.SyncToolbarWindowButtons(),
LoadCharacterSettings: _interaction.Settings.LoadCharacterContext,
ArmPlayerModeAutoEntry: _interaction.PlayerModeAutoEntry.Arm),
Connecting: (host, port, user) =>
_domain.Chat.OnSystemMessage(
$"connecting to {host}:{port} as {user}",
chatType: 1),
Connected: () =>
_domain.Chat.OnSystemMessage(
"connected — character list received",
chatType: 1)));
}
private LiveSessionResetBindings CreateResetBindings() => new()
{
MouseCapture = _interaction.GameplayInput.ResetSession,
PlayerPresentation = ResetPlayerPresentation,
TeleportTransit = _world.Teleport.ResetSession,
SessionDialogs = () => _ui.RetailUi?.ResetSessionDialogs(),
ChatCommandTargets = () => _ui.RetailChat?.ResetSessionTargets(),
SettingsCharacterContext =
_interaction.Settings.RestoreDefaultCharacterContext,
EquippedChildren = _world.EquippedChildren.Clear,
ExternalContainer = () => _domain.ExternalContainers.Reset(),
InteractionAndSelection = _interaction.SelectionInteractions.ResetSession,
SelectionPresentation = _world.SelectionScene.Reset,
ObjectTable = _domain.Objects.Clear,
Spellbook = _domain.Spellbook.Clear,
MagicRuntime = () => _ui.Magic?.Reset(),
CombatAttack = _interaction.CombatAttack.ResetSession,
CombatState = _domain.Combat.Clear,
ItemMana = _domain.ItemMana.Clear,
LocalPlayer = _domain.LocalPlayer.Clear,
Friends = _domain.Friends.Clear,
Squelch = _domain.Squelch.Clear,
TurbineChat = _domain.TurbineChat.Reset,
ParticleVisibility = _world.ParticleVisibility.Reset,
InboundEventFifo = _world.InboundEvents.Clear,
LiveLiveness = _world.Liveness.Clear,
LiveRuntime = ResetLiveRuntime,
SessionIdentity = ResetIdentity,
RemoteTeleport = _world.RemoteTeleport.Clear,
NetworkEffects = _world.EntityEffects.ClearNetworkState,
AnimationHookFrames = _world.AnimationHookFrames.Clear,
LivePresentation = _world.Presentation.Clear,
RemoteMovementDiagnostics = _world.RemoteMovementObservations.Clear,
};
private void ResetPlayerPresentation()
{
_interaction.PlayerMode.ResetSession();
_world.SpawnClaims.Reset();
}
private void ResetLiveRuntime() =>
LiveSessionEntityRuntimeReset.ClearAndRequireConvergence(
_world.LiveEntities);
private void ResetIdentity()
{
LiveSessionEntityRuntimeReset.RequireConvergence(_world.LiveEntities);
// PlayerModule::Clear @ 0x005D48A0 clears shortcuts, desired
// components, and character option objects. CPlayerSystem::Begin
// @ 0x0055D410 resets player identity and session fields. The option
// default comes from PlayerModule::PlayerModule @ 0x005D51F0.
_player.Identity.ServerGuid = 0u;
_ui.Vitals?.SetLocalPlayerGuid(0u);
_domain.Chat.ResetSessionIdentity();
EntityVanishProbe.PlayerGuid = 0u;
_interaction.Settings.ResetActiveCharacterKey();
_player.CharacterOptions.Reset();
_player.Skills.ResetSession();
_world.NetworkUpdates.ResetSessionState();
_world.Hydration.ResetSessionState();
_player.Shortcuts.Items = Array.Empty<ShortcutEntry>();
_player.DesiredComponents.Clear();
_ui.Paperdoll?.MarkDirty();
// X/Y are ignored until the next logical player CreateObject claims a
// new origin. Keeping the last values avoids inventing a synthetic
// landblock while still forcing first-spawn initialization.
_player.WorldOrigin.Reset();
}
private LiveSessionEventRouter CreateEventRouter(WorldSession session)
{
SkillTable? skillTable = _world.Dats.Get<SkillTable>(0x0E000004u);
if (_ui.CharacterSheet is not null)
{
_ui.CharacterSheet.SkillTable = skillTable;
_ui.CharacterSheet.ExperienceTable =
CharacterSheetProvider.LoadExperienceTable(_world.Dats, _log);
}
return new LiveSessionEventRouter(
session,
_world.EntitySession.CreateSink(),
new LiveEnvironmentSessionSink(
_world.Environment.ApplyAdminEnvirons,
_world.Environment.SynchronizeFromServer),
CreateInventoryBindings(),
CreateCharacterBindings(skillTable),
new LiveSocialSessionBindings(
_domain.Chat,
_domain.TurbineChat,
_domain.Friends,
_domain.Squelch));
}
private LiveInventorySessionBindings CreateInventoryBindings() => new(
_domain.Objects,
_domain.LocalPlayer,
PlayerGuid: () => _player.Identity.ServerGuid,
OnShortcuts: list => _player.Shortcuts.Items = list,
OnUseDone: error =>
{
_domain.ExternalContainers.ApplyUseDone(error);
_interaction.ItemInteraction.CompleteUse(error);
},
_domain.ItemMana,
OnDesiredComponents: components =>
_player.DesiredComponents.Items = components,
ExternalContainers: _domain.ExternalContainers);
private LiveCharacterSessionBindings CreateCharacterBindings(
SkillTable? skillTable)
{
var skillCreditResolver = new LiveSkillCreditResolver(skillTable);
return new LiveCharacterSessionBindings(
_domain.Combat,
_domain.Spellbook,
ResolveSkillFormulaBonus: skillCreditResolver.Resolve,
OnSkillsUpdated: (runSkill, jumpSkill) =>
{
_player.Skills.Update(
runSkill,
jumpSkill,
_player.Controller.Controller);
if (_player.Skills.IsComplete
&& _player.Controller.Controller is not null)
{
_log(
$"player: applied server skills " +
$"run={_player.Skills.RunSkill} " +
$"jump={_player.Skills.JumpSkill}");
}
},
OnConfirmationRequest: request =>
_ui.RetailUi?.HandleConfirmationRequest(request),
OnConfirmationDone: done =>
_ui.RetailUi?.HandleConfirmationDone(done),
OnCharacterOptions: (options1, _) =>
_player.CharacterOptions.Options =
(Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1)
options1,
ClientTime: ClientTimerNow);
}
private LiveSessionCommandBindings CreateCommandBindings(
WorldSession session) => new(
ClientCommands: new ClientCommandController.Bindings(
TeleportToLifestone: session.SendTeleportToLifestone,
TeleportToMarketplace: session.SendTeleportToMarketplace,
TeleportToPkArena: session.SendTeleportToPkArena,
TeleportToPkLiteArena: session.SendTeleportToPkLiteArena,
TeleportToHouse: session.SendTeleportToHouse,
TeleportToMansion: session.SendTeleportToMansion,
QueryAge: session.SendQueryAge,
QueryBirth: session.SendQueryBirth,
ToggleFrameRate: _interaction.Settings.ToggleFrameRate,
ToggleUiLock: () =>
_interaction.Settings.SetUiLocked(
!_interaction.Settings.Gameplay.LockUI),
ShowSystemMessage: text => _domain.Chat.OnSystemMessage(text, 0u),
ShowWeenieError: code => _domain.Chat.OnWeenieError(code, null),
PlayerPublicWeenieBitfield: () =>
_domain.Objects.Get(_player.Identity.ServerGuid)?
.PublicWeenieBitfield,
ClientVersion: () =>
typeof(LiveSessionRuntimeFactory).Assembly
.GetName().Version?.ToString(3)
?? "unknown",
CurrentPosition: () => _player.Controller.Controller?.CellPosition,
LastOutsideCorpsePosition: () =>
_domain.LocalPlayer.GetPosition(0x0Eu),
ShowConfirmation: (message, completed) =>
_ui.RetailUi?.ShowConfirmation(message, completed),
Suicide: session.SendSuicide,
ClearChat: _ => _domain.Chat.Clear(),
SaveUi: name => _ui.RetailUi?.SaveNamedLayout(name),
LoadUi: name => _ui.RetailUi?.RestoreNamedLayout(name),
SaveAutoUi: () => _ui.RetailUi?.SaveLayout(),
LoadAutoUi: () => _ui.RetailUi?.RestoreLayout(),
IsAway: () =>
_domain.Objects.Get(_player.Identity.ServerGuid)?
.Properties.GetBool(0x6Eu) == true,
SetAway: session.SendSetAfkMode,
SetAwayMessage: session.SendSetAfkMessage,
AcceptLootPermits: () =>
_interaction.Settings.Gameplay.AcceptLootPermits,
SetAcceptLootPermits: _interaction.Settings.SetAcceptLootPermits,
DisplayConsent: session.SendDisplayConsent,
ClearConsent: session.SendClearConsent,
RemoveConsent: session.SendRemoveConsent,
SendEmote: session.SendEmote,
_domain.Friends,
AddFriend: session.SendAddFriend,
RemoveFriend: session.SendRemoveFriend,
ClearFriends: session.SendClearFriends,
RequestLegacyFriends: session.SendLegacyFriendsListRequest,
_domain.Squelch,
ModifyCharacterSquelch: session.SendModifyCharacterSquelch,
ModifyAccountSquelch: session.SendModifyAccountSquelch,
ModifyGlobalSquelch: session.SendModifyGlobalSquelch,
LastTeller: () => _ui.RetailChat?.LastIncomingTellSender,
ClearDesiredComponents: () =>
{
session.SendClearDesiredComponents();
_player.DesiredComponents.Clear();
},
HasOpenVendor: () => false,
FillComponentBuyList: (_, _) => { }),
_domain.Chat,
_domain.TurbineChat,
PlayerGuid: () => _player.Identity.ServerGuid,
SendTalk: session.SendTalk,
SendTell: session.SendTell,
SendChannel: session.SendChannel,
SendTurbineChat: (
roomId,
chatType,
dispatchType,
senderGuid,
text,
cookie) => session.SendTurbineChatTo(
roomId,
chatType,
dispatchType,
senderGuid,
text,
cookie),
Log: _log);
private static double ClientTimerNow() =>
Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
}

View file

@ -326,8 +326,9 @@ public sealed class GameWindow :
public readonly AcDream.Core.Items.ItemManaState ItemMana = new(); public readonly AcDream.Core.Items.ItemManaState ItemMana = new();
public readonly AcDream.Core.Social.FriendsState Friends = new(); public readonly AcDream.Core.Social.FriendsState Friends = new();
public readonly AcDream.Core.Social.SquelchState Squelch = new(); public readonly AcDream.Core.Social.SquelchState Squelch = new();
public IReadOnlyList<(uint Id, uint Amount)> DesiredComponents { get; private set; } private readonly DesiredComponentSnapshotState _desiredComponents = new();
= System.Array.Empty<(uint Id, uint Amount)>(); public IReadOnlyList<(uint Id, uint Amount)> DesiredComponents =>
_desiredComponents.Items;
// Retail resolves learned IDs through portal.dat's CSpellTable. MagicCatalog // Retail resolves learned IDs through portal.dat's CSpellTable. MagicCatalog
// installs that immutable table once DatCollection opens in OnLoad. // installs that immutable table once DatCollection opens in OnLoad.
public AcDream.Core.Spells.SpellTable SpellTable => SpellBook.Metadata; public AcDream.Core.Spells.SpellTable SpellTable => SpellBook.Metadata;
@ -1010,6 +1011,8 @@ public sealed class GameWindow :
|| _playerModeController is not null || _playerModeController is not null
|| _playerModeAutoEntry is not null || _playerModeAutoEntry is not null
|| _localPlayerTeleport is not null || _localPlayerTeleport is not null
|| _liveSessionHost is not null
|| _gameplayInputActions is not null
|| _sessionPlayerBindings is not null) || _sessionPlayerBindings is not null)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
@ -1032,6 +1035,8 @@ public sealed class GameWindow :
_playerModeController = result.PlayerMode; _playerModeController = result.PlayerMode;
_playerModeAutoEntry = result.PlayerModeAutoEntry; _playerModeAutoEntry = result.PlayerModeAutoEntry;
_localPlayerTeleport = result.LocalTeleport; _localPlayerTeleport = result.LocalTeleport;
_liveSessionHost = result.SessionHost;
_gameplayInputActions = result.GameplayActions;
_sessionPlayerBindings = result.RuntimeBindings; _sessionPlayerBindings = result.RuntimeBindings;
} }
@ -1287,6 +1292,11 @@ public sealed class GameWindow :
_datLock, _datLock,
_runtimeSettings, _runtimeSettings,
settingsDevTools, settingsDevTools,
_worldEnvironment,
_worldSceneDebugState,
_runtimeDiagnosticCommands,
_liveCombatModeCommands,
_hostQuiescence,
_physicsEngine, _physicsEngine,
_physicsDataCache, _physicsDataCache,
_worldGameState, _worldGameState,
@ -1314,6 +1324,9 @@ public sealed class GameWindow :
_localPlayerSkills, _localPlayerSkills,
_viewportAspect, _viewportAspect,
_playerApproachCompletions, _playerApproachCompletions,
_characterOptions,
_shortcutSnapshots,
_desiredComponents,
_pointerPosition, _pointerPosition,
_movementInput, _movementInput,
_inputCapture, _inputCapture,
@ -1325,6 +1338,14 @@ public sealed class GameWindow :
_combatAttackOperations, _combatAttackOperations,
Combat, Combat,
_combatFeedback, _combatFeedback,
Chat,
LocalPlayer,
SpellBook,
ItemMana,
Friends,
Squelch,
TurbineChat,
_externalContainers,
_portalTunnelFallback, _portalTunnelFallback,
Console.WriteLine), Console.WriteLine),
this).Compose( this).Compose(
@ -1593,84 +1614,9 @@ public sealed class GameWindow :
sessionPlayer.PlayerModeAutoEntry), sessionPlayer.PlayerModeAutoEntry),
cameraFrame); cameraFrame);
_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator); _frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);
_liveSessionHost = CreateLiveSessionHost();
AcDream.UI.Abstractions.Panels.Debug.DebugVM? debugVm = _debugVm;
Action<string>? debugToast = debugVm is null
? null
: message => debugVm.AddToast(message);
AcDream.Core.Chat.ChatLog chat = Chat;
var combatCommand =
new AcDream.App.Combat.LiveCombatModeCommandController(
new AcDream.App.Combat.LiveSessionCombatModeAuthority(
_liveSessionHost),
new AcDream.App.Combat.LocalPlayerCombatEquipmentSource(
Objects,
_localPlayerIdentity),
Combat,
new AcDream.App.Combat.ItemInteractionCombatModeIntentSink(
_itemInteractionController!),
Console.WriteLine,
debugToast,
text => chat.OnSystemMessage(text, 0x1Au));
_liveCombatModeCommands.Bind(combatCommand);
var nearbyDiagnostics =
new AcDream.App.Diagnostics.NearbyWorldDiagnosticDumper(
new AcDream.App.Diagnostics.RuntimeNearbyWorldDiagnosticSource(
_localPlayerMode,
_playerControllerSlot,
_cameraController!,
_liveWorldOrigin,
_worldState,
_physicsEngine),
Console.WriteLine);
var runtimeDiagnostics =
new AcDream.App.Diagnostics.RuntimeDiagnosticCommandController(
_worldEnvironment,
_worldSceneDebugState,
_cameraPointerInput,
nearbyDiagnostics,
debugToast);
_runtimeDiagnosticCommands.Bind(runtimeDiagnostics);
if (_inputDispatcher is { } dispatcher)
{
var pointer = _cameraPointerInput
?? throw new InvalidOperationException(
"Gameplay action routing requires the composed pointer owner.");
var commands = new AcDream.App.Input.GameplayInputCommandController(
new AcDream.App.Input.RetainedGameplayWindowCommands(
_retailUiRuntime),
new AcDream.App.Input.DevToolsGameplayCommands(
_devToolsFramePresenter),
runtimeDiagnostics,
new AcDream.App.Input.PlayerModeGameplayCommands(
_localPlayerMode,
sessionPlayer.PlayerMode),
new AcDream.App.Input.ItemTargetModeCommands(
_itemInteractionController!),
new AcDream.App.Input.GameplayCameraModeCommands(
_cameraController!),
combatCommand,
new AcDream.App.Input.GameplayWindowCommands(_window!.Close));
var targets = new AcDream.App.Input.RuntimeGameplayInputPriorityTargets(
sessionPlayer.GameplayInput,
pointer,
_retailUiRuntime,
_selectionInteractions,
commands);
_gameplayInputActions = AcDream.App.Input.GameplayInputActionRouter.Create(
dispatcher,
Combat,
targets,
_hostQuiescence,
Console.WriteLine);
_gameplayInputActions.Attach();
}
AcDream.App.Net.LiveSessionStartResult liveStart = AcDream.App.Net.LiveSessionStartResult liveStart =
_liveSessionHost.Start(_options); sessionPlayer.SessionHost.Start(_options);
switch (liveStart.Status) switch (liveStart.Status)
{ {
case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials: case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials:
@ -1683,272 +1629,6 @@ public sealed class GameWindow :
} }
} }
private AcDream.App.Net.LiveSessionHost CreateLiveSessionHost() =>
new(_liveSessionController!, new AcDream.App.Net.LiveSessionHostBindings(
Routing: new(
CreateLiveSessionEventRouter,
session => new AcDream.App.Net.LiveSessionCommandRouter(
CreateLiveSessionCommandBindings(session))),
Reset: CreateLiveSessionResetBindings(),
Selection: new(
SetPlayerIdentity: id => _playerServerGuid = id,
SetVitalsIdentity: id => _vitalsVm?.SetLocalPlayerGuid(id),
SetChatIdentity: Chat.SetLocalPlayerGuid,
MarkPersistent: _worldState.MarkPersistent,
SetVanishProbeIdentity: id =>
AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = id,
ClearCombat: Combat.Clear),
EnteredWorld: new(
SetActiveCharacter: _runtimeSettings.SetActiveCharacter,
RestoreLayout: () => _retailUiRuntime?.RestoreLayout(),
SyncToolbar: SyncToolbarWindowButtons,
LoadCharacterSettings: _runtimeSettings.LoadCharacterContext,
ArmPlayerModeAutoEntry: () => _playerModeAutoEntry?.Arm()),
Connecting: (host, port, user) =>
Chat.OnSystemMessage(
$"connecting to {host}:{port} as {user}",
chatType: 1),
Connected: () =>
Chat.OnSystemMessage(
"connected — character list received",
chatType: 1)));
private AcDream.App.Net.LiveSessionResetBindings
CreateLiveSessionResetBindings() => new()
{
MouseCapture = ResetSessionMouseCapture,
PlayerPresentation = ResetSessionPlayerPresentation,
TeleportTransit = _localPlayerTeleportSink.ResetSession,
SessionDialogs = () => _retailUiRuntime?.ResetSessionDialogs(),
ChatCommandTargets = () => _retailChatVm?.ResetSessionTargets(),
SettingsCharacterContext = _runtimeSettings.RestoreDefaultCharacterContext,
EquippedChildren = () => _equippedChildRenderer?.Clear(),
ExternalContainer = () => _externalContainers.Reset(),
InteractionAndSelection = ResetSessionInteraction,
SelectionPresentation = () => _retailSelectionScene?.Reset(),
ObjectTable = Objects.Clear,
Spellbook = SpellBook.Clear,
MagicRuntime = () => _magicRuntime?.Reset(),
CombatAttack = () => _combatAttackController?.ResetSession(),
CombatState = Combat.Clear,
ItemMana = ItemMana.Clear,
LocalPlayer = LocalPlayer.Clear,
Friends = Friends.Clear,
Squelch = Squelch.Clear,
TurbineChat = TurbineChat.Reset,
ParticleVisibility = _particleVisibility.Reset,
InboundEventFifo = _inboundEntityEvents.Clear,
LiveLiveness = () => _liveEntityLiveness?.Clear(),
LiveRuntime = ResetSessionLiveRuntime,
SessionIdentity = ResetSessionIdentity,
RemoteTeleport = () => _remoteTeleportController?.Clear(),
NetworkEffects = () => _entityEffects?.ClearNetworkState(),
AnimationHookFrames = () => _animationHookFrames?.Clear(),
LivePresentation = () => _liveEntityPresentation?.Clear(),
RemoteMovementDiagnostics = _remoteMovementObservations.Clear,
};
private void ResetSessionMouseCapture()
=> _gameplayInputFrame?.ResetSession();
private void ResetSessionPlayerPresentation()
{
_playerModeController?.ResetSession();
_spawnClaimHydration?.Reset();
}
private void ResetSessionIdentity()
{
AcDream.App.Net.LiveSessionEntityRuntimeReset.RequireConvergence(_liveEntities);
// PlayerModule::Clear @ 0x005D48A0 clears shortcuts, desired
// components, and character option objects. CPlayerSystem::Begin
// @ 0x0055D410 resets player identity and session fields. The option
// default comes from PlayerModule::PlayerModule @ 0x005D51F0.
_playerServerGuid = 0u;
_vitalsVm?.SetLocalPlayerGuid(0u);
Chat.ResetSessionIdentity();
AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = 0u;
_runtimeSettings.ResetActiveCharacterKey();
_characterOptions.Reset();
_localPlayerSkills.ResetSession();
_liveEntityNetworkUpdates?.ResetSessionState();
_liveEntityHydration?.ResetSessionState();
Shortcuts = Array.Empty<AcDream.Core.Items.ShortcutEntry>();
DesiredComponents = Array.Empty<(uint Id, uint Amount)>();
_paperdollFramePresenter?.MarkDirty();
// X/Y are ignored until the next logical player CreateObject claims a
// new origin. Keeping the last values avoids inventing a synthetic
// landblock while still forcing first-spawn initialization.
_liveWorldOrigin.Reset();
}
private void ResetSessionLiveRuntime()
{
AcDream.App.Net.LiveSessionEntityRuntimeReset.ClearAndRequireConvergence(
_liveEntities);
}
private void ResetSessionInteraction()
{
if (_selectionInteractions is { } interactions)
interactions.ResetSession();
else
{
_itemInteractionController?.ResetSession();
_selection.Reset();
}
}
private AcDream.App.Net.LiveSessionEventRouter CreateLiveSessionEventRouter(
AcDream.Core.Net.WorldSession session)
{
var skillTable = _dats?.Get<DatReaderWriter.DBObjs.SkillTable>(0x0E000004u);
if (_characterSheetProvider is not null && _dats is not null)
{
_characterSheetProvider.SkillTable = skillTable;
_characterSheetProvider.ExperienceTable =
AcDream.App.UI.Layout.CharacterSheetProvider.LoadExperienceTable(
_dats,
Console.WriteLine);
}
return new AcDream.App.Net.LiveSessionEventRouter(
session,
(_liveEntitySessionEvents
?? throw new InvalidOperationException(
"Live entity session routing was not composed."))
.CreateSink(),
new AcDream.App.Net.LiveEnvironmentSessionSink(
_worldEnvironment.ApplyAdminEnvirons,
_worldEnvironment.SynchronizeFromServer),
CreateLiveInventorySessionBindings(),
CreateLiveCharacterSessionBindings(skillTable),
CreateLiveSocialSessionBindings());
}
private AcDream.App.Net.LiveInventorySessionBindings
CreateLiveInventorySessionBindings() => new(
Objects,
LocalPlayer,
PlayerGuid: () => _playerServerGuid,
OnShortcuts: list => Shortcuts = list,
OnUseDone: error =>
{
_externalContainers.ApplyUseDone(error);
_itemInteractionController?.CompleteUse(error);
},
ItemMana,
OnDesiredComponents: components => DesiredComponents = components,
ExternalContainers: _externalContainers);
private AcDream.App.Net.LiveCharacterSessionBindings
CreateLiveCharacterSessionBindings(
DatReaderWriter.DBObjs.SkillTable? skillTable)
{
var skillCreditResolver =
new AcDream.App.Net.LiveSkillCreditResolver(skillTable);
return new(
Combat,
SpellBook,
ResolveSkillFormulaBonus: skillCreditResolver.Resolve,
OnSkillsUpdated: (runSkill, jumpSkill) =>
{
_localPlayerSkills.Update(runSkill, jumpSkill, _playerController);
if (_localPlayerSkills.IsComplete && _playerController is not null)
{
Console.WriteLine(
$"player: applied server skills run={_localPlayerSkills.RunSkill} " +
$"jump={_localPlayerSkills.JumpSkill}");
}
},
OnConfirmationRequest: request =>
_retailUiRuntime?.HandleConfirmationRequest(request),
OnConfirmationDone: done =>
_retailUiRuntime?.HandleConfirmationDone(done),
OnCharacterOptions: (options1, _) =>
_characterOptions.Options =
(AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1)
options1,
ClientTime: ClientTimerNow);
}
private AcDream.App.Net.LiveSocialSessionBindings
CreateLiveSocialSessionBindings() => new(
Chat,
TurbineChat,
Friends,
Squelch);
private AcDream.App.Net.LiveSessionCommandBindings CreateLiveSessionCommandBindings(
AcDream.Core.Net.WorldSession session) => new(
ClientCommands: new AcDream.App.UI.ClientCommandController.Bindings(
TeleportToLifestone: session.SendTeleportToLifestone,
TeleportToMarketplace: session.SendTeleportToMarketplace,
TeleportToPkArena: session.SendTeleportToPkArena,
TeleportToPkLiteArena: session.SendTeleportToPkLiteArena,
TeleportToHouse: session.SendTeleportToHouse,
TeleportToMansion: session.SendTeleportToMansion,
QueryAge: session.SendQueryAge,
QueryBirth: session.SendQueryBirth,
ToggleFrameRate: _runtimeSettings.ToggleFrameRate,
ToggleUiLock: () =>
_runtimeSettings.SetUiLocked(!_runtimeSettings.Gameplay.LockUI),
ShowSystemMessage: text => Chat.OnSystemMessage(text, 0u),
ShowWeenieError: code => Chat.OnWeenieError(code, null),
PlayerPublicWeenieBitfield: () =>
Objects.Get(_playerServerGuid)?.PublicWeenieBitfield,
ClientVersion: () =>
typeof(GameWindow).Assembly.GetName().Version?.ToString(3)
?? "unknown",
CurrentPosition: () => _playerController?.CellPosition,
LastOutsideCorpsePosition: () => LocalPlayer.GetPosition(0x0Eu),
ShowConfirmation: (message, completed) =>
_retailUiRuntime?.ShowConfirmation(message, completed),
Suicide: session.SendSuicide,
ClearChat: _ => Chat.Clear(),
SaveUi: name => _retailUiRuntime?.SaveNamedLayout(name),
LoadUi: name => _retailUiRuntime?.RestoreNamedLayout(name),
SaveAutoUi: () => _retailUiRuntime?.SaveLayout(),
LoadAutoUi: () => _retailUiRuntime?.RestoreLayout(),
IsAway: () =>
Objects.Get(_playerServerGuid)?.Properties.GetBool(0x6Eu) == true,
SetAway: away => SetRetailAway(session, away),
SetAwayMessage: session.SendSetAfkMessage,
AcceptLootPermits: () => _runtimeSettings.Gameplay.AcceptLootPermits,
SetAcceptLootPermits: _runtimeSettings.SetAcceptLootPermits,
DisplayConsent: session.SendDisplayConsent,
ClearConsent: session.SendClearConsent,
RemoveConsent: session.SendRemoveConsent,
SendEmote: session.SendEmote,
Friends,
AddFriend: session.SendAddFriend,
RemoveFriend: session.SendRemoveFriend,
ClearFriends: session.SendClearFriends,
RequestLegacyFriends: session.SendLegacyFriendsListRequest,
Squelch,
ModifyCharacterSquelch: session.SendModifyCharacterSquelch,
ModifyAccountSquelch: session.SendModifyAccountSquelch,
ModifyGlobalSquelch: session.SendModifyGlobalSquelch,
LastTeller: () => _retailChatVm?.LastIncomingTellSender,
ClearDesiredComponents: () =>
{
session.SendClearDesiredComponents();
DesiredComponents = Array.Empty<(uint Id, uint Amount)>();
},
HasOpenVendor: () => false,
FillComponentBuyList: (_, _) => { }),
Chat,
TurbineChat,
PlayerGuid: () => _playerServerGuid,
SendTalk: session.SendTalk,
SendTell: session.SendTell,
SendChannel: session.SendChannel,
SendTurbineChat: (roomId, chatType, dispatchType, senderGuid, text, cookie) =>
session.SendTurbineChatTo(
roomId, chatType, dispatchType, senderGuid, text, cookie),
Log: Console.WriteLine);
private void OnUpdate(double dt) private void OnUpdate(double dt)
{ {
using var _updStage = _frameProfiler.BeginStage( using var _updStage = _frameProfiler.BeginStage(
@ -2032,16 +1712,6 @@ public sealed class GameWindow :
// EXPECTED-DIFF: local sidestep pacing now matches how remotes have // EXPECTED-DIFF: local sidestep pacing now matches how remotes have
// always played (w6-cutover-map.md R3). // always played (w6-cutover-map.md R3).
private void SyncToolbarWindowButtons()
{
_retailUiRuntime?.SyncToolbarWindowButtons();
}
private void SetRetailAway(AcDream.Core.Net.WorldSession session, bool away)
{
session.SendSetAfkMode(away);
}
/// <summary> /// <summary>
/// L.0 Display tab: framebuffer-resize handler — update GL viewport /// L.0 Display tab: framebuffer-resize handler — update GL viewport
/// + camera aspect when the window is resized (by the user dragging /// + camera aspect when the window is resized (by the user dragging

View file

@ -122,6 +122,25 @@ public sealed class LiveCombatModeCommandControllerTests
Assert.Throws<ObjectDisposedException>(() => slot.Bind(target)); Assert.Throws<ObjectDisposedException>(() => slot.Bind(target));
} }
[Fact]
public void DeferredSlotOwnedBindingReleasesExactlyAndAllowsRebind()
{
var slot = new LiveCombatModeCommandSlot();
var first = new FakeCommand();
var second = new FakeCommand();
IDisposable firstBinding = slot.BindOwned(first);
Assert.Throws<InvalidOperationException>(() => slot.BindOwned(second));
firstBinding.Dispose();
using IDisposable secondBinding = slot.BindOwned(second);
firstBinding.Dispose();
slot.Toggle();
Assert.Equal(0, first.ToggleCount);
Assert.Equal(1, second.ToggleCount);
}
private sealed class Harness private sealed class Harness
{ {
public Harness() public Harness()

View file

@ -15,6 +15,23 @@ namespace AcDream.App.Tests.Composition;
public sealed class InteractionUiRuntimeSourcesTests public sealed class InteractionUiRuntimeSourcesTests
{ {
[Fact]
public void DesiredComponentSnapshotNormalizesNullAndClearsExactly()
{
var state = new DesiredComponentSnapshotState();
IReadOnlyList<(uint Id, uint Amount)> items =
[(0x01020304u, 7u)];
Assert.Empty(state.Items);
state.Items = items;
Assert.Same(items, state.Items);
state.Clear();
Assert.Empty(state.Items);
state.Items = null!;
Assert.Empty(state.Items);
}
[Fact] [Fact]
public void SessionAuthorityDefaultsBindUnbindRebindAndDeactivateExactly() public void SessionAuthorityDefaultsBindUnbindRebindAndDeactivateExactly()
{ {

View file

@ -31,6 +31,24 @@ public sealed class LivePresentationCompositionTests
bindings.Adopt("late", new RetryBinding("late", calls, 0))); bindings.Adopt("late", new RetryBinding("late", calls, 0)));
} }
[Fact]
public void TransferableAdoptionRollsBackExactlyAndRetriesFailure()
{
var calls = new List<string>();
var bindings = new LivePresentationRuntimeBindings();
bindings.Adopt("stable", new RetryBinding("stable", calls, 0));
IDisposable adoption = bindings.AdoptOwned(
"candidate",
new RetryBinding("candidate", calls, 1));
Assert.Throws<InvalidOperationException>(adoption.Dispose);
adoption.Dispose();
adoption.Dispose();
bindings.Dispose();
Assert.Equal(["candidate", "candidate", "stable"], calls);
}
[Fact] [Fact]
public void CanonicalRuntimeSlotUsesExactOwnerBinding() public void CanonicalRuntimeSlotUsesExactOwnerBinding()
{ {

View file

@ -116,7 +116,19 @@ public sealed class SessionPlayerCompositionTests
"new PlayerModeController(", "new PlayerModeController(",
"d.PortalTunnelFallback.Transfer(", "d.PortalTunnelFallback.Transfer(",
"d.TeleportSink.BindOwned(localTeleport)", "d.TeleportSink.BindOwned(localTeleport)",
"sessionRuntimeFactory.Create(liveSession)",
"d.CombatModeCommands.BindOwned(combatCommand)",
"d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)",
"GameplayInputActionRouter.Create(",
"gameplayActions.Attach();",
"_publication.PublishSessionPlayer(result);"); "_publication.PublishSessionPlayer(result);");
Assert.DoesNotContain("GameWindow", File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Net",
"LiveSessionRuntimeFactory.cs")), StringComparison.Ordinal);
} }
private sealed class RetryBinding( private sealed class RetryBinding(

View file

@ -108,6 +108,25 @@ public sealed class RuntimeDiagnosticCommandControllerTests
Assert.Throws<ObjectDisposedException>(() => slot.Bind(target)); Assert.Throws<ObjectDisposedException>(() => slot.Bind(target));
} }
[Fact]
public void DeferredSlotOwnedBindingReleasesExactlyAndAllowsRebind()
{
var slot = new RuntimeDiagnosticCommandSlot();
var first = new FakeCommands();
var second = new FakeCommands();
IDisposable firstBinding = slot.BindOwned(first);
Assert.Throws<InvalidOperationException>(() => slot.BindOwned(second));
firstBinding.Dispose();
using IDisposable secondBinding = slot.BindOwned(second);
firstBinding.Dispose();
slot.CycleWeather();
Assert.Equal(0, first.CallCount);
Assert.Equal(1, second.CallCount);
}
[Fact] [Fact]
public void NullToast_DoesNotSuppressDiagnosticStateChanges() public void NullToast_DoesNotSuppressDiagnosticStateChanges()
{ {

View file

@ -47,7 +47,7 @@ public sealed class GameWindowSlice8BoundaryTests
{ {
string body = MethodBody( string body = MethodBody(
"private void OnLoad()", "private void OnLoad()",
"private AcDream.App.Net.LiveSessionHost"); "private void OnUpdate(double dt)");
AssertAppearsInOrder( AssertAppearsInOrder(
body, body,
@ -64,16 +64,29 @@ public sealed class GameWindowSlice8BoundaryTests
"LivePresentationResult livePresentation =", "LivePresentationResult livePresentation =",
"new LivePresentationCompositionPhase(", "new LivePresentationCompositionPhase(",
"this).Compose(", "this).Compose(",
"new SessionPlayerCompositionPhase(",
"var renderFrameOrchestrator =", "var renderFrameOrchestrator =",
"var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(", "var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(",
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);", "_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);",
"_liveSessionHost = CreateLiveSessionHost();", "sessionPlayer.SessionHost.Start(_options)");
"_liveCombatModeCommands.Bind(combatCommand);",
"_runtimeDiagnosticCommands.Bind(runtimeDiagnostics);", string sessionPhase = File.ReadAllText(Path.Combine(
"_gameplayInputActions = AcDream.App.Input.GameplayInputActionRouter.Create(", FindRepoRoot(),
"_gameplayInputActions.Attach();", "src",
"_liveSessionHost.Start(_options)"); "AcDream.App",
Assert.Equal(1, CountOccurrences(body, "_liveSessionHost.Start(_options)")); "Composition",
"SessionPlayerComposition.cs"));
AssertAppearsInOrder(
sessionPhase,
"sessionRuntimeFactory.Create(liveSession)",
"d.CombatModeCommands.BindOwned(combatCommand)",
"d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)",
"GameplayInputActionRouter.Create(",
"gameplayActions.Attach();",
"_publication.PublishSessionPlayer(result);");
Assert.Equal(1, CountOccurrences(
body,
"sessionPlayer.SessionHost.Start(_options)"));
string phaseOne = Slice( string phaseOne = Slice(
File.ReadAllText(Path.Combine( File.ReadAllText(Path.Combine(
FindRepoRoot(), FindRepoRoot(),
@ -93,7 +106,9 @@ public sealed class GameWindowSlice8BoundaryTests
"if (firstKeyboard is not null && firstMouse is not null)", "if (firstKeyboard is not null && firstMouse is not null)",
phaseOne, phaseOne,
StringComparison.Ordinal); StringComparison.Ordinal);
int start = body.IndexOf("_liveSessionHost.Start(_options)", StringComparison.Ordinal); int start = body.IndexOf(
"sessionPlayer.SessionHost.Start(_options)",
StringComparison.Ordinal);
string postStart = body[start..]; string postStart = body[start..];
Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal); Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal);
Assert.Empty(System.Text.RegularExpressions.Regex.Matches( Assert.Empty(System.Text.RegularExpressions.Regex.Matches(
@ -113,10 +128,17 @@ public sealed class GameWindowSlice8BoundaryTests
string source = GameWindowSource(); string source = GameWindowSource();
string load = MethodBody( string load = MethodBody(
"private void OnLoad()", "private void OnLoad()",
"private AcDream.App.Net.LiveSessionHost"); "private void OnUpdate(double dt)");
string sessionFactory = MethodBody( string sessionFactorySource = File.ReadAllText(Path.Combine(
"private AcDream.App.Net.LiveSessionEventRouter CreateLiveSessionEventRouter(", FindRepoRoot(),
"private AcDream.App.Net.LiveInventorySessionBindings"); "src",
"AcDream.App",
"Net",
"LiveSessionRuntimeFactory.cs"));
string sessionFactory = Slice(
sessionFactorySource,
"private LiveSessionEventRouter CreateEventRouter(",
"private LiveInventorySessionBindings CreateInventoryBindings()");
string worldPhase = File.ReadAllText(Path.Combine( string worldPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(), FindRepoRoot(),
"src", "src",
@ -143,9 +165,9 @@ public sealed class GameWindowSlice8BoundaryTests
Assert.Contains("environment.Initialize(region);", worldPhase, StringComparison.Ordinal); Assert.Contains("environment.Initialize(region);", worldPhase, StringComparison.Ordinal);
AssertAppearsInOrder( AssertAppearsInOrder(
sessionFactory, sessionFactory,
"new AcDream.App.Net.LiveEnvironmentSessionSink(", "new LiveEnvironmentSessionSink(",
"_worldEnvironment.ApplyAdminEnvirons,", "_world.Environment.ApplyAdminEnvirons,",
"_worldEnvironment.SynchronizeFromServer)"); "_world.Environment.SynchronizeFromServer)");
Assert.DoesNotContain("_loadedSkyDesc", source, StringComparison.Ordinal); Assert.DoesNotContain("_loadedSkyDesc", source, StringComparison.Ordinal);
Assert.DoesNotContain("_loadedSkyDayIndex", source, StringComparison.Ordinal); Assert.DoesNotContain("_loadedSkyDayIndex", source, StringComparison.Ordinal);
Assert.DoesNotContain("private void RefreshSkyForCurrentDay()", source, StringComparison.Ordinal); Assert.DoesNotContain("private void RefreshSkyForCurrentDay()", source, StringComparison.Ordinal);
@ -164,10 +186,16 @@ public sealed class GameWindowSlice8BoundaryTests
"debugVm.CycleTimeOfDay = _dependencies.DiagnosticCommands.CycleTimeOfDay;", "debugVm.CycleTimeOfDay = _dependencies.DiagnosticCommands.CycleTimeOfDay;",
"debugVm.CycleWeather = _dependencies.DiagnosticCommands.CycleWeather;", "debugVm.CycleWeather = _dependencies.DiagnosticCommands.CycleWeather;",
"debugVm.ToggleCollisionWires ="); "debugVm.ToggleCollisionWires =");
string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SessionPlayerComposition.cs"));
AssertAppearsInOrder( AssertAppearsInOrder(
load, sessionPhase,
"new AcDream.App.Diagnostics.RuntimeDiagnosticCommandController(", "new RuntimeDiagnosticCommandController(",
"_runtimeDiagnosticCommands.Bind(runtimeDiagnostics);"); "d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)");
} }
[Fact] [Fact]
@ -176,7 +204,13 @@ public sealed class GameWindowSlice8BoundaryTests
string source = GameWindowSource(); string source = GameWindowSource();
string load = MethodBody( string load = MethodBody(
"private void OnLoad()", "private void OnLoad()",
"private AcDream.App.Net.LiveSessionHost"); "private void OnUpdate(double dt)");
string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SessionPlayerComposition.cs"));
string shutdown = MethodBody( string shutdown = MethodBody(
"private ResourceShutdownTransaction CreateShutdownTransaction()", "private ResourceShutdownTransaction CreateShutdownTransaction()",
"private void OnFocusChanged(bool focused)"); "private void OnFocusChanged(bool focused)");
@ -187,9 +221,11 @@ public sealed class GameWindowSlice8BoundaryTests
Assert.DoesNotContain("private void OnUiDragReleasedOutside(", source, StringComparison.Ordinal); Assert.DoesNotContain("private void OnUiDragReleasedOutside(", source, StringComparison.Ordinal);
Assert.DoesNotContain("DragReleasedOutsideUi +=", source, StringComparison.Ordinal); Assert.DoesNotContain("DragReleasedOutsideUi +=", source, StringComparison.Ordinal);
Assert.Equal(1, CountOccurrences( Assert.Equal(1, CountOccurrences(
load, sessionPhase,
"AcDream.App.Input.GameplayInputActionRouter.Create(")); "GameplayInputActionRouter.Create("));
Assert.Equal(1, CountOccurrences(load, "_gameplayInputActions.Attach();")); Assert.Equal(1, CountOccurrences(
sessionPhase,
"gameplayActions.Attach();"));
Assert.Equal(1, CountOccurrences( Assert.Equal(1, CountOccurrences(
LivePresentationSource(), LivePresentationSource(),
"RetainedUiGameplayBinding.Create(")); "RetainedUiGameplayBinding.Create("));
@ -214,7 +250,7 @@ public sealed class GameWindowSlice8BoundaryTests
{ {
string load = MethodBody( string load = MethodBody(
"private void OnLoad()", "private void OnLoad()",
"private AcDream.App.Net.LiveSessionHost"); "private void OnUpdate(double dt)");
string body = MethodBody( string body = MethodBody(
"private void OnFramebufferResize(", "private void OnFramebufferResize(",
"private void OnClosing()"); "private void OnClosing()");
@ -251,7 +287,7 @@ public sealed class GameWindowSlice8BoundaryTests
string run = MethodBody("public void Run()", "private void OnLoad()"); string run = MethodBody("public void Run()", "private void OnLoad()");
string load = MethodBody( string load = MethodBody(
"private void OnLoad()", "private void OnLoad()",
"private AcDream.App.Net.LiveSessionHost"); "private void OnUpdate(double dt)");
string shutdown = MethodBody( string shutdown = MethodBody(
"private ResourceShutdownTransaction CreateShutdownTransaction()", "private ResourceShutdownTransaction CreateShutdownTransaction()",
"private void OnFocusChanged(bool focused)"); "private void OnFocusChanged(bool focused)");
@ -295,7 +331,7 @@ public sealed class GameWindowSlice8BoundaryTests
"this).Compose(platform, hostInputCamera, contentEffectsAudio);", "this).Compose(platform, hostInputCamera, contentEffectsAudio);",
"new WorldRenderCompositionPhase(", "new WorldRenderCompositionPhase(",
"new SessionPlayerCompositionPhase(", "new SessionPlayerCompositionPhase(",
"_liveSessionHost.Start(_options)"); "sessionPlayer.SessionHost.Start(_options)");
string sessionPhase = File.ReadAllText(Path.Combine( string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(), FindRepoRoot(),
"src", "src",
@ -464,7 +500,7 @@ public sealed class GameWindowSlice8BoundaryTests
string source = GameWindowSource(); string source = GameWindowSource();
string load = MethodBody( string load = MethodBody(
"private void OnLoad()", "private void OnLoad()",
"private AcDream.App.Net.LiveSessionHost"); "private void OnUpdate(double dt)");
string shutdown = Slice( string shutdown = Slice(
source, source,
"private ResourceShutdownTransaction CreateShutdownTransaction()", "private ResourceShutdownTransaction CreateShutdownTransaction()",

View file

@ -179,9 +179,9 @@ public sealed class GameWindowLiveEntityCompositionTests
public void SessionReset_ClosesEveryStreamingReadinessOwner() public void SessionReset_ClosesEveryStreamingReadinessOwner()
{ {
string source = File.ReadAllText(Path.Combine( string source = File.ReadAllText(Path.Combine(
FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs")); FindRepoRoot(), "src", "AcDream.App", "Net", "LiveSessionRuntimeFactory.cs"));
Assert.Contains("_playerModeController?.ResetSession();", source, Assert.Contains("_interaction.PlayerMode.ResetSession();", source,
StringComparison.Ordinal); StringComparison.Ordinal);
string playerModeSource = File.ReadAllText(Path.Combine( string playerModeSource = File.ReadAllText(Path.Combine(
FindRepoRoot(), FindRepoRoot(),
@ -192,10 +192,10 @@ public sealed class GameWindowLiveEntityCompositionTests
Assert.Contains("_mode.ResetSession();", playerModeSource, Assert.Contains("_mode.ResetSession();", playerModeSource,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Contains( Assert.Contains(
"_liveEntityNetworkUpdates?.ResetSessionState();", "_world.NetworkUpdates.ResetSessionState();",
source, source,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Contains("_liveWorldOrigin.Reset();", source, StringComparison.Ordinal); Assert.Contains("_player.WorldOrigin.Reset();", source, StringComparison.Ordinal);
} }
private static string FindRepoRoot() private static string FindRepoRoot()

View file

@ -807,6 +807,12 @@ public sealed class UpdateFrameOrchestratorTests
"AcDream.App", "AcDream.App",
"Composition", "Composition",
"LivePresentationComposition.cs")); "LivePresentationComposition.cs"));
string sessionRuntime = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Net",
"LiveSessionRuntimeFactory.cs"));
Assert.DoesNotContain("PublishLocalPhysicsTimestamps", source, Assert.DoesNotContain("PublishLocalPhysicsTimestamps", source,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.DoesNotContain("LoginWorldReady", source, Assert.DoesNotContain("LoginWorldReady", source,
@ -823,7 +829,11 @@ public sealed class UpdateFrameOrchestratorTests
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Contains("d.WorldOrigin.CellLocalForSeed", livePresentation, Assert.Contains("d.WorldOrigin.CellLocalForSeed", livePresentation,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Contains("Live entity session routing was not composed.", source, Assert.DoesNotContain("CreateLiveSessionEventRouter", source,
StringComparison.Ordinal);
Assert.Contains("_world.EntitySession.CreateSink()", sessionRuntime,
StringComparison.Ordinal);
Assert.DoesNotContain("GameWindow", sessionRuntime,
StringComparison.Ordinal); StringComparison.Ordinal);
} }