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
386
src/AcDream.App/Net/LiveSessionRuntimeFactory.cs
Normal file
386
src/AcDream.App/Net/LiveSessionRuntimeFactory.cs
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
using System.Diagnostics;
|
||||
using AcDream.App.Combat;
|
||||
using AcDream.App.Composition;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Interaction;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Selection;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Settings;
|
||||
using AcDream.App.Spells;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Chat;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Player;
|
||||
using AcDream.Core.Social;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Content;
|
||||
using AcDream.UI.Abstractions.Panels.Chat;
|
||||
using AcDream.UI.Abstractions.Panels.Vitals;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Net;
|
||||
|
||||
internal sealed record LiveSessionPlayerRuntime(
|
||||
LocalPlayerIdentityState Identity,
|
||||
LocalPlayerControllerSlot Controller,
|
||||
LocalPlayerSkillState Skills,
|
||||
LiveWorldOriginState WorldOrigin,
|
||||
PlayerCharacterOptionsState CharacterOptions,
|
||||
ShortcutSnapshotState Shortcuts,
|
||||
DesiredComponentSnapshotState DesiredComponents);
|
||||
|
||||
internal sealed record LiveSessionDomainRuntime(
|
||||
ClientObjectTable Objects,
|
||||
LocalPlayerState LocalPlayer,
|
||||
Spellbook Spellbook,
|
||||
CombatState Combat,
|
||||
ItemManaState ItemMana,
|
||||
FriendsState Friends,
|
||||
SquelchState Squelch,
|
||||
TurbineChatState TurbineChat,
|
||||
ExternalContainerState ExternalContainers,
|
||||
ChatLog Chat);
|
||||
|
||||
internal sealed record LiveSessionUiRuntime(
|
||||
RetailUiRuntime? RetailUi,
|
||||
VitalsVM? Vitals,
|
||||
ChatVM? RetailChat,
|
||||
CharacterSheetProvider? CharacterSheet,
|
||||
MagicRuntime? Magic,
|
||||
PaperdollFramePresenter? Paperdoll);
|
||||
|
||||
internal sealed record LiveSessionInteractionRuntime(
|
||||
RuntimeSettingsController Settings,
|
||||
GameplayInputFrameController GameplayInput,
|
||||
PlayerModeController PlayerMode,
|
||||
PlayerModeAutoEntry PlayerModeAutoEntry,
|
||||
ItemInteractionController ItemInteraction,
|
||||
CombatAttackController CombatAttack,
|
||||
SelectionInteractionController SelectionInteractions);
|
||||
|
||||
internal sealed record LiveSessionWorldRuntime(
|
||||
IDatReaderWriter Dats,
|
||||
GpuWorldState WorldState,
|
||||
LiveEntityRuntime LiveEntities,
|
||||
LiveEntitySessionController EntitySession,
|
||||
WorldEnvironmentController Environment,
|
||||
DeferredLocalPlayerTeleportNetworkSink Teleport,
|
||||
DatSpawnClaimHydrationClassifier SpawnClaims,
|
||||
EquippedChildRenderController EquippedChildren,
|
||||
RetailSelectionScene SelectionScene,
|
||||
ParticleVisibilityController ParticleVisibility,
|
||||
RetailInboundEventDispatcher InboundEvents,
|
||||
LiveEntityLivenessController Liveness,
|
||||
LiveEntityNetworkUpdateController NetworkUpdates,
|
||||
LiveEntityHydrationController Hydration,
|
||||
RemoteTeleportController RemoteTeleport,
|
||||
EntityEffectController EntityEffects,
|
||||
AnimationHookFrameQueue AnimationHookFrames,
|
||||
LiveEntityPresentationController Presentation,
|
||||
RemoteMovementObservationTracker RemoteMovementObservations);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the exact per-generation route/reset graph for the canonical live
|
||||
/// session. It owns no mirrored session state and retains no window callback.
|
||||
/// </summary>
|
||||
internal sealed class LiveSessionRuntimeFactory
|
||||
{
|
||||
private readonly LiveSessionPlayerRuntime _player;
|
||||
private readonly LiveSessionDomainRuntime _domain;
|
||||
private readonly LiveSessionUiRuntime _ui;
|
||||
private readonly LiveSessionInteractionRuntime _interaction;
|
||||
private readonly LiveSessionWorldRuntime _world;
|
||||
private readonly Action<string> _log;
|
||||
|
||||
public LiveSessionRuntimeFactory(
|
||||
LiveSessionPlayerRuntime player,
|
||||
LiveSessionDomainRuntime domain,
|
||||
LiveSessionUiRuntime ui,
|
||||
LiveSessionInteractionRuntime interaction,
|
||||
LiveSessionWorldRuntime world,
|
||||
Action<string> log)
|
||||
{
|
||||
_player = player ?? throw new ArgumentNullException(nameof(player));
|
||||
_domain = domain ?? throw new ArgumentNullException(nameof(domain));
|
||||
_ui = ui ?? throw new ArgumentNullException(nameof(ui));
|
||||
_interaction = interaction
|
||||
?? throw new ArgumentNullException(nameof(interaction));
|
||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||
_log = log ?? throw new ArgumentNullException(nameof(log));
|
||||
}
|
||||
|
||||
public LiveSessionHost Create(LiveSessionController controller)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(controller);
|
||||
return new LiveSessionHost(controller, new LiveSessionHostBindings(
|
||||
Routing: new(
|
||||
CreateEventRouter,
|
||||
session => new LiveSessionCommandRouter(
|
||||
CreateCommandBindings(session))),
|
||||
Reset: CreateResetBindings(),
|
||||
Selection: new(
|
||||
SetPlayerIdentity: id => _player.Identity.ServerGuid = id,
|
||||
SetVitalsIdentity: id => _ui.Vitals?.SetLocalPlayerGuid(id),
|
||||
SetChatIdentity: _domain.Chat.SetLocalPlayerGuid,
|
||||
MarkPersistent: _world.WorldState.MarkPersistent,
|
||||
SetVanishProbeIdentity: id => EntityVanishProbe.PlayerGuid = id,
|
||||
ClearCombat: _domain.Combat.Clear),
|
||||
EnteredWorld: new(
|
||||
SetActiveCharacter: _interaction.Settings.SetActiveCharacter,
|
||||
RestoreLayout: () => _ui.RetailUi?.RestoreLayout(),
|
||||
SyncToolbar: () => _ui.RetailUi?.SyncToolbarWindowButtons(),
|
||||
LoadCharacterSettings: _interaction.Settings.LoadCharacterContext,
|
||||
ArmPlayerModeAutoEntry: _interaction.PlayerModeAutoEntry.Arm),
|
||||
Connecting: (host, port, user) =>
|
||||
_domain.Chat.OnSystemMessage(
|
||||
$"connecting to {host}:{port} as {user}",
|
||||
chatType: 1),
|
||||
Connected: () =>
|
||||
_domain.Chat.OnSystemMessage(
|
||||
"connected — character list received",
|
||||
chatType: 1)));
|
||||
}
|
||||
|
||||
private LiveSessionResetBindings CreateResetBindings() => new()
|
||||
{
|
||||
MouseCapture = _interaction.GameplayInput.ResetSession,
|
||||
PlayerPresentation = ResetPlayerPresentation,
|
||||
TeleportTransit = _world.Teleport.ResetSession,
|
||||
SessionDialogs = () => _ui.RetailUi?.ResetSessionDialogs(),
|
||||
ChatCommandTargets = () => _ui.RetailChat?.ResetSessionTargets(),
|
||||
SettingsCharacterContext =
|
||||
_interaction.Settings.RestoreDefaultCharacterContext,
|
||||
EquippedChildren = _world.EquippedChildren.Clear,
|
||||
ExternalContainer = () => _domain.ExternalContainers.Reset(),
|
||||
InteractionAndSelection = _interaction.SelectionInteractions.ResetSession,
|
||||
SelectionPresentation = _world.SelectionScene.Reset,
|
||||
ObjectTable = _domain.Objects.Clear,
|
||||
Spellbook = _domain.Spellbook.Clear,
|
||||
MagicRuntime = () => _ui.Magic?.Reset(),
|
||||
CombatAttack = _interaction.CombatAttack.ResetSession,
|
||||
CombatState = _domain.Combat.Clear,
|
||||
ItemMana = _domain.ItemMana.Clear,
|
||||
LocalPlayer = _domain.LocalPlayer.Clear,
|
||||
Friends = _domain.Friends.Clear,
|
||||
Squelch = _domain.Squelch.Clear,
|
||||
TurbineChat = _domain.TurbineChat.Reset,
|
||||
ParticleVisibility = _world.ParticleVisibility.Reset,
|
||||
InboundEventFifo = _world.InboundEvents.Clear,
|
||||
LiveLiveness = _world.Liveness.Clear,
|
||||
LiveRuntime = ResetLiveRuntime,
|
||||
SessionIdentity = ResetIdentity,
|
||||
RemoteTeleport = _world.RemoteTeleport.Clear,
|
||||
NetworkEffects = _world.EntityEffects.ClearNetworkState,
|
||||
AnimationHookFrames = _world.AnimationHookFrames.Clear,
|
||||
LivePresentation = _world.Presentation.Clear,
|
||||
RemoteMovementDiagnostics = _world.RemoteMovementObservations.Clear,
|
||||
};
|
||||
|
||||
private void ResetPlayerPresentation()
|
||||
{
|
||||
_interaction.PlayerMode.ResetSession();
|
||||
_world.SpawnClaims.Reset();
|
||||
}
|
||||
|
||||
private void ResetLiveRuntime() =>
|
||||
LiveSessionEntityRuntimeReset.ClearAndRequireConvergence(
|
||||
_world.LiveEntities);
|
||||
|
||||
private void ResetIdentity()
|
||||
{
|
||||
LiveSessionEntityRuntimeReset.RequireConvergence(_world.LiveEntities);
|
||||
|
||||
// PlayerModule::Clear @ 0x005D48A0 clears shortcuts, desired
|
||||
// components, and character option objects. CPlayerSystem::Begin
|
||||
// @ 0x0055D410 resets player identity and session fields. The option
|
||||
// default comes from PlayerModule::PlayerModule @ 0x005D51F0.
|
||||
_player.Identity.ServerGuid = 0u;
|
||||
_ui.Vitals?.SetLocalPlayerGuid(0u);
|
||||
_domain.Chat.ResetSessionIdentity();
|
||||
EntityVanishProbe.PlayerGuid = 0u;
|
||||
_interaction.Settings.ResetActiveCharacterKey();
|
||||
_player.CharacterOptions.Reset();
|
||||
_player.Skills.ResetSession();
|
||||
_world.NetworkUpdates.ResetSessionState();
|
||||
_world.Hydration.ResetSessionState();
|
||||
_player.Shortcuts.Items = Array.Empty<ShortcutEntry>();
|
||||
_player.DesiredComponents.Clear();
|
||||
_ui.Paperdoll?.MarkDirty();
|
||||
|
||||
// X/Y are ignored until the next logical player CreateObject claims a
|
||||
// new origin. Keeping the last values avoids inventing a synthetic
|
||||
// landblock while still forcing first-spawn initialization.
|
||||
_player.WorldOrigin.Reset();
|
||||
}
|
||||
|
||||
private LiveSessionEventRouter CreateEventRouter(WorldSession session)
|
||||
{
|
||||
SkillTable? skillTable = _world.Dats.Get<SkillTable>(0x0E000004u);
|
||||
if (_ui.CharacterSheet is not null)
|
||||
{
|
||||
_ui.CharacterSheet.SkillTable = skillTable;
|
||||
_ui.CharacterSheet.ExperienceTable =
|
||||
CharacterSheetProvider.LoadExperienceTable(_world.Dats, _log);
|
||||
}
|
||||
|
||||
return new LiveSessionEventRouter(
|
||||
session,
|
||||
_world.EntitySession.CreateSink(),
|
||||
new LiveEnvironmentSessionSink(
|
||||
_world.Environment.ApplyAdminEnvirons,
|
||||
_world.Environment.SynchronizeFromServer),
|
||||
CreateInventoryBindings(),
|
||||
CreateCharacterBindings(skillTable),
|
||||
new LiveSocialSessionBindings(
|
||||
_domain.Chat,
|
||||
_domain.TurbineChat,
|
||||
_domain.Friends,
|
||||
_domain.Squelch));
|
||||
}
|
||||
|
||||
private LiveInventorySessionBindings CreateInventoryBindings() => new(
|
||||
_domain.Objects,
|
||||
_domain.LocalPlayer,
|
||||
PlayerGuid: () => _player.Identity.ServerGuid,
|
||||
OnShortcuts: list => _player.Shortcuts.Items = list,
|
||||
OnUseDone: error =>
|
||||
{
|
||||
_domain.ExternalContainers.ApplyUseDone(error);
|
||||
_interaction.ItemInteraction.CompleteUse(error);
|
||||
},
|
||||
_domain.ItemMana,
|
||||
OnDesiredComponents: components =>
|
||||
_player.DesiredComponents.Items = components,
|
||||
ExternalContainers: _domain.ExternalContainers);
|
||||
|
||||
private LiveCharacterSessionBindings CreateCharacterBindings(
|
||||
SkillTable? skillTable)
|
||||
{
|
||||
var skillCreditResolver = new LiveSkillCreditResolver(skillTable);
|
||||
return new LiveCharacterSessionBindings(
|
||||
_domain.Combat,
|
||||
_domain.Spellbook,
|
||||
ResolveSkillFormulaBonus: skillCreditResolver.Resolve,
|
||||
OnSkillsUpdated: (runSkill, jumpSkill) =>
|
||||
{
|
||||
_player.Skills.Update(
|
||||
runSkill,
|
||||
jumpSkill,
|
||||
_player.Controller.Controller);
|
||||
if (_player.Skills.IsComplete
|
||||
&& _player.Controller.Controller is not null)
|
||||
{
|
||||
_log(
|
||||
$"player: applied server skills " +
|
||||
$"run={_player.Skills.RunSkill} " +
|
||||
$"jump={_player.Skills.JumpSkill}");
|
||||
}
|
||||
},
|
||||
OnConfirmationRequest: request =>
|
||||
_ui.RetailUi?.HandleConfirmationRequest(request),
|
||||
OnConfirmationDone: done =>
|
||||
_ui.RetailUi?.HandleConfirmationDone(done),
|
||||
OnCharacterOptions: (options1, _) =>
|
||||
_player.CharacterOptions.Options =
|
||||
(Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1)
|
||||
options1,
|
||||
ClientTime: ClientTimerNow);
|
||||
}
|
||||
|
||||
private LiveSessionCommandBindings CreateCommandBindings(
|
||||
WorldSession session) => new(
|
||||
ClientCommands: new ClientCommandController.Bindings(
|
||||
TeleportToLifestone: session.SendTeleportToLifestone,
|
||||
TeleportToMarketplace: session.SendTeleportToMarketplace,
|
||||
TeleportToPkArena: session.SendTeleportToPkArena,
|
||||
TeleportToPkLiteArena: session.SendTeleportToPkLiteArena,
|
||||
TeleportToHouse: session.SendTeleportToHouse,
|
||||
TeleportToMansion: session.SendTeleportToMansion,
|
||||
QueryAge: session.SendQueryAge,
|
||||
QueryBirth: session.SendQueryBirth,
|
||||
ToggleFrameRate: _interaction.Settings.ToggleFrameRate,
|
||||
ToggleUiLock: () =>
|
||||
_interaction.Settings.SetUiLocked(
|
||||
!_interaction.Settings.Gameplay.LockUI),
|
||||
ShowSystemMessage: text => _domain.Chat.OnSystemMessage(text, 0u),
|
||||
ShowWeenieError: code => _domain.Chat.OnWeenieError(code, null),
|
||||
PlayerPublicWeenieBitfield: () =>
|
||||
_domain.Objects.Get(_player.Identity.ServerGuid)?
|
||||
.PublicWeenieBitfield,
|
||||
ClientVersion: () =>
|
||||
typeof(LiveSessionRuntimeFactory).Assembly
|
||||
.GetName().Version?.ToString(3)
|
||||
?? "unknown",
|
||||
CurrentPosition: () => _player.Controller.Controller?.CellPosition,
|
||||
LastOutsideCorpsePosition: () =>
|
||||
_domain.LocalPlayer.GetPosition(0x0Eu),
|
||||
ShowConfirmation: (message, completed) =>
|
||||
_ui.RetailUi?.ShowConfirmation(message, completed),
|
||||
Suicide: session.SendSuicide,
|
||||
ClearChat: _ => _domain.Chat.Clear(),
|
||||
SaveUi: name => _ui.RetailUi?.SaveNamedLayout(name),
|
||||
LoadUi: name => _ui.RetailUi?.RestoreNamedLayout(name),
|
||||
SaveAutoUi: () => _ui.RetailUi?.SaveLayout(),
|
||||
LoadAutoUi: () => _ui.RetailUi?.RestoreLayout(),
|
||||
IsAway: () =>
|
||||
_domain.Objects.Get(_player.Identity.ServerGuid)?
|
||||
.Properties.GetBool(0x6Eu) == true,
|
||||
SetAway: session.SendSetAfkMode,
|
||||
SetAwayMessage: session.SendSetAfkMessage,
|
||||
AcceptLootPermits: () =>
|
||||
_interaction.Settings.Gameplay.AcceptLootPermits,
|
||||
SetAcceptLootPermits: _interaction.Settings.SetAcceptLootPermits,
|
||||
DisplayConsent: session.SendDisplayConsent,
|
||||
ClearConsent: session.SendClearConsent,
|
||||
RemoveConsent: session.SendRemoveConsent,
|
||||
SendEmote: session.SendEmote,
|
||||
_domain.Friends,
|
||||
AddFriend: session.SendAddFriend,
|
||||
RemoveFriend: session.SendRemoveFriend,
|
||||
ClearFriends: session.SendClearFriends,
|
||||
RequestLegacyFriends: session.SendLegacyFriendsListRequest,
|
||||
_domain.Squelch,
|
||||
ModifyCharacterSquelch: session.SendModifyCharacterSquelch,
|
||||
ModifyAccountSquelch: session.SendModifyAccountSquelch,
|
||||
ModifyGlobalSquelch: session.SendModifyGlobalSquelch,
|
||||
LastTeller: () => _ui.RetailChat?.LastIncomingTellSender,
|
||||
ClearDesiredComponents: () =>
|
||||
{
|
||||
session.SendClearDesiredComponents();
|
||||
_player.DesiredComponents.Clear();
|
||||
},
|
||||
HasOpenVendor: () => false,
|
||||
FillComponentBuyList: (_, _) => { }),
|
||||
_domain.Chat,
|
||||
_domain.TurbineChat,
|
||||
PlayerGuid: () => _player.Identity.ServerGuid,
|
||||
SendTalk: session.SendTalk,
|
||||
SendTell: session.SendTell,
|
||||
SendChannel: session.SendChannel,
|
||||
SendTurbineChat: (
|
||||
roomId,
|
||||
chatType,
|
||||
dispatchType,
|
||||
senderGuid,
|
||||
text,
|
||||
cookie) => session.SendTurbineChatTo(
|
||||
roomId,
|
||||
chatType,
|
||||
dispatchType,
|
||||
senderGuid,
|
||||
text,
|
||||
cookie),
|
||||
Log: _log);
|
||||
|
||||
private static double ClientTimerNow() =>
|
||||
Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue