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
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue