refactor(net): cut GameWindow over to session owner
This commit is contained in:
parent
783ef1d6db
commit
6a5d9e2e6a
5 changed files with 363 additions and 268 deletions
|
|
@ -216,7 +216,6 @@ public sealed class LiveSessionController : IDisposable
|
||||||
private SessionScope? _retiredScope;
|
private SessionScope? _retiredScope;
|
||||||
private ILiveSessionLifecycleHost? _pendingInitialResetHost;
|
private ILiveSessionLifecycleHost? _pendingInitialResetHost;
|
||||||
private PendingOperation? _pendingOperation;
|
private PendingOperation? _pendingOperation;
|
||||||
private WorldSession? _legacySession;
|
|
||||||
private int _operationDepth;
|
private int _operationDepth;
|
||||||
private bool _inWorld;
|
private bool _inWorld;
|
||||||
private bool _disposeRequested;
|
private bool _disposeRequested;
|
||||||
|
|
@ -236,12 +235,9 @@ public sealed class LiveSessionController : IDisposable
|
||||||
|
|
||||||
public WorldSession? CurrentSession
|
public WorldSession? CurrentSession
|
||||||
{
|
{
|
||||||
get { lock (_gate) return _scope?.Session ?? _legacySession; }
|
get { lock (_gate) return _scope?.Session; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Temporary Slice-3 compatibility alias; removed at GameWindow cutover.</summary>
|
|
||||||
public WorldSession? Session => CurrentSession;
|
|
||||||
|
|
||||||
public ICommandBus Commands
|
public ICommandBus Commands
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -319,11 +315,6 @@ public sealed class LiveSessionController : IDisposable
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
if (_legacySession is not null && _scope is null)
|
|
||||||
{
|
|
||||||
_legacySession.Tick();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!_inWorld || _scope is null || _operationDepth != 0)
|
if (!_inWorld || _scope is null || _operationDepth != 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -629,11 +620,6 @@ public sealed class LiveSessionController : IDisposable
|
||||||
private void DisposeCore()
|
private void DisposeCore()
|
||||||
{
|
{
|
||||||
StopCore();
|
StopCore();
|
||||||
if (_legacySession is { } legacy)
|
|
||||||
{
|
|
||||||
_operations.DisposeSession(legacy);
|
|
||||||
_legacySession = null;
|
|
||||||
}
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -649,40 +635,4 @@ public sealed class LiveSessionController : IDisposable
|
||||||
throw new ObjectDisposedException(nameof(LiveSessionController));
|
throw new ObjectDisposedException(nameof(LiveSessionController));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary compatibility surface for Slice 3 Commit E. The complete
|
|
||||||
// controller above is exercised independently before GameWindow cuts over.
|
|
||||||
public WorldSession? CreateAndWire(RuntimeOptions options, Action<WorldSession> wireEvents)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(options);
|
|
||||||
ArgumentNullException.ThrowIfNull(wireEvents);
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
ThrowIfDisposing();
|
|
||||||
if (!options.LiveMode)
|
|
||||||
return null;
|
|
||||||
if (string.IsNullOrEmpty(options.LiveUser) || string.IsNullOrEmpty(options.LivePass))
|
|
||||||
{
|
|
||||||
Console.WriteLine(
|
|
||||||
"live: ACDREAM_LIVE set but TEST_USER/TEST_PASS missing; skipping");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
IPEndPoint endpoint = _operations.ResolveEndpoint(options.LiveHost, options.LivePort);
|
|
||||||
Console.WriteLine($"live: connecting to {endpoint} as {options.LiveUser}");
|
|
||||||
_legacySession = _operations.CreateSession(endpoint);
|
|
||||||
wireEvents(_legacySession);
|
|
||||||
return _legacySession;
|
|
||||||
}
|
|
||||||
catch (Exception error)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"live: session setup failed: {error.Message}");
|
|
||||||
if (_legacySession is not null)
|
|
||||||
_operations.DisposeSession(_legacySession);
|
|
||||||
_legacySession = null;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
65
src/AcDream.App/Net/LiveSessionLifecycleHost.cs
Normal file
65
src/AcDream.App/Net/LiveSessionLifecycleHost.cs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
using AcDream.Core.Net;
|
||||||
|
|
||||||
|
namespace AcDream.App.Net;
|
||||||
|
|
||||||
|
internal sealed record LiveSessionLifecycleBindings(
|
||||||
|
Func<WorldSession, LiveSessionBinding> Bind,
|
||||||
|
Action Reset,
|
||||||
|
Action<string, int, string> Connecting,
|
||||||
|
Action Connected,
|
||||||
|
Action<LiveSessionCharacterSelection> Selected,
|
||||||
|
Action<LiveSessionCharacterSelection> Entered);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Focused adapter between the session lifetime owner and App composition.
|
||||||
|
/// It tracks only the exact borrowed session attached to the host; domain and
|
||||||
|
/// presentation state remain behind the supplied lifecycle callbacks.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LiveSessionLifecycleHost : ILiveSessionLifecycleHost
|
||||||
|
{
|
||||||
|
private readonly LiveSessionLifecycleBindings _bindings;
|
||||||
|
private WorldSession? _boundSession;
|
||||||
|
|
||||||
|
public LiveSessionLifecycleHost(LiveSessionLifecycleBindings bindings)
|
||||||
|
{
|
||||||
|
_bindings = bindings ?? throw new ArgumentNullException(nameof(bindings));
|
||||||
|
ArgumentNullException.ThrowIfNull(bindings.Bind);
|
||||||
|
ArgumentNullException.ThrowIfNull(bindings.Reset);
|
||||||
|
ArgumentNullException.ThrowIfNull(bindings.Connecting);
|
||||||
|
ArgumentNullException.ThrowIfNull(bindings.Connected);
|
||||||
|
ArgumentNullException.ThrowIfNull(bindings.Selected);
|
||||||
|
ArgumentNullException.ThrowIfNull(bindings.Entered);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveSessionBinding BindSession(WorldSession session)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(session);
|
||||||
|
if (_boundSession is not null)
|
||||||
|
throw new InvalidOperationException("A live session is already attached to this host.");
|
||||||
|
|
||||||
|
LiveSessionBinding binding = _bindings.Bind(session);
|
||||||
|
_boundSession = session;
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetSessionState() => _bindings.Reset();
|
||||||
|
|
||||||
|
public void ReportConnecting(string host, int port, string user) =>
|
||||||
|
_bindings.Connecting(host, port, user);
|
||||||
|
|
||||||
|
public void ReportConnected() => _bindings.Connected();
|
||||||
|
|
||||||
|
public void ApplySelectedCharacter(LiveSessionCharacterSelection selection) =>
|
||||||
|
_bindings.Selected(selection);
|
||||||
|
|
||||||
|
public void ApplyEnteredWorld(LiveSessionCharacterSelection selection) =>
|
||||||
|
_bindings.Entered(selection);
|
||||||
|
|
||||||
|
public void DetachSession(WorldSession session)
|
||||||
|
{
|
||||||
|
if (!ReferenceEquals(_boundSession, session))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The live-session controller attempted to detach a session that is not bound.");
|
||||||
|
_boundSession = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -813,10 +813,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
private bool DevToolsEnabled => _options.DevTools;
|
private bool DevToolsEnabled => _options.DevTools;
|
||||||
private bool DumpMoveTruthEnabled => _options.DumpMoveTruth;
|
private bool DumpMoveTruthEnabled => _options.DumpMoveTruth;
|
||||||
|
|
||||||
// Slice 3: exact session-generation owners. The command router is itself
|
// Slice 3: the reset transaction remains cached by the host, while the
|
||||||
// an ICommandBus and becomes inert before the displaced socket closes.
|
// lifecycle controller owns each exact event/command/session generation.
|
||||||
private AcDream.App.Net.LiveSessionEventRouter? _liveSessionEvents;
|
|
||||||
private AcDream.App.Net.LiveSessionCommandRouter? _liveSessionCommands;
|
|
||||||
private AcDream.App.Net.LiveSessionResetPlan? _liveSessionResetPlan;
|
private AcDream.App.Net.LiveSessionResetPlan? _liveSessionResetPlan;
|
||||||
|
|
||||||
// Phase G.1-G.2 world lighting/time state.
|
// Phase G.1-G.2 world lighting/time state.
|
||||||
|
|
@ -954,7 +952,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
private bool _autoRunActive;
|
private bool _autoRunActive;
|
||||||
|
|
||||||
// Phase K.2 — auto-enter player mode after a successful login. Armed
|
// Phase K.2 — auto-enter player mode after a successful login. Armed
|
||||||
// by the EnterWorld branch in BeginLiveSessionAsync; ticked from
|
// by ApplyLiveSessionEnteredWorld; ticked from
|
||||||
// OnUpdate; disarmed if the user manually enters fly mode (or any
|
// OnUpdate; disarmed if the user manually enters fly mode (or any
|
||||||
// other path that pre-empts the chase camera). Skipped entirely
|
// other path that pre-empts the chase camera). Skipped entirely
|
||||||
// offline (orbit camera stays the foreground). The class internally
|
// offline (orbit camera stays the foreground). The class internally
|
||||||
|
|
@ -1004,12 +1002,11 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
// Phase 4.7: optional live connection to an ACE server. Enabled only when
|
// Phase 4.7: optional live connection to an ACE server. Enabled only when
|
||||||
// ACDREAM_LIVE=1 is in the environment — fully backward compatible with
|
// ACDREAM_LIVE=1 is in the environment — fully backward compatible with
|
||||||
// the offline rendering pipeline.
|
// the offline rendering pipeline.
|
||||||
// Step 2 re-attempt (2026-05-16, debug pass): the network-side lifecycle
|
// Slice 3: the controller is the sole App owner. Outbound feature
|
||||||
// (DNS, endpoint, WorldSession construction, Tick, Dispose) lives in
|
// callbacks resolve this borrowed handle at call time and never cache it.
|
||||||
// LiveSessionController. _liveSession remains as a convenience handle
|
|
||||||
// for the ~60 outbound SendXxx call sites; it tracks Controller.Session.
|
|
||||||
private AcDream.App.Net.LiveSessionController? _liveSessionController;
|
private AcDream.App.Net.LiveSessionController? _liveSessionController;
|
||||||
private AcDream.Core.Net.WorldSession? _liveSession;
|
private AcDream.Core.Net.WorldSession? LiveSession =>
|
||||||
|
_liveSessionController?.CurrentSession;
|
||||||
private int _liveCenterX;
|
private int _liveCenterX;
|
||||||
private int _liveCenterY;
|
private int _liveCenterY;
|
||||||
// #192 (2026-07-09): true once the player's first canonical Position has
|
// #192 (2026-07-09): true once the player's first canonical Position has
|
||||||
|
|
@ -1184,13 +1181,13 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
_localPlayerProjection.Project(controller, movement, hidden),
|
_localPlayerProjection.Project(controller, movement, hidden),
|
||||||
sendPreNetwork: (controller, movement, hidden) =>
|
sendPreNetwork: (controller, movement, hidden) =>
|
||||||
_localPlayerOutbound.SendPreNetworkActions(
|
_localPlayerOutbound.SendPreNetworkActions(
|
||||||
_liveSession,
|
LiveSession,
|
||||||
controller,
|
controller,
|
||||||
movement,
|
movement,
|
||||||
hidden),
|
hidden),
|
||||||
sendPostNetwork: (controller, hidden) =>
|
sendPostNetwork: (controller, hidden) =>
|
||||||
_localPlayerOutbound.SendPostNetworkPosition(
|
_localPlayerOutbound.SendPostNetworkPosition(
|
||||||
_liveSession,
|
LiveSession,
|
||||||
controller,
|
controller,
|
||||||
hidden),
|
hidden),
|
||||||
objectClockDisposition: () =>
|
objectClockDisposition: () =>
|
||||||
|
|
@ -1324,9 +1321,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
// when the entity isn't ready (the third predicate
|
// when the entity isn't ready (the third predicate
|
||||||
// guarantees readiness before this fires).
|
// guarantees readiness before this fires).
|
||||||
_playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry(
|
_playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry(
|
||||||
isLiveInWorld: () => _liveSession is not null
|
isLiveInWorld: () => _liveSessionController?.IsInWorld == true,
|
||||||
&& _liveSession.CurrentState ==
|
|
||||||
AcDream.Core.Net.WorldSession.State.InWorld,
|
|
||||||
isPlayerEntityPresent: () => _entitiesByServerGuid.ContainsKey(_playerServerGuid),
|
isPlayerEntityPresent: () => _entitiesByServerGuid.ContainsKey(_playerServerGuid),
|
||||||
isPlayerControllerReady: () => true,
|
isPlayerControllerReady: () => true,
|
||||||
// Retail SmartBox::UseTime (0x00455410) completes the player
|
// Retail SmartBox::UseTime (0x00455410) completes the player
|
||||||
|
|
@ -1781,7 +1776,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// _activeToonKey is updated by
|
// _activeToonKey is updated by
|
||||||
// BeginLiveSessionAsync after EnterWorld
|
// ApplyLiveSessionEnteredWorld
|
||||||
// so saving character settings always
|
// so saving character settings always
|
||||||
// writes under the chosen character's
|
// writes under the chosen character's
|
||||||
// name (or "default" pre-login).
|
// name (or "default" pre-login).
|
||||||
|
|
@ -2015,7 +2010,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
CanStartLiveCombatAttack,
|
CanStartLiveCombatAttack,
|
||||||
SendLiveCombatAttack,
|
SendLiveCombatAttack,
|
||||||
prepareAttackRequest: PreparePlayerForAttackRequest,
|
prepareAttackRequest: PreparePlayerForAttackRequest,
|
||||||
sendCancelAttack: () => _liveSession?.SendCancelAttack(),
|
sendCancelAttack: () => LiveSession?.SendCancelAttack(),
|
||||||
isDualWield: () => _playerController?.Motion.InterpretedState.CurrentStyle
|
isDualWield: () => _playerController?.Motion.InterpretedState.CurrentStyle
|
||||||
== AcDream.Core.Combat.CombatInputPlanner.DualWieldCombatStyle,
|
== AcDream.Core.Combat.CombatInputPlanner.DualWieldCombatStyle,
|
||||||
playerReadyForAttack: IsPlayerReadyForCombatAttack,
|
playerReadyForAttack: IsPlayerReadyForCombatAttack,
|
||||||
|
|
@ -2028,7 +2023,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
_externalContainerLifecycle = new AcDream.App.World.ExternalContainerLifecycleController(
|
_externalContainerLifecycle = new AcDream.App.World.ExternalContainerLifecycleController(
|
||||||
_externalContainers,
|
_externalContainers,
|
||||||
Objects,
|
Objects,
|
||||||
guid => _liveSession?.SendNoLongerViewingContents(guid));
|
guid => LiveSession?.SendNoLongerViewingContents(guid));
|
||||||
|
|
||||||
AcDream.App.Spells.MagicCatalog magicCatalog = _magicCatalog!;
|
AcDream.App.Spells.MagicCatalog magicCatalog = _magicCatalog!;
|
||||||
_itemInteractionController = new AcDream.App.UI.ItemInteractionController(
|
_itemInteractionController = new AcDream.App.UI.ItemInteractionController(
|
||||||
|
|
@ -2041,25 +2036,24 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
// WorldSession directly here made keyboard R approach a corpse
|
// WorldSession directly here made keyboard R approach a corpse
|
||||||
// without completing the same open transaction as double-click.
|
// without completing the same open transaction as double-click.
|
||||||
sendUse: SendUse,
|
sendUse: SendUse,
|
||||||
sendExamine: g => _liveSession?.SendAppraise(g),
|
sendExamine: g => LiveSession?.SendAppraise(g),
|
||||||
sendUseWithTarget: (source, target) => _liveSession?.SendUseWithTarget(source, target),
|
sendUseWithTarget: (source, target) => LiveSession?.SendUseWithTarget(source, target),
|
||||||
sendWield: (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask),
|
sendWield: (item, mask) => LiveSession?.SendGetAndWieldItem(item, mask),
|
||||||
sendDrop: item => _liveSession?.SendDropItem(item),
|
sendDrop: item => LiveSession?.SendDropItem(item),
|
||||||
sendGive: (target, item, amount) =>
|
sendGive: (target, item, amount) =>
|
||||||
_liveSession?.SendGiveObject(target, item, amount),
|
LiveSession?.SendGiveObject(target, item, amount),
|
||||||
dragOnPlayerOpensSecureTrade: () =>
|
dragOnPlayerOpensSecureTrade: () =>
|
||||||
(_characterOptions1
|
(_characterOptions1
|
||||||
& AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1
|
& AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1
|
||||||
.DragItemOnPlayerOpensSecureTrade) != 0,
|
.DragItemOnPlayerOpensSecureTrade) != 0,
|
||||||
toast: text => _debugVm?.AddToast(text),
|
toast: text => _debugVm?.AddToast(text),
|
||||||
readyForInventoryRequest: () => _liveSession is not null
|
readyForInventoryRequest: () => _liveSessionController?.IsInWorld == true,
|
||||||
&& _liveSession.CurrentState == AcDream.Core.Net.WorldSession.State.InWorld,
|
|
||||||
playerOnGround: GetDebugPlayerOnGround,
|
playerOnGround: GetDebugPlayerOnGround,
|
||||||
inNonCombatMode: () => Combat.CurrentMode
|
inNonCombatMode: () => Combat.CurrentMode
|
||||||
== AcDream.Core.Combat.CombatMode.NonCombat,
|
== AcDream.Core.Combat.CombatMode.NonCombat,
|
||||||
combatState: Combat,
|
combatState: Combat,
|
||||||
sendChangeCombatMode: mode =>
|
sendChangeCombatMode: mode =>
|
||||||
_liveSession?.SendChangeCombatMode(mode),
|
LiveSession?.SendChangeCombatMode(mode),
|
||||||
isComponentPack: magicCatalog.IsComponentPack,
|
isComponentPack: magicCatalog.IsComponentPack,
|
||||||
placeInBackpack: SendPickUp,
|
placeInBackpack: SendPickUp,
|
||||||
backpackContainerId: () =>
|
backpackContainerId: () =>
|
||||||
|
|
@ -2071,14 +2065,14 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
// the retained controller remains alive.
|
// the retained controller remains alive.
|
||||||
groundObjectId: () => _externalContainers.CurrentContainerId,
|
groundObjectId: () => _externalContainers.CurrentContainerId,
|
||||||
sendSplitToWorld: (item, amount) =>
|
sendSplitToWorld: (item, amount) =>
|
||||||
_liveSession?.SendStackableSplitTo3D(item, amount),
|
LiveSession?.SendStackableSplitTo3D(item, amount),
|
||||||
selectedObjectId: () => _selection.SelectedObjectId ?? 0u,
|
selectedObjectId: () => _selection.SelectedObjectId ?? 0u,
|
||||||
stackSplitQuantity: _stackSplitQuantity,
|
stackSplitQuantity: _stackSplitQuantity,
|
||||||
systemMessage: text => Chat.OnSystemMessage(text, 0x1Au),
|
systemMessage: text => Chat.OnSystemMessage(text, 0x1Au),
|
||||||
sendPutItemInContainer: (item, container, placement) =>
|
sendPutItemInContainer: (item, container, placement) =>
|
||||||
_liveSession?.SendPutItemInContainer(item, container, placement),
|
LiveSession?.SendPutItemInContainer(item, container, placement),
|
||||||
sendSplitToContainer: (item, container, placement, amount) =>
|
sendSplitToContainer: (item, container, placement, amount) =>
|
||||||
_liveSession?.SendStackableSplitToContainer(
|
LiveSession?.SendStackableSplitToContainer(
|
||||||
item, container, placement, amount),
|
item, container, placement, amount),
|
||||||
requestExternalContainer: guid => _externalContainers.RequestOpen(guid));
|
requestExternalContainer: guid => _externalContainers.RequestOpen(guid));
|
||||||
|
|
||||||
|
|
@ -2101,12 +2095,11 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
playerGuid: () => _playerServerGuid,
|
playerGuid: () => _playerServerGuid,
|
||||||
activeToonName: () => _activeToonKey,
|
activeToonName: () => _activeToonKey,
|
||||||
fallbackSheet: AcDream.App.Studio.SampleData.SampleCharacter,
|
fallbackSheet: AcDream.App.Studio.SampleData.SampleCharacter,
|
||||||
canSendRaise: () => _liveSession is not null
|
canSendRaise: () => _liveSessionController?.IsInWorld == true,
|
||||||
&& _liveSession.CurrentState == AcDream.Core.Net.WorldSession.State.InWorld,
|
sendRaiseAttribute: (statId, cost) => LiveSession?.SendRaiseAttribute(statId, cost),
|
||||||
sendRaiseAttribute: (statId, cost) => _liveSession?.SendRaiseAttribute(statId, cost),
|
sendRaiseVital: (statId, cost) => LiveSession?.SendRaiseVital(statId, cost),
|
||||||
sendRaiseVital: (statId, cost) => _liveSession?.SendRaiseVital(statId, cost),
|
sendRaiseSkill: (statId, cost) => LiveSession?.SendRaiseSkill(statId, cost),
|
||||||
sendRaiseSkill: (statId, cost) => _liveSession?.SendRaiseSkill(statId, cost),
|
sendTrainSkill: (statId, credits) => LiveSession?.SendTrainSkill(statId, credits));
|
||||||
sendTrainSkill: (statId, credits) => _liveSession?.SendTrainSkill(statId, credits));
|
|
||||||
_magicRuntime = AcDream.App.Spells.MagicRuntime.Create(
|
_magicRuntime = AcDream.App.Spells.MagicRuntime.Create(
|
||||||
magicCatalog,
|
magicCatalog,
|
||||||
SpellBook,
|
SpellBook,
|
||||||
|
|
@ -2115,16 +2108,15 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
localPlayerId: () => _playerServerGuid,
|
localPlayerId: () => _playerServerGuid,
|
||||||
// SpellFormula::Randomize hashes the canonical account spelling
|
// SpellFormula::Randomize hashes the canonical account spelling
|
||||||
// delivered by CharacterList.
|
// delivered by CharacterList.
|
||||||
accountName: () => _liveSession?.Characters?.AccountName
|
accountName: () => LiveSession?.Characters?.AccountName
|
||||||
?? _options.LiveUser
|
?? _options.LiveUser
|
||||||
?? string.Empty,
|
?? string.Empty,
|
||||||
stopCompletely: PreparePlayerForAttackRequest,
|
stopCompletely: PreparePlayerForAttackRequest,
|
||||||
sendUntargeted: spellId => _liveSession?.SendCastUntargetedSpell(spellId),
|
sendUntargeted: spellId => LiveSession?.SendCastUntargetedSpell(spellId),
|
||||||
sendTargeted: (target, spellId) => _liveSession?.SendCastTargetedSpell(target, spellId),
|
sendTargeted: (target, spellId) => LiveSession?.SendCastTargetedSpell(target, spellId),
|
||||||
displayMessage: text => Chat.OnSystemMessage(text, 0x1Au),
|
displayMessage: text => Chat.OnSystemMessage(text, 0x1Au),
|
||||||
incrementBusy: () => _itemInteractionController.IncrementBusyCount(),
|
incrementBusy: () => _itemInteractionController.IncrementBusyCount(),
|
||||||
canSend: () => _liveSession is not null
|
canSend: () => _liveSessionController?.IsInWorld == true);
|
||||||
&& _liveSession.CurrentState == AcDream.Core.Net.WorldSession.State.InWorld);
|
|
||||||
_uiHost.Root.DragReleasedOutsideUi += OnUiDragReleasedOutside;
|
_uiHost.Root.DragReleasedOutsideUi += OnUiDragReleasedOutside;
|
||||||
|
|
||||||
// Feed Silk input to the UiRoot tree so windows drag / close / select.
|
// Feed Silk input to the UiRoot tree so windows drag / close / select.
|
||||||
|
|
@ -2223,8 +2215,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
Vitals: new AcDream.App.UI.VitalsRuntimeBindings(_vitalsVm),
|
Vitals: new AcDream.App.UI.VitalsRuntimeBindings(_vitalsVm),
|
||||||
Chat: new AcDream.App.UI.ChatRuntimeBindings(
|
Chat: new AcDream.App.UI.ChatRuntimeBindings(
|
||||||
retailChatVm,
|
retailChatVm,
|
||||||
() => _liveSessionCommands ?? (AcDream.UI.Abstractions.ICommandBus)
|
() => _liveSessionController?.Commands
|
||||||
AcDream.UI.Abstractions.NullCommandBus.Instance),
|
?? AcDream.UI.Abstractions.NullCommandBus.Instance),
|
||||||
Radar: new AcDream.App.UI.RadarRuntimeBindings(
|
Radar: new AcDream.App.UI.RadarRuntimeBindings(
|
||||||
radarSnapshotProvider.BuildSnapshot,
|
radarSnapshotProvider.BuildSnapshot,
|
||||||
_selection,
|
_selection,
|
||||||
|
|
@ -2252,13 +2244,13 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
guid, AcDream.Core.Selection.SelectionChangeSource.Inventory),
|
guid, AcDream.Core.Selection.SelectionChangeSource.Inventory),
|
||||||
UseItemByGuid,
|
UseItemByGuid,
|
||||||
(tab, position, spellId) =>
|
(tab, position, spellId) =>
|
||||||
_liveSession?.SendAddSpellFavorite(spellId, position, tab),
|
LiveSession?.SendAddSpellFavorite(spellId, position, tab),
|
||||||
(tab, spellId) =>
|
(tab, spellId) =>
|
||||||
_liveSession?.SendRemoveSpellFavorite(spellId, tab),
|
LiveSession?.SendRemoveSpellFavorite(spellId, tab),
|
||||||
filters => _liveSession?.SendSpellbookFilter(filters),
|
filters => LiveSession?.SendSpellbookFilter(filters),
|
||||||
spellId => _liveSession?.SendRemoveSpell(spellId),
|
spellId => LiveSession?.SendRemoveSpell(spellId),
|
||||||
(componentId, amount) =>
|
(componentId, amount) =>
|
||||||
_liveSession?.SendSetDesiredComponentLevel(componentId, amount),
|
LiveSession?.SendSetDesiredComponentLevel(componentId, amount),
|
||||||
ClientTimerNow),
|
ClientTimerNow),
|
||||||
JumpPowerbar: new AcDream.App.UI.JumpPowerbarRuntimeBindings(
|
JumpPowerbar: new AcDream.App.UI.JumpPowerbarRuntimeBindings(
|
||||||
() => _playerController?.JumpCharge ?? default),
|
() => _playerController?.JumpCharge ?? default),
|
||||||
|
|
@ -2284,10 +2276,10 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
() => LocalPlayer.GetAttribute(
|
() => LocalPlayer.GetAttribute(
|
||||||
AcDream.Core.Player.LocalPlayerState.AttributeKind.Strength)
|
AcDream.Core.Player.LocalPlayerState.AttributeKind.Strength)
|
||||||
is { } strength ? (int?)strength.Current : null,
|
is { } strength ? (int?)strength.Current : null,
|
||||||
() => _liveSession?.LinkStatus
|
() => LiveSession?.LinkStatus
|
||||||
?? AcDream.Core.Net.LinkStatusSnapshot.Disconnected,
|
?? AcDream.Core.Net.LinkStatusSnapshot.Disconnected,
|
||||||
ClientTimerNow,
|
ClientTimerNow,
|
||||||
() => _liveSession?.RequestLinkStatusPing(),
|
() => LiveSession?.RequestLinkStatusPing(),
|
||||||
() => _window?.Close()),
|
() => _window?.Close()),
|
||||||
Toolbar: new AcDream.App.UI.ToolbarRuntimeBindings(
|
Toolbar: new AcDream.App.UI.ToolbarRuntimeBindings(
|
||||||
Objects,
|
Objects,
|
||||||
|
|
@ -2299,8 +2291,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
ItemMana,
|
ItemMana,
|
||||||
ToggleLiveCombatMode,
|
ToggleLiveCombatMode,
|
||||||
_itemInteractionController,
|
_itemInteractionController,
|
||||||
entry => _liveSession?.SendAddShortcut(entry),
|
entry => LiveSession?.SendAddShortcut(entry),
|
||||||
index => _liveSession?.SendRemoveShortcut(index),
|
index => LiveSession?.SendRemoveShortcut(index),
|
||||||
_selection,
|
_selection,
|
||||||
handler => Combat.HealthChanged += handler,
|
handler => Combat.HealthChanged += handler,
|
||||||
handler => Combat.HealthChanged -= handler,
|
handler => Combat.HealthChanged -= handler,
|
||||||
|
|
@ -2309,11 +2301,11 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
Combat.GetHealthPercent,
|
Combat.GetHealthPercent,
|
||||||
Combat.HasHealth,
|
Combat.HasHealth,
|
||||||
guid => (uint)(Objects.Get(guid)?.StackSize ?? 0),
|
guid => (uint)(Objects.Get(guid)?.StackSize ?? 0),
|
||||||
guid => _liveSession?.SendQueryHealth(guid),
|
guid => LiveSession?.SendQueryHealth(guid),
|
||||||
guid => _liveSession?.SendQueryItemMana(guid),
|
guid => LiveSession?.SendQueryItemMana(guid),
|
||||||
() => _playerServerGuid,
|
() => _playerServerGuid,
|
||||||
(item, container, placement) =>
|
(item, container, placement) =>
|
||||||
_liveSession?.SendPutItemInContainer(item, container, placement)),
|
LiveSession?.SendPutItemInContainer(item, container, placement)),
|
||||||
Character: new AcDream.App.UI.CharacterRuntimeBindings(_characterSheetProvider),
|
Character: new AcDream.App.UI.CharacterRuntimeBindings(_characterSheetProvider),
|
||||||
Inventory: new AcDream.App.UI.InventoryRuntimeBindings(
|
Inventory: new AcDream.App.UI.InventoryRuntimeBindings(
|
||||||
Objects,
|
Objects,
|
||||||
|
|
@ -2323,14 +2315,14 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
() => LocalPlayer.GetAttribute(
|
() => LocalPlayer.GetAttribute(
|
||||||
AcDream.Core.Player.LocalPlayerState.AttributeKind.Strength)
|
AcDream.Core.Player.LocalPlayerState.AttributeKind.Strength)
|
||||||
is { } strength ? (int?)strength.Current : null,
|
is { } strength ? (int?)strength.Current : null,
|
||||||
guid => _liveSession?.SendUse(guid),
|
guid => LiveSession?.SendUse(guid),
|
||||||
(item, container, placement) =>
|
(item, container, placement) =>
|
||||||
_liveSession?.SendPutItemInContainer(item, container, placement),
|
LiveSession?.SendPutItemInContainer(item, container, placement),
|
||||||
(item, container, placement, amount) =>
|
(item, container, placement, amount) =>
|
||||||
_liveSession?.SendStackableSplitToContainer(
|
LiveSession?.SendStackableSplitToContainer(
|
||||||
item, container, placement, amount),
|
item, container, placement, amount),
|
||||||
(source, target, amount) =>
|
(source, target, amount) =>
|
||||||
_liveSession?.SendStackableMerge(source, target, amount),
|
LiveSession?.SendStackableMerge(source, target, amount),
|
||||||
_itemInteractionController,
|
_itemInteractionController,
|
||||||
_selection),
|
_selection),
|
||||||
ExternalContainer: new AcDream.App.UI.ExternalContainerRuntimeBindings(
|
ExternalContainer: new AcDream.App.UI.ExternalContainerRuntimeBindings(
|
||||||
|
|
@ -2342,11 +2334,11 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
iconComposer.GetDragIcon(type, icon, under, over, effects),
|
iconComposer.GetDragIcon(type, icon, under, over, effects),
|
||||||
_itemInteractionController,
|
_itemInteractionController,
|
||||||
_selection,
|
_selection,
|
||||||
guid => _liveSession?.SendUse(guid),
|
guid => LiveSession?.SendUse(guid),
|
||||||
(item, container, placement) =>
|
(item, container, placement) =>
|
||||||
_liveSession?.SendPutItemInContainer(item, container, placement),
|
LiveSession?.SendPutItemInContainer(item, container, placement),
|
||||||
(item, container, placement, amount) =>
|
(item, container, placement, amount) =>
|
||||||
_liveSession?.SendStackableSplitToContainer(
|
LiveSession?.SendStackableSplitToContainer(
|
||||||
item, container, placement, amount),
|
item, container, placement, amount),
|
||||||
IsWithinExternalContainerUseRange),
|
IsWithinExternalContainerUseRange),
|
||||||
Cursor: new AcDream.App.UI.RetailUiCursorBindings(
|
Cursor: new AcDream.App.UI.RetailUiCursorBindings(
|
||||||
|
|
@ -2354,7 +2346,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
retailCursorManager),
|
retailCursorManager),
|
||||||
Confirmations: new AcDream.App.UI.ConfirmationRuntimeBindings(
|
Confirmations: new AcDream.App.UI.ConfirmationRuntimeBindings(
|
||||||
(type, context, accepted) =>
|
(type, context, accepted) =>
|
||||||
_liveSession?.SendConfirmationResponse(type, context, accepted)),
|
LiveSession?.SendConfirmationResponse(type, context, accepted)),
|
||||||
StackSplitQuantity: _stackSplitQuantity,
|
StackSplitQuantity: _stackSplitQuantity,
|
||||||
Plugins: _uiRegistry,
|
Plugins: _uiRegistry,
|
||||||
Persistence: persistence,
|
Persistence: persistence,
|
||||||
|
|
@ -2704,7 +2696,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
_worldSelectionQuery,
|
_worldSelectionQuery,
|
||||||
itemInteractions,
|
itemInteractions,
|
||||||
new AcDream.App.Interaction.WorldSessionSelectionInteractionTransport(
|
new AcDream.App.Interaction.WorldSessionSelectionInteractionTransport(
|
||||||
() => _liveSession),
|
() => LiveSession),
|
||||||
new AcDream.App.Interaction.PlayerInteractionMovementSink(
|
new AcDream.App.Interaction.PlayerInteractionMovementSink(
|
||||||
() => _playerController),
|
() => _playerController),
|
||||||
text => _debugVm?.AddToast(text));
|
text => _debugVm?.AddToast(text));
|
||||||
|
|
@ -2854,75 +2846,54 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
// gated behind ACDREAM_LIVE=1 so the default run path is unchanged.
|
// gated behind ACDREAM_LIVE=1 so the default run path is unchanged.
|
||||||
_liveCenterX = centerX;
|
_liveCenterX = centerX;
|
||||||
_liveCenterY = centerY;
|
_liveCenterY = centerY;
|
||||||
TryStartLiveSession();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TryStartLiveSession()
|
|
||||||
{
|
|
||||||
ClearInboundEntityState();
|
|
||||||
|
|
||||||
// Step 2 (2026-05-16): delegate pre-Connect setup to LiveSessionController.
|
|
||||||
// The controller owns DNS resolution + WorldSession instantiation + the
|
|
||||||
// wireEvents callback; this method keeps the Connect → CharacterList →
|
|
||||||
// EnterWorld → post-setup dance because those touch GameWindow state.
|
|
||||||
_liveSessionController = new AcDream.App.Net.LiveSessionController();
|
_liveSessionController = new AcDream.App.Net.LiveSessionController();
|
||||||
_liveSession = _liveSessionController.CreateAndWire(_options, WireLiveSessionEvents);
|
AcDream.App.Net.LiveSessionStartResult liveStart =
|
||||||
if (_liveSession is null)
|
_liveSessionController.Start(
|
||||||
|
_options,
|
||||||
|
CreateLiveSessionLifecycleHost());
|
||||||
|
switch (liveStart.Status)
|
||||||
{
|
{
|
||||||
try
|
case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials:
|
||||||
{
|
Console.WriteLine(
|
||||||
DisposeLiveSessionRouting();
|
"live: ACDREAM_LIVE set but TEST_USER/TEST_PASS missing; skipping");
|
||||||
|
break;
|
||||||
|
case AcDream.App.Net.LiveSessionStartStatus.Failed:
|
||||||
|
Console.WriteLine($"live: session failed: {liveStart.Error}");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
_liveSessionController.Dispose();
|
|
||||||
_liveSessionController = null;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = _options.LiveUser!;
|
private AcDream.App.Net.LiveSessionLifecycleHost CreateLiveSessionLifecycleHost() =>
|
||||||
var pass = _options.LivePass!;
|
new(new AcDream.App.Net.LiveSessionLifecycleBindings(
|
||||||
var host = _options.LiveHost;
|
Bind: CreateLiveSessionBinding,
|
||||||
var port = _options.LivePort;
|
Reset: () =>
|
||||||
try
|
(_liveSessionResetPlan ??= CreateLiveSessionResetPlan()).Execute(),
|
||||||
{
|
Connecting: (host, port, user) =>
|
||||||
Chat.OnSystemMessage($"connecting to {host}:{port} as {user}", chatType: 1);
|
Chat.OnSystemMessage(
|
||||||
_liveSession.Connect(user, pass);
|
$"connecting to {host}:{port} as {user}",
|
||||||
Chat.OnSystemMessage("connected — character list received", chatType: 1);
|
chatType: 1),
|
||||||
|
Connected: () =>
|
||||||
|
Chat.OnSystemMessage(
|
||||||
|
"connected — character list received",
|
||||||
|
chatType: 1),
|
||||||
|
Selected: ApplyLiveSessionSelection,
|
||||||
|
Entered: ApplyLiveSessionEnteredWorld));
|
||||||
|
|
||||||
if (_liveSession.Characters is null
|
private void ApplyLiveSessionSelection(
|
||||||
|| !AcDream.Core.Net.Messages.CharacterList.TrySelectFirstAvailable(
|
AcDream.App.Net.LiveSessionCharacterSelection selection)
|
||||||
_liveSession.Characters,
|
|
||||||
out AcDream.Core.Net.Messages.CharacterList.Selection selection))
|
|
||||||
{
|
{
|
||||||
Console.WriteLine("live: no available characters on account; disconnecting");
|
_playerServerGuid = selection.CharacterId;
|
||||||
try
|
_vitalsVm?.SetLocalPlayerGuid(selection.CharacterId);
|
||||||
{
|
Chat.SetLocalPlayerGuid(selection.CharacterId);
|
||||||
DisposeLiveSessionRouting();
|
_worldState.MarkPersistent(selection.CharacterId);
|
||||||
}
|
AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = selection.CharacterId;
|
||||||
finally
|
|
||||||
{
|
|
||||||
_liveSessionController.Dispose();
|
|
||||||
_liveSessionController = null;
|
|
||||||
_liveSession = null;
|
|
||||||
}
|
|
||||||
ClearInboundEntityState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var chosen = selection.Character;
|
|
||||||
_playerServerGuid = chosen.Id;
|
|
||||||
_vitalsVm?.SetLocalPlayerGuid(chosen.Id);
|
|
||||||
Chat.SetLocalPlayerGuid(chosen.Id);
|
|
||||||
_worldState.MarkPersistent(chosen.Id);
|
|
||||||
AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = chosen.Id; // TEMP #138-B probe
|
|
||||||
Console.WriteLine($"live: entering world as 0x{chosen.Id:X8} {chosen.Name}");
|
|
||||||
Combat.Clear();
|
Combat.Clear();
|
||||||
_liveSession.EnterWorld(characterIndex: selection.ActiveIndex);
|
}
|
||||||
_liveSessionCommands?.Activate();
|
|
||||||
|
|
||||||
_activeToonKey = chosen.Name;
|
private void ApplyLiveSessionEnteredWorld(
|
||||||
|
AcDream.App.Net.LiveSessionCharacterSelection selection)
|
||||||
|
{
|
||||||
|
_activeToonKey = selection.CharacterName;
|
||||||
_retailUiRuntime?.RestoreLayout();
|
_retailUiRuntime?.RestoreLayout();
|
||||||
SyncToolbarWindowButtons();
|
SyncToolbarWindowButtons();
|
||||||
if (_settingsStore is not null && _settingsVm is not null)
|
if (_settingsStore is not null && _settingsVm is not null)
|
||||||
|
|
@ -2932,29 +2903,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
Console.WriteLine($"settings: loaded character[{_activeToonKey}] preferences");
|
Console.WriteLine($"settings: loaded character[{_activeToonKey}] preferences");
|
||||||
}
|
}
|
||||||
_playerModeAutoEntry?.Arm();
|
_playerModeAutoEntry?.Arm();
|
||||||
Console.WriteLine($"live: in world — CreateObject stream active " +
|
|
||||||
$"(so far: {_liveSpawnReceived} received, {_liveSpawnHydrated} hydrated)");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"live: session failed: {ex.Message}");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
DisposeLiveSessionRouting();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_liveSessionController?.Dispose();
|
|
||||||
_liveSessionController = null;
|
|
||||||
_liveSession = null;
|
|
||||||
}
|
|
||||||
ClearInboundEntityState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearInboundEntityState()
|
|
||||||
{
|
|
||||||
(_liveSessionResetPlan ??= CreateLiveSessionResetPlan()).Execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private AcDream.App.Net.LiveSessionResetPlan CreateLiveSessionResetPlan() =>
|
private AcDream.App.Net.LiveSessionResetPlan CreateLiveSessionResetPlan() =>
|
||||||
|
|
@ -3087,12 +3035,9 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
/// Registration completes before Connect; outbound commands remain inert
|
/// Registration completes before Connect; outbound commands remain inert
|
||||||
/// until EnterWorld succeeds.
|
/// until EnterWorld succeeds.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void WireLiveSessionEvents(AcDream.Core.Net.WorldSession session)
|
private AcDream.App.Net.LiveSessionBinding CreateLiveSessionBinding(
|
||||||
|
AcDream.Core.Net.WorldSession session)
|
||||||
{
|
{
|
||||||
_liveSession = session;
|
|
||||||
if (_liveSessionEvents is not null || _liveSessionCommands is not null)
|
|
||||||
throw new InvalidOperationException("live-session routing is already bound");
|
|
||||||
|
|
||||||
var skillTable = _dats?.Get<DatReaderWriter.DBObjs.SkillTable>(0x0E000004u);
|
var skillTable = _dats?.Get<DatReaderWriter.DBObjs.SkillTable>(0x0E000004u);
|
||||||
if (_characterSheetProvider is not null && _dats is not null)
|
if (_characterSheetProvider is not null && _dats is not null)
|
||||||
{
|
{
|
||||||
|
|
@ -3123,8 +3068,12 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
|
|
||||||
commands = new AcDream.App.Net.LiveSessionCommandRouter(
|
commands = new AcDream.App.Net.LiveSessionCommandRouter(
|
||||||
CreateLiveSessionCommandBindings(session));
|
CreateLiveSessionCommandBindings(session));
|
||||||
_liveSessionEvents = events;
|
return new AcDream.App.Net.LiveSessionBinding(
|
||||||
_liveSessionCommands = commands;
|
session,
|
||||||
|
commands,
|
||||||
|
activateCommands: commands.Activate,
|
||||||
|
deactivateCommands: commands.Dispose,
|
||||||
|
detachEvents: events.Dispose);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
@ -3311,22 +3260,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
roomId, chatType, dispatchType, senderGuid, text, cookie),
|
roomId, chatType, dispatchType, senderGuid, text, cookie),
|
||||||
Log: Console.WriteLine);
|
Log: Console.WriteLine);
|
||||||
|
|
||||||
private void DisposeLiveSessionRouting()
|
|
||||||
{
|
|
||||||
AcDream.App.Net.LiveSessionCommandRouter? commands = _liveSessionCommands;
|
|
||||||
AcDream.App.Net.LiveSessionEventRouter? events = _liveSessionEvents;
|
|
||||||
_liveSessionCommands = null;
|
|
||||||
_liveSessionEvents = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
commands?.Dispose();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
events?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum LiveProjectionPurpose
|
private enum LiveProjectionPurpose
|
||||||
{
|
{
|
||||||
LogicalRegistration,
|
LogicalRegistration,
|
||||||
|
|
@ -4936,8 +4869,9 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
uint guid,
|
uint guid,
|
||||||
AcDream.App.World.AcceptedPhysicsTimestamps timestamps)
|
AcDream.App.World.AcceptedPhysicsTimestamps timestamps)
|
||||||
{
|
{
|
||||||
if (guid != _playerServerGuid || _liveSession is null) return;
|
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||||
_liveSession.PublishAcceptedLocalPhysicsTimestamps(
|
if (guid != _playerServerGuid || session is null) return;
|
||||||
|
session.PublishAcceptedLocalPhysicsTimestamps(
|
||||||
timestamps.Instance,
|
timestamps.Instance,
|
||||||
timestamps.ServerControlledMove,
|
timestamps.ServerControlledMove,
|
||||||
timestamps.Teleport,
|
timestamps.Teleport,
|
||||||
|
|
@ -6460,7 +6394,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void SendImmediateLocalPositionEvent()
|
private void SendImmediateLocalPositionEvent()
|
||||||
{
|
{
|
||||||
if (_liveSession is null
|
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||||
|
if (session is null
|
||||||
|| _playerController is null
|
|| _playerController is null
|
||||||
|| !_playerController.CanSendPositionEvent)
|
|| !_playerController.CanSendPositionEvent)
|
||||||
{
|
{
|
||||||
|
|
@ -6477,18 +6412,18 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint sequence = _liveSession.NextGameActionSequence();
|
uint sequence = session.NextGameActionSequence();
|
||||||
var body = AcDream.Core.Net.Messages.AutonomousPosition.Build(
|
var body = AcDream.Core.Net.Messages.AutonomousPosition.Build(
|
||||||
gameActionSequence: sequence,
|
gameActionSequence: sequence,
|
||||||
cellId: cellId,
|
cellId: cellId,
|
||||||
position: position,
|
position: position,
|
||||||
rotation: rotation,
|
rotation: rotation,
|
||||||
instanceSequence: _liveSession.InstanceSequence,
|
instanceSequence: session.InstanceSequence,
|
||||||
serverControlSequence: _liveSession.ServerControlSequence,
|
serverControlSequence: session.ServerControlSequence,
|
||||||
teleportSequence: _liveSession.TeleportSequence,
|
teleportSequence: session.TeleportSequence,
|
||||||
forcePositionSequence: _liveSession.ForcePositionSequence,
|
forcePositionSequence: session.ForcePositionSequence,
|
||||||
lastContact: 1);
|
lastContact: 1);
|
||||||
_liveSession.SendGameAction(body);
|
session.SendGameAction(body);
|
||||||
_playerController.NotePositionSent(
|
_playerController.NotePositionSent(
|
||||||
new AcDream.Core.Physics.Position(cellId, position, rotation),
|
new AcDream.Core.Physics.Position(cellId, position, rotation),
|
||||||
_playerController.ContactPlane,
|
_playerController.ContactPlane,
|
||||||
|
|
@ -9409,8 +9344,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
// The world-geometry RENDER gate (IsLiveModeWaitingForLogin at the
|
// The world-geometry RENDER gate (IsLiveModeWaitingForLogin at the
|
||||||
// draw site) is unchanged — the pre-entry screen still shows sky
|
// draw site) is unchanged — the pre-entry screen still shows sky
|
||||||
// only.
|
// only.
|
||||||
bool liveInWorld = _liveSession is not null
|
bool liveInWorld = _liveSessionController?.IsInWorld == true;
|
||||||
&& _liveSession.CurrentState == AcDream.Core.Net.WorldSession.State.InWorld;
|
|
||||||
// #192: liveInWorld alone used to be sufficient here — but InWorld fires
|
// #192: liveInWorld alone used to be sufficient here — but InWorld fires
|
||||||
// right after the login handshake, before the player's own spawn
|
// right after the login handshake, before the player's own spawn
|
||||||
// CreateObject (and hence the real center) has arrived. Streaming that
|
// CreateObject (and hence the real center) has arrived. Streaming that
|
||||||
|
|
@ -9456,8 +9390,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
observerCx = _liveCenterX + (int)System.Math.Floor(pp.X / 192f);
|
observerCx = _liveCenterX + (int)System.Math.Floor(pp.X / 192f);
|
||||||
observerCy = _liveCenterY + (int)System.Math.Floor(pp.Y / 192f);
|
observerCy = _liveCenterY + (int)System.Math.Floor(pp.Y / 192f);
|
||||||
}
|
}
|
||||||
else if (_liveSession is not null
|
else if (liveInWorld)
|
||||||
&& _liveSession.CurrentState == AcDream.Core.Net.WorldSession.State.InWorld)
|
|
||||||
{
|
{
|
||||||
// Live, not yet in player mode: the login auto-entry hold, or a live
|
// Live, not yet in player mode: the login auto-entry hold, or a live
|
||||||
// fly-camera spectator. Follow the PLAYER's server-known landblock; if it
|
// fly-camera spectator. Follow the PLAYER's server-known landblock; if it
|
||||||
|
|
@ -9657,7 +9590,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
_playerController.State = AcDream.App.Input.PlayerState.InWorld;
|
_playerController.State = AcDream.App.Input.PlayerState.InWorld;
|
||||||
// holtburger client/messages.rs:434 — re-send LoginComplete after
|
// holtburger client/messages.rs:434 — re-send LoginComplete after
|
||||||
// each portal transition.
|
// each portal transition.
|
||||||
_liveSession?.SendGameAction(
|
LiveSession?.SendGameAction(
|
||||||
AcDream.Core.Net.Messages.GameActionLoginComplete.Build());
|
AcDream.Core.Net.Messages.GameActionLoginComplete.Build());
|
||||||
_worldReveal?.Complete();
|
_worldReveal?.Complete();
|
||||||
ResetTeleportTransitState();
|
ResetTeleportTransitState();
|
||||||
|
|
@ -9682,7 +9615,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
|
|
||||||
// Phase K.2 — auto-enter player mode at login. The guard
|
// Phase K.2 — auto-enter player mode at login. The guard
|
||||||
// returns true on the firing tick (one-shot); subsequent ticks
|
// returns true on the firing tick (one-shot); subsequent ticks
|
||||||
// are no-ops. Skipped offline (no _liveSession → IsLiveInWorld
|
// are no-ops. Skipped offline (no active session → IsLiveInWorld
|
||||||
// predicate stays false). Cancelled by manual fly-toggle in
|
// predicate stays false). Cancelled by manual fly-toggle in
|
||||||
// OnInputAction (Ctrl+Tab → TogglePlayerMode) or DebugPanel.
|
// OnInputAction (Ctrl+Tab → TogglePlayerMode) or DebugPanel.
|
||||||
_playerModeAutoEntry?.TryEnter();
|
_playerModeAutoEntry?.TryEnter();
|
||||||
|
|
@ -11038,8 +10971,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
// is up so panel-emitted SendChatCmd actually flows server-ward.
|
// is up so panel-emitted SendChatCmd actually flows server-ward.
|
||||||
// Fall back to NullCommandBus for offline / pre-connect renders.
|
// Fall back to NullCommandBus for offline / pre-connect renders.
|
||||||
AcDream.UI.Abstractions.ICommandBus bus =
|
AcDream.UI.Abstractions.ICommandBus bus =
|
||||||
_liveSessionCommands ?? (AcDream.UI.Abstractions.ICommandBus)
|
_liveSessionController?.Commands
|
||||||
AcDream.UI.Abstractions.NullCommandBus.Instance;
|
?? AcDream.UI.Abstractions.NullCommandBus.Instance;
|
||||||
var ctx = new AcDream.UI.Abstractions.PanelContext(
|
var ctx = new AcDream.UI.Abstractions.PanelContext(
|
||||||
(float)deltaSeconds,
|
(float)deltaSeconds,
|
||||||
bus);
|
bus);
|
||||||
|
|
@ -12519,7 +12452,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
private AcDream.UI.Abstractions.Panels.Settings.SettingsPanel? _settingsPanel;
|
private AcDream.UI.Abstractions.Panels.Settings.SettingsPanel? _settingsPanel;
|
||||||
private AcDream.UI.Abstractions.Panels.Settings.SettingsVM? _settingsVm;
|
private AcDream.UI.Abstractions.Panels.Settings.SettingsVM? _settingsVm;
|
||||||
// L.0: settings.json store + active toon key. The store is held as
|
// L.0: settings.json store + active toon key. The store is held as
|
||||||
// a field so BeginLiveSessionAsync can re-load the chosen toon's
|
// a field so ApplyLiveSessionEnteredWorld can re-load the chosen toon's
|
||||||
// bag once we know its name (post-EnterWorld). Toon key starts as
|
// bag once we know its name (post-EnterWorld). Toon key starts as
|
||||||
// "default" and gets swapped to the actual character name on the
|
// "default" and gets swapped to the actual character name on the
|
||||||
// first EnterWorld.
|
// first EnterWorld.
|
||||||
|
|
@ -12654,7 +12587,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
_persistedGameplay = _settingsStore.LoadGameplay();
|
_persistedGameplay = _settingsStore.LoadGameplay();
|
||||||
_persistedChat = _settingsStore.LoadChat();
|
_persistedChat = _settingsStore.LoadChat();
|
||||||
// _activeToonKey is "default" pre-EnterWorld; the post-login
|
// _activeToonKey is "default" pre-EnterWorld; the post-login
|
||||||
// branch in BeginLiveSessionAsync swaps to the chosen toon's
|
// ApplyLiveSessionEnteredWorld swaps to the chosen toon's
|
||||||
// name and re-loads via SettingsVM.LoadCharacterContext.
|
// name and re-loads via SettingsVM.LoadCharacterContext.
|
||||||
_persistedCharacter = _settingsStore.LoadCharacter(_activeToonKey);
|
_persistedCharacter = _settingsStore.LoadCharacter(_activeToonKey);
|
||||||
|
|
||||||
|
|
@ -13153,8 +13086,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
|
|
||||||
private void ToggleLiveCombatMode()
|
private void ToggleLiveCombatMode()
|
||||||
{
|
{
|
||||||
if (_liveSession is null
|
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
if (_liveSessionController?.IsInWorld != true || session is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
IReadOnlyList<AcDream.Core.Items.ClientObject> orderedEquipment =
|
IReadOnlyList<AcDream.Core.Items.ClientObject> orderedEquipment =
|
||||||
|
|
@ -13165,7 +13098,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
Combat.CurrentMode,
|
Combat.CurrentMode,
|
||||||
defaultMode);
|
defaultMode);
|
||||||
_itemInteractionController?.NotifyExplicitCombatModeRequest();
|
_itemInteractionController?.NotifyExplicitCombatModeRequest();
|
||||||
_liveSession.SendChangeCombatMode(nextMode);
|
session.SendChangeCombatMode(nextMode);
|
||||||
Combat.SetCombatMode(nextMode);
|
Combat.SetCombatMode(nextMode);
|
||||||
string text = $"Combat mode {nextMode}";
|
string text = $"Combat mode {nextMode}";
|
||||||
Console.WriteLine($"combat: {text}");
|
Console.WriteLine($"combat: {text}");
|
||||||
|
|
@ -13185,8 +13118,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
|
|
||||||
private bool CanStartLiveCombatAttack()
|
private bool CanStartLiveCombatAttack()
|
||||||
{
|
{
|
||||||
if (_liveSession is null
|
if (_liveSessionController?.IsInWorld != true)
|
||||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!AcDream.Core.Combat.CombatInputPlanner.SupportsTargetedAttack(Combat.CurrentMode))
|
if (!AcDream.Core.Combat.CombatInputPlanner.SupportsTargetedAttack(Combat.CurrentMode))
|
||||||
|
|
@ -13214,16 +13146,20 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
if (!CanStartLiveCombatAttack())
|
if (!CanStartLiveCombatAttack())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||||
|
if (session is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
uint target = _selection.SelectedObjectId!.Value;
|
uint target = _selection.SelectedObjectId!.Value;
|
||||||
power = Math.Clamp(power, 0f, 1f);
|
power = Math.Clamp(power, 0f, 1f);
|
||||||
if (Combat.CurrentMode == AcDream.Core.Combat.CombatMode.Missile)
|
if (Combat.CurrentMode == AcDream.Core.Combat.CombatMode.Missile)
|
||||||
{
|
{
|
||||||
_liveSession!.SendMissileAttack(target, height, power);
|
session.SendMissileAttack(target, height, power);
|
||||||
Console.WriteLine($"combat: missile attack target=0x{target:X8} height={height} accuracy={power:F2}");
|
Console.WriteLine($"combat: missile attack target=0x{target:X8} height={height} accuracy={power:F2}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_liveSession!.SendMeleeAttack(target, height, power);
|
session.SendMeleeAttack(target, height, power);
|
||||||
Console.WriteLine($"combat: melee attack target=0x{target:X8} height={height} power={power:F2}");
|
Console.WriteLine($"combat: melee attack target=0x{target:X8} height={height} power={power:F2}");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -13252,7 +13188,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
|
|
||||||
private bool TrySendPlayerMovementEvent(AcDream.App.Input.MovementResult result)
|
private bool TrySendPlayerMovementEvent(AcDream.App.Input.MovementResult result)
|
||||||
=> _localPlayerOutbound.TrySendMovement(
|
=> _localPlayerOutbound.TrySendMovement(
|
||||||
_liveSession,
|
LiveSession,
|
||||||
_playerController,
|
_playerController,
|
||||||
result);
|
result);
|
||||||
|
|
||||||
|
|
@ -13291,11 +13227,11 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
// sends directly without speculative TurnToObject/MoveToObject.
|
// sends directly without speculative TurnToObject/MoveToObject.
|
||||||
private void UseItemByGuid(uint guid)
|
private void UseItemByGuid(uint guid)
|
||||||
{
|
{
|
||||||
if (_liveSession is null
|
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
if (_liveSessionController?.IsInWorld != true || session is null)
|
||||||
return;
|
return;
|
||||||
uint sequence = _liveSession.NextGameActionSequence();
|
uint sequence = session.NextGameActionSequence();
|
||||||
_liveSession.SendGameAction(
|
session.SendGameAction(
|
||||||
AcDream.Core.Net.Messages.InteractRequests.BuildUse(sequence, guid));
|
AcDream.Core.Net.Messages.InteractRequests.BuildUse(sequence, guid));
|
||||||
Console.WriteLine($"[D.5.1] toolbar use-item guid=0x{guid:X8} seq={sequence}");
|
Console.WriteLine($"[D.5.1] toolbar use-item guid=0x{guid:X8} seq={sequence}");
|
||||||
}
|
}
|
||||||
|
|
@ -13337,8 +13273,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
private void TogglePlayerMode()
|
private void TogglePlayerMode()
|
||||||
{
|
{
|
||||||
// Phase B.2 guard: only active when a live session is in-world.
|
// Phase B.2 guard: only active when a live session is in-world.
|
||||||
if (_liveSession is null
|
if (_liveSessionController?.IsInWorld != true)
|
||||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Manual toggle pre-empts the K.2 auto-entry trigger regardless
|
// Manual toggle pre-empts the K.2 auto-entry trigger regardless
|
||||||
|
|
@ -14223,14 +14158,12 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
_magicCatalog = null;
|
_magicCatalog = null;
|
||||||
}),
|
}),
|
||||||
new("streamer", () => _streamer?.Dispose()),
|
new("streamer", () => _streamer?.Dispose()),
|
||||||
new("live session routing", DisposeLiveSessionRouting),
|
|
||||||
new("equipped children", () => _equippedChildRenderer?.Dispose()),
|
|
||||||
new("live session", () =>
|
new("live session", () =>
|
||||||
{
|
{
|
||||||
_liveSessionController?.Dispose();
|
_liveSessionController?.Dispose();
|
||||||
_liveSessionController = null;
|
_liveSessionController = null;
|
||||||
_liveSession = null;
|
|
||||||
}),
|
}),
|
||||||
|
new("equipped children", () => _equippedChildRenderer?.Dispose()),
|
||||||
]),
|
]),
|
||||||
// Retained tombstones are retried while every callback owner is alive.
|
// Retained tombstones are retried while every callback owner is alive.
|
||||||
new ResourceShutdownStage("live entities",
|
new ResourceShutdownStage("live entities",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Net;
|
||||||
|
using AcDream.Core.Net;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Net;
|
||||||
|
|
||||||
|
public sealed class GameWindowLiveSessionOwnershipTests
|
||||||
|
{
|
||||||
|
private const BindingFlags PrivateInstance =
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GameWindowRetainsOnlyTheLifecycleControllerSessionOwner()
|
||||||
|
{
|
||||||
|
FieldInfo[] fields = typeof(GameWindow).GetFields(PrivateInstance);
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
fields,
|
||||||
|
field => field.Name == "_liveSessionController"
|
||||||
|
&& field.FieldType == typeof(LiveSessionController));
|
||||||
|
Assert.DoesNotContain(fields, field => field.Name == "_liveSession");
|
||||||
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(WorldSession));
|
||||||
|
Assert.DoesNotContain(fields, field => field.Name == "_liveSessionEvents");
|
||||||
|
Assert.DoesNotContain(fields, field => field.Name == "_liveSessionCommands");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("TryStartLiveSession")]
|
||||||
|
[InlineData("ClearInboundEntityState")]
|
||||||
|
[InlineData("WireLiveSessionEvents")]
|
||||||
|
[InlineData("DisposeLiveSessionRouting")]
|
||||||
|
public void DisplacedLifecycleBodiesAreAbsent(string methodName)
|
||||||
|
{
|
||||||
|
Assert.Null(typeof(GameWindow).GetMethod(methodName, PrivateInstance));
|
||||||
|
}
|
||||||
|
}
|
||||||
110
tests/AcDream.App.Tests/Net/LiveSessionLifecycleHostTests.cs
Normal file
110
tests/AcDream.App.Tests/Net/LiveSessionLifecycleHostTests.cs
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
using System.Net;
|
||||||
|
using AcDream.App.Net;
|
||||||
|
using AcDream.Core.Net;
|
||||||
|
using AcDream.UI.Abstractions;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Net;
|
||||||
|
|
||||||
|
public sealed class LiveSessionLifecycleHostTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void HostRoutesLifecycleAndReleasesOnlyTheExactBoundSession()
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
using var sessionA = CreateSession(9000);
|
||||||
|
using var sessionB = CreateSession(9001);
|
||||||
|
var host = CreateHost(calls);
|
||||||
|
|
||||||
|
LiveSessionBinding binding = host.BindSession(sessionA);
|
||||||
|
host.ResetSessionState();
|
||||||
|
host.ReportConnecting("host", 9000, "user");
|
||||||
|
host.ReportConnected();
|
||||||
|
var selection = new LiveSessionCharacterSelection(2, 3u, "toon", "account");
|
||||||
|
host.ApplySelectedCharacter(selection);
|
||||||
|
binding.ActivateCommands();
|
||||||
|
host.ApplyEnteredWorld(selection);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => host.DetachSession(sessionB));
|
||||||
|
binding.Dispose();
|
||||||
|
host.DetachSession(sessionA);
|
||||||
|
LiveSessionBinding replacement = host.BindSession(sessionB);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
"bind", "reset", "connecting:host:9000:user",
|
||||||
|
"connected", "selected:toon", "activate", "entered:toon",
|
||||||
|
"deactivate", "detach-events", "bind",
|
||||||
|
],
|
||||||
|
calls);
|
||||||
|
replacement.Dispose();
|
||||||
|
host.DetachSession(sessionB);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FailedBindingFactoryDoesNotClaimTheHost()
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
using var session = CreateSession(9000);
|
||||||
|
bool fail = true;
|
||||||
|
LiveSessionLifecycleHost host = CreateHost(calls, _ =>
|
||||||
|
{
|
||||||
|
if (fail)
|
||||||
|
{
|
||||||
|
fail = false;
|
||||||
|
throw new InvalidOperationException("bind failure");
|
||||||
|
}
|
||||||
|
return CreateBinding(session, calls);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => host.BindSession(session));
|
||||||
|
LiveSessionBinding retry = host.BindSession(session);
|
||||||
|
|
||||||
|
retry.Dispose();
|
||||||
|
host.DetachSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LiveSessionLifecycleHost CreateHost(
|
||||||
|
List<string> calls,
|
||||||
|
Func<WorldSession, LiveSessionBinding>? bind = null) =>
|
||||||
|
new(new LiveSessionLifecycleBindings(
|
||||||
|
Bind: bind ?? (session => CreateBinding(session, calls)),
|
||||||
|
Reset: () => calls.Add("reset"),
|
||||||
|
Connecting: (host, port, user) =>
|
||||||
|
calls.Add($"connecting:{host}:{port}:{user}"),
|
||||||
|
Connected: () => calls.Add("connected"),
|
||||||
|
Selected: selection => calls.Add($"selected:{selection.CharacterName}"),
|
||||||
|
Entered: selection => calls.Add($"entered:{selection.CharacterName}")));
|
||||||
|
|
||||||
|
private static LiveSessionBinding CreateBinding(
|
||||||
|
WorldSession session,
|
||||||
|
List<string> calls)
|
||||||
|
{
|
||||||
|
calls.Add("bind");
|
||||||
|
return new LiveSessionBinding(
|
||||||
|
session,
|
||||||
|
NullCommandBus.Instance,
|
||||||
|
activateCommands: () => calls.Add("activate"),
|
||||||
|
deactivateCommands: () => calls.Add("deactivate"),
|
||||||
|
detachEvents: () => calls.Add("detach-events"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldSession CreateSession(int port) =>
|
||||||
|
new(
|
||||||
|
new IPEndPoint(IPAddress.Loopback, port),
|
||||||
|
new TestTransport());
|
||||||
|
|
||||||
|
private sealed class TestTransport : IWorldSessionTransport
|
||||||
|
{
|
||||||
|
public void Send(ReadOnlySpan<byte> datagram) { }
|
||||||
|
|
||||||
|
public void Send(IPEndPoint remote, ReadOnlySpan<byte> datagram) { }
|
||||||
|
|
||||||
|
public byte[]? Receive(TimeSpan timeout, out IPEndPoint? from)
|
||||||
|
{
|
||||||
|
from = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue