refactor(app): complete session startup composition
Move the live-session reset and routing graph, combat and diagnostic command targets, and the sole gameplay input subscriber into Phase 7 before frame publication. Add exact retryable ownership for late bindings so partial startup cannot strand session or component teardown edges. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
7fa60971e2
commit
826f9ea9b5
22 changed files with 924 additions and 412 deletions
|
|
@ -326,8 +326,9 @@ public sealed class GameWindow :
|
|||
public readonly AcDream.Core.Items.ItemManaState ItemMana = new();
|
||||
public readonly AcDream.Core.Social.FriendsState Friends = new();
|
||||
public readonly AcDream.Core.Social.SquelchState Squelch = new();
|
||||
public IReadOnlyList<(uint Id, uint Amount)> DesiredComponents { get; private set; }
|
||||
= System.Array.Empty<(uint Id, uint Amount)>();
|
||||
private readonly DesiredComponentSnapshotState _desiredComponents = new();
|
||||
public IReadOnlyList<(uint Id, uint Amount)> DesiredComponents =>
|
||||
_desiredComponents.Items;
|
||||
// Retail resolves learned IDs through portal.dat's CSpellTable. MagicCatalog
|
||||
// installs that immutable table once DatCollection opens in OnLoad.
|
||||
public AcDream.Core.Spells.SpellTable SpellTable => SpellBook.Metadata;
|
||||
|
|
@ -1010,6 +1011,8 @@ public sealed class GameWindow :
|
|||
|| _playerModeController is not null
|
||||
|| _playerModeAutoEntry is not null
|
||||
|| _localPlayerTeleport is not null
|
||||
|| _liveSessionHost is not null
|
||||
|| _gameplayInputActions is not null
|
||||
|| _sessionPlayerBindings is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
|
|
@ -1032,6 +1035,8 @@ public sealed class GameWindow :
|
|||
_playerModeController = result.PlayerMode;
|
||||
_playerModeAutoEntry = result.PlayerModeAutoEntry;
|
||||
_localPlayerTeleport = result.LocalTeleport;
|
||||
_liveSessionHost = result.SessionHost;
|
||||
_gameplayInputActions = result.GameplayActions;
|
||||
_sessionPlayerBindings = result.RuntimeBindings;
|
||||
}
|
||||
|
||||
|
|
@ -1287,6 +1292,11 @@ public sealed class GameWindow :
|
|||
_datLock,
|
||||
_runtimeSettings,
|
||||
settingsDevTools,
|
||||
_worldEnvironment,
|
||||
_worldSceneDebugState,
|
||||
_runtimeDiagnosticCommands,
|
||||
_liveCombatModeCommands,
|
||||
_hostQuiescence,
|
||||
_physicsEngine,
|
||||
_physicsDataCache,
|
||||
_worldGameState,
|
||||
|
|
@ -1314,6 +1324,9 @@ public sealed class GameWindow :
|
|||
_localPlayerSkills,
|
||||
_viewportAspect,
|
||||
_playerApproachCompletions,
|
||||
_characterOptions,
|
||||
_shortcutSnapshots,
|
||||
_desiredComponents,
|
||||
_pointerPosition,
|
||||
_movementInput,
|
||||
_inputCapture,
|
||||
|
|
@ -1325,6 +1338,14 @@ public sealed class GameWindow :
|
|||
_combatAttackOperations,
|
||||
Combat,
|
||||
_combatFeedback,
|
||||
Chat,
|
||||
LocalPlayer,
|
||||
SpellBook,
|
||||
ItemMana,
|
||||
Friends,
|
||||
Squelch,
|
||||
TurbineChat,
|
||||
_externalContainers,
|
||||
_portalTunnelFallback,
|
||||
Console.WriteLine),
|
||||
this).Compose(
|
||||
|
|
@ -1593,84 +1614,9 @@ public sealed class GameWindow :
|
|||
sessionPlayer.PlayerModeAutoEntry),
|
||||
cameraFrame);
|
||||
_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);
|
||||
_liveSessionHost = CreateLiveSessionHost();
|
||||
|
||||
AcDream.UI.Abstractions.Panels.Debug.DebugVM? debugVm = _debugVm;
|
||||
Action<string>? debugToast = debugVm is null
|
||||
? null
|
||||
: message => debugVm.AddToast(message);
|
||||
AcDream.Core.Chat.ChatLog chat = Chat;
|
||||
var combatCommand =
|
||||
new AcDream.App.Combat.LiveCombatModeCommandController(
|
||||
new AcDream.App.Combat.LiveSessionCombatModeAuthority(
|
||||
_liveSessionHost),
|
||||
new AcDream.App.Combat.LocalPlayerCombatEquipmentSource(
|
||||
Objects,
|
||||
_localPlayerIdentity),
|
||||
Combat,
|
||||
new AcDream.App.Combat.ItemInteractionCombatModeIntentSink(
|
||||
_itemInteractionController!),
|
||||
Console.WriteLine,
|
||||
debugToast,
|
||||
text => chat.OnSystemMessage(text, 0x1Au));
|
||||
_liveCombatModeCommands.Bind(combatCommand);
|
||||
|
||||
var nearbyDiagnostics =
|
||||
new AcDream.App.Diagnostics.NearbyWorldDiagnosticDumper(
|
||||
new AcDream.App.Diagnostics.RuntimeNearbyWorldDiagnosticSource(
|
||||
_localPlayerMode,
|
||||
_playerControllerSlot,
|
||||
_cameraController!,
|
||||
_liveWorldOrigin,
|
||||
_worldState,
|
||||
_physicsEngine),
|
||||
Console.WriteLine);
|
||||
var runtimeDiagnostics =
|
||||
new AcDream.App.Diagnostics.RuntimeDiagnosticCommandController(
|
||||
_worldEnvironment,
|
||||
_worldSceneDebugState,
|
||||
_cameraPointerInput,
|
||||
nearbyDiagnostics,
|
||||
debugToast);
|
||||
_runtimeDiagnosticCommands.Bind(runtimeDiagnostics);
|
||||
|
||||
if (_inputDispatcher is { } dispatcher)
|
||||
{
|
||||
var pointer = _cameraPointerInput
|
||||
?? throw new InvalidOperationException(
|
||||
"Gameplay action routing requires the composed pointer owner.");
|
||||
var commands = new AcDream.App.Input.GameplayInputCommandController(
|
||||
new AcDream.App.Input.RetainedGameplayWindowCommands(
|
||||
_retailUiRuntime),
|
||||
new AcDream.App.Input.DevToolsGameplayCommands(
|
||||
_devToolsFramePresenter),
|
||||
runtimeDiagnostics,
|
||||
new AcDream.App.Input.PlayerModeGameplayCommands(
|
||||
_localPlayerMode,
|
||||
sessionPlayer.PlayerMode),
|
||||
new AcDream.App.Input.ItemTargetModeCommands(
|
||||
_itemInteractionController!),
|
||||
new AcDream.App.Input.GameplayCameraModeCommands(
|
||||
_cameraController!),
|
||||
combatCommand,
|
||||
new AcDream.App.Input.GameplayWindowCommands(_window!.Close));
|
||||
var targets = new AcDream.App.Input.RuntimeGameplayInputPriorityTargets(
|
||||
sessionPlayer.GameplayInput,
|
||||
pointer,
|
||||
_retailUiRuntime,
|
||||
_selectionInteractions,
|
||||
commands);
|
||||
_gameplayInputActions = AcDream.App.Input.GameplayInputActionRouter.Create(
|
||||
dispatcher,
|
||||
Combat,
|
||||
targets,
|
||||
_hostQuiescence,
|
||||
Console.WriteLine);
|
||||
_gameplayInputActions.Attach();
|
||||
}
|
||||
|
||||
AcDream.App.Net.LiveSessionStartResult liveStart =
|
||||
_liveSessionHost.Start(_options);
|
||||
sessionPlayer.SessionHost.Start(_options);
|
||||
switch (liveStart.Status)
|
||||
{
|
||||
case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials:
|
||||
|
|
@ -1683,272 +1629,6 @@ public sealed class GameWindow :
|
|||
}
|
||||
}
|
||||
|
||||
private AcDream.App.Net.LiveSessionHost CreateLiveSessionHost() =>
|
||||
new(_liveSessionController!, new AcDream.App.Net.LiveSessionHostBindings(
|
||||
Routing: new(
|
||||
CreateLiveSessionEventRouter,
|
||||
session => new AcDream.App.Net.LiveSessionCommandRouter(
|
||||
CreateLiveSessionCommandBindings(session))),
|
||||
Reset: CreateLiveSessionResetBindings(),
|
||||
Selection: new(
|
||||
SetPlayerIdentity: id => _playerServerGuid = id,
|
||||
SetVitalsIdentity: id => _vitalsVm?.SetLocalPlayerGuid(id),
|
||||
SetChatIdentity: Chat.SetLocalPlayerGuid,
|
||||
MarkPersistent: _worldState.MarkPersistent,
|
||||
SetVanishProbeIdentity: id =>
|
||||
AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = id,
|
||||
ClearCombat: Combat.Clear),
|
||||
EnteredWorld: new(
|
||||
SetActiveCharacter: _runtimeSettings.SetActiveCharacter,
|
||||
RestoreLayout: () => _retailUiRuntime?.RestoreLayout(),
|
||||
SyncToolbar: SyncToolbarWindowButtons,
|
||||
LoadCharacterSettings: _runtimeSettings.LoadCharacterContext,
|
||||
ArmPlayerModeAutoEntry: () => _playerModeAutoEntry?.Arm()),
|
||||
Connecting: (host, port, user) =>
|
||||
Chat.OnSystemMessage(
|
||||
$"connecting to {host}:{port} as {user}",
|
||||
chatType: 1),
|
||||
Connected: () =>
|
||||
Chat.OnSystemMessage(
|
||||
"connected — character list received",
|
||||
chatType: 1)));
|
||||
|
||||
private AcDream.App.Net.LiveSessionResetBindings
|
||||
CreateLiveSessionResetBindings() => new()
|
||||
{
|
||||
MouseCapture = ResetSessionMouseCapture,
|
||||
PlayerPresentation = ResetSessionPlayerPresentation,
|
||||
TeleportTransit = _localPlayerTeleportSink.ResetSession,
|
||||
SessionDialogs = () => _retailUiRuntime?.ResetSessionDialogs(),
|
||||
ChatCommandTargets = () => _retailChatVm?.ResetSessionTargets(),
|
||||
SettingsCharacterContext = _runtimeSettings.RestoreDefaultCharacterContext,
|
||||
EquippedChildren = () => _equippedChildRenderer?.Clear(),
|
||||
ExternalContainer = () => _externalContainers.Reset(),
|
||||
InteractionAndSelection = ResetSessionInteraction,
|
||||
SelectionPresentation = () => _retailSelectionScene?.Reset(),
|
||||
ObjectTable = Objects.Clear,
|
||||
Spellbook = SpellBook.Clear,
|
||||
MagicRuntime = () => _magicRuntime?.Reset(),
|
||||
CombatAttack = () => _combatAttackController?.ResetSession(),
|
||||
CombatState = Combat.Clear,
|
||||
ItemMana = ItemMana.Clear,
|
||||
LocalPlayer = LocalPlayer.Clear,
|
||||
Friends = Friends.Clear,
|
||||
Squelch = Squelch.Clear,
|
||||
TurbineChat = TurbineChat.Reset,
|
||||
ParticleVisibility = _particleVisibility.Reset,
|
||||
InboundEventFifo = _inboundEntityEvents.Clear,
|
||||
LiveLiveness = () => _liveEntityLiveness?.Clear(),
|
||||
LiveRuntime = ResetSessionLiveRuntime,
|
||||
SessionIdentity = ResetSessionIdentity,
|
||||
RemoteTeleport = () => _remoteTeleportController?.Clear(),
|
||||
NetworkEffects = () => _entityEffects?.ClearNetworkState(),
|
||||
AnimationHookFrames = () => _animationHookFrames?.Clear(),
|
||||
LivePresentation = () => _liveEntityPresentation?.Clear(),
|
||||
RemoteMovementDiagnostics = _remoteMovementObservations.Clear,
|
||||
};
|
||||
|
||||
private void ResetSessionMouseCapture()
|
||||
=> _gameplayInputFrame?.ResetSession();
|
||||
|
||||
private void ResetSessionPlayerPresentation()
|
||||
{
|
||||
_playerModeController?.ResetSession();
|
||||
_spawnClaimHydration?.Reset();
|
||||
}
|
||||
|
||||
private void ResetSessionIdentity()
|
||||
{
|
||||
AcDream.App.Net.LiveSessionEntityRuntimeReset.RequireConvergence(_liveEntities);
|
||||
|
||||
// PlayerModule::Clear @ 0x005D48A0 clears shortcuts, desired
|
||||
// components, and character option objects. CPlayerSystem::Begin
|
||||
// @ 0x0055D410 resets player identity and session fields. The option
|
||||
// default comes from PlayerModule::PlayerModule @ 0x005D51F0.
|
||||
_playerServerGuid = 0u;
|
||||
_vitalsVm?.SetLocalPlayerGuid(0u);
|
||||
Chat.ResetSessionIdentity();
|
||||
AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = 0u;
|
||||
_runtimeSettings.ResetActiveCharacterKey();
|
||||
_characterOptions.Reset();
|
||||
_localPlayerSkills.ResetSession();
|
||||
_liveEntityNetworkUpdates?.ResetSessionState();
|
||||
_liveEntityHydration?.ResetSessionState();
|
||||
Shortcuts = Array.Empty<AcDream.Core.Items.ShortcutEntry>();
|
||||
DesiredComponents = Array.Empty<(uint Id, uint Amount)>();
|
||||
_paperdollFramePresenter?.MarkDirty();
|
||||
|
||||
// X/Y are ignored until the next logical player CreateObject claims a
|
||||
// new origin. Keeping the last values avoids inventing a synthetic
|
||||
// landblock while still forcing first-spawn initialization.
|
||||
_liveWorldOrigin.Reset();
|
||||
}
|
||||
|
||||
private void ResetSessionLiveRuntime()
|
||||
{
|
||||
AcDream.App.Net.LiveSessionEntityRuntimeReset.ClearAndRequireConvergence(
|
||||
_liveEntities);
|
||||
}
|
||||
|
||||
private void ResetSessionInteraction()
|
||||
{
|
||||
if (_selectionInteractions is { } interactions)
|
||||
interactions.ResetSession();
|
||||
else
|
||||
{
|
||||
_itemInteractionController?.ResetSession();
|
||||
_selection.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
private AcDream.App.Net.LiveSessionEventRouter CreateLiveSessionEventRouter(
|
||||
AcDream.Core.Net.WorldSession session)
|
||||
{
|
||||
var skillTable = _dats?.Get<DatReaderWriter.DBObjs.SkillTable>(0x0E000004u);
|
||||
if (_characterSheetProvider is not null && _dats is not null)
|
||||
{
|
||||
_characterSheetProvider.SkillTable = skillTable;
|
||||
_characterSheetProvider.ExperienceTable =
|
||||
AcDream.App.UI.Layout.CharacterSheetProvider.LoadExperienceTable(
|
||||
_dats,
|
||||
Console.WriteLine);
|
||||
}
|
||||
|
||||
return new AcDream.App.Net.LiveSessionEventRouter(
|
||||
session,
|
||||
(_liveEntitySessionEvents
|
||||
?? throw new InvalidOperationException(
|
||||
"Live entity session routing was not composed."))
|
||||
.CreateSink(),
|
||||
new AcDream.App.Net.LiveEnvironmentSessionSink(
|
||||
_worldEnvironment.ApplyAdminEnvirons,
|
||||
_worldEnvironment.SynchronizeFromServer),
|
||||
CreateLiveInventorySessionBindings(),
|
||||
CreateLiveCharacterSessionBindings(skillTable),
|
||||
CreateLiveSocialSessionBindings());
|
||||
}
|
||||
|
||||
private AcDream.App.Net.LiveInventorySessionBindings
|
||||
CreateLiveInventorySessionBindings() => new(
|
||||
Objects,
|
||||
LocalPlayer,
|
||||
PlayerGuid: () => _playerServerGuid,
|
||||
OnShortcuts: list => Shortcuts = list,
|
||||
OnUseDone: error =>
|
||||
{
|
||||
_externalContainers.ApplyUseDone(error);
|
||||
_itemInteractionController?.CompleteUse(error);
|
||||
},
|
||||
ItemMana,
|
||||
OnDesiredComponents: components => DesiredComponents = components,
|
||||
ExternalContainers: _externalContainers);
|
||||
|
||||
private AcDream.App.Net.LiveCharacterSessionBindings
|
||||
CreateLiveCharacterSessionBindings(
|
||||
DatReaderWriter.DBObjs.SkillTable? skillTable)
|
||||
{
|
||||
var skillCreditResolver =
|
||||
new AcDream.App.Net.LiveSkillCreditResolver(skillTable);
|
||||
return new(
|
||||
Combat,
|
||||
SpellBook,
|
||||
ResolveSkillFormulaBonus: skillCreditResolver.Resolve,
|
||||
OnSkillsUpdated: (runSkill, jumpSkill) =>
|
||||
{
|
||||
_localPlayerSkills.Update(runSkill, jumpSkill, _playerController);
|
||||
if (_localPlayerSkills.IsComplete && _playerController is not null)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"player: applied server skills run={_localPlayerSkills.RunSkill} " +
|
||||
$"jump={_localPlayerSkills.JumpSkill}");
|
||||
}
|
||||
},
|
||||
OnConfirmationRequest: request =>
|
||||
_retailUiRuntime?.HandleConfirmationRequest(request),
|
||||
OnConfirmationDone: done =>
|
||||
_retailUiRuntime?.HandleConfirmationDone(done),
|
||||
OnCharacterOptions: (options1, _) =>
|
||||
_characterOptions.Options =
|
||||
(AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1)
|
||||
options1,
|
||||
ClientTime: ClientTimerNow);
|
||||
}
|
||||
|
||||
private AcDream.App.Net.LiveSocialSessionBindings
|
||||
CreateLiveSocialSessionBindings() => new(
|
||||
Chat,
|
||||
TurbineChat,
|
||||
Friends,
|
||||
Squelch);
|
||||
|
||||
private AcDream.App.Net.LiveSessionCommandBindings CreateLiveSessionCommandBindings(
|
||||
AcDream.Core.Net.WorldSession session) => new(
|
||||
ClientCommands: new AcDream.App.UI.ClientCommandController.Bindings(
|
||||
TeleportToLifestone: session.SendTeleportToLifestone,
|
||||
TeleportToMarketplace: session.SendTeleportToMarketplace,
|
||||
TeleportToPkArena: session.SendTeleportToPkArena,
|
||||
TeleportToPkLiteArena: session.SendTeleportToPkLiteArena,
|
||||
TeleportToHouse: session.SendTeleportToHouse,
|
||||
TeleportToMansion: session.SendTeleportToMansion,
|
||||
QueryAge: session.SendQueryAge,
|
||||
QueryBirth: session.SendQueryBirth,
|
||||
ToggleFrameRate: _runtimeSettings.ToggleFrameRate,
|
||||
ToggleUiLock: () =>
|
||||
_runtimeSettings.SetUiLocked(!_runtimeSettings.Gameplay.LockUI),
|
||||
ShowSystemMessage: text => Chat.OnSystemMessage(text, 0u),
|
||||
ShowWeenieError: code => Chat.OnWeenieError(code, null),
|
||||
PlayerPublicWeenieBitfield: () =>
|
||||
Objects.Get(_playerServerGuid)?.PublicWeenieBitfield,
|
||||
ClientVersion: () =>
|
||||
typeof(GameWindow).Assembly.GetName().Version?.ToString(3)
|
||||
?? "unknown",
|
||||
CurrentPosition: () => _playerController?.CellPosition,
|
||||
LastOutsideCorpsePosition: () => LocalPlayer.GetPosition(0x0Eu),
|
||||
ShowConfirmation: (message, completed) =>
|
||||
_retailUiRuntime?.ShowConfirmation(message, completed),
|
||||
Suicide: session.SendSuicide,
|
||||
ClearChat: _ => Chat.Clear(),
|
||||
SaveUi: name => _retailUiRuntime?.SaveNamedLayout(name),
|
||||
LoadUi: name => _retailUiRuntime?.RestoreNamedLayout(name),
|
||||
SaveAutoUi: () => _retailUiRuntime?.SaveLayout(),
|
||||
LoadAutoUi: () => _retailUiRuntime?.RestoreLayout(),
|
||||
IsAway: () =>
|
||||
Objects.Get(_playerServerGuid)?.Properties.GetBool(0x6Eu) == true,
|
||||
SetAway: away => SetRetailAway(session, away),
|
||||
SetAwayMessage: session.SendSetAfkMessage,
|
||||
AcceptLootPermits: () => _runtimeSettings.Gameplay.AcceptLootPermits,
|
||||
SetAcceptLootPermits: _runtimeSettings.SetAcceptLootPermits,
|
||||
DisplayConsent: session.SendDisplayConsent,
|
||||
ClearConsent: session.SendClearConsent,
|
||||
RemoveConsent: session.SendRemoveConsent,
|
||||
SendEmote: session.SendEmote,
|
||||
Friends,
|
||||
AddFriend: session.SendAddFriend,
|
||||
RemoveFriend: session.SendRemoveFriend,
|
||||
ClearFriends: session.SendClearFriends,
|
||||
RequestLegacyFriends: session.SendLegacyFriendsListRequest,
|
||||
Squelch,
|
||||
ModifyCharacterSquelch: session.SendModifyCharacterSquelch,
|
||||
ModifyAccountSquelch: session.SendModifyAccountSquelch,
|
||||
ModifyGlobalSquelch: session.SendModifyGlobalSquelch,
|
||||
LastTeller: () => _retailChatVm?.LastIncomingTellSender,
|
||||
ClearDesiredComponents: () =>
|
||||
{
|
||||
session.SendClearDesiredComponents();
|
||||
DesiredComponents = Array.Empty<(uint Id, uint Amount)>();
|
||||
},
|
||||
HasOpenVendor: () => false,
|
||||
FillComponentBuyList: (_, _) => { }),
|
||||
Chat,
|
||||
TurbineChat,
|
||||
PlayerGuid: () => _playerServerGuid,
|
||||
SendTalk: session.SendTalk,
|
||||
SendTell: session.SendTell,
|
||||
SendChannel: session.SendChannel,
|
||||
SendTurbineChat: (roomId, chatType, dispatchType, senderGuid, text, cookie) =>
|
||||
session.SendTurbineChatTo(
|
||||
roomId, chatType, dispatchType, senderGuid, text, cookie),
|
||||
Log: Console.WriteLine);
|
||||
private void OnUpdate(double dt)
|
||||
{
|
||||
using var _updStage = _frameProfiler.BeginStage(
|
||||
|
|
@ -2032,16 +1712,6 @@ public sealed class GameWindow :
|
|||
// EXPECTED-DIFF: local sidestep pacing now matches how remotes have
|
||||
// always played (w6-cutover-map.md R3).
|
||||
|
||||
private void SyncToolbarWindowButtons()
|
||||
{
|
||||
_retailUiRuntime?.SyncToolbarWindowButtons();
|
||||
}
|
||||
|
||||
private void SetRetailAway(AcDream.Core.Net.WorldSession session, bool away)
|
||||
{
|
||||
session.SendSetAfkMode(away);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// L.0 Display tab: framebuffer-resize handler — update GL viewport
|
||||
/// + camera aspect when the window is resized (by the user dragging
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue