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:
parent
7fa60971e2
commit
826f9ea9b5
22 changed files with 924 additions and 412 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
/// 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.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ namespace AcDream.App.Composition;
|
|||
/// </summary>
|
||||
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;
|
||||
|
||||
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<Exception>? 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;
|
||||
|
|
|
|||
|
|
@ -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<PortalTunnelPresentation> PortalTunnelFallback,
|
||||
Action<string> 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<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(
|
||||
"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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
386
src/AcDream.App/Net/LiveSessionRuntimeFactory.cs
Normal file
386
src/AcDream.App/Net/LiveSessionRuntimeFactory.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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<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 =
|
||||
_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<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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// L.0 Display tab: framebuffer-resize handler — update GL viewport
|
||||
/// + camera aspect when the window is resized (by the user dragging
|
||||
|
|
|
|||
|
|
@ -122,6 +122,25 @@ public sealed class LiveCombatModeCommandControllerTests
|
|||
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
|
||||
{
|
||||
public Harness()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -31,6 +31,24 @@ public sealed class LivePresentationCompositionTests
|
|||
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]
|
||||
public void CanonicalRuntimeSlotUsesExactOwnerBinding()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -108,6 +108,25 @@ public sealed class RuntimeDiagnosticCommandControllerTests
|
|||
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]
|
||||
public void NullToast_DoesNotSuppressDiagnosticStateChanges()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue