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:
Erik 2026-07-22 18:49:31 +02:00
parent 7fa60971e2
commit 826f9ea9b5
22 changed files with 924 additions and 412 deletions

View file

@ -1,6 +1,7 @@
using AcDream.App.Combat;
using AcDream.App.Input;
using AcDream.App.Interaction;
using AcDream.App.Diagnostics;
using AcDream.App.Net;
using AcDream.App.Physics;
using AcDream.App.Rendering;
@ -12,11 +13,15 @@ using AcDream.App.Update;
using AcDream.App.World;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Chat;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using AcDream.Core.Plugins;
using AcDream.Core.Rendering;
using AcDream.Core.Selection;
using AcDream.Core.Player;
using AcDream.Core.Social;
using AcDream.Core.Spells;
using AcDream.Core.World;
using Silk.NET.Windowing;
@ -28,6 +33,11 @@ internal sealed record SessionPlayerDependencies(
object DatLock,
RuntimeSettingsController Settings,
SettingsDevToolsResult SettingsDevTools,
WorldEnvironmentController WorldEnvironment,
WorldSceneDebugState WorldSceneDebugState,
RuntimeDiagnosticCommandSlot RuntimeDiagnosticCommands,
LiveCombatModeCommandSlot CombatModeCommands,
HostQuiescenceGate HostQuiescence,
PhysicsEngine PhysicsEngine,
PhysicsDataCache PhysicsDataCache,
WorldGameState WorldGameState,
@ -55,6 +65,9 @@ internal sealed record SessionPlayerDependencies(
LocalPlayerSkillState PlayerSkills,
ViewportAspectState ViewportAspect,
PlayerApproachCompletionState PlayerApproachCompletions,
PlayerCharacterOptionsState CharacterOptions,
ShortcutSnapshotState Shortcuts,
DesiredComponentSnapshotState DesiredComponents,
PointerPositionState PointerPosition,
DispatcherMovementInputSource MovementInput,
IInputCaptureSource InputCapture,
@ -66,6 +79,14 @@ internal sealed record SessionPlayerDependencies(
CombatAttackOperationsSlot CombatAttackOperations,
CombatState Combat,
CombatFeedbackSlot CombatFeedback,
ChatLog Chat,
LocalPlayerState LocalPlayer,
Spellbook Spellbook,
ItemManaState ItemMana,
FriendsState Friends,
SquelchState Squelch,
TurbineChatState TurbineChat,
ExternalContainerState ExternalContainers,
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
Action<string> Log);
@ -91,6 +112,8 @@ internal sealed record SessionPlayerResult(
PlayerModeController PlayerMode,
PlayerModeAutoEntry PlayerModeAutoEntry,
LocalPlayerTeleportController LocalTeleport,
LiveSessionHost SessionHost,
GameplayInputActionRouter? GameplayActions,
SessionPlayerRuntimeBindings RuntimeBindings);
internal interface IGameWindowSessionPlayerPublication
@ -115,6 +138,9 @@ internal enum SessionPlayerCompositionPoint
PlayerModeBound,
PortalTransferred,
TeleportBound,
SessionHostCreated,
CommandsBound,
GameplayActionsAttached,
ResultPublished,
}
@ -694,9 +720,10 @@ internal sealed class SessionPlayerCompositionPhase
IDisposable componentLifecycleBinding =
live.ComponentLifecycle.BindOwned(teardown);
IDisposable componentLifecycleAdoption;
try
{
live.RuntimeBindings.Adopt(
componentLifecycleAdoption = live.RuntimeBindings.AdoptOwned(
"live runtime component teardown",
componentLifecycleBinding);
}
@ -705,6 +732,153 @@ internal sealed class SessionPlayerCompositionPhase
componentLifecycleBinding.Dispose();
throw;
}
var componentLifecycleLease = scope.Own(
"live runtime component teardown adoption",
componentLifecycleAdoption,
static value => value.Dispose());
AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? vitals =
d.SettingsDevTools.DevTools?.Vitals
?? interaction.RetainedUi?.Vitals;
AcDream.UI.Abstractions.Panels.Chat.ChatVM? retailChat =
interaction.RetainedUi?.Chat;
var sessionRuntimeFactory = new LiveSessionRuntimeFactory(
new LiveSessionPlayerRuntime(
d.PlayerIdentity,
d.PlayerController,
d.PlayerSkills,
d.WorldOrigin,
d.CharacterOptions,
d.Shortcuts,
d.DesiredComponents),
new LiveSessionDomainRuntime(
d.Objects,
d.LocalPlayer,
d.Spellbook,
d.Combat,
d.ItemMana,
d.Friends,
d.Squelch,
d.TurbineChat,
d.ExternalContainers,
d.Chat),
new LiveSessionUiRuntime(
interaction.RetainedUi?.Runtime,
vitals,
retailChat,
interaction.RetainedUi?.CharacterSheet,
interaction.RetainedUi?.Magic,
live.PaperdollPresenter),
new LiveSessionInteractionRuntime(
d.Settings,
gameplayInput,
playerMode,
playerModeAutoEntry,
interaction.ItemInteraction,
interaction.CombatAttack,
live.SelectionInteractions),
new LiveSessionWorldRuntime(
content.Dats,
live.WorldState,
live.LiveEntities,
sessionEvents,
d.WorldEnvironment,
d.TeleportSink,
spawnClaimClassifier,
live.EquippedChildren,
live.SelectionScene,
d.ParticleVisibility,
d.InboundEntityEvents,
liveness,
networkUpdates,
hydration,
live.RemoteTeleport,
live.EntityEffects,
content.AnimationHookFrames,
live.Presentation,
d.RemoteMovementObservations),
d.Log);
LiveSessionHost sessionHost = sessionRuntimeFactory.Create(liveSession);
Fault(SessionPlayerCompositionPoint.SessionHostCreated);
Action<string>? debugToast = d.SettingsDevTools.DevTools is { } devOwner
? message => devOwner.Debug.AddToast(message)
: null;
var combatCommand = new LiveCombatModeCommandController(
new LiveSessionCombatModeAuthority(sessionHost),
new LocalPlayerCombatEquipmentSource(
d.Objects,
d.PlayerIdentity),
d.Combat,
new ItemInteractionCombatModeIntentSink(
interaction.ItemInteraction),
d.Log,
debugToast,
text => d.Chat.OnSystemMessage(text, 0x1Au));
bindings.Adopt(
"live combat-mode commands",
d.CombatModeCommands.BindOwned(combatCommand));
var nearbyDiagnostics = new NearbyWorldDiagnosticDumper(
new RuntimeNearbyWorldDiagnosticSource(
d.PlayerMode,
d.PlayerController,
host.CameraController,
d.WorldOrigin,
live.WorldState,
d.PhysicsEngine),
d.Log);
var runtimeDiagnostics = new RuntimeDiagnosticCommandController(
d.WorldEnvironment,
d.WorldSceneDebugState,
host.CameraPointerInput,
nearbyDiagnostics,
debugToast);
bindings.Adopt(
"runtime diagnostic commands",
d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics));
Fault(SessionPlayerCompositionPoint.CommandsBound);
CompositionAcquisitionScope.CompositionAcquisitionLease<
GameplayInputActionRouter>? gameplayActionsLease = null;
if (host.InputDispatcher is { } dispatcher)
{
CameraPointerInputController pointer = host.CameraPointerInput
?? throw new InvalidOperationException(
"Gameplay action routing requires the composed pointer owner.");
var commands = new GameplayInputCommandController(
new RetainedGameplayWindowCommands(
interaction.RetainedUi?.Runtime),
new DevToolsGameplayCommands(
d.SettingsDevTools.DevTools?.Presenter),
runtimeDiagnostics,
new PlayerModeGameplayCommands(
d.PlayerMode,
playerMode),
new ItemTargetModeCommands(interaction.ItemInteraction),
new GameplayCameraModeCommands(host.CameraController),
combatCommand,
new GameplayWindowCommands(d.Window.Close));
var targets = new RuntimeGameplayInputPriorityTargets(
gameplayInput,
pointer,
interaction.RetainedUi?.Runtime,
live.SelectionInteractions,
commands);
GameplayInputActionRouter gameplayActions =
GameplayInputActionRouter.Create(
dispatcher,
d.Combat,
targets,
d.HostQuiescence,
d.Log);
gameplayActionsLease = scope.Own(
"gameplay input actions",
gameplayActions,
static value => value.Dispose());
gameplayActions.Attach();
}
Fault(SessionPlayerCompositionPoint.GameplayActionsAttached);
var bindingsLease = scope.Own(
"session/player runtime bindings",
@ -733,12 +907,16 @@ internal sealed class SessionPlayerCompositionPhase
playerMode,
playerModeAutoEntry,
teleportLease.Resource,
sessionHost,
gameplayActionsLease?.Resource,
bindings);
_publication.PublishSessionPlayer(result);
streamerLease.Transfer();
liveSessionLease.Transfer();
teleportLease.Transfer();
gameplayActionsLease?.Transfer();
componentLifecycleLease.Transfer();
bindingsLease.Transfer();
Fault(SessionPlayerCompositionPoint.ResultPublished);
return result;