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
|
|
@ -813,10 +813,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
private bool DevToolsEnabled => _options.DevTools;
|
||||
private bool DumpMoveTruthEnabled => _options.DumpMoveTruth;
|
||||
|
||||
// Slice 3: exact session-generation owners. The command router is itself
|
||||
// an ICommandBus and becomes inert before the displaced socket closes.
|
||||
private AcDream.App.Net.LiveSessionEventRouter? _liveSessionEvents;
|
||||
private AcDream.App.Net.LiveSessionCommandRouter? _liveSessionCommands;
|
||||
// Slice 3: the reset transaction remains cached by the host, while the
|
||||
// lifecycle controller owns each exact event/command/session generation.
|
||||
private AcDream.App.Net.LiveSessionResetPlan? _liveSessionResetPlan;
|
||||
|
||||
// Phase G.1-G.2 world lighting/time state.
|
||||
|
|
@ -954,7 +952,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
private bool _autoRunActive;
|
||||
|
||||
// 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
|
||||
// other path that pre-empts the chase camera). Skipped entirely
|
||||
// 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
|
||||
// ACDREAM_LIVE=1 is in the environment — fully backward compatible with
|
||||
// the offline rendering pipeline.
|
||||
// Step 2 re-attempt (2026-05-16, debug pass): the network-side lifecycle
|
||||
// (DNS, endpoint, WorldSession construction, Tick, Dispose) lives in
|
||||
// LiveSessionController. _liveSession remains as a convenience handle
|
||||
// for the ~60 outbound SendXxx call sites; it tracks Controller.Session.
|
||||
// Slice 3: the controller is the sole App owner. Outbound feature
|
||||
// callbacks resolve this borrowed handle at call time and never cache it.
|
||||
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 _liveCenterY;
|
||||
// #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),
|
||||
sendPreNetwork: (controller, movement, hidden) =>
|
||||
_localPlayerOutbound.SendPreNetworkActions(
|
||||
_liveSession,
|
||||
LiveSession,
|
||||
controller,
|
||||
movement,
|
||||
hidden),
|
||||
sendPostNetwork: (controller, hidden) =>
|
||||
_localPlayerOutbound.SendPostNetworkPosition(
|
||||
_liveSession,
|
||||
LiveSession,
|
||||
controller,
|
||||
hidden),
|
||||
objectClockDisposition: () =>
|
||||
|
|
@ -1324,9 +1321,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// when the entity isn't ready (the third predicate
|
||||
// guarantees readiness before this fires).
|
||||
_playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry(
|
||||
isLiveInWorld: () => _liveSession is not null
|
||||
&& _liveSession.CurrentState ==
|
||||
AcDream.Core.Net.WorldSession.State.InWorld,
|
||||
isLiveInWorld: () => _liveSessionController?.IsInWorld == true,
|
||||
isPlayerEntityPresent: () => _entitiesByServerGuid.ContainsKey(_playerServerGuid),
|
||||
isPlayerControllerReady: () => true,
|
||||
// Retail SmartBox::UseTime (0x00455410) completes the player
|
||||
|
|
@ -1781,7 +1776,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
try
|
||||
{
|
||||
// _activeToonKey is updated by
|
||||
// BeginLiveSessionAsync after EnterWorld
|
||||
// ApplyLiveSessionEnteredWorld
|
||||
// so saving character settings always
|
||||
// writes under the chosen character's
|
||||
// name (or "default" pre-login).
|
||||
|
|
@ -2015,7 +2010,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
CanStartLiveCombatAttack,
|
||||
SendLiveCombatAttack,
|
||||
prepareAttackRequest: PreparePlayerForAttackRequest,
|
||||
sendCancelAttack: () => _liveSession?.SendCancelAttack(),
|
||||
sendCancelAttack: () => LiveSession?.SendCancelAttack(),
|
||||
isDualWield: () => _playerController?.Motion.InterpretedState.CurrentStyle
|
||||
== AcDream.Core.Combat.CombatInputPlanner.DualWieldCombatStyle,
|
||||
playerReadyForAttack: IsPlayerReadyForCombatAttack,
|
||||
|
|
@ -2028,7 +2023,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_externalContainerLifecycle = new AcDream.App.World.ExternalContainerLifecycleController(
|
||||
_externalContainers,
|
||||
Objects,
|
||||
guid => _liveSession?.SendNoLongerViewingContents(guid));
|
||||
guid => LiveSession?.SendNoLongerViewingContents(guid));
|
||||
|
||||
AcDream.App.Spells.MagicCatalog magicCatalog = _magicCatalog!;
|
||||
_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
|
||||
// without completing the same open transaction as double-click.
|
||||
sendUse: SendUse,
|
||||
sendExamine: g => _liveSession?.SendAppraise(g),
|
||||
sendUseWithTarget: (source, target) => _liveSession?.SendUseWithTarget(source, target),
|
||||
sendWield: (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask),
|
||||
sendDrop: item => _liveSession?.SendDropItem(item),
|
||||
sendExamine: g => LiveSession?.SendAppraise(g),
|
||||
sendUseWithTarget: (source, target) => LiveSession?.SendUseWithTarget(source, target),
|
||||
sendWield: (item, mask) => LiveSession?.SendGetAndWieldItem(item, mask),
|
||||
sendDrop: item => LiveSession?.SendDropItem(item),
|
||||
sendGive: (target, item, amount) =>
|
||||
_liveSession?.SendGiveObject(target, item, amount),
|
||||
LiveSession?.SendGiveObject(target, item, amount),
|
||||
dragOnPlayerOpensSecureTrade: () =>
|
||||
(_characterOptions1
|
||||
& AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1
|
||||
.DragItemOnPlayerOpensSecureTrade) != 0,
|
||||
toast: text => _debugVm?.AddToast(text),
|
||||
readyForInventoryRequest: () => _liveSession is not null
|
||||
&& _liveSession.CurrentState == AcDream.Core.Net.WorldSession.State.InWorld,
|
||||
readyForInventoryRequest: () => _liveSessionController?.IsInWorld == true,
|
||||
playerOnGround: GetDebugPlayerOnGround,
|
||||
inNonCombatMode: () => Combat.CurrentMode
|
||||
== AcDream.Core.Combat.CombatMode.NonCombat,
|
||||
combatState: Combat,
|
||||
sendChangeCombatMode: mode =>
|
||||
_liveSession?.SendChangeCombatMode(mode),
|
||||
LiveSession?.SendChangeCombatMode(mode),
|
||||
isComponentPack: magicCatalog.IsComponentPack,
|
||||
placeInBackpack: SendPickUp,
|
||||
backpackContainerId: () =>
|
||||
|
|
@ -2071,14 +2065,14 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// the retained controller remains alive.
|
||||
groundObjectId: () => _externalContainers.CurrentContainerId,
|
||||
sendSplitToWorld: (item, amount) =>
|
||||
_liveSession?.SendStackableSplitTo3D(item, amount),
|
||||
LiveSession?.SendStackableSplitTo3D(item, amount),
|
||||
selectedObjectId: () => _selection.SelectedObjectId ?? 0u,
|
||||
stackSplitQuantity: _stackSplitQuantity,
|
||||
systemMessage: text => Chat.OnSystemMessage(text, 0x1Au),
|
||||
sendPutItemInContainer: (item, container, placement) =>
|
||||
_liveSession?.SendPutItemInContainer(item, container, placement),
|
||||
LiveSession?.SendPutItemInContainer(item, container, placement),
|
||||
sendSplitToContainer: (item, container, placement, amount) =>
|
||||
_liveSession?.SendStackableSplitToContainer(
|
||||
LiveSession?.SendStackableSplitToContainer(
|
||||
item, container, placement, amount),
|
||||
requestExternalContainer: guid => _externalContainers.RequestOpen(guid));
|
||||
|
||||
|
|
@ -2101,12 +2095,11 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
playerGuid: () => _playerServerGuid,
|
||||
activeToonName: () => _activeToonKey,
|
||||
fallbackSheet: AcDream.App.Studio.SampleData.SampleCharacter,
|
||||
canSendRaise: () => _liveSession is not null
|
||||
&& _liveSession.CurrentState == AcDream.Core.Net.WorldSession.State.InWorld,
|
||||
sendRaiseAttribute: (statId, cost) => _liveSession?.SendRaiseAttribute(statId, cost),
|
||||
sendRaiseVital: (statId, cost) => _liveSession?.SendRaiseVital(statId, cost),
|
||||
sendRaiseSkill: (statId, cost) => _liveSession?.SendRaiseSkill(statId, cost),
|
||||
sendTrainSkill: (statId, credits) => _liveSession?.SendTrainSkill(statId, credits));
|
||||
canSendRaise: () => _liveSessionController?.IsInWorld == true,
|
||||
sendRaiseAttribute: (statId, cost) => LiveSession?.SendRaiseAttribute(statId, cost),
|
||||
sendRaiseVital: (statId, cost) => LiveSession?.SendRaiseVital(statId, cost),
|
||||
sendRaiseSkill: (statId, cost) => LiveSession?.SendRaiseSkill(statId, cost),
|
||||
sendTrainSkill: (statId, credits) => LiveSession?.SendTrainSkill(statId, credits));
|
||||
_magicRuntime = AcDream.App.Spells.MagicRuntime.Create(
|
||||
magicCatalog,
|
||||
SpellBook,
|
||||
|
|
@ -2115,16 +2108,15 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
localPlayerId: () => _playerServerGuid,
|
||||
// SpellFormula::Randomize hashes the canonical account spelling
|
||||
// delivered by CharacterList.
|
||||
accountName: () => _liveSession?.Characters?.AccountName
|
||||
accountName: () => LiveSession?.Characters?.AccountName
|
||||
?? _options.LiveUser
|
||||
?? string.Empty,
|
||||
stopCompletely: PreparePlayerForAttackRequest,
|
||||
sendUntargeted: spellId => _liveSession?.SendCastUntargetedSpell(spellId),
|
||||
sendTargeted: (target, spellId) => _liveSession?.SendCastTargetedSpell(target, spellId),
|
||||
sendUntargeted: spellId => LiveSession?.SendCastUntargetedSpell(spellId),
|
||||
sendTargeted: (target, spellId) => LiveSession?.SendCastTargetedSpell(target, spellId),
|
||||
displayMessage: text => Chat.OnSystemMessage(text, 0x1Au),
|
||||
incrementBusy: () => _itemInteractionController.IncrementBusyCount(),
|
||||
canSend: () => _liveSession is not null
|
||||
&& _liveSession.CurrentState == AcDream.Core.Net.WorldSession.State.InWorld);
|
||||
canSend: () => _liveSessionController?.IsInWorld == true);
|
||||
_uiHost.Root.DragReleasedOutsideUi += OnUiDragReleasedOutside;
|
||||
|
||||
// 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),
|
||||
Chat: new AcDream.App.UI.ChatRuntimeBindings(
|
||||
retailChatVm,
|
||||
() => _liveSessionCommands ?? (AcDream.UI.Abstractions.ICommandBus)
|
||||
AcDream.UI.Abstractions.NullCommandBus.Instance),
|
||||
() => _liveSessionController?.Commands
|
||||
?? AcDream.UI.Abstractions.NullCommandBus.Instance),
|
||||
Radar: new AcDream.App.UI.RadarRuntimeBindings(
|
||||
radarSnapshotProvider.BuildSnapshot,
|
||||
_selection,
|
||||
|
|
@ -2252,13 +2244,13 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
guid, AcDream.Core.Selection.SelectionChangeSource.Inventory),
|
||||
UseItemByGuid,
|
||||
(tab, position, spellId) =>
|
||||
_liveSession?.SendAddSpellFavorite(spellId, position, tab),
|
||||
LiveSession?.SendAddSpellFavorite(spellId, position, tab),
|
||||
(tab, spellId) =>
|
||||
_liveSession?.SendRemoveSpellFavorite(spellId, tab),
|
||||
filters => _liveSession?.SendSpellbookFilter(filters),
|
||||
spellId => _liveSession?.SendRemoveSpell(spellId),
|
||||
LiveSession?.SendRemoveSpellFavorite(spellId, tab),
|
||||
filters => LiveSession?.SendSpellbookFilter(filters),
|
||||
spellId => LiveSession?.SendRemoveSpell(spellId),
|
||||
(componentId, amount) =>
|
||||
_liveSession?.SendSetDesiredComponentLevel(componentId, amount),
|
||||
LiveSession?.SendSetDesiredComponentLevel(componentId, amount),
|
||||
ClientTimerNow),
|
||||
JumpPowerbar: new AcDream.App.UI.JumpPowerbarRuntimeBindings(
|
||||
() => _playerController?.JumpCharge ?? default),
|
||||
|
|
@ -2284,10 +2276,10 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
() => LocalPlayer.GetAttribute(
|
||||
AcDream.Core.Player.LocalPlayerState.AttributeKind.Strength)
|
||||
is { } strength ? (int?)strength.Current : null,
|
||||
() => _liveSession?.LinkStatus
|
||||
() => LiveSession?.LinkStatus
|
||||
?? AcDream.Core.Net.LinkStatusSnapshot.Disconnected,
|
||||
ClientTimerNow,
|
||||
() => _liveSession?.RequestLinkStatusPing(),
|
||||
() => LiveSession?.RequestLinkStatusPing(),
|
||||
() => _window?.Close()),
|
||||
Toolbar: new AcDream.App.UI.ToolbarRuntimeBindings(
|
||||
Objects,
|
||||
|
|
@ -2299,8 +2291,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
ItemMana,
|
||||
ToggleLiveCombatMode,
|
||||
_itemInteractionController,
|
||||
entry => _liveSession?.SendAddShortcut(entry),
|
||||
index => _liveSession?.SendRemoveShortcut(index),
|
||||
entry => LiveSession?.SendAddShortcut(entry),
|
||||
index => LiveSession?.SendRemoveShortcut(index),
|
||||
_selection,
|
||||
handler => Combat.HealthChanged += handler,
|
||||
handler => Combat.HealthChanged -= handler,
|
||||
|
|
@ -2309,11 +2301,11 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
Combat.GetHealthPercent,
|
||||
Combat.HasHealth,
|
||||
guid => (uint)(Objects.Get(guid)?.StackSize ?? 0),
|
||||
guid => _liveSession?.SendQueryHealth(guid),
|
||||
guid => _liveSession?.SendQueryItemMana(guid),
|
||||
guid => LiveSession?.SendQueryHealth(guid),
|
||||
guid => LiveSession?.SendQueryItemMana(guid),
|
||||
() => _playerServerGuid,
|
||||
(item, container, placement) =>
|
||||
_liveSession?.SendPutItemInContainer(item, container, placement)),
|
||||
LiveSession?.SendPutItemInContainer(item, container, placement)),
|
||||
Character: new AcDream.App.UI.CharacterRuntimeBindings(_characterSheetProvider),
|
||||
Inventory: new AcDream.App.UI.InventoryRuntimeBindings(
|
||||
Objects,
|
||||
|
|
@ -2323,14 +2315,14 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
() => LocalPlayer.GetAttribute(
|
||||
AcDream.Core.Player.LocalPlayerState.AttributeKind.Strength)
|
||||
is { } strength ? (int?)strength.Current : null,
|
||||
guid => _liveSession?.SendUse(guid),
|
||||
guid => LiveSession?.SendUse(guid),
|
||||
(item, container, placement) =>
|
||||
_liveSession?.SendPutItemInContainer(item, container, placement),
|
||||
LiveSession?.SendPutItemInContainer(item, container, placement),
|
||||
(item, container, placement, amount) =>
|
||||
_liveSession?.SendStackableSplitToContainer(
|
||||
LiveSession?.SendStackableSplitToContainer(
|
||||
item, container, placement, amount),
|
||||
(source, target, amount) =>
|
||||
_liveSession?.SendStackableMerge(source, target, amount),
|
||||
LiveSession?.SendStackableMerge(source, target, amount),
|
||||
_itemInteractionController,
|
||||
_selection),
|
||||
ExternalContainer: new AcDream.App.UI.ExternalContainerRuntimeBindings(
|
||||
|
|
@ -2342,11 +2334,11 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
iconComposer.GetDragIcon(type, icon, under, over, effects),
|
||||
_itemInteractionController,
|
||||
_selection,
|
||||
guid => _liveSession?.SendUse(guid),
|
||||
guid => LiveSession?.SendUse(guid),
|
||||
(item, container, placement) =>
|
||||
_liveSession?.SendPutItemInContainer(item, container, placement),
|
||||
LiveSession?.SendPutItemInContainer(item, container, placement),
|
||||
(item, container, placement, amount) =>
|
||||
_liveSession?.SendStackableSplitToContainer(
|
||||
LiveSession?.SendStackableSplitToContainer(
|
||||
item, container, placement, amount),
|
||||
IsWithinExternalContainerUseRange),
|
||||
Cursor: new AcDream.App.UI.RetailUiCursorBindings(
|
||||
|
|
@ -2354,7 +2346,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
retailCursorManager),
|
||||
Confirmations: new AcDream.App.UI.ConfirmationRuntimeBindings(
|
||||
(type, context, accepted) =>
|
||||
_liveSession?.SendConfirmationResponse(type, context, accepted)),
|
||||
LiveSession?.SendConfirmationResponse(type, context, accepted)),
|
||||
StackSplitQuantity: _stackSplitQuantity,
|
||||
Plugins: _uiRegistry,
|
||||
Persistence: persistence,
|
||||
|
|
@ -2704,7 +2696,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_worldSelectionQuery,
|
||||
itemInteractions,
|
||||
new AcDream.App.Interaction.WorldSessionSelectionInteractionTransport(
|
||||
() => _liveSession),
|
||||
() => LiveSession),
|
||||
new AcDream.App.Interaction.PlayerInteractionMovementSink(
|
||||
() => _playerController),
|
||||
text => _debugVm?.AddToast(text));
|
||||
|
|
@ -2854,107 +2846,63 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// gated behind ACDREAM_LIVE=1 so the default run path is unchanged.
|
||||
_liveCenterX = centerX;
|
||||
_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();
|
||||
_liveSession = _liveSessionController.CreateAndWire(_options, WireLiveSessionEvents);
|
||||
if (_liveSession is null)
|
||||
AcDream.App.Net.LiveSessionStartResult liveStart =
|
||||
_liveSessionController.Start(
|
||||
_options,
|
||||
CreateLiveSessionLifecycleHost());
|
||||
switch (liveStart.Status)
|
||||
{
|
||||
try
|
||||
{
|
||||
DisposeLiveSessionRouting();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_liveSessionController.Dispose();
|
||||
_liveSessionController = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var user = _options.LiveUser!;
|
||||
var pass = _options.LivePass!;
|
||||
var host = _options.LiveHost;
|
||||
var port = _options.LivePort;
|
||||
try
|
||||
{
|
||||
Chat.OnSystemMessage($"connecting to {host}:{port} as {user}", chatType: 1);
|
||||
_liveSession.Connect(user, pass);
|
||||
Chat.OnSystemMessage("connected — character list received", chatType: 1);
|
||||
|
||||
if (_liveSession.Characters is null
|
||||
|| !AcDream.Core.Net.Messages.CharacterList.TrySelectFirstAvailable(
|
||||
_liveSession.Characters,
|
||||
out AcDream.Core.Net.Messages.CharacterList.Selection selection))
|
||||
{
|
||||
Console.WriteLine("live: no available characters on account; disconnecting");
|
||||
try
|
||||
{
|
||||
DisposeLiveSessionRouting();
|
||||
}
|
||||
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();
|
||||
_liveSession.EnterWorld(characterIndex: selection.ActiveIndex);
|
||||
_liveSessionCommands?.Activate();
|
||||
|
||||
_activeToonKey = chosen.Name;
|
||||
_retailUiRuntime?.RestoreLayout();
|
||||
SyncToolbarWindowButtons();
|
||||
if (_settingsStore is not null && _settingsVm is not null)
|
||||
{
|
||||
var toonBag = _settingsStore.LoadCharacter(_activeToonKey);
|
||||
_settingsVm.LoadCharacterContext(toonBag);
|
||||
Console.WriteLine($"settings: loaded character[{_activeToonKey}] preferences");
|
||||
}
|
||||
_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();
|
||||
case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials:
|
||||
Console.WriteLine(
|
||||
"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;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearInboundEntityState()
|
||||
private AcDream.App.Net.LiveSessionLifecycleHost CreateLiveSessionLifecycleHost() =>
|
||||
new(new AcDream.App.Net.LiveSessionLifecycleBindings(
|
||||
Bind: CreateLiveSessionBinding,
|
||||
Reset: () =>
|
||||
(_liveSessionResetPlan ??= CreateLiveSessionResetPlan()).Execute(),
|
||||
Connecting: (host, port, user) =>
|
||||
Chat.OnSystemMessage(
|
||||
$"connecting to {host}:{port} as {user}",
|
||||
chatType: 1),
|
||||
Connected: () =>
|
||||
Chat.OnSystemMessage(
|
||||
"connected — character list received",
|
||||
chatType: 1),
|
||||
Selected: ApplyLiveSessionSelection,
|
||||
Entered: ApplyLiveSessionEnteredWorld));
|
||||
|
||||
private void ApplyLiveSessionSelection(
|
||||
AcDream.App.Net.LiveSessionCharacterSelection selection)
|
||||
{
|
||||
(_liveSessionResetPlan ??= CreateLiveSessionResetPlan()).Execute();
|
||||
_playerServerGuid = selection.CharacterId;
|
||||
_vitalsVm?.SetLocalPlayerGuid(selection.CharacterId);
|
||||
Chat.SetLocalPlayerGuid(selection.CharacterId);
|
||||
_worldState.MarkPersistent(selection.CharacterId);
|
||||
AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = selection.CharacterId;
|
||||
Combat.Clear();
|
||||
}
|
||||
|
||||
private void ApplyLiveSessionEnteredWorld(
|
||||
AcDream.App.Net.LiveSessionCharacterSelection selection)
|
||||
{
|
||||
_activeToonKey = selection.CharacterName;
|
||||
_retailUiRuntime?.RestoreLayout();
|
||||
SyncToolbarWindowButtons();
|
||||
if (_settingsStore is not null && _settingsVm is not null)
|
||||
{
|
||||
var toonBag = _settingsStore.LoadCharacter(_activeToonKey);
|
||||
_settingsVm.LoadCharacterContext(toonBag);
|
||||
Console.WriteLine($"settings: loaded character[{_activeToonKey}] preferences");
|
||||
}
|
||||
_playerModeAutoEntry?.Arm();
|
||||
}
|
||||
|
||||
private AcDream.App.Net.LiveSessionResetPlan CreateLiveSessionResetPlan() =>
|
||||
|
|
@ -3087,12 +3035,9 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
/// Registration completes before Connect; outbound commands remain inert
|
||||
/// until EnterWorld succeeds.
|
||||
/// </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);
|
||||
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(
|
||||
CreateLiveSessionCommandBindings(session));
|
||||
_liveSessionEvents = events;
|
||||
_liveSessionCommands = commands;
|
||||
return new AcDream.App.Net.LiveSessionBinding(
|
||||
session,
|
||||
commands,
|
||||
activateCommands: commands.Activate,
|
||||
deactivateCommands: commands.Dispose,
|
||||
detachEvents: events.Dispose);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -3311,22 +3260,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
roomId, chatType, dispatchType, senderGuid, text, cookie),
|
||||
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
|
||||
{
|
||||
LogicalRegistration,
|
||||
|
|
@ -4936,8 +4869,9 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
uint guid,
|
||||
AcDream.App.World.AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
if (guid != _playerServerGuid || _liveSession is null) return;
|
||||
_liveSession.PublishAcceptedLocalPhysicsTimestamps(
|
||||
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||
if (guid != _playerServerGuid || session is null) return;
|
||||
session.PublishAcceptedLocalPhysicsTimestamps(
|
||||
timestamps.Instance,
|
||||
timestamps.ServerControlledMove,
|
||||
timestamps.Teleport,
|
||||
|
|
@ -6460,7 +6394,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
/// </summary>
|
||||
private void SendImmediateLocalPositionEvent()
|
||||
{
|
||||
if (_liveSession is null
|
||||
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||
if (session is null
|
||||
|| _playerController is null
|
||||
|| !_playerController.CanSendPositionEvent)
|
||||
{
|
||||
|
|
@ -6477,18 +6412,18 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
return;
|
||||
}
|
||||
|
||||
uint sequence = _liveSession.NextGameActionSequence();
|
||||
uint sequence = session.NextGameActionSequence();
|
||||
var body = AcDream.Core.Net.Messages.AutonomousPosition.Build(
|
||||
gameActionSequence: sequence,
|
||||
cellId: cellId,
|
||||
position: position,
|
||||
rotation: rotation,
|
||||
instanceSequence: _liveSession.InstanceSequence,
|
||||
serverControlSequence: _liveSession.ServerControlSequence,
|
||||
teleportSequence: _liveSession.TeleportSequence,
|
||||
forcePositionSequence: _liveSession.ForcePositionSequence,
|
||||
instanceSequence: session.InstanceSequence,
|
||||
serverControlSequence: session.ServerControlSequence,
|
||||
teleportSequence: session.TeleportSequence,
|
||||
forcePositionSequence: session.ForcePositionSequence,
|
||||
lastContact: 1);
|
||||
_liveSession.SendGameAction(body);
|
||||
session.SendGameAction(body);
|
||||
_playerController.NotePositionSent(
|
||||
new AcDream.Core.Physics.Position(cellId, position, rotation),
|
||||
_playerController.ContactPlane,
|
||||
|
|
@ -9409,8 +9344,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// The world-geometry RENDER gate (IsLiveModeWaitingForLogin at the
|
||||
// draw site) is unchanged — the pre-entry screen still shows sky
|
||||
// only.
|
||||
bool liveInWorld = _liveSession is not null
|
||||
&& _liveSession.CurrentState == AcDream.Core.Net.WorldSession.State.InWorld;
|
||||
bool liveInWorld = _liveSessionController?.IsInWorld == true;
|
||||
// #192: liveInWorld alone used to be sufficient here — but InWorld fires
|
||||
// right after the login handshake, before the player's own spawn
|
||||
// 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);
|
||||
observerCy = _liveCenterY + (int)System.Math.Floor(pp.Y / 192f);
|
||||
}
|
||||
else if (_liveSession is not null
|
||||
&& _liveSession.CurrentState == AcDream.Core.Net.WorldSession.State.InWorld)
|
||||
else if (liveInWorld)
|
||||
{
|
||||
// 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
|
||||
|
|
@ -9657,7 +9590,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_playerController.State = AcDream.App.Input.PlayerState.InWorld;
|
||||
// holtburger client/messages.rs:434 — re-send LoginComplete after
|
||||
// each portal transition.
|
||||
_liveSession?.SendGameAction(
|
||||
LiveSession?.SendGameAction(
|
||||
AcDream.Core.Net.Messages.GameActionLoginComplete.Build());
|
||||
_worldReveal?.Complete();
|
||||
ResetTeleportTransitState();
|
||||
|
|
@ -9682,7 +9615,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
|
||||
// Phase K.2 — auto-enter player mode at login. The guard
|
||||
// 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
|
||||
// OnInputAction (Ctrl+Tab → TogglePlayerMode) or DebugPanel.
|
||||
_playerModeAutoEntry?.TryEnter();
|
||||
|
|
@ -11038,8 +10971,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// is up so panel-emitted SendChatCmd actually flows server-ward.
|
||||
// Fall back to NullCommandBus for offline / pre-connect renders.
|
||||
AcDream.UI.Abstractions.ICommandBus bus =
|
||||
_liveSessionCommands ?? (AcDream.UI.Abstractions.ICommandBus)
|
||||
AcDream.UI.Abstractions.NullCommandBus.Instance;
|
||||
_liveSessionController?.Commands
|
||||
?? AcDream.UI.Abstractions.NullCommandBus.Instance;
|
||||
var ctx = new AcDream.UI.Abstractions.PanelContext(
|
||||
(float)deltaSeconds,
|
||||
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.SettingsVM? _settingsVm;
|
||||
// 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
|
||||
// "default" and gets swapped to the actual character name on the
|
||||
// first EnterWorld.
|
||||
|
|
@ -12654,7 +12587,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_persistedGameplay = _settingsStore.LoadGameplay();
|
||||
_persistedChat = _settingsStore.LoadChat();
|
||||
// _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.
|
||||
_persistedCharacter = _settingsStore.LoadCharacter(_activeToonKey);
|
||||
|
||||
|
|
@ -13153,8 +13086,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
|
||||
private void ToggleLiveCombatMode()
|
||||
{
|
||||
if (_liveSession is null
|
||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
||||
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||
if (_liveSessionController?.IsInWorld != true || session is null)
|
||||
return;
|
||||
|
||||
IReadOnlyList<AcDream.Core.Items.ClientObject> orderedEquipment =
|
||||
|
|
@ -13165,7 +13098,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
Combat.CurrentMode,
|
||||
defaultMode);
|
||||
_itemInteractionController?.NotifyExplicitCombatModeRequest();
|
||||
_liveSession.SendChangeCombatMode(nextMode);
|
||||
session.SendChangeCombatMode(nextMode);
|
||||
Combat.SetCombatMode(nextMode);
|
||||
string text = $"Combat mode {nextMode}";
|
||||
Console.WriteLine($"combat: {text}");
|
||||
|
|
@ -13185,8 +13118,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
|
||||
private bool CanStartLiveCombatAttack()
|
||||
{
|
||||
if (_liveSession is null
|
||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
||||
if (_liveSessionController?.IsInWorld != true)
|
||||
return false;
|
||||
|
||||
if (!AcDream.Core.Combat.CombatInputPlanner.SupportsTargetedAttack(Combat.CurrentMode))
|
||||
|
|
@ -13214,16 +13146,20 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
if (!CanStartLiveCombatAttack())
|
||||
return false;
|
||||
|
||||
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||
if (session is null)
|
||||
return false;
|
||||
|
||||
uint target = _selection.SelectedObjectId!.Value;
|
||||
power = Math.Clamp(power, 0f, 1f);
|
||||
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}");
|
||||
}
|
||||
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}");
|
||||
}
|
||||
return true;
|
||||
|
|
@ -13252,7 +13188,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
|
||||
private bool TrySendPlayerMovementEvent(AcDream.App.Input.MovementResult result)
|
||||
=> _localPlayerOutbound.TrySendMovement(
|
||||
_liveSession,
|
||||
LiveSession,
|
||||
_playerController,
|
||||
result);
|
||||
|
||||
|
|
@ -13291,11 +13227,11 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// sends directly without speculative TurnToObject/MoveToObject.
|
||||
private void UseItemByGuid(uint guid)
|
||||
{
|
||||
if (_liveSession is null
|
||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
||||
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||
if (_liveSessionController?.IsInWorld != true || session is null)
|
||||
return;
|
||||
uint sequence = _liveSession.NextGameActionSequence();
|
||||
_liveSession.SendGameAction(
|
||||
uint sequence = session.NextGameActionSequence();
|
||||
session.SendGameAction(
|
||||
AcDream.Core.Net.Messages.InteractRequests.BuildUse(sequence, guid));
|
||||
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()
|
||||
{
|
||||
// Phase B.2 guard: only active when a live session is in-world.
|
||||
if (_liveSession is null
|
||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
||||
if (_liveSessionController?.IsInWorld != true)
|
||||
return;
|
||||
|
||||
// Manual toggle pre-empts the K.2 auto-entry trigger regardless
|
||||
|
|
@ -14223,14 +14158,12 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_magicCatalog = null;
|
||||
}),
|
||||
new("streamer", () => _streamer?.Dispose()),
|
||||
new("live session routing", DisposeLiveSessionRouting),
|
||||
new("equipped children", () => _equippedChildRenderer?.Dispose()),
|
||||
new("live session", () =>
|
||||
{
|
||||
_liveSessionController?.Dispose();
|
||||
_liveSessionController = null;
|
||||
_liveSession = null;
|
||||
}),
|
||||
new("equipped children", () => _equippedChildRenderer?.Dispose()),
|
||||
]),
|
||||
// Retained tombstones are retried while every callback owner is alive.
|
||||
new ResourceShutdownStage("live entities",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue