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
|
canonical runtime/world, selection/radar, landblock publishers, and
|
||||||
portal/sky/particle resources.
|
portal/sky/particle resources.
|
||||||
I.7 now owns streaming/session/hydration/local-player/teleport startup and
|
I.7 now owns streaming/session/hydration/local-player/teleport startup and
|
||||||
every late edge explicitly. `GameWindow.OnUpdate` and `GameWindow.OnRender`
|
every late edge explicitly. I.8a now owns the focused session factory,
|
||||||
are typed orchestration handoffs and the class is 2,479 raw lines. I.8–I.9
|
command targets, and sole gameplay input subscriber before frame publication.
|
||||||
plus the remaining shutdown/closeout
|
`GameWindow.OnUpdate` and `GameWindow.OnRender` are typed orchestration
|
||||||
|
handoffs and the class is 2,149 raw lines. I.8b–I.9 plus the remaining
|
||||||
|
shutdown/closeout
|
||||||
cleanup is active in
|
cleanup is active in
|
||||||
[`docs/architecture/code-structure.md`](architecture/code-structure.md).
|
[`docs/architecture/code-structure.md`](architecture/code-structure.md).
|
||||||
This is the behavior-preserving prerequisite before new M4 feature bodies.
|
This is the behavior-preserving prerequisite before new M4 feature bodies.
|
||||||
|
|
|
||||||
|
|
@ -734,11 +734,13 @@ owners without callbacks to the window. The old item-use and selection wrapper
|
||||||
methods are deleted. I.7 now owns the complete streaming/session/hydration/
|
methods are deleted. I.7 now owns the complete streaming/session/hydration/
|
||||||
local-player/teleport construction phase and every late edge through named
|
local-player/teleport construction phase and every late edge through named
|
||||||
exact-owner tokens; the former 432-line inline body and spawn-claim memo are
|
exact-owner tokens; the former 432-line inline body and spawn-claim memo are
|
||||||
gone. `GameWindow` is 2,479 raw lines after I.7. I.8–L remain active, with
|
gone. I.8a moves the exact live-session reset/router graph, combat and
|
||||||
atomic frame-root publication and terminal session start next. The App gate
|
diagnostic command targets, and sole gameplay input subscriber into Phase 7
|
||||||
passes 3,420 tests / 3 intentional skips and the complete Release suite passes
|
before frame publication. `GameWindow` is 2,149 raw lines after I.8a. I.8b–L
|
||||||
7,792 tests / 5 intentional skips. The clean solution build retains only the
|
remain active, with atomic frame-root publication next. The App gate passes
|
||||||
17 test-project warnings tracked by #228; the I.7 behavior, architecture, and
|
3,424 tests / 3 intentional skips and the complete Release suite passes 7,796
|
||||||
|
tests / 5 intentional skips. The clean solution build retains only the 17
|
||||||
|
test-project warnings tracked by #228; the I.8a behavior, architecture, and
|
||||||
adversarial reviews are clean.
|
adversarial reviews are clean.
|
||||||
|
|
||||||
### 4.4 Exit criteria
|
### 4.4 Exit criteria
|
||||||
|
|
|
||||||
|
|
@ -104,10 +104,11 @@ controllers, selection/radar, landblock publishers, and portal/sky/particle
|
||||||
resources have transactional phase ownership and exact-owner late bindings.
|
resources have transactional phase ownership and exact-owner late bindings.
|
||||||
I.7 adds the complete streaming/session/hydration/local-player/teleport phase,
|
I.7 adds the complete streaming/session/hydration/local-player/teleport phase,
|
||||||
including explicit reversible ownership for every late edge. `GameWindow` is
|
including explicit reversible ownership for every late edge. `GameWindow` is
|
||||||
now 2,479 raw lines. I.8–I.9, shutdown, canonical soak snapshots, and closeout
|
now 2,149 raw lines after I.8a, which composes the session factory, command
|
||||||
checkpoints J–L remain. The App gate passes 3,420 tests / 3 intentional skips,
|
targets, and sole gameplay input subscriber before frame publication. I.8b–I.9,
|
||||||
the complete Release suite passes 7,792 tests / 5 skips, and all three I.7
|
shutdown, canonical soak snapshots, and closeout checkpoints J–L remain. The
|
||||||
corrected-diff reviews are clean.
|
App gate passes 3,424 tests / 3 intentional skips, the complete Release suite
|
||||||
|
passes 7,796 tests / 5 skips, and the I.8a corrected-diff review is clean.
|
||||||
|
|
||||||
This is a behavior-preserving structural program. Severe regressions still get
|
This is a behavior-preserving structural program. Severe regressions still get
|
||||||
root-cause fixes in separate commits; ordinary feature work resumes with M4
|
root-cause fixes in separate commits; ordinary feature work resumes with M4
|
||||||
|
|
|
||||||
|
|
@ -59,11 +59,12 @@ runtime/world, presentation/effect controllers, selection/radar, landblock
|
||||||
publishers, and portal/sky/particle resources have transactional phase
|
publishers, and portal/sky/particle resources have transactional phase
|
||||||
ownership and exact-owner late bindings. I.7 now owns the complete streaming,
|
ownership and exact-owner late bindings. I.7 now owns the complete streaming,
|
||||||
session, hydration, local-player, combat, and teleport startup graph, including
|
session, hydration, local-player, combat, and teleport startup graph, including
|
||||||
named reversible late edges. `GameWindow` is 2,479 raw lines. I.8–I.9 and
|
named reversible late edges. I.8a adds the focused live-session runtime factory
|
||||||
checkpoints J–L remain active. Issue #232 tracks process-residency variance in
|
and attaches command/input owners before frame publication. `GameWindow` is
|
||||||
the soak without loosening its leak threshold. The App gate passes 3,420 tests
|
2,149 raw lines. I.8b–I.9 and checkpoints J–L remain active. Issue #232 tracks
|
||||||
/ 3 skips, the complete Release suite passes 7,792 tests / 5 skips, and all
|
process-residency variance in the soak without loosening its leak threshold.
|
||||||
three I.7 corrected-diff reviews are clean.
|
The App gate passes 3,424 tests / 3 skips, the complete Release suite passes
|
||||||
|
7,796 tests / 5 skips, and the I.8a corrected-diff review is clean.
|
||||||
|
|
||||||
Carried:
|
Carried:
|
||||||
#153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus
|
#153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,10 @@ ledger only after the complete pipeline is live.
|
||||||
frame-root publication and terminal session start is the active cut. Its
|
frame-root publication and terminal session start is the active cut. Its
|
||||||
detailed session-binding, atomic-root, and terminal-start plan is
|
detailed session-binding, atomic-root, and terminal-start plan is
|
||||||
[`2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md`](2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md).
|
[`2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md`](2026-07-22-gamewindow-slice-8-checkpoint-i8-frame-roots-session-start.md).
|
||||||
|
I.8a is complete: the session factory, command targets, and sole gameplay
|
||||||
|
input subscriber now belong to Phase 7 and publish before any frame root;
|
||||||
|
`GameWindow.cs` is 2,149 raw lines. I.8b atomic frame-root composition is the
|
||||||
|
active cut.
|
||||||
Checkpoint I remains active until I.9 closes the complete nine-phase pipeline.
|
Checkpoint I remains active until I.9 closes the complete nine-phase pipeline.
|
||||||
|
|
||||||
## 6. Automated acceptance
|
## 6. Automated acceptance
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# GameWindow Slice 8 — Checkpoint I.8 Frame Roots and Session Start
|
# GameWindow Slice 8 — Checkpoint I.8 Frame Roots and Session Start
|
||||||
|
|
||||||
**Status:** Active
|
**Status:** Active — I.8a complete; I.8b frame-root composition next
|
||||||
|
|
||||||
**Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`
|
**Parent:** `2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`
|
||||||
|
|
||||||
|
|
@ -27,6 +27,15 @@ unchanged, so no divergence-register row is added.
|
||||||
|
|
||||||
## 2. I.8a — complete session/player composition
|
## 2. I.8a — complete session/player composition
|
||||||
|
|
||||||
|
**Completed 2026-07-22.** `LiveSessionRuntimeFactory` now owns the exact
|
||||||
|
reset/router/command graph without a window reference. Phase 7 publishes the
|
||||||
|
session host, exact-owner combat and diagnostic command bindings, and the sole
|
||||||
|
gameplay input subscriber before Phase 8 can expose a frame root. Desired
|
||||||
|
components have a focused canonical snapshot owner. The component-lifecycle
|
||||||
|
handoff into the earlier live-presentation phase is an exact, retryable
|
||||||
|
adoption lease, so any later Phase-7 fault rolls it back without stranding or
|
||||||
|
replaying teardown.
|
||||||
|
|
||||||
### 2.1 Focused session runtime factory
|
### 2.1 Focused session runtime factory
|
||||||
|
|
||||||
Add `LiveSessionRuntimeFactory` under `AcDream.App/Net`. It owns the existing
|
Add `LiveSessionRuntimeFactory` under `AcDream.App/Net`. It owns the existing
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ audit.
|
||||||
- [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots
|
- [x] H — give terrain atlas, sky shader, retained `UiHost`, and both frame roots
|
||||||
explicit single ownership and transfer seams.
|
explicit single ownership and transfer seams.
|
||||||
- [ ] I — group `OnLoad` into small ordered, fakeable composition phases with
|
- [ ] I — group `OnLoad` into small ordered, fakeable composition phases with
|
||||||
transactional partial-acquisition rollback. I.1–I.7 are complete; I.8
|
transactional partial-acquisition rollback. I.1–I.7 and I.8a are complete;
|
||||||
frame-root publication and terminal session start is active. Detailed plan:
|
I.8b atomic frame-root publication is active. Detailed plan:
|
||||||
[`2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`](2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md).
|
[`2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`](2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md).
|
||||||
- [ ] J — move the exact retryable shutdown manifest to a focused lifetime
|
- [ ] J — move the exact retryable shutdown manifest to a focused lifetime
|
||||||
owner and prove all partial-load/reentrant/retry paths.
|
owner and prove all partial-load/reentrant/retry paths.
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,24 @@ internal sealed class LiveCombatModeCommandSlot : ILiveCombatModeCommand
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable BindOwned(ILiveCombatModeCommand target)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_deactivated, this);
|
||||||
|
if (_target is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Live combat-mode commands are already bound.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Binding(this, target);
|
||||||
|
}
|
||||||
|
|
||||||
public void Deactivate()
|
public void Deactivate()
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
|
|
@ -126,6 +144,23 @@ internal sealed class LiveCombatModeCommandSlot : ILiveCombatModeCommand
|
||||||
_target?.Toggle();
|
_target?.Toggle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class Binding : IDisposable
|
||||||
|
{
|
||||||
|
private LiveCombatModeCommandSlot? _owner;
|
||||||
|
private readonly ILiveCombatModeCommand _expected;
|
||||||
|
|
||||||
|
public Binding(
|
||||||
|
LiveCombatModeCommandSlot owner,
|
||||||
|
ILiveCombatModeCommand expected)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -406,6 +406,20 @@ internal sealed class ShortcutSnapshotState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed class DesiredComponentSnapshotState
|
||||||
|
{
|
||||||
|
private IReadOnlyList<(uint Id, uint Amount)> _items =
|
||||||
|
Array.Empty<(uint Id, uint Amount)>();
|
||||||
|
|
||||||
|
public IReadOnlyList<(uint Id, uint Amount)> Items
|
||||||
|
{
|
||||||
|
get => _items;
|
||||||
|
set => _items = value ?? Array.Empty<(uint Id, uint Amount)>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear() => _items = Array.Empty<(uint Id, uint Amount)>();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Probe scripts are mounted in Phase 5; reveal/resource facts become valid in
|
/// Probe scripts are mounted in Phase 5; reveal/resource facts become valid in
|
||||||
/// Phase 7. Calls remain inert and diagnostic until that exact owner binds.
|
/// Phase 7. Calls remain inert and diagnostic until that exact owner binds.
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ namespace AcDream.App.Composition;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class LivePresentationRuntimeBindings : IDisposable
|
internal sealed class LivePresentationRuntimeBindings : IDisposable
|
||||||
{
|
{
|
||||||
private readonly List<(string Name, IDisposable Binding)> _bindings = [];
|
private sealed record Entry(string Name, IDisposable Binding);
|
||||||
|
|
||||||
|
private readonly List<Entry> _bindings = [];
|
||||||
private bool _deactivationStarted;
|
private bool _deactivationStarted;
|
||||||
|
|
||||||
public void Adopt(string name, IDisposable binding)
|
public void Adopt(string name, IDisposable binding)
|
||||||
|
|
@ -18,7 +20,17 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||||
ArgumentNullException.ThrowIfNull(binding);
|
ArgumentNullException.ThrowIfNull(binding);
|
||||||
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
|
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
|
||||||
_bindings.Add((name, binding));
|
_bindings.Add(new Entry(name, binding));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable AdoptOwned(string name, IDisposable binding)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||||
|
ArgumentNullException.ThrowIfNull(binding);
|
||||||
|
ObjectDisposedException.ThrowIf(_deactivationStarted, this);
|
||||||
|
var entry = new Entry(name, binding);
|
||||||
|
_bindings.Add(entry);
|
||||||
|
return new Adoption(this, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AdoptRelease(string name, Action release) =>
|
public void AdoptRelease(string name, Action release) =>
|
||||||
|
|
@ -56,16 +68,16 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable
|
||||||
List<Exception>? failures = null;
|
List<Exception>? failures = null;
|
||||||
for (int i = _bindings.Count - 1; i >= 0; i--)
|
for (int i = _bindings.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
(string name, IDisposable binding) = _bindings[i];
|
Entry entry = _bindings[i];
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
binding.Dispose();
|
entry.Binding.Dispose();
|
||||||
_bindings.RemoveAt(i);
|
_bindings.RemoveAt(i);
|
||||||
}
|
}
|
||||||
catch (Exception failure)
|
catch (Exception failure)
|
||||||
{
|
{
|
||||||
(failures ??= []).Add(new InvalidOperationException(
|
(failures ??= []).Add(new InvalidOperationException(
|
||||||
$"Live-presentation binding '{name}' did not detach.",
|
$"Live-presentation binding '{entry.Name}' did not detach.",
|
||||||
failure));
|
failure));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,6 +90,38 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ReleaseAdoption(Entry expected)
|
||||||
|
{
|
||||||
|
int index = _bindings.IndexOf(expected);
|
||||||
|
if (index < 0)
|
||||||
|
return;
|
||||||
|
expected.Binding.Dispose();
|
||||||
|
_bindings.RemoveAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Adoption : IDisposable
|
||||||
|
{
|
||||||
|
private LivePresentationRuntimeBindings? _owner;
|
||||||
|
private readonly Entry _expected;
|
||||||
|
|
||||||
|
public Adoption(
|
||||||
|
LivePresentationRuntimeBindings owner,
|
||||||
|
Entry expected)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
LivePresentationRuntimeBindings? owner = _owner;
|
||||||
|
if (owner is null)
|
||||||
|
return;
|
||||||
|
owner.ReleaseAdoption(_expected);
|
||||||
|
_owner = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class DelegateBinding : IDisposable
|
private sealed class DelegateBinding : IDisposable
|
||||||
{
|
{
|
||||||
private Action? _release;
|
private Action? _release;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using AcDream.App.Combat;
|
using AcDream.App.Combat;
|
||||||
using AcDream.App.Input;
|
using AcDream.App.Input;
|
||||||
using AcDream.App.Interaction;
|
using AcDream.App.Interaction;
|
||||||
|
using AcDream.App.Diagnostics;
|
||||||
using AcDream.App.Net;
|
using AcDream.App.Net;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
|
@ -12,11 +13,15 @@ using AcDream.App.Update;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Combat;
|
using AcDream.Core.Combat;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
|
using AcDream.Core.Chat;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Physics.Motion;
|
using AcDream.Core.Physics.Motion;
|
||||||
using AcDream.Core.Plugins;
|
using AcDream.Core.Plugins;
|
||||||
using AcDream.Core.Rendering;
|
using AcDream.Core.Rendering;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
|
using AcDream.Core.Player;
|
||||||
|
using AcDream.Core.Social;
|
||||||
|
using AcDream.Core.Spells;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using Silk.NET.Windowing;
|
using Silk.NET.Windowing;
|
||||||
|
|
||||||
|
|
@ -28,6 +33,11 @@ internal sealed record SessionPlayerDependencies(
|
||||||
object DatLock,
|
object DatLock,
|
||||||
RuntimeSettingsController Settings,
|
RuntimeSettingsController Settings,
|
||||||
SettingsDevToolsResult SettingsDevTools,
|
SettingsDevToolsResult SettingsDevTools,
|
||||||
|
WorldEnvironmentController WorldEnvironment,
|
||||||
|
WorldSceneDebugState WorldSceneDebugState,
|
||||||
|
RuntimeDiagnosticCommandSlot RuntimeDiagnosticCommands,
|
||||||
|
LiveCombatModeCommandSlot CombatModeCommands,
|
||||||
|
HostQuiescenceGate HostQuiescence,
|
||||||
PhysicsEngine PhysicsEngine,
|
PhysicsEngine PhysicsEngine,
|
||||||
PhysicsDataCache PhysicsDataCache,
|
PhysicsDataCache PhysicsDataCache,
|
||||||
WorldGameState WorldGameState,
|
WorldGameState WorldGameState,
|
||||||
|
|
@ -55,6 +65,9 @@ internal sealed record SessionPlayerDependencies(
|
||||||
LocalPlayerSkillState PlayerSkills,
|
LocalPlayerSkillState PlayerSkills,
|
||||||
ViewportAspectState ViewportAspect,
|
ViewportAspectState ViewportAspect,
|
||||||
PlayerApproachCompletionState PlayerApproachCompletions,
|
PlayerApproachCompletionState PlayerApproachCompletions,
|
||||||
|
PlayerCharacterOptionsState CharacterOptions,
|
||||||
|
ShortcutSnapshotState Shortcuts,
|
||||||
|
DesiredComponentSnapshotState DesiredComponents,
|
||||||
PointerPositionState PointerPosition,
|
PointerPositionState PointerPosition,
|
||||||
DispatcherMovementInputSource MovementInput,
|
DispatcherMovementInputSource MovementInput,
|
||||||
IInputCaptureSource InputCapture,
|
IInputCaptureSource InputCapture,
|
||||||
|
|
@ -66,6 +79,14 @@ internal sealed record SessionPlayerDependencies(
|
||||||
CombatAttackOperationsSlot CombatAttackOperations,
|
CombatAttackOperationsSlot CombatAttackOperations,
|
||||||
CombatState Combat,
|
CombatState Combat,
|
||||||
CombatFeedbackSlot CombatFeedback,
|
CombatFeedbackSlot CombatFeedback,
|
||||||
|
ChatLog Chat,
|
||||||
|
LocalPlayerState LocalPlayer,
|
||||||
|
Spellbook Spellbook,
|
||||||
|
ItemManaState ItemMana,
|
||||||
|
FriendsState Friends,
|
||||||
|
SquelchState Squelch,
|
||||||
|
TurbineChatState TurbineChat,
|
||||||
|
ExternalContainerState ExternalContainers,
|
||||||
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
|
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
|
||||||
Action<string> Log);
|
Action<string> Log);
|
||||||
|
|
||||||
|
|
@ -91,6 +112,8 @@ internal sealed record SessionPlayerResult(
|
||||||
PlayerModeController PlayerMode,
|
PlayerModeController PlayerMode,
|
||||||
PlayerModeAutoEntry PlayerModeAutoEntry,
|
PlayerModeAutoEntry PlayerModeAutoEntry,
|
||||||
LocalPlayerTeleportController LocalTeleport,
|
LocalPlayerTeleportController LocalTeleport,
|
||||||
|
LiveSessionHost SessionHost,
|
||||||
|
GameplayInputActionRouter? GameplayActions,
|
||||||
SessionPlayerRuntimeBindings RuntimeBindings);
|
SessionPlayerRuntimeBindings RuntimeBindings);
|
||||||
|
|
||||||
internal interface IGameWindowSessionPlayerPublication
|
internal interface IGameWindowSessionPlayerPublication
|
||||||
|
|
@ -115,6 +138,9 @@ internal enum SessionPlayerCompositionPoint
|
||||||
PlayerModeBound,
|
PlayerModeBound,
|
||||||
PortalTransferred,
|
PortalTransferred,
|
||||||
TeleportBound,
|
TeleportBound,
|
||||||
|
SessionHostCreated,
|
||||||
|
CommandsBound,
|
||||||
|
GameplayActionsAttached,
|
||||||
ResultPublished,
|
ResultPublished,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -694,9 +720,10 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
|
|
||||||
IDisposable componentLifecycleBinding =
|
IDisposable componentLifecycleBinding =
|
||||||
live.ComponentLifecycle.BindOwned(teardown);
|
live.ComponentLifecycle.BindOwned(teardown);
|
||||||
|
IDisposable componentLifecycleAdoption;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
live.RuntimeBindings.Adopt(
|
componentLifecycleAdoption = live.RuntimeBindings.AdoptOwned(
|
||||||
"live runtime component teardown",
|
"live runtime component teardown",
|
||||||
componentLifecycleBinding);
|
componentLifecycleBinding);
|
||||||
}
|
}
|
||||||
|
|
@ -705,6 +732,153 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
componentLifecycleBinding.Dispose();
|
componentLifecycleBinding.Dispose();
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
var componentLifecycleLease = scope.Own(
|
||||||
|
"live runtime component teardown adoption",
|
||||||
|
componentLifecycleAdoption,
|
||||||
|
static value => value.Dispose());
|
||||||
|
|
||||||
|
AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? vitals =
|
||||||
|
d.SettingsDevTools.DevTools?.Vitals
|
||||||
|
?? interaction.RetainedUi?.Vitals;
|
||||||
|
AcDream.UI.Abstractions.Panels.Chat.ChatVM? retailChat =
|
||||||
|
interaction.RetainedUi?.Chat;
|
||||||
|
var sessionRuntimeFactory = new LiveSessionRuntimeFactory(
|
||||||
|
new LiveSessionPlayerRuntime(
|
||||||
|
d.PlayerIdentity,
|
||||||
|
d.PlayerController,
|
||||||
|
d.PlayerSkills,
|
||||||
|
d.WorldOrigin,
|
||||||
|
d.CharacterOptions,
|
||||||
|
d.Shortcuts,
|
||||||
|
d.DesiredComponents),
|
||||||
|
new LiveSessionDomainRuntime(
|
||||||
|
d.Objects,
|
||||||
|
d.LocalPlayer,
|
||||||
|
d.Spellbook,
|
||||||
|
d.Combat,
|
||||||
|
d.ItemMana,
|
||||||
|
d.Friends,
|
||||||
|
d.Squelch,
|
||||||
|
d.TurbineChat,
|
||||||
|
d.ExternalContainers,
|
||||||
|
d.Chat),
|
||||||
|
new LiveSessionUiRuntime(
|
||||||
|
interaction.RetainedUi?.Runtime,
|
||||||
|
vitals,
|
||||||
|
retailChat,
|
||||||
|
interaction.RetainedUi?.CharacterSheet,
|
||||||
|
interaction.RetainedUi?.Magic,
|
||||||
|
live.PaperdollPresenter),
|
||||||
|
new LiveSessionInteractionRuntime(
|
||||||
|
d.Settings,
|
||||||
|
gameplayInput,
|
||||||
|
playerMode,
|
||||||
|
playerModeAutoEntry,
|
||||||
|
interaction.ItemInteraction,
|
||||||
|
interaction.CombatAttack,
|
||||||
|
live.SelectionInteractions),
|
||||||
|
new LiveSessionWorldRuntime(
|
||||||
|
content.Dats,
|
||||||
|
live.WorldState,
|
||||||
|
live.LiveEntities,
|
||||||
|
sessionEvents,
|
||||||
|
d.WorldEnvironment,
|
||||||
|
d.TeleportSink,
|
||||||
|
spawnClaimClassifier,
|
||||||
|
live.EquippedChildren,
|
||||||
|
live.SelectionScene,
|
||||||
|
d.ParticleVisibility,
|
||||||
|
d.InboundEntityEvents,
|
||||||
|
liveness,
|
||||||
|
networkUpdates,
|
||||||
|
hydration,
|
||||||
|
live.RemoteTeleport,
|
||||||
|
live.EntityEffects,
|
||||||
|
content.AnimationHookFrames,
|
||||||
|
live.Presentation,
|
||||||
|
d.RemoteMovementObservations),
|
||||||
|
d.Log);
|
||||||
|
LiveSessionHost sessionHost = sessionRuntimeFactory.Create(liveSession);
|
||||||
|
Fault(SessionPlayerCompositionPoint.SessionHostCreated);
|
||||||
|
|
||||||
|
Action<string>? debugToast = d.SettingsDevTools.DevTools is { } devOwner
|
||||||
|
? message => devOwner.Debug.AddToast(message)
|
||||||
|
: null;
|
||||||
|
var combatCommand = new LiveCombatModeCommandController(
|
||||||
|
new LiveSessionCombatModeAuthority(sessionHost),
|
||||||
|
new LocalPlayerCombatEquipmentSource(
|
||||||
|
d.Objects,
|
||||||
|
d.PlayerIdentity),
|
||||||
|
d.Combat,
|
||||||
|
new ItemInteractionCombatModeIntentSink(
|
||||||
|
interaction.ItemInteraction),
|
||||||
|
d.Log,
|
||||||
|
debugToast,
|
||||||
|
text => d.Chat.OnSystemMessage(text, 0x1Au));
|
||||||
|
bindings.Adopt(
|
||||||
|
"live combat-mode commands",
|
||||||
|
d.CombatModeCommands.BindOwned(combatCommand));
|
||||||
|
|
||||||
|
var nearbyDiagnostics = new NearbyWorldDiagnosticDumper(
|
||||||
|
new RuntimeNearbyWorldDiagnosticSource(
|
||||||
|
d.PlayerMode,
|
||||||
|
d.PlayerController,
|
||||||
|
host.CameraController,
|
||||||
|
d.WorldOrigin,
|
||||||
|
live.WorldState,
|
||||||
|
d.PhysicsEngine),
|
||||||
|
d.Log);
|
||||||
|
var runtimeDiagnostics = new RuntimeDiagnosticCommandController(
|
||||||
|
d.WorldEnvironment,
|
||||||
|
d.WorldSceneDebugState,
|
||||||
|
host.CameraPointerInput,
|
||||||
|
nearbyDiagnostics,
|
||||||
|
debugToast);
|
||||||
|
bindings.Adopt(
|
||||||
|
"runtime diagnostic commands",
|
||||||
|
d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics));
|
||||||
|
Fault(SessionPlayerCompositionPoint.CommandsBound);
|
||||||
|
|
||||||
|
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
||||||
|
GameplayInputActionRouter>? gameplayActionsLease = null;
|
||||||
|
if (host.InputDispatcher is { } dispatcher)
|
||||||
|
{
|
||||||
|
CameraPointerInputController pointer = host.CameraPointerInput
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"Gameplay action routing requires the composed pointer owner.");
|
||||||
|
var commands = new GameplayInputCommandController(
|
||||||
|
new RetainedGameplayWindowCommands(
|
||||||
|
interaction.RetainedUi?.Runtime),
|
||||||
|
new DevToolsGameplayCommands(
|
||||||
|
d.SettingsDevTools.DevTools?.Presenter),
|
||||||
|
runtimeDiagnostics,
|
||||||
|
new PlayerModeGameplayCommands(
|
||||||
|
d.PlayerMode,
|
||||||
|
playerMode),
|
||||||
|
new ItemTargetModeCommands(interaction.ItemInteraction),
|
||||||
|
new GameplayCameraModeCommands(host.CameraController),
|
||||||
|
combatCommand,
|
||||||
|
new GameplayWindowCommands(d.Window.Close));
|
||||||
|
var targets = new RuntimeGameplayInputPriorityTargets(
|
||||||
|
gameplayInput,
|
||||||
|
pointer,
|
||||||
|
interaction.RetainedUi?.Runtime,
|
||||||
|
live.SelectionInteractions,
|
||||||
|
commands);
|
||||||
|
GameplayInputActionRouter gameplayActions =
|
||||||
|
GameplayInputActionRouter.Create(
|
||||||
|
dispatcher,
|
||||||
|
d.Combat,
|
||||||
|
targets,
|
||||||
|
d.HostQuiescence,
|
||||||
|
d.Log);
|
||||||
|
gameplayActionsLease = scope.Own(
|
||||||
|
"gameplay input actions",
|
||||||
|
gameplayActions,
|
||||||
|
static value => value.Dispose());
|
||||||
|
gameplayActions.Attach();
|
||||||
|
}
|
||||||
|
Fault(SessionPlayerCompositionPoint.GameplayActionsAttached);
|
||||||
|
|
||||||
var bindingsLease = scope.Own(
|
var bindingsLease = scope.Own(
|
||||||
"session/player runtime bindings",
|
"session/player runtime bindings",
|
||||||
|
|
@ -733,12 +907,16 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
playerMode,
|
playerMode,
|
||||||
playerModeAutoEntry,
|
playerModeAutoEntry,
|
||||||
teleportLease.Resource,
|
teleportLease.Resource,
|
||||||
|
sessionHost,
|
||||||
|
gameplayActionsLease?.Resource,
|
||||||
bindings);
|
bindings);
|
||||||
_publication.PublishSessionPlayer(result);
|
_publication.PublishSessionPlayer(result);
|
||||||
|
|
||||||
streamerLease.Transfer();
|
streamerLease.Transfer();
|
||||||
liveSessionLease.Transfer();
|
liveSessionLease.Transfer();
|
||||||
teleportLease.Transfer();
|
teleportLease.Transfer();
|
||||||
|
gameplayActionsLease?.Transfer();
|
||||||
|
componentLifecycleLease.Transfer();
|
||||||
bindingsLease.Transfer();
|
bindingsLease.Transfer();
|
||||||
Fault(SessionPlayerCompositionPoint.ResultPublished);
|
Fault(SessionPlayerCompositionPoint.ResultPublished);
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,24 @@ internal sealed class RuntimeDiagnosticCommandSlot : IRuntimeDiagnosticCommands
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable BindOwned(IRuntimeDiagnosticCommands target)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_deactivated, this);
|
||||||
|
if (_target is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Runtime diagnostic commands are already bound.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Binding(this, target);
|
||||||
|
}
|
||||||
|
|
||||||
public void Deactivate()
|
public void Deactivate()
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
|
|
@ -103,6 +121,23 @@ internal sealed class RuntimeDiagnosticCommandSlot : IRuntimeDiagnosticCommands
|
||||||
_target?.ToggleCollisionWireframes();
|
_target?.ToggleCollisionWireframes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class Binding : IDisposable
|
||||||
|
{
|
||||||
|
private RuntimeDiagnosticCommandSlot? _owner;
|
||||||
|
private readonly IRuntimeDiagnosticCommands _expected;
|
||||||
|
|
||||||
|
public Binding(
|
||||||
|
RuntimeDiagnosticCommandSlot owner,
|
||||||
|
IRuntimeDiagnosticCommands expected)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_expected = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() =>
|
||||||
|
Interlocked.Exchange(ref _owner, null)?.Unbind(_expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal interface INearbyWorldDiagnosticSource
|
internal interface INearbyWorldDiagnosticSource
|
||||||
|
|
|
||||||
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.Items.ItemManaState ItemMana = new();
|
||||||
public readonly AcDream.Core.Social.FriendsState Friends = new();
|
public readonly AcDream.Core.Social.FriendsState Friends = new();
|
||||||
public readonly AcDream.Core.Social.SquelchState Squelch = new();
|
public readonly AcDream.Core.Social.SquelchState Squelch = new();
|
||||||
public IReadOnlyList<(uint Id, uint Amount)> DesiredComponents { get; private set; }
|
private readonly DesiredComponentSnapshotState _desiredComponents = new();
|
||||||
= System.Array.Empty<(uint Id, uint Amount)>();
|
public IReadOnlyList<(uint Id, uint Amount)> DesiredComponents =>
|
||||||
|
_desiredComponents.Items;
|
||||||
// Retail resolves learned IDs through portal.dat's CSpellTable. MagicCatalog
|
// Retail resolves learned IDs through portal.dat's CSpellTable. MagicCatalog
|
||||||
// installs that immutable table once DatCollection opens in OnLoad.
|
// installs that immutable table once DatCollection opens in OnLoad.
|
||||||
public AcDream.Core.Spells.SpellTable SpellTable => SpellBook.Metadata;
|
public AcDream.Core.Spells.SpellTable SpellTable => SpellBook.Metadata;
|
||||||
|
|
@ -1010,6 +1011,8 @@ public sealed class GameWindow :
|
||||||
|| _playerModeController is not null
|
|| _playerModeController is not null
|
||||||
|| _playerModeAutoEntry is not null
|
|| _playerModeAutoEntry is not null
|
||||||
|| _localPlayerTeleport is not null
|
|| _localPlayerTeleport is not null
|
||||||
|
|| _liveSessionHost is not null
|
||||||
|
|| _gameplayInputActions is not null
|
||||||
|| _sessionPlayerBindings is not null)
|
|| _sessionPlayerBindings is not null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
|
|
@ -1032,6 +1035,8 @@ public sealed class GameWindow :
|
||||||
_playerModeController = result.PlayerMode;
|
_playerModeController = result.PlayerMode;
|
||||||
_playerModeAutoEntry = result.PlayerModeAutoEntry;
|
_playerModeAutoEntry = result.PlayerModeAutoEntry;
|
||||||
_localPlayerTeleport = result.LocalTeleport;
|
_localPlayerTeleport = result.LocalTeleport;
|
||||||
|
_liveSessionHost = result.SessionHost;
|
||||||
|
_gameplayInputActions = result.GameplayActions;
|
||||||
_sessionPlayerBindings = result.RuntimeBindings;
|
_sessionPlayerBindings = result.RuntimeBindings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1287,6 +1292,11 @@ public sealed class GameWindow :
|
||||||
_datLock,
|
_datLock,
|
||||||
_runtimeSettings,
|
_runtimeSettings,
|
||||||
settingsDevTools,
|
settingsDevTools,
|
||||||
|
_worldEnvironment,
|
||||||
|
_worldSceneDebugState,
|
||||||
|
_runtimeDiagnosticCommands,
|
||||||
|
_liveCombatModeCommands,
|
||||||
|
_hostQuiescence,
|
||||||
_physicsEngine,
|
_physicsEngine,
|
||||||
_physicsDataCache,
|
_physicsDataCache,
|
||||||
_worldGameState,
|
_worldGameState,
|
||||||
|
|
@ -1314,6 +1324,9 @@ public sealed class GameWindow :
|
||||||
_localPlayerSkills,
|
_localPlayerSkills,
|
||||||
_viewportAspect,
|
_viewportAspect,
|
||||||
_playerApproachCompletions,
|
_playerApproachCompletions,
|
||||||
|
_characterOptions,
|
||||||
|
_shortcutSnapshots,
|
||||||
|
_desiredComponents,
|
||||||
_pointerPosition,
|
_pointerPosition,
|
||||||
_movementInput,
|
_movementInput,
|
||||||
_inputCapture,
|
_inputCapture,
|
||||||
|
|
@ -1325,6 +1338,14 @@ public sealed class GameWindow :
|
||||||
_combatAttackOperations,
|
_combatAttackOperations,
|
||||||
Combat,
|
Combat,
|
||||||
_combatFeedback,
|
_combatFeedback,
|
||||||
|
Chat,
|
||||||
|
LocalPlayer,
|
||||||
|
SpellBook,
|
||||||
|
ItemMana,
|
||||||
|
Friends,
|
||||||
|
Squelch,
|
||||||
|
TurbineChat,
|
||||||
|
_externalContainers,
|
||||||
_portalTunnelFallback,
|
_portalTunnelFallback,
|
||||||
Console.WriteLine),
|
Console.WriteLine),
|
||||||
this).Compose(
|
this).Compose(
|
||||||
|
|
@ -1593,84 +1614,9 @@ public sealed class GameWindow :
|
||||||
sessionPlayer.PlayerModeAutoEntry),
|
sessionPlayer.PlayerModeAutoEntry),
|
||||||
cameraFrame);
|
cameraFrame);
|
||||||
_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);
|
_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);
|
||||||
_liveSessionHost = CreateLiveSessionHost();
|
|
||||||
|
|
||||||
AcDream.UI.Abstractions.Panels.Debug.DebugVM? debugVm = _debugVm;
|
|
||||||
Action<string>? debugToast = debugVm is null
|
|
||||||
? null
|
|
||||||
: message => debugVm.AddToast(message);
|
|
||||||
AcDream.Core.Chat.ChatLog chat = Chat;
|
|
||||||
var combatCommand =
|
|
||||||
new AcDream.App.Combat.LiveCombatModeCommandController(
|
|
||||||
new AcDream.App.Combat.LiveSessionCombatModeAuthority(
|
|
||||||
_liveSessionHost),
|
|
||||||
new AcDream.App.Combat.LocalPlayerCombatEquipmentSource(
|
|
||||||
Objects,
|
|
||||||
_localPlayerIdentity),
|
|
||||||
Combat,
|
|
||||||
new AcDream.App.Combat.ItemInteractionCombatModeIntentSink(
|
|
||||||
_itemInteractionController!),
|
|
||||||
Console.WriteLine,
|
|
||||||
debugToast,
|
|
||||||
text => chat.OnSystemMessage(text, 0x1Au));
|
|
||||||
_liveCombatModeCommands.Bind(combatCommand);
|
|
||||||
|
|
||||||
var nearbyDiagnostics =
|
|
||||||
new AcDream.App.Diagnostics.NearbyWorldDiagnosticDumper(
|
|
||||||
new AcDream.App.Diagnostics.RuntimeNearbyWorldDiagnosticSource(
|
|
||||||
_localPlayerMode,
|
|
||||||
_playerControllerSlot,
|
|
||||||
_cameraController!,
|
|
||||||
_liveWorldOrigin,
|
|
||||||
_worldState,
|
|
||||||
_physicsEngine),
|
|
||||||
Console.WriteLine);
|
|
||||||
var runtimeDiagnostics =
|
|
||||||
new AcDream.App.Diagnostics.RuntimeDiagnosticCommandController(
|
|
||||||
_worldEnvironment,
|
|
||||||
_worldSceneDebugState,
|
|
||||||
_cameraPointerInput,
|
|
||||||
nearbyDiagnostics,
|
|
||||||
debugToast);
|
|
||||||
_runtimeDiagnosticCommands.Bind(runtimeDiagnostics);
|
|
||||||
|
|
||||||
if (_inputDispatcher is { } dispatcher)
|
|
||||||
{
|
|
||||||
var pointer = _cameraPointerInput
|
|
||||||
?? throw new InvalidOperationException(
|
|
||||||
"Gameplay action routing requires the composed pointer owner.");
|
|
||||||
var commands = new AcDream.App.Input.GameplayInputCommandController(
|
|
||||||
new AcDream.App.Input.RetainedGameplayWindowCommands(
|
|
||||||
_retailUiRuntime),
|
|
||||||
new AcDream.App.Input.DevToolsGameplayCommands(
|
|
||||||
_devToolsFramePresenter),
|
|
||||||
runtimeDiagnostics,
|
|
||||||
new AcDream.App.Input.PlayerModeGameplayCommands(
|
|
||||||
_localPlayerMode,
|
|
||||||
sessionPlayer.PlayerMode),
|
|
||||||
new AcDream.App.Input.ItemTargetModeCommands(
|
|
||||||
_itemInteractionController!),
|
|
||||||
new AcDream.App.Input.GameplayCameraModeCommands(
|
|
||||||
_cameraController!),
|
|
||||||
combatCommand,
|
|
||||||
new AcDream.App.Input.GameplayWindowCommands(_window!.Close));
|
|
||||||
var targets = new AcDream.App.Input.RuntimeGameplayInputPriorityTargets(
|
|
||||||
sessionPlayer.GameplayInput,
|
|
||||||
pointer,
|
|
||||||
_retailUiRuntime,
|
|
||||||
_selectionInteractions,
|
|
||||||
commands);
|
|
||||||
_gameplayInputActions = AcDream.App.Input.GameplayInputActionRouter.Create(
|
|
||||||
dispatcher,
|
|
||||||
Combat,
|
|
||||||
targets,
|
|
||||||
_hostQuiescence,
|
|
||||||
Console.WriteLine);
|
|
||||||
_gameplayInputActions.Attach();
|
|
||||||
}
|
|
||||||
|
|
||||||
AcDream.App.Net.LiveSessionStartResult liveStart =
|
AcDream.App.Net.LiveSessionStartResult liveStart =
|
||||||
_liveSessionHost.Start(_options);
|
sessionPlayer.SessionHost.Start(_options);
|
||||||
switch (liveStart.Status)
|
switch (liveStart.Status)
|
||||||
{
|
{
|
||||||
case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials:
|
case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials:
|
||||||
|
|
@ -1683,272 +1629,6 @@ public sealed class GameWindow :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AcDream.App.Net.LiveSessionHost CreateLiveSessionHost() =>
|
|
||||||
new(_liveSessionController!, new AcDream.App.Net.LiveSessionHostBindings(
|
|
||||||
Routing: new(
|
|
||||||
CreateLiveSessionEventRouter,
|
|
||||||
session => new AcDream.App.Net.LiveSessionCommandRouter(
|
|
||||||
CreateLiveSessionCommandBindings(session))),
|
|
||||||
Reset: CreateLiveSessionResetBindings(),
|
|
||||||
Selection: new(
|
|
||||||
SetPlayerIdentity: id => _playerServerGuid = id,
|
|
||||||
SetVitalsIdentity: id => _vitalsVm?.SetLocalPlayerGuid(id),
|
|
||||||
SetChatIdentity: Chat.SetLocalPlayerGuid,
|
|
||||||
MarkPersistent: _worldState.MarkPersistent,
|
|
||||||
SetVanishProbeIdentity: id =>
|
|
||||||
AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = id,
|
|
||||||
ClearCombat: Combat.Clear),
|
|
||||||
EnteredWorld: new(
|
|
||||||
SetActiveCharacter: _runtimeSettings.SetActiveCharacter,
|
|
||||||
RestoreLayout: () => _retailUiRuntime?.RestoreLayout(),
|
|
||||||
SyncToolbar: SyncToolbarWindowButtons,
|
|
||||||
LoadCharacterSettings: _runtimeSettings.LoadCharacterContext,
|
|
||||||
ArmPlayerModeAutoEntry: () => _playerModeAutoEntry?.Arm()),
|
|
||||||
Connecting: (host, port, user) =>
|
|
||||||
Chat.OnSystemMessage(
|
|
||||||
$"connecting to {host}:{port} as {user}",
|
|
||||||
chatType: 1),
|
|
||||||
Connected: () =>
|
|
||||||
Chat.OnSystemMessage(
|
|
||||||
"connected — character list received",
|
|
||||||
chatType: 1)));
|
|
||||||
|
|
||||||
private AcDream.App.Net.LiveSessionResetBindings
|
|
||||||
CreateLiveSessionResetBindings() => new()
|
|
||||||
{
|
|
||||||
MouseCapture = ResetSessionMouseCapture,
|
|
||||||
PlayerPresentation = ResetSessionPlayerPresentation,
|
|
||||||
TeleportTransit = _localPlayerTeleportSink.ResetSession,
|
|
||||||
SessionDialogs = () => _retailUiRuntime?.ResetSessionDialogs(),
|
|
||||||
ChatCommandTargets = () => _retailChatVm?.ResetSessionTargets(),
|
|
||||||
SettingsCharacterContext = _runtimeSettings.RestoreDefaultCharacterContext,
|
|
||||||
EquippedChildren = () => _equippedChildRenderer?.Clear(),
|
|
||||||
ExternalContainer = () => _externalContainers.Reset(),
|
|
||||||
InteractionAndSelection = ResetSessionInteraction,
|
|
||||||
SelectionPresentation = () => _retailSelectionScene?.Reset(),
|
|
||||||
ObjectTable = Objects.Clear,
|
|
||||||
Spellbook = SpellBook.Clear,
|
|
||||||
MagicRuntime = () => _magicRuntime?.Reset(),
|
|
||||||
CombatAttack = () => _combatAttackController?.ResetSession(),
|
|
||||||
CombatState = Combat.Clear,
|
|
||||||
ItemMana = ItemMana.Clear,
|
|
||||||
LocalPlayer = LocalPlayer.Clear,
|
|
||||||
Friends = Friends.Clear,
|
|
||||||
Squelch = Squelch.Clear,
|
|
||||||
TurbineChat = TurbineChat.Reset,
|
|
||||||
ParticleVisibility = _particleVisibility.Reset,
|
|
||||||
InboundEventFifo = _inboundEntityEvents.Clear,
|
|
||||||
LiveLiveness = () => _liveEntityLiveness?.Clear(),
|
|
||||||
LiveRuntime = ResetSessionLiveRuntime,
|
|
||||||
SessionIdentity = ResetSessionIdentity,
|
|
||||||
RemoteTeleport = () => _remoteTeleportController?.Clear(),
|
|
||||||
NetworkEffects = () => _entityEffects?.ClearNetworkState(),
|
|
||||||
AnimationHookFrames = () => _animationHookFrames?.Clear(),
|
|
||||||
LivePresentation = () => _liveEntityPresentation?.Clear(),
|
|
||||||
RemoteMovementDiagnostics = _remoteMovementObservations.Clear,
|
|
||||||
};
|
|
||||||
|
|
||||||
private void ResetSessionMouseCapture()
|
|
||||||
=> _gameplayInputFrame?.ResetSession();
|
|
||||||
|
|
||||||
private void ResetSessionPlayerPresentation()
|
|
||||||
{
|
|
||||||
_playerModeController?.ResetSession();
|
|
||||||
_spawnClaimHydration?.Reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ResetSessionIdentity()
|
|
||||||
{
|
|
||||||
AcDream.App.Net.LiveSessionEntityRuntimeReset.RequireConvergence(_liveEntities);
|
|
||||||
|
|
||||||
// PlayerModule::Clear @ 0x005D48A0 clears shortcuts, desired
|
|
||||||
// components, and character option objects. CPlayerSystem::Begin
|
|
||||||
// @ 0x0055D410 resets player identity and session fields. The option
|
|
||||||
// default comes from PlayerModule::PlayerModule @ 0x005D51F0.
|
|
||||||
_playerServerGuid = 0u;
|
|
||||||
_vitalsVm?.SetLocalPlayerGuid(0u);
|
|
||||||
Chat.ResetSessionIdentity();
|
|
||||||
AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = 0u;
|
|
||||||
_runtimeSettings.ResetActiveCharacterKey();
|
|
||||||
_characterOptions.Reset();
|
|
||||||
_localPlayerSkills.ResetSession();
|
|
||||||
_liveEntityNetworkUpdates?.ResetSessionState();
|
|
||||||
_liveEntityHydration?.ResetSessionState();
|
|
||||||
Shortcuts = Array.Empty<AcDream.Core.Items.ShortcutEntry>();
|
|
||||||
DesiredComponents = Array.Empty<(uint Id, uint Amount)>();
|
|
||||||
_paperdollFramePresenter?.MarkDirty();
|
|
||||||
|
|
||||||
// X/Y are ignored until the next logical player CreateObject claims a
|
|
||||||
// new origin. Keeping the last values avoids inventing a synthetic
|
|
||||||
// landblock while still forcing first-spawn initialization.
|
|
||||||
_liveWorldOrigin.Reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ResetSessionLiveRuntime()
|
|
||||||
{
|
|
||||||
AcDream.App.Net.LiveSessionEntityRuntimeReset.ClearAndRequireConvergence(
|
|
||||||
_liveEntities);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ResetSessionInteraction()
|
|
||||||
{
|
|
||||||
if (_selectionInteractions is { } interactions)
|
|
||||||
interactions.ResetSession();
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_itemInteractionController?.ResetSession();
|
|
||||||
_selection.Reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private AcDream.App.Net.LiveSessionEventRouter CreateLiveSessionEventRouter(
|
|
||||||
AcDream.Core.Net.WorldSession session)
|
|
||||||
{
|
|
||||||
var skillTable = _dats?.Get<DatReaderWriter.DBObjs.SkillTable>(0x0E000004u);
|
|
||||||
if (_characterSheetProvider is not null && _dats is not null)
|
|
||||||
{
|
|
||||||
_characterSheetProvider.SkillTable = skillTable;
|
|
||||||
_characterSheetProvider.ExperienceTable =
|
|
||||||
AcDream.App.UI.Layout.CharacterSheetProvider.LoadExperienceTable(
|
|
||||||
_dats,
|
|
||||||
Console.WriteLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AcDream.App.Net.LiveSessionEventRouter(
|
|
||||||
session,
|
|
||||||
(_liveEntitySessionEvents
|
|
||||||
?? throw new InvalidOperationException(
|
|
||||||
"Live entity session routing was not composed."))
|
|
||||||
.CreateSink(),
|
|
||||||
new AcDream.App.Net.LiveEnvironmentSessionSink(
|
|
||||||
_worldEnvironment.ApplyAdminEnvirons,
|
|
||||||
_worldEnvironment.SynchronizeFromServer),
|
|
||||||
CreateLiveInventorySessionBindings(),
|
|
||||||
CreateLiveCharacterSessionBindings(skillTable),
|
|
||||||
CreateLiveSocialSessionBindings());
|
|
||||||
}
|
|
||||||
|
|
||||||
private AcDream.App.Net.LiveInventorySessionBindings
|
|
||||||
CreateLiveInventorySessionBindings() => new(
|
|
||||||
Objects,
|
|
||||||
LocalPlayer,
|
|
||||||
PlayerGuid: () => _playerServerGuid,
|
|
||||||
OnShortcuts: list => Shortcuts = list,
|
|
||||||
OnUseDone: error =>
|
|
||||||
{
|
|
||||||
_externalContainers.ApplyUseDone(error);
|
|
||||||
_itemInteractionController?.CompleteUse(error);
|
|
||||||
},
|
|
||||||
ItemMana,
|
|
||||||
OnDesiredComponents: components => DesiredComponents = components,
|
|
||||||
ExternalContainers: _externalContainers);
|
|
||||||
|
|
||||||
private AcDream.App.Net.LiveCharacterSessionBindings
|
|
||||||
CreateLiveCharacterSessionBindings(
|
|
||||||
DatReaderWriter.DBObjs.SkillTable? skillTable)
|
|
||||||
{
|
|
||||||
var skillCreditResolver =
|
|
||||||
new AcDream.App.Net.LiveSkillCreditResolver(skillTable);
|
|
||||||
return new(
|
|
||||||
Combat,
|
|
||||||
SpellBook,
|
|
||||||
ResolveSkillFormulaBonus: skillCreditResolver.Resolve,
|
|
||||||
OnSkillsUpdated: (runSkill, jumpSkill) =>
|
|
||||||
{
|
|
||||||
_localPlayerSkills.Update(runSkill, jumpSkill, _playerController);
|
|
||||||
if (_localPlayerSkills.IsComplete && _playerController is not null)
|
|
||||||
{
|
|
||||||
Console.WriteLine(
|
|
||||||
$"player: applied server skills run={_localPlayerSkills.RunSkill} " +
|
|
||||||
$"jump={_localPlayerSkills.JumpSkill}");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
OnConfirmationRequest: request =>
|
|
||||||
_retailUiRuntime?.HandleConfirmationRequest(request),
|
|
||||||
OnConfirmationDone: done =>
|
|
||||||
_retailUiRuntime?.HandleConfirmationDone(done),
|
|
||||||
OnCharacterOptions: (options1, _) =>
|
|
||||||
_characterOptions.Options =
|
|
||||||
(AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1)
|
|
||||||
options1,
|
|
||||||
ClientTime: ClientTimerNow);
|
|
||||||
}
|
|
||||||
|
|
||||||
private AcDream.App.Net.LiveSocialSessionBindings
|
|
||||||
CreateLiveSocialSessionBindings() => new(
|
|
||||||
Chat,
|
|
||||||
TurbineChat,
|
|
||||||
Friends,
|
|
||||||
Squelch);
|
|
||||||
|
|
||||||
private AcDream.App.Net.LiveSessionCommandBindings CreateLiveSessionCommandBindings(
|
|
||||||
AcDream.Core.Net.WorldSession session) => new(
|
|
||||||
ClientCommands: new AcDream.App.UI.ClientCommandController.Bindings(
|
|
||||||
TeleportToLifestone: session.SendTeleportToLifestone,
|
|
||||||
TeleportToMarketplace: session.SendTeleportToMarketplace,
|
|
||||||
TeleportToPkArena: session.SendTeleportToPkArena,
|
|
||||||
TeleportToPkLiteArena: session.SendTeleportToPkLiteArena,
|
|
||||||
TeleportToHouse: session.SendTeleportToHouse,
|
|
||||||
TeleportToMansion: session.SendTeleportToMansion,
|
|
||||||
QueryAge: session.SendQueryAge,
|
|
||||||
QueryBirth: session.SendQueryBirth,
|
|
||||||
ToggleFrameRate: _runtimeSettings.ToggleFrameRate,
|
|
||||||
ToggleUiLock: () =>
|
|
||||||
_runtimeSettings.SetUiLocked(!_runtimeSettings.Gameplay.LockUI),
|
|
||||||
ShowSystemMessage: text => Chat.OnSystemMessage(text, 0u),
|
|
||||||
ShowWeenieError: code => Chat.OnWeenieError(code, null),
|
|
||||||
PlayerPublicWeenieBitfield: () =>
|
|
||||||
Objects.Get(_playerServerGuid)?.PublicWeenieBitfield,
|
|
||||||
ClientVersion: () =>
|
|
||||||
typeof(GameWindow).Assembly.GetName().Version?.ToString(3)
|
|
||||||
?? "unknown",
|
|
||||||
CurrentPosition: () => _playerController?.CellPosition,
|
|
||||||
LastOutsideCorpsePosition: () => LocalPlayer.GetPosition(0x0Eu),
|
|
||||||
ShowConfirmation: (message, completed) =>
|
|
||||||
_retailUiRuntime?.ShowConfirmation(message, completed),
|
|
||||||
Suicide: session.SendSuicide,
|
|
||||||
ClearChat: _ => Chat.Clear(),
|
|
||||||
SaveUi: name => _retailUiRuntime?.SaveNamedLayout(name),
|
|
||||||
LoadUi: name => _retailUiRuntime?.RestoreNamedLayout(name),
|
|
||||||
SaveAutoUi: () => _retailUiRuntime?.SaveLayout(),
|
|
||||||
LoadAutoUi: () => _retailUiRuntime?.RestoreLayout(),
|
|
||||||
IsAway: () =>
|
|
||||||
Objects.Get(_playerServerGuid)?.Properties.GetBool(0x6Eu) == true,
|
|
||||||
SetAway: away => SetRetailAway(session, away),
|
|
||||||
SetAwayMessage: session.SendSetAfkMessage,
|
|
||||||
AcceptLootPermits: () => _runtimeSettings.Gameplay.AcceptLootPermits,
|
|
||||||
SetAcceptLootPermits: _runtimeSettings.SetAcceptLootPermits,
|
|
||||||
DisplayConsent: session.SendDisplayConsent,
|
|
||||||
ClearConsent: session.SendClearConsent,
|
|
||||||
RemoveConsent: session.SendRemoveConsent,
|
|
||||||
SendEmote: session.SendEmote,
|
|
||||||
Friends,
|
|
||||||
AddFriend: session.SendAddFriend,
|
|
||||||
RemoveFriend: session.SendRemoveFriend,
|
|
||||||
ClearFriends: session.SendClearFriends,
|
|
||||||
RequestLegacyFriends: session.SendLegacyFriendsListRequest,
|
|
||||||
Squelch,
|
|
||||||
ModifyCharacterSquelch: session.SendModifyCharacterSquelch,
|
|
||||||
ModifyAccountSquelch: session.SendModifyAccountSquelch,
|
|
||||||
ModifyGlobalSquelch: session.SendModifyGlobalSquelch,
|
|
||||||
LastTeller: () => _retailChatVm?.LastIncomingTellSender,
|
|
||||||
ClearDesiredComponents: () =>
|
|
||||||
{
|
|
||||||
session.SendClearDesiredComponents();
|
|
||||||
DesiredComponents = Array.Empty<(uint Id, uint Amount)>();
|
|
||||||
},
|
|
||||||
HasOpenVendor: () => false,
|
|
||||||
FillComponentBuyList: (_, _) => { }),
|
|
||||||
Chat,
|
|
||||||
TurbineChat,
|
|
||||||
PlayerGuid: () => _playerServerGuid,
|
|
||||||
SendTalk: session.SendTalk,
|
|
||||||
SendTell: session.SendTell,
|
|
||||||
SendChannel: session.SendChannel,
|
|
||||||
SendTurbineChat: (roomId, chatType, dispatchType, senderGuid, text, cookie) =>
|
|
||||||
session.SendTurbineChatTo(
|
|
||||||
roomId, chatType, dispatchType, senderGuid, text, cookie),
|
|
||||||
Log: Console.WriteLine);
|
|
||||||
private void OnUpdate(double dt)
|
private void OnUpdate(double dt)
|
||||||
{
|
{
|
||||||
using var _updStage = _frameProfiler.BeginStage(
|
using var _updStage = _frameProfiler.BeginStage(
|
||||||
|
|
@ -2032,16 +1712,6 @@ public sealed class GameWindow :
|
||||||
// EXPECTED-DIFF: local sidestep pacing now matches how remotes have
|
// EXPECTED-DIFF: local sidestep pacing now matches how remotes have
|
||||||
// always played (w6-cutover-map.md R3).
|
// always played (w6-cutover-map.md R3).
|
||||||
|
|
||||||
private void SyncToolbarWindowButtons()
|
|
||||||
{
|
|
||||||
_retailUiRuntime?.SyncToolbarWindowButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetRetailAway(AcDream.Core.Net.WorldSession session, bool away)
|
|
||||||
{
|
|
||||||
session.SendSetAfkMode(away);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// L.0 Display tab: framebuffer-resize handler — update GL viewport
|
/// L.0 Display tab: framebuffer-resize handler — update GL viewport
|
||||||
/// + camera aspect when the window is resized (by the user dragging
|
/// + camera aspect when the window is resized (by the user dragging
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,25 @@ public sealed class LiveCombatModeCommandControllerTests
|
||||||
Assert.Throws<ObjectDisposedException>(() => slot.Bind(target));
|
Assert.Throws<ObjectDisposedException>(() => slot.Bind(target));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeferredSlotOwnedBindingReleasesExactlyAndAllowsRebind()
|
||||||
|
{
|
||||||
|
var slot = new LiveCombatModeCommandSlot();
|
||||||
|
var first = new FakeCommand();
|
||||||
|
var second = new FakeCommand();
|
||||||
|
|
||||||
|
IDisposable firstBinding = slot.BindOwned(first);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => slot.BindOwned(second));
|
||||||
|
|
||||||
|
firstBinding.Dispose();
|
||||||
|
using IDisposable secondBinding = slot.BindOwned(second);
|
||||||
|
firstBinding.Dispose();
|
||||||
|
slot.Toggle();
|
||||||
|
|
||||||
|
Assert.Equal(0, first.ToggleCount);
|
||||||
|
Assert.Equal(1, second.ToggleCount);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class Harness
|
private sealed class Harness
|
||||||
{
|
{
|
||||||
public Harness()
|
public Harness()
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,23 @@ namespace AcDream.App.Tests.Composition;
|
||||||
|
|
||||||
public sealed class InteractionUiRuntimeSourcesTests
|
public sealed class InteractionUiRuntimeSourcesTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void DesiredComponentSnapshotNormalizesNullAndClearsExactly()
|
||||||
|
{
|
||||||
|
var state = new DesiredComponentSnapshotState();
|
||||||
|
IReadOnlyList<(uint Id, uint Amount)> items =
|
||||||
|
[(0x01020304u, 7u)];
|
||||||
|
|
||||||
|
Assert.Empty(state.Items);
|
||||||
|
state.Items = items;
|
||||||
|
Assert.Same(items, state.Items);
|
||||||
|
|
||||||
|
state.Clear();
|
||||||
|
Assert.Empty(state.Items);
|
||||||
|
state.Items = null!;
|
||||||
|
Assert.Empty(state.Items);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SessionAuthorityDefaultsBindUnbindRebindAndDeactivateExactly()
|
public void SessionAuthorityDefaultsBindUnbindRebindAndDeactivateExactly()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,24 @@ public sealed class LivePresentationCompositionTests
|
||||||
bindings.Adopt("late", new RetryBinding("late", calls, 0)));
|
bindings.Adopt("late", new RetryBinding("late", calls, 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TransferableAdoptionRollsBackExactlyAndRetriesFailure()
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var bindings = new LivePresentationRuntimeBindings();
|
||||||
|
bindings.Adopt("stable", new RetryBinding("stable", calls, 0));
|
||||||
|
IDisposable adoption = bindings.AdoptOwned(
|
||||||
|
"candidate",
|
||||||
|
new RetryBinding("candidate", calls, 1));
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(adoption.Dispose);
|
||||||
|
adoption.Dispose();
|
||||||
|
adoption.Dispose();
|
||||||
|
bindings.Dispose();
|
||||||
|
|
||||||
|
Assert.Equal(["candidate", "candidate", "stable"], calls);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanonicalRuntimeSlotUsesExactOwnerBinding()
|
public void CanonicalRuntimeSlotUsesExactOwnerBinding()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,19 @@ public sealed class SessionPlayerCompositionTests
|
||||||
"new PlayerModeController(",
|
"new PlayerModeController(",
|
||||||
"d.PortalTunnelFallback.Transfer(",
|
"d.PortalTunnelFallback.Transfer(",
|
||||||
"d.TeleportSink.BindOwned(localTeleport)",
|
"d.TeleportSink.BindOwned(localTeleport)",
|
||||||
|
"sessionRuntimeFactory.Create(liveSession)",
|
||||||
|
"d.CombatModeCommands.BindOwned(combatCommand)",
|
||||||
|
"d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)",
|
||||||
|
"GameplayInputActionRouter.Create(",
|
||||||
|
"gameplayActions.Attach();",
|
||||||
"_publication.PublishSessionPlayer(result);");
|
"_publication.PublishSessionPlayer(result);");
|
||||||
|
|
||||||
|
Assert.DoesNotContain("GameWindow", File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Net",
|
||||||
|
"LiveSessionRuntimeFactory.cs")), StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class RetryBinding(
|
private sealed class RetryBinding(
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,25 @@ public sealed class RuntimeDiagnosticCommandControllerTests
|
||||||
Assert.Throws<ObjectDisposedException>(() => slot.Bind(target));
|
Assert.Throws<ObjectDisposedException>(() => slot.Bind(target));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeferredSlotOwnedBindingReleasesExactlyAndAllowsRebind()
|
||||||
|
{
|
||||||
|
var slot = new RuntimeDiagnosticCommandSlot();
|
||||||
|
var first = new FakeCommands();
|
||||||
|
var second = new FakeCommands();
|
||||||
|
|
||||||
|
IDisposable firstBinding = slot.BindOwned(first);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => slot.BindOwned(second));
|
||||||
|
|
||||||
|
firstBinding.Dispose();
|
||||||
|
using IDisposable secondBinding = slot.BindOwned(second);
|
||||||
|
firstBinding.Dispose();
|
||||||
|
slot.CycleWeather();
|
||||||
|
|
||||||
|
Assert.Equal(0, first.CallCount);
|
||||||
|
Assert.Equal(1, second.CallCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void NullToast_DoesNotSuppressDiagnosticStateChanges()
|
public void NullToast_DoesNotSuppressDiagnosticStateChanges()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
{
|
{
|
||||||
string body = MethodBody(
|
string body = MethodBody(
|
||||||
"private void OnLoad()",
|
"private void OnLoad()",
|
||||||
"private AcDream.App.Net.LiveSessionHost");
|
"private void OnUpdate(double dt)");
|
||||||
|
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
body,
|
body,
|
||||||
|
|
@ -64,16 +64,29 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"LivePresentationResult livePresentation =",
|
"LivePresentationResult livePresentation =",
|
||||||
"new LivePresentationCompositionPhase(",
|
"new LivePresentationCompositionPhase(",
|
||||||
"this).Compose(",
|
"this).Compose(",
|
||||||
|
"new SessionPlayerCompositionPhase(",
|
||||||
"var renderFrameOrchestrator =",
|
"var renderFrameOrchestrator =",
|
||||||
"var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(",
|
"var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(",
|
||||||
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);",
|
"_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);",
|
||||||
"_liveSessionHost = CreateLiveSessionHost();",
|
"sessionPlayer.SessionHost.Start(_options)");
|
||||||
"_liveCombatModeCommands.Bind(combatCommand);",
|
|
||||||
"_runtimeDiagnosticCommands.Bind(runtimeDiagnostics);",
|
string sessionPhase = File.ReadAllText(Path.Combine(
|
||||||
"_gameplayInputActions = AcDream.App.Input.GameplayInputActionRouter.Create(",
|
FindRepoRoot(),
|
||||||
"_gameplayInputActions.Attach();",
|
"src",
|
||||||
"_liveSessionHost.Start(_options)");
|
"AcDream.App",
|
||||||
Assert.Equal(1, CountOccurrences(body, "_liveSessionHost.Start(_options)"));
|
"Composition",
|
||||||
|
"SessionPlayerComposition.cs"));
|
||||||
|
AssertAppearsInOrder(
|
||||||
|
sessionPhase,
|
||||||
|
"sessionRuntimeFactory.Create(liveSession)",
|
||||||
|
"d.CombatModeCommands.BindOwned(combatCommand)",
|
||||||
|
"d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)",
|
||||||
|
"GameplayInputActionRouter.Create(",
|
||||||
|
"gameplayActions.Attach();",
|
||||||
|
"_publication.PublishSessionPlayer(result);");
|
||||||
|
Assert.Equal(1, CountOccurrences(
|
||||||
|
body,
|
||||||
|
"sessionPlayer.SessionHost.Start(_options)"));
|
||||||
string phaseOne = Slice(
|
string phaseOne = Slice(
|
||||||
File.ReadAllText(Path.Combine(
|
File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
|
|
@ -93,7 +106,9 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"if (firstKeyboard is not null && firstMouse is not null)",
|
"if (firstKeyboard is not null && firstMouse is not null)",
|
||||||
phaseOne,
|
phaseOne,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
int start = body.IndexOf("_liveSessionHost.Start(_options)", StringComparison.Ordinal);
|
int start = body.IndexOf(
|
||||||
|
"sessionPlayer.SessionHost.Start(_options)",
|
||||||
|
StringComparison.Ordinal);
|
||||||
string postStart = body[start..];
|
string postStart = body[start..];
|
||||||
Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal);
|
Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal);
|
||||||
Assert.Empty(System.Text.RegularExpressions.Regex.Matches(
|
Assert.Empty(System.Text.RegularExpressions.Regex.Matches(
|
||||||
|
|
@ -113,10 +128,17 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
string source = GameWindowSource();
|
string source = GameWindowSource();
|
||||||
string load = MethodBody(
|
string load = MethodBody(
|
||||||
"private void OnLoad()",
|
"private void OnLoad()",
|
||||||
"private AcDream.App.Net.LiveSessionHost");
|
"private void OnUpdate(double dt)");
|
||||||
string sessionFactory = MethodBody(
|
string sessionFactorySource = File.ReadAllText(Path.Combine(
|
||||||
"private AcDream.App.Net.LiveSessionEventRouter CreateLiveSessionEventRouter(",
|
FindRepoRoot(),
|
||||||
"private AcDream.App.Net.LiveInventorySessionBindings");
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Net",
|
||||||
|
"LiveSessionRuntimeFactory.cs"));
|
||||||
|
string sessionFactory = Slice(
|
||||||
|
sessionFactorySource,
|
||||||
|
"private LiveSessionEventRouter CreateEventRouter(",
|
||||||
|
"private LiveInventorySessionBindings CreateInventoryBindings()");
|
||||||
string worldPhase = File.ReadAllText(Path.Combine(
|
string worldPhase = File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
"src",
|
"src",
|
||||||
|
|
@ -143,9 +165,9 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
Assert.Contains("environment.Initialize(region);", worldPhase, StringComparison.Ordinal);
|
Assert.Contains("environment.Initialize(region);", worldPhase, StringComparison.Ordinal);
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
sessionFactory,
|
sessionFactory,
|
||||||
"new AcDream.App.Net.LiveEnvironmentSessionSink(",
|
"new LiveEnvironmentSessionSink(",
|
||||||
"_worldEnvironment.ApplyAdminEnvirons,",
|
"_world.Environment.ApplyAdminEnvirons,",
|
||||||
"_worldEnvironment.SynchronizeFromServer)");
|
"_world.Environment.SynchronizeFromServer)");
|
||||||
Assert.DoesNotContain("_loadedSkyDesc", source, StringComparison.Ordinal);
|
Assert.DoesNotContain("_loadedSkyDesc", source, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("_loadedSkyDayIndex", source, StringComparison.Ordinal);
|
Assert.DoesNotContain("_loadedSkyDayIndex", source, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("private void RefreshSkyForCurrentDay()", source, StringComparison.Ordinal);
|
Assert.DoesNotContain("private void RefreshSkyForCurrentDay()", source, StringComparison.Ordinal);
|
||||||
|
|
@ -164,10 +186,16 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"debugVm.CycleTimeOfDay = _dependencies.DiagnosticCommands.CycleTimeOfDay;",
|
"debugVm.CycleTimeOfDay = _dependencies.DiagnosticCommands.CycleTimeOfDay;",
|
||||||
"debugVm.CycleWeather = _dependencies.DiagnosticCommands.CycleWeather;",
|
"debugVm.CycleWeather = _dependencies.DiagnosticCommands.CycleWeather;",
|
||||||
"debugVm.ToggleCollisionWires =");
|
"debugVm.ToggleCollisionWires =");
|
||||||
|
string sessionPhase = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"SessionPlayerComposition.cs"));
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
load,
|
sessionPhase,
|
||||||
"new AcDream.App.Diagnostics.RuntimeDiagnosticCommandController(",
|
"new RuntimeDiagnosticCommandController(",
|
||||||
"_runtimeDiagnosticCommands.Bind(runtimeDiagnostics);");
|
"d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -176,7 +204,13 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
string source = GameWindowSource();
|
string source = GameWindowSource();
|
||||||
string load = MethodBody(
|
string load = MethodBody(
|
||||||
"private void OnLoad()",
|
"private void OnLoad()",
|
||||||
"private AcDream.App.Net.LiveSessionHost");
|
"private void OnUpdate(double dt)");
|
||||||
|
string sessionPhase = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Composition",
|
||||||
|
"SessionPlayerComposition.cs"));
|
||||||
string shutdown = MethodBody(
|
string shutdown = MethodBody(
|
||||||
"private ResourceShutdownTransaction CreateShutdownTransaction()",
|
"private ResourceShutdownTransaction CreateShutdownTransaction()",
|
||||||
"private void OnFocusChanged(bool focused)");
|
"private void OnFocusChanged(bool focused)");
|
||||||
|
|
@ -187,9 +221,11 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
Assert.DoesNotContain("private void OnUiDragReleasedOutside(", source, StringComparison.Ordinal);
|
Assert.DoesNotContain("private void OnUiDragReleasedOutside(", source, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("DragReleasedOutsideUi +=", source, StringComparison.Ordinal);
|
Assert.DoesNotContain("DragReleasedOutsideUi +=", source, StringComparison.Ordinal);
|
||||||
Assert.Equal(1, CountOccurrences(
|
Assert.Equal(1, CountOccurrences(
|
||||||
load,
|
sessionPhase,
|
||||||
"AcDream.App.Input.GameplayInputActionRouter.Create("));
|
"GameplayInputActionRouter.Create("));
|
||||||
Assert.Equal(1, CountOccurrences(load, "_gameplayInputActions.Attach();"));
|
Assert.Equal(1, CountOccurrences(
|
||||||
|
sessionPhase,
|
||||||
|
"gameplayActions.Attach();"));
|
||||||
Assert.Equal(1, CountOccurrences(
|
Assert.Equal(1, CountOccurrences(
|
||||||
LivePresentationSource(),
|
LivePresentationSource(),
|
||||||
"RetainedUiGameplayBinding.Create("));
|
"RetainedUiGameplayBinding.Create("));
|
||||||
|
|
@ -214,7 +250,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
{
|
{
|
||||||
string load = MethodBody(
|
string load = MethodBody(
|
||||||
"private void OnLoad()",
|
"private void OnLoad()",
|
||||||
"private AcDream.App.Net.LiveSessionHost");
|
"private void OnUpdate(double dt)");
|
||||||
string body = MethodBody(
|
string body = MethodBody(
|
||||||
"private void OnFramebufferResize(",
|
"private void OnFramebufferResize(",
|
||||||
"private void OnClosing()");
|
"private void OnClosing()");
|
||||||
|
|
@ -251,7 +287,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
string run = MethodBody("public void Run()", "private void OnLoad()");
|
string run = MethodBody("public void Run()", "private void OnLoad()");
|
||||||
string load = MethodBody(
|
string load = MethodBody(
|
||||||
"private void OnLoad()",
|
"private void OnLoad()",
|
||||||
"private AcDream.App.Net.LiveSessionHost");
|
"private void OnUpdate(double dt)");
|
||||||
string shutdown = MethodBody(
|
string shutdown = MethodBody(
|
||||||
"private ResourceShutdownTransaction CreateShutdownTransaction()",
|
"private ResourceShutdownTransaction CreateShutdownTransaction()",
|
||||||
"private void OnFocusChanged(bool focused)");
|
"private void OnFocusChanged(bool focused)");
|
||||||
|
|
@ -295,7 +331,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
"this).Compose(platform, hostInputCamera, contentEffectsAudio);",
|
"this).Compose(platform, hostInputCamera, contentEffectsAudio);",
|
||||||
"new WorldRenderCompositionPhase(",
|
"new WorldRenderCompositionPhase(",
|
||||||
"new SessionPlayerCompositionPhase(",
|
"new SessionPlayerCompositionPhase(",
|
||||||
"_liveSessionHost.Start(_options)");
|
"sessionPlayer.SessionHost.Start(_options)");
|
||||||
string sessionPhase = File.ReadAllText(Path.Combine(
|
string sessionPhase = File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
"src",
|
"src",
|
||||||
|
|
@ -464,7 +500,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
||||||
string source = GameWindowSource();
|
string source = GameWindowSource();
|
||||||
string load = MethodBody(
|
string load = MethodBody(
|
||||||
"private void OnLoad()",
|
"private void OnLoad()",
|
||||||
"private AcDream.App.Net.LiveSessionHost");
|
"private void OnUpdate(double dt)");
|
||||||
string shutdown = Slice(
|
string shutdown = Slice(
|
||||||
source,
|
source,
|
||||||
"private ResourceShutdownTransaction CreateShutdownTransaction()",
|
"private ResourceShutdownTransaction CreateShutdownTransaction()",
|
||||||
|
|
|
||||||
|
|
@ -179,9 +179,9 @@ public sealed class GameWindowLiveEntityCompositionTests
|
||||||
public void SessionReset_ClosesEveryStreamingReadinessOwner()
|
public void SessionReset_ClosesEveryStreamingReadinessOwner()
|
||||||
{
|
{
|
||||||
string source = File.ReadAllText(Path.Combine(
|
string source = File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs"));
|
FindRepoRoot(), "src", "AcDream.App", "Net", "LiveSessionRuntimeFactory.cs"));
|
||||||
|
|
||||||
Assert.Contains("_playerModeController?.ResetSession();", source,
|
Assert.Contains("_interaction.PlayerMode.ResetSession();", source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
string playerModeSource = File.ReadAllText(Path.Combine(
|
string playerModeSource = File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
|
|
@ -192,10 +192,10 @@ public sealed class GameWindowLiveEntityCompositionTests
|
||||||
Assert.Contains("_mode.ResetSession();", playerModeSource,
|
Assert.Contains("_mode.ResetSession();", playerModeSource,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"_liveEntityNetworkUpdates?.ResetSessionState();",
|
"_world.NetworkUpdates.ResetSessionState();",
|
||||||
source,
|
source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains("_liveWorldOrigin.Reset();", source, StringComparison.Ordinal);
|
Assert.Contains("_player.WorldOrigin.Reset();", source, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FindRepoRoot()
|
private static string FindRepoRoot()
|
||||||
|
|
|
||||||
|
|
@ -807,6 +807,12 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"AcDream.App",
|
"AcDream.App",
|
||||||
"Composition",
|
"Composition",
|
||||||
"LivePresentationComposition.cs"));
|
"LivePresentationComposition.cs"));
|
||||||
|
string sessionRuntime = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Net",
|
||||||
|
"LiveSessionRuntimeFactory.cs"));
|
||||||
Assert.DoesNotContain("PublishLocalPhysicsTimestamps", source,
|
Assert.DoesNotContain("PublishLocalPhysicsTimestamps", source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("LoginWorldReady", source,
|
Assert.DoesNotContain("LoginWorldReady", source,
|
||||||
|
|
@ -823,7 +829,11 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains("d.WorldOrigin.CellLocalForSeed", livePresentation,
|
Assert.Contains("d.WorldOrigin.CellLocalForSeed", livePresentation,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains("Live entity session routing was not composed.", source,
|
Assert.DoesNotContain("CreateLiveSessionEventRouter", source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("_world.EntitySession.CreateSink()", sessionRuntime,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("GameWindow", sessionRuntime,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue