diff --git a/docs/ISSUES.md b/docs/ISSUES.md index c8a4dfa5..4853ad6e 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -32,9 +32,11 @@ What does NOT go here: canonical runtime/world, selection/radar, landblock publishers, and portal/sky/particle resources. I.7 now owns streaming/session/hydration/local-player/teleport startup and - every late edge explicitly. `GameWindow.OnUpdate` and `GameWindow.OnRender` - are typed orchestration handoffs and the class is 2,479 raw lines. I.8–I.9 - plus the remaining shutdown/closeout + every late edge explicitly. I.8a now owns the focused session factory, + command targets, and sole gameplay input subscriber before frame publication. + `GameWindow.OnUpdate` and `GameWindow.OnRender` are typed orchestration + handoffs and the class is 2,149 raw lines. I.8b–I.9 plus the remaining + shutdown/closeout cleanup is active in [`docs/architecture/code-structure.md`](architecture/code-structure.md). This is the behavior-preserving prerequisite before new M4 feature bodies. diff --git a/docs/architecture/code-structure.md b/docs/architecture/code-structure.md index d4e43d0d..4b46210c 100644 --- a/docs/architecture/code-structure.md +++ b/docs/architecture/code-structure.md @@ -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/ 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 -gone. `GameWindow` is 2,479 raw lines after I.7. I.8–L remain active, with -atomic frame-root publication and terminal session start next. The App gate -passes 3,420 tests / 3 intentional skips and the complete Release suite passes -7,792 tests / 5 intentional skips. The clean solution build retains only the -17 test-project warnings tracked by #228; the I.7 behavior, architecture, and +gone. I.8a moves the exact live-session reset/router graph, combat and +diagnostic command targets, and sole gameplay input subscriber into Phase 7 +before frame publication. `GameWindow` is 2,149 raw lines after I.8a. I.8b–L +remain active, with atomic frame-root publication next. The App gate passes +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. ### 4.4 Exit criteria diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index 44200d11..bdf31e09 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -104,10 +104,11 @@ controllers, selection/radar, landblock publishers, and portal/sky/particle resources have transactional phase ownership and exact-owner late bindings. I.7 adds the complete streaming/session/hydration/local-player/teleport phase, including explicit reversible ownership for every late edge. `GameWindow` is -now 2,479 raw lines. I.8–I.9, shutdown, canonical soak snapshots, and closeout -checkpoints J–L remain. The App gate passes 3,420 tests / 3 intentional skips, -the complete Release suite passes 7,792 tests / 5 skips, and all three I.7 -corrected-diff reviews are clean. +now 2,149 raw lines after I.8a, which composes the session factory, command +targets, and sole gameplay input subscriber before frame publication. I.8b–I.9, +shutdown, canonical soak snapshots, and closeout checkpoints J–L remain. The +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 root-cause fixes in separate commits; ordinary feature work resumes with M4 diff --git a/docs/plans/2026-05-12-milestones.md b/docs/plans/2026-05-12-milestones.md index 8b7cbef6..a4bb7c4d 100644 --- a/docs/plans/2026-05-12-milestones.md +++ b/docs/plans/2026-05-12-milestones.md @@ -59,11 +59,12 @@ runtime/world, presentation/effect controllers, selection/radar, landblock publishers, and portal/sky/particle resources have transactional phase ownership and exact-owner late bindings. I.7 now owns the complete streaming, session, hydration, local-player, combat, and teleport startup graph, including -named reversible late edges. `GameWindow` is 2,479 raw lines. I.8–I.9 and -checkpoints J–L remain active. Issue #232 tracks process-residency variance in -the soak without loosening its leak threshold. The App gate passes 3,420 tests -/ 3 skips, the complete Release suite passes 7,792 tests / 5 skips, and all -three I.7 corrected-diff reviews are clean. +named reversible late edges. I.8a adds the focused live-session runtime factory +and attaches command/input owners before frame publication. `GameWindow` is +2,149 raw lines. I.8b–I.9 and checkpoints J–L remain active. Issue #232 tracks +process-residency variance in the soak without loosening its leak threshold. +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: #153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus diff --git a/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md b/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md index 9da5b2ff..8e567ba5 100644 --- a/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md +++ b/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md @@ -293,6 +293,10 @@ ledger only after the complete pipeline is live. frame-root publication and terminal session start is the active cut. Its 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). + 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. ## 6. Automated acceptance diff --git a/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md b/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md index b7aebf8d..bf225c3d 100644 --- a/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md +++ b/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md @@ -1,6 +1,6 @@ # 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` @@ -27,6 +27,15 @@ unchanged, so no divergence-register row is added. ## 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 Add `LiveSessionRuntimeFactory` under `AcDream.App/Net`. It owns the existing diff --git a/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md b/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md index 0ff209a9..5f919468 100644 --- a/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md +++ b/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md @@ -29,8 +29,8 @@ audit. - [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots explicit single ownership and transfer seams. - [ ] I — group `OnLoad` into small ordered, fakeable composition phases with - transactional partial-acquisition rollback. I.1–I.7 are complete; I.8 - frame-root publication and terminal session start is active. Detailed plan: + transactional partial-acquisition rollback. I.1–I.7 and I.8a are complete; + 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). - [ ] J — move the exact retryable shutdown manifest to a focused lifetime owner and prove all partial-load/reentrant/retry paths. diff --git a/src/AcDream.App/Combat/LiveCombatModeCommandController.cs b/src/AcDream.App/Combat/LiveCombatModeCommandController.cs index c84cf5ab..4ba48660 100644 --- a/src/AcDream.App/Combat/LiveCombatModeCommandController.cs +++ b/src/AcDream.App/Combat/LiveCombatModeCommandController.cs @@ -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() { lock (_gate) @@ -126,6 +144,23 @@ internal sealed class LiveCombatModeCommandSlot : ILiveCombatModeCommand _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); + } } /// diff --git a/src/AcDream.App/Composition/InteractionUiRuntimeSources.cs b/src/AcDream.App/Composition/InteractionUiRuntimeSources.cs index add7a020..0c3d158b 100644 --- a/src/AcDream.App/Composition/InteractionUiRuntimeSources.cs +++ b/src/AcDream.App/Composition/InteractionUiRuntimeSources.cs @@ -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)>(); +} + /// /// 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. diff --git a/src/AcDream.App/Composition/LivePresentationRuntimeBindings.cs b/src/AcDream.App/Composition/LivePresentationRuntimeBindings.cs index 764106e0..c26f61db 100644 --- a/src/AcDream.App/Composition/LivePresentationRuntimeBindings.cs +++ b/src/AcDream.App/Composition/LivePresentationRuntimeBindings.cs @@ -10,7 +10,9 @@ namespace AcDream.App.Composition; /// internal sealed class LivePresentationRuntimeBindings : IDisposable { - private readonly List<(string Name, IDisposable Binding)> _bindings = []; + private sealed record Entry(string Name, IDisposable Binding); + + private readonly List _bindings = []; private bool _deactivationStarted; public void Adopt(string name, IDisposable binding) @@ -18,7 +20,17 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentNullException.ThrowIfNull(binding); 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) => @@ -56,16 +68,16 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable List? failures = null; for (int i = _bindings.Count - 1; i >= 0; i--) { - (string name, IDisposable binding) = _bindings[i]; + Entry entry = _bindings[i]; try { - binding.Dispose(); + entry.Binding.Dispose(); _bindings.RemoveAt(i); } catch (Exception failure) { (failures ??= []).Add(new InvalidOperationException( - $"Live-presentation binding '{name}' did not detach.", + $"Live-presentation binding '{entry.Name}' did not detach.", 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 Action? _release; diff --git a/src/AcDream.App/Composition/SessionPlayerComposition.cs b/src/AcDream.App/Composition/SessionPlayerComposition.cs index f867a6df..341dd6c9 100644 --- a/src/AcDream.App/Composition/SessionPlayerComposition.cs +++ b/src/AcDream.App/Composition/SessionPlayerComposition.cs @@ -1,6 +1,7 @@ using AcDream.App.Combat; using AcDream.App.Input; using AcDream.App.Interaction; +using AcDream.App.Diagnostics; using AcDream.App.Net; using AcDream.App.Physics; using AcDream.App.Rendering; @@ -12,11 +13,15 @@ using AcDream.App.Update; using AcDream.App.World; using AcDream.Core.Combat; using AcDream.Core.Items; +using AcDream.Core.Chat; using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; using AcDream.Core.Plugins; using AcDream.Core.Rendering; using AcDream.Core.Selection; +using AcDream.Core.Player; +using AcDream.Core.Social; +using AcDream.Core.Spells; using AcDream.Core.World; using Silk.NET.Windowing; @@ -28,6 +33,11 @@ internal sealed record SessionPlayerDependencies( object DatLock, RuntimeSettingsController Settings, SettingsDevToolsResult SettingsDevTools, + WorldEnvironmentController WorldEnvironment, + WorldSceneDebugState WorldSceneDebugState, + RuntimeDiagnosticCommandSlot RuntimeDiagnosticCommands, + LiveCombatModeCommandSlot CombatModeCommands, + HostQuiescenceGate HostQuiescence, PhysicsEngine PhysicsEngine, PhysicsDataCache PhysicsDataCache, WorldGameState WorldGameState, @@ -55,6 +65,9 @@ internal sealed record SessionPlayerDependencies( LocalPlayerSkillState PlayerSkills, ViewportAspectState ViewportAspect, PlayerApproachCompletionState PlayerApproachCompletions, + PlayerCharacterOptionsState CharacterOptions, + ShortcutSnapshotState Shortcuts, + DesiredComponentSnapshotState DesiredComponents, PointerPositionState PointerPosition, DispatcherMovementInputSource MovementInput, IInputCaptureSource InputCapture, @@ -66,6 +79,14 @@ internal sealed record SessionPlayerDependencies( CombatAttackOperationsSlot CombatAttackOperations, CombatState Combat, CombatFeedbackSlot CombatFeedback, + ChatLog Chat, + LocalPlayerState LocalPlayer, + Spellbook Spellbook, + ItemManaState ItemMana, + FriendsState Friends, + SquelchState Squelch, + TurbineChatState TurbineChat, + ExternalContainerState ExternalContainers, TransferableResourceSlot PortalTunnelFallback, Action Log); @@ -91,6 +112,8 @@ internal sealed record SessionPlayerResult( PlayerModeController PlayerMode, PlayerModeAutoEntry PlayerModeAutoEntry, LocalPlayerTeleportController LocalTeleport, + LiveSessionHost SessionHost, + GameplayInputActionRouter? GameplayActions, SessionPlayerRuntimeBindings RuntimeBindings); internal interface IGameWindowSessionPlayerPublication @@ -115,6 +138,9 @@ internal enum SessionPlayerCompositionPoint PlayerModeBound, PortalTransferred, TeleportBound, + SessionHostCreated, + CommandsBound, + GameplayActionsAttached, ResultPublished, } @@ -694,9 +720,10 @@ internal sealed class SessionPlayerCompositionPhase IDisposable componentLifecycleBinding = live.ComponentLifecycle.BindOwned(teardown); + IDisposable componentLifecycleAdoption; try { - live.RuntimeBindings.Adopt( + componentLifecycleAdoption = live.RuntimeBindings.AdoptOwned( "live runtime component teardown", componentLifecycleBinding); } @@ -705,6 +732,153 @@ internal sealed class SessionPlayerCompositionPhase componentLifecycleBinding.Dispose(); 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? 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( "session/player runtime bindings", @@ -733,12 +907,16 @@ internal sealed class SessionPlayerCompositionPhase playerMode, playerModeAutoEntry, teleportLease.Resource, + sessionHost, + gameplayActionsLease?.Resource, bindings); _publication.PublishSessionPlayer(result); streamerLease.Transfer(); liveSessionLease.Transfer(); teleportLease.Transfer(); + gameplayActionsLease?.Transfer(); + componentLifecycleLease.Transfer(); bindingsLease.Transfer(); Fault(SessionPlayerCompositionPoint.ResultPublished); return result; diff --git a/src/AcDream.App/Diagnostics/RuntimeDiagnosticCommandController.cs b/src/AcDream.App/Diagnostics/RuntimeDiagnosticCommandController.cs index b5d1926f..56b941f4 100644 --- a/src/AcDream.App/Diagnostics/RuntimeDiagnosticCommandController.cs +++ b/src/AcDream.App/Diagnostics/RuntimeDiagnosticCommandController.cs @@ -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() { lock (_gate) @@ -103,6 +121,23 @@ internal sealed class RuntimeDiagnosticCommandSlot : IRuntimeDiagnosticCommands _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 diff --git a/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs b/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs new file mode 100644 index 00000000..025adc72 --- /dev/null +++ b/src/AcDream.App/Net/LiveSessionRuntimeFactory.cs @@ -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); + +/// +/// Builds the exact per-generation route/reset graph for the canonical live +/// session. It owns no mirrored session state and retains no window callback. +/// +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 _log; + + public LiveSessionRuntimeFactory( + LiveSessionPlayerRuntime player, + LiveSessionDomainRuntime domain, + LiveSessionUiRuntime ui, + LiveSessionInteractionRuntime interaction, + LiveSessionWorldRuntime world, + Action 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(); + _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(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; +} diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 9c5177af..e5b35827 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -326,8 +326,9 @@ public sealed class GameWindow : public readonly AcDream.Core.Items.ItemManaState ItemMana = new(); public readonly AcDream.Core.Social.FriendsState Friends = new(); public readonly AcDream.Core.Social.SquelchState Squelch = new(); - public IReadOnlyList<(uint Id, uint Amount)> DesiredComponents { get; private set; } - = System.Array.Empty<(uint Id, uint Amount)>(); + private readonly DesiredComponentSnapshotState _desiredComponents = new(); + public IReadOnlyList<(uint Id, uint Amount)> DesiredComponents => + _desiredComponents.Items; // Retail resolves learned IDs through portal.dat's CSpellTable. MagicCatalog // installs that immutable table once DatCollection opens in OnLoad. public AcDream.Core.Spells.SpellTable SpellTable => SpellBook.Metadata; @@ -1010,6 +1011,8 @@ public sealed class GameWindow : || _playerModeController is not null || _playerModeAutoEntry is not null || _localPlayerTeleport is not null + || _liveSessionHost is not null + || _gameplayInputActions is not null || _sessionPlayerBindings is not null) { throw new InvalidOperationException( @@ -1032,6 +1035,8 @@ public sealed class GameWindow : _playerModeController = result.PlayerMode; _playerModeAutoEntry = result.PlayerModeAutoEntry; _localPlayerTeleport = result.LocalTeleport; + _liveSessionHost = result.SessionHost; + _gameplayInputActions = result.GameplayActions; _sessionPlayerBindings = result.RuntimeBindings; } @@ -1287,6 +1292,11 @@ public sealed class GameWindow : _datLock, _runtimeSettings, settingsDevTools, + _worldEnvironment, + _worldSceneDebugState, + _runtimeDiagnosticCommands, + _liveCombatModeCommands, + _hostQuiescence, _physicsEngine, _physicsDataCache, _worldGameState, @@ -1314,6 +1324,9 @@ public sealed class GameWindow : _localPlayerSkills, _viewportAspect, _playerApproachCompletions, + _characterOptions, + _shortcutSnapshots, + _desiredComponents, _pointerPosition, _movementInput, _inputCapture, @@ -1325,6 +1338,14 @@ public sealed class GameWindow : _combatAttackOperations, Combat, _combatFeedback, + Chat, + LocalPlayer, + SpellBook, + ItemMana, + Friends, + Squelch, + TurbineChat, + _externalContainers, _portalTunnelFallback, Console.WriteLine), this).Compose( @@ -1593,84 +1614,9 @@ public sealed class GameWindow : sessionPlayer.PlayerModeAutoEntry), cameraFrame); _frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator); - _liveSessionHost = CreateLiveSessionHost(); - - AcDream.UI.Abstractions.Panels.Debug.DebugVM? debugVm = _debugVm; - Action? 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 = - _liveSessionHost.Start(_options); + sessionPlayer.SessionHost.Start(_options); switch (liveStart.Status) { 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(); - 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(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) { using var _updStage = _frameProfiler.BeginStage( @@ -2032,16 +1712,6 @@ public sealed class GameWindow : // EXPECTED-DIFF: local sidestep pacing now matches how remotes have // 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); - } - /// /// L.0 Display tab: framebuffer-resize handler — update GL viewport /// + camera aspect when the window is resized (by the user dragging diff --git a/tests/AcDream.App.Tests/Combat/LiveCombatModeCommandControllerTests.cs b/tests/AcDream.App.Tests/Combat/LiveCombatModeCommandControllerTests.cs index 39136863..178bdd9a 100644 --- a/tests/AcDream.App.Tests/Combat/LiveCombatModeCommandControllerTests.cs +++ b/tests/AcDream.App.Tests/Combat/LiveCombatModeCommandControllerTests.cs @@ -122,6 +122,25 @@ public sealed class LiveCombatModeCommandControllerTests Assert.Throws(() => 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(() => 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 { public Harness() diff --git a/tests/AcDream.App.Tests/Composition/InteractionUiRuntimeSourcesTests.cs b/tests/AcDream.App.Tests/Composition/InteractionUiRuntimeSourcesTests.cs index d22ad561..9bc42169 100644 --- a/tests/AcDream.App.Tests/Composition/InteractionUiRuntimeSourcesTests.cs +++ b/tests/AcDream.App.Tests/Composition/InteractionUiRuntimeSourcesTests.cs @@ -15,6 +15,23 @@ namespace AcDream.App.Tests.Composition; 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] public void SessionAuthorityDefaultsBindUnbindRebindAndDeactivateExactly() { diff --git a/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs b/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs index 5e76a757..974e7463 100644 --- a/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs @@ -31,6 +31,24 @@ public sealed class LivePresentationCompositionTests bindings.Adopt("late", new RetryBinding("late", calls, 0))); } + [Fact] + public void TransferableAdoptionRollsBackExactlyAndRetriesFailure() + { + var calls = new List(); + var bindings = new LivePresentationRuntimeBindings(); + bindings.Adopt("stable", new RetryBinding("stable", calls, 0)); + IDisposable adoption = bindings.AdoptOwned( + "candidate", + new RetryBinding("candidate", calls, 1)); + + Assert.Throws(adoption.Dispose); + adoption.Dispose(); + adoption.Dispose(); + bindings.Dispose(); + + Assert.Equal(["candidate", "candidate", "stable"], calls); + } + [Fact] public void CanonicalRuntimeSlotUsesExactOwnerBinding() { diff --git a/tests/AcDream.App.Tests/Composition/SessionPlayerCompositionTests.cs b/tests/AcDream.App.Tests/Composition/SessionPlayerCompositionTests.cs index 4703d7cc..5d8bc9fe 100644 --- a/tests/AcDream.App.Tests/Composition/SessionPlayerCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/SessionPlayerCompositionTests.cs @@ -116,7 +116,19 @@ public sealed class SessionPlayerCompositionTests "new PlayerModeController(", "d.PortalTunnelFallback.Transfer(", "d.TeleportSink.BindOwned(localTeleport)", + "sessionRuntimeFactory.Create(liveSession)", + "d.CombatModeCommands.BindOwned(combatCommand)", + "d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)", + "GameplayInputActionRouter.Create(", + "gameplayActions.Attach();", "_publication.PublishSessionPlayer(result);"); + + Assert.DoesNotContain("GameWindow", File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Net", + "LiveSessionRuntimeFactory.cs")), StringComparison.Ordinal); } private sealed class RetryBinding( diff --git a/tests/AcDream.App.Tests/Diagnostics/RuntimeDiagnosticCommandControllerTests.cs b/tests/AcDream.App.Tests/Diagnostics/RuntimeDiagnosticCommandControllerTests.cs index 6c10f259..f675e850 100644 --- a/tests/AcDream.App.Tests/Diagnostics/RuntimeDiagnosticCommandControllerTests.cs +++ b/tests/AcDream.App.Tests/Diagnostics/RuntimeDiagnosticCommandControllerTests.cs @@ -108,6 +108,25 @@ public sealed class RuntimeDiagnosticCommandControllerTests Assert.Throws(() => 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(() => 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] public void NullToast_DoesNotSuppressDiagnosticStateChanges() { diff --git a/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs index 953a37d4..3f081ef2 100644 --- a/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs +++ b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs @@ -47,7 +47,7 @@ public sealed class GameWindowSlice8BoundaryTests { string body = MethodBody( "private void OnLoad()", - "private AcDream.App.Net.LiveSessionHost"); + "private void OnUpdate(double dt)"); AssertAppearsInOrder( body, @@ -64,16 +64,29 @@ public sealed class GameWindowSlice8BoundaryTests "LivePresentationResult livePresentation =", "new LivePresentationCompositionPhase(", "this).Compose(", + "new SessionPlayerCompositionPhase(", "var renderFrameOrchestrator =", "var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(", "_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);", - "_liveSessionHost = CreateLiveSessionHost();", - "_liveCombatModeCommands.Bind(combatCommand);", - "_runtimeDiagnosticCommands.Bind(runtimeDiagnostics);", - "_gameplayInputActions = AcDream.App.Input.GameplayInputActionRouter.Create(", - "_gameplayInputActions.Attach();", - "_liveSessionHost.Start(_options)"); - Assert.Equal(1, CountOccurrences(body, "_liveSessionHost.Start(_options)")); + "sessionPlayer.SessionHost.Start(_options)"); + + string sessionPhase = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "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( File.ReadAllText(Path.Combine( FindRepoRoot(), @@ -93,7 +106,9 @@ public sealed class GameWindowSlice8BoundaryTests "if (firstKeyboard is not null && firstMouse is not null)", phaseOne, 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..]; Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal); Assert.Empty(System.Text.RegularExpressions.Regex.Matches( @@ -113,10 +128,17 @@ public sealed class GameWindowSlice8BoundaryTests string source = GameWindowSource(); string load = MethodBody( "private void OnLoad()", - "private AcDream.App.Net.LiveSessionHost"); - string sessionFactory = MethodBody( - "private AcDream.App.Net.LiveSessionEventRouter CreateLiveSessionEventRouter(", - "private AcDream.App.Net.LiveInventorySessionBindings"); + "private void OnUpdate(double dt)"); + string sessionFactorySource = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Net", + "LiveSessionRuntimeFactory.cs")); + string sessionFactory = Slice( + sessionFactorySource, + "private LiveSessionEventRouter CreateEventRouter(", + "private LiveInventorySessionBindings CreateInventoryBindings()"); string worldPhase = File.ReadAllText(Path.Combine( FindRepoRoot(), "src", @@ -143,9 +165,9 @@ public sealed class GameWindowSlice8BoundaryTests Assert.Contains("environment.Initialize(region);", worldPhase, StringComparison.Ordinal); AssertAppearsInOrder( sessionFactory, - "new AcDream.App.Net.LiveEnvironmentSessionSink(", - "_worldEnvironment.ApplyAdminEnvirons,", - "_worldEnvironment.SynchronizeFromServer)"); + "new LiveEnvironmentSessionSink(", + "_world.Environment.ApplyAdminEnvirons,", + "_world.Environment.SynchronizeFromServer)"); Assert.DoesNotContain("_loadedSkyDesc", source, StringComparison.Ordinal); Assert.DoesNotContain("_loadedSkyDayIndex", 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.CycleWeather = _dependencies.DiagnosticCommands.CycleWeather;", "debugVm.ToggleCollisionWires ="); + string sessionPhase = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Composition", + "SessionPlayerComposition.cs")); AssertAppearsInOrder( - load, - "new AcDream.App.Diagnostics.RuntimeDiagnosticCommandController(", - "_runtimeDiagnosticCommands.Bind(runtimeDiagnostics);"); + sessionPhase, + "new RuntimeDiagnosticCommandController(", + "d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)"); } [Fact] @@ -176,7 +204,13 @@ public sealed class GameWindowSlice8BoundaryTests string source = GameWindowSource(); string load = MethodBody( "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( "private ResourceShutdownTransaction CreateShutdownTransaction()", "private void OnFocusChanged(bool focused)"); @@ -187,9 +221,11 @@ public sealed class GameWindowSlice8BoundaryTests Assert.DoesNotContain("private void OnUiDragReleasedOutside(", source, StringComparison.Ordinal); Assert.DoesNotContain("DragReleasedOutsideUi +=", source, StringComparison.Ordinal); Assert.Equal(1, CountOccurrences( - load, - "AcDream.App.Input.GameplayInputActionRouter.Create(")); - Assert.Equal(1, CountOccurrences(load, "_gameplayInputActions.Attach();")); + sessionPhase, + "GameplayInputActionRouter.Create(")); + Assert.Equal(1, CountOccurrences( + sessionPhase, + "gameplayActions.Attach();")); Assert.Equal(1, CountOccurrences( LivePresentationSource(), "RetainedUiGameplayBinding.Create(")); @@ -214,7 +250,7 @@ public sealed class GameWindowSlice8BoundaryTests { string load = MethodBody( "private void OnLoad()", - "private AcDream.App.Net.LiveSessionHost"); + "private void OnUpdate(double dt)"); string body = MethodBody( "private void OnFramebufferResize(", "private void OnClosing()"); @@ -251,7 +287,7 @@ public sealed class GameWindowSlice8BoundaryTests string run = MethodBody("public void Run()", "private void OnLoad()"); string load = MethodBody( "private void OnLoad()", - "private AcDream.App.Net.LiveSessionHost"); + "private void OnUpdate(double dt)"); string shutdown = MethodBody( "private ResourceShutdownTransaction CreateShutdownTransaction()", "private void OnFocusChanged(bool focused)"); @@ -295,7 +331,7 @@ public sealed class GameWindowSlice8BoundaryTests "this).Compose(platform, hostInputCamera, contentEffectsAudio);", "new WorldRenderCompositionPhase(", "new SessionPlayerCompositionPhase(", - "_liveSessionHost.Start(_options)"); + "sessionPlayer.SessionHost.Start(_options)"); string sessionPhase = File.ReadAllText(Path.Combine( FindRepoRoot(), "src", @@ -464,7 +500,7 @@ public sealed class GameWindowSlice8BoundaryTests string source = GameWindowSource(); string load = MethodBody( "private void OnLoad()", - "private AcDream.App.Net.LiveSessionHost"); + "private void OnUpdate(double dt)"); string shutdown = Slice( source, "private ResourceShutdownTransaction CreateShutdownTransaction()", diff --git a/tests/AcDream.App.Tests/World/GameWindowLiveEntityCompositionTests.cs b/tests/AcDream.App.Tests/World/GameWindowLiveEntityCompositionTests.cs index fb1a1c7d..663e87c8 100644 --- a/tests/AcDream.App.Tests/World/GameWindowLiveEntityCompositionTests.cs +++ b/tests/AcDream.App.Tests/World/GameWindowLiveEntityCompositionTests.cs @@ -179,9 +179,9 @@ public sealed class GameWindowLiveEntityCompositionTests public void SessionReset_ClosesEveryStreamingReadinessOwner() { 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); string playerModeSource = File.ReadAllText(Path.Combine( FindRepoRoot(), @@ -192,10 +192,10 @@ public sealed class GameWindowLiveEntityCompositionTests Assert.Contains("_mode.ResetSession();", playerModeSource, StringComparison.Ordinal); Assert.Contains( - "_liveEntityNetworkUpdates?.ResetSessionState();", + "_world.NetworkUpdates.ResetSessionState();", source, StringComparison.Ordinal); - Assert.Contains("_liveWorldOrigin.Reset();", source, StringComparison.Ordinal); + Assert.Contains("_player.WorldOrigin.Reset();", source, StringComparison.Ordinal); } private static string FindRepoRoot() diff --git a/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs b/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs index 542a8f41..66a732a9 100644 --- a/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs +++ b/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs @@ -807,6 +807,12 @@ public sealed class UpdateFrameOrchestratorTests "AcDream.App", "Composition", "LivePresentationComposition.cs")); + string sessionRuntime = File.ReadAllText(Path.Combine( + FindRepoRoot(), + "src", + "AcDream.App", + "Net", + "LiveSessionRuntimeFactory.cs")); Assert.DoesNotContain("PublishLocalPhysicsTimestamps", source, StringComparison.Ordinal); Assert.DoesNotContain("LoginWorldReady", source, @@ -823,7 +829,11 @@ public sealed class UpdateFrameOrchestratorTests StringComparison.Ordinal); Assert.Contains("d.WorldOrigin.CellLocalForSeed", livePresentation, 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); }