refactor(runtime): own canonical action state

Move selection, combat, and interaction target mode under one Runtime owner; make plugins, retained UI, session routing, and typed runtime views borrow its exact children; and add failure-safe reset, instance isolation, source ownership, and normalized checkpoint coverage without changing retail ordering.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 10:44:09 +02:00
parent bb45afef33
commit b298f99f91
38 changed files with 711 additions and 93 deletions

View file

@ -44,9 +44,8 @@ internal sealed record InteractionRetainedUiDependencies(
RetainedUiInputCaptureSlot RetainedInputCapture,
InputDispatcher? InputDispatcher,
RuntimeSettingsController Settings,
CombatState Combat,
RuntimeActionState Actions,
ICombatAttackOperations CombatAttackOperations,
SelectionState Selection,
RuntimeInventoryState Inventory,
MagicCatalog MagicCatalog,
RuntimeCharacterState Character,
@ -257,14 +256,14 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
{
public CombatAttackController CreateCombatAttack(
InteractionRetainedUiDependencies d) =>
new(d.Combat, d.CombatAttackOperations);
new(d.Actions.Combat, d.CombatAttackOperations);
public CombatTargetController CreateCombatTarget(
InteractionRetainedUiDependencies d,
DeferredSelectionUiAuthority selection) =>
new(
d.Combat,
d.Selection,
d.Actions.Combat,
d.Actions.Selection,
autoTarget: () => d.Settings.Gameplay.AutoTarget,
selectClosestTarget: () =>
selection.SelectClosestCombatTarget(showToast: false));
@ -286,6 +285,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
return new ItemInteractionController(
d.Inventory.Objects,
d.Inventory.Transactions,
d.Actions.Interaction,
playerGuid: () => d.PlayerIdentity.ServerGuid,
sendUse: null,
sendExamine: guid => session.CurrentSession?.SendAppraise(guid),
@ -304,8 +304,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
d.PlayerMode.IsPlayerMode
&& d.PlayerController.Controller is { IsAirborne: false },
inNonCombatMode: () =>
d.Combat.CurrentMode == CombatMode.NonCombat,
combatState: d.Combat,
d.Actions.Combat.CurrentMode == CombatMode.NonCombat,
combatState: d.Actions.Combat,
sendChangeCombatMode: mode =>
session.CurrentSession?.SendChangeCombatMode(mode),
isComponentPack: d.MagicCatalog.IsComponentPack,
@ -316,7 +316,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
d.Inventory.ExternalContainers.CurrentContainerId,
sendSplitToWorld: (item, amount) =>
session.CurrentSession?.SendStackableSplitTo3D(item, amount),
selectedObjectId: () => d.Selection.SelectedObjectId ?? 0u,
selectedObjectId: () =>
d.Actions.Selection.SelectedObjectId ?? 0u,
stackSplitQuantity: d.StackSplitQuantity,
systemMessage:
text => d.Communication.Chat.OnSystemMessage(text, 0x1Au),
@ -351,7 +352,9 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
try
{
VitalsVM vitals = d.ExistingVitals
?? new VitalsVM(d.Combat, d.Character.LocalPlayer);
?? new VitalsVM(
d.Actions.Combat,
d.Character.LocalPlayer);
UiHost host = lease.AcquireHost(
() => new UiHost(
d.Gl,
@ -367,7 +370,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
itemInteraction,
worldTargetProvider: () =>
late.Selection.PickAtCursor(includeSelf: true) ?? 0u,
combatModeProvider: () => d.Combat.CurrentMode);
combatModeProvider: () =>
d.Actions.Combat.CurrentMode);
var cursorManager = new RetailCursorManager(d.Dats, d.DatLock);
checkpoint(InteractionRetainedUiCompositionPoint.CursorAssetsCreated);
@ -404,7 +408,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
d.MagicCatalog,
d.Character.Spellbook,
d.Inventory.Objects,
selectedObject: () => d.Selection.SelectedObjectId,
selectedObject: () =>
d.Actions.Selection.SelectedObjectId,
localPlayerId: () => d.PlayerIdentity.ServerGuid,
accountName: () => late.Session.AccountName
?? d.Options.LiveUser
@ -533,10 +538,10 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
Chat: new ChatRuntimeBindings(chat, () => late.Session.Commands),
Radar: new RadarRuntimeBindings(
late.Radar.Snapshot,
d.Selection,
d.Actions.Selection,
d.Settings.SetUiLocked),
Combat: new CombatRuntimeBindings(
d.Combat,
d.Actions.Combat,
combatAttack,
() => d.Settings.Gameplay,
d.Settings.SetCombatGameplay),
@ -550,11 +555,13 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
iconComposer.GetDragIcon,
iconComposer.GetSpellIcon,
iconComposer.GetSpellComponentIcon,
d.Selection,
d.Actions.Selection,
d.MagicCatalog.GetSpellLevel,
magic.GetExamineComponents,
MagicSkillLevel,
guid => d.Selection.Select(guid, SelectionChangeSource.Inventory),
guid => d.Actions.Selection.Select(
guid,
SelectionChangeSource.Inventory),
guid => late.Session.TryUseItem(guid, d.Log),
(tab, position, spellId) =>
late.GameRuntime.AddFavorite(tab, position, spellId),
@ -574,7 +581,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
() => 1.0,
() => d.Settings.DisplayPreview.ShowFps),
VividTarget: new VividTargetRuntimeBindings(
d.Selection,
d.Actions.Selection,
() => d.PlayerIdentity.ServerGuid,
() => d.Settings.Gameplay.VividTargetingIndicator,
late.Selection.ResolveVividTargetInfo,
@ -596,19 +603,19 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
iconComposer.GetIcon,
iconComposer.GetDragIcon,
guid => late.Session.TryUseItem(guid, d.Log),
d.Combat,
d.Actions.Combat,
d.Inventory.ItemMana,
d.CombatModeCommands.Toggle,
itemInteraction,
entry => late.GameRuntime.AddShortcut(entry),
index => late.GameRuntime.RemoveShortcut(index),
d.Selection,
handler => d.Combat.HealthChanged += handler,
handler => d.Combat.HealthChanged -= handler,
d.Actions.Selection,
handler => d.Actions.Combat.HealthChanged += handler,
handler => d.Actions.Combat.HealthChanged -= handler,
late.Selection.ShouldShowHealth,
guid => d.Inventory.Objects.Get(guid)?.GetAppropriateName(),
d.Combat.GetHealthPercent,
d.Combat.HasHealth,
d.Actions.Combat.GetHealthPercent,
d.Actions.Combat.HasHealth,
guid =>
(uint)(d.Inventory.Objects.Get(guid)?.StackSize ?? 0),
guid => late.Session.CurrentSession?.SendQueryHealth(guid),
@ -646,14 +653,14 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
target,
amount),
itemInteraction,
d.Selection),
d.Actions.Selection),
ExternalContainer: new ExternalContainerRuntimeBindings(
d.Inventory.ExternalContainers,
d.Inventory.Objects,
iconComposer.GetIcon,
iconComposer.GetDragIcon,
itemInteraction,
d.Selection,
d.Actions.Selection,
guid => late.Session.CurrentSession?.SendUse(guid),
(item, container, placement) =>
late.Session.CurrentSession?.SendPutItemInContainer(

View file

@ -48,7 +48,7 @@ internal sealed record SessionPlayerDependencies(
PhysicsDataCache PhysicsDataCache,
WorldGameState WorldGameState,
WorldEvents WorldEvents,
SelectionState Selection,
RuntimeActionState Actions,
RuntimeEntityObjectLifetime EntityObjects,
EntityClassificationCache ClassificationCache,
LiveEntityRuntimeSlot RuntimeSlot,
@ -80,7 +80,6 @@ internal sealed record SessionPlayerDependencies(
UpdateFrameClock UpdateClock,
MovementTruthDiagnosticController MovementDiagnostics,
CombatAttackOperationsSlot CombatAttackOperations,
CombatState Combat,
CombatFeedbackSlot CombatFeedback,
RuntimeCommunicationState Communication,
RuntimeCharacterState Character,
@ -326,7 +325,7 @@ internal sealed class SessionPlayerCompositionPhase
d.DatLock);
var worldQuiescence = new WorldGenerationQuiescence(
live.WorldAvailability,
d.Selection,
d.Actions.Selection,
live.WorldState,
guid => live.LiveEntities.TryGetProjectionKey(
guid,
@ -455,7 +454,7 @@ internal sealed class SessionPlayerCompositionPhase
live.EntityEffects,
live.RemoteTeleport,
live.SelectionInteractions,
d.Selection,
d.Actions.Selection,
d.AnimatedEntities,
d.RemoteMovementObservations,
d.TranslucencyFades,
@ -563,9 +562,9 @@ internal sealed class SessionPlayerCompositionPhase
"combat attack operations",
d.CombatAttackOperations.BindOwned(
new LiveCombatAttackOperations(
d.Combat,
d.Actions.Combat,
new CombatAttackTargetSource(
d.Selection,
d.Actions.Selection,
live.LiveEntities,
d.EntityObjects.Objects,
d.PlayerIdentity),
@ -798,7 +797,7 @@ internal sealed class SessionPlayerCompositionPhase
new LiveSessionDomainRuntime(
d.EntityObjects,
d.Character,
d.Combat,
d.Actions,
d.Inventory,
d.Communication),
new LiveSessionUiRuntime(
@ -856,7 +855,7 @@ internal sealed class SessionPlayerCompositionPhase
new LocalPlayerCombatEquipmentSource(
d.EntityObjects.Objects,
d.PlayerIdentity),
d.Combat,
d.Actions.Combat,
new ItemInteractionCombatModeIntentSink(
interaction.ItemInteraction),
d.Log,
@ -875,10 +874,10 @@ internal sealed class SessionPlayerCompositionPhase
d.Inventory,
d.Character,
d.Communication,
d.Actions,
d.PlayerController,
worldReveal,
d.UpdateClock,
d.Selection,
live.SelectionInteractions,
gameplayInput,
combatCommand);
@ -940,7 +939,7 @@ internal sealed class SessionPlayerCompositionPhase
GameplayInputActionRouter gameplayActions =
GameplayInputActionRouter.Create(
dispatcher,
d.Combat,
d.Actions.Combat,
targets,
d.HostQuiescence,
d.Log);