refactor(runtime): own inventory transaction state

Move the retail one-request-at-a-time gate, shared use busy references, external-container state, item mana, shortcuts, and desired-component snapshots into one Runtime-owned graph over J3's exact ClientObjectTable. Retained UI and session routing now borrow that owner; reset and shutdown preserve the existing order while failure/reentrancy tests protect the transaction boundary.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 08:20:32 +02:00
parent 595d5e6b01
commit 011efbeaa7
18 changed files with 1337 additions and 406 deletions

View file

@ -46,13 +46,11 @@ internal sealed record InteractionRetainedUiDependencies(
CombatState Combat,
ICombatAttackOperations CombatAttackOperations,
SelectionState Selection,
ExternalContainerState ExternalContainers,
ClientObjectTable Objects,
RuntimeInventoryState Inventory,
MagicCatalog MagicCatalog,
Spellbook Spellbook,
RuntimeCommunicationState Communication,
LocalPlayerState LocalPlayer,
ItemManaState ItemMana,
StackSplitQuantityState StackSplitQuantity,
BufferedUiRegistry? UiRegistry,
LiveCombatModeCommandSlot CombatModeCommands,
@ -60,7 +58,6 @@ internal sealed record InteractionRetainedUiDependencies(
ILocalPlayerControllerSource PlayerController,
ILocalPlayerModeSource PlayerMode,
PlayerCharacterOptionsState CharacterOptions,
ShortcutSnapshotState Shortcuts,
Func<DeferredSelectionViewPlaneSource, SelectionCameraSource>
SelectionCameraFactory,
DeferredRenderFrameDiagnosticsSource FrameDiagnostics,
@ -275,8 +272,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
InteractionRetainedUiDependencies d,
DeferredLiveSessionUiAuthority session) =>
new(
d.ExternalContainers,
d.Objects,
d.Inventory.ExternalContainers,
d.Inventory.Objects,
guid => session.CurrentSession?.SendNoLongerViewingContents(guid));
public ItemInteractionController CreateItemInteraction(
@ -286,7 +283,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
DeferredLiveSessionUiAuthority session = late.Session;
DeferredSelectionUiAuthority selection = late.Selection;
return new ItemInteractionController(
d.Objects,
d.Inventory.Objects,
playerGuid: () => d.PlayerIdentity.ServerGuid,
sendUse: null,
sendExamine: guid => session.CurrentSession?.SendAppraise(guid),
@ -313,7 +310,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
placeInBackpack: selection.SendPickup,
backpackContainerId: () => late.InventoryContainer.Current(
d.PlayerIdentity.ServerGuid),
groundObjectId: () => d.ExternalContainers.CurrentContainerId,
groundObjectId: () =>
d.Inventory.ExternalContainers.CurrentContainerId,
sendSplitToWorld: (item, amount) =>
session.CurrentSession?.SendStackableSplitTo3D(item, amount),
selectedObjectId: () => d.Selection.SelectedObjectId ?? 0u,
@ -333,9 +331,10 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
amount),
requestExternalContainer: guid =>
{
d.ExternalContainers.RequestOpen(guid);
d.Inventory.ExternalContainers.RequestOpen(guid);
},
requestUse: selection.RequestUse);
requestUse: selection.RequestUse,
transactions: d.Inventory.Transactions);
}
public RetainedUiComposition CreateRetainedUi(
@ -372,7 +371,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
checkpoint(InteractionRetainedUiCompositionPoint.CursorAssetsCreated);
var characterSheet = new CharacterSheetProvider(
d.Objects,
d.Inventory.Objects,
d.LocalPlayer,
playerGuid: () => d.PlayerIdentity.ServerGuid,
activeToonName: () => d.Settings.ActiveToonKey,
@ -391,7 +390,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
MagicRuntime magic = MagicRuntime.Create(
d.MagicCatalog,
d.Spellbook,
d.Objects,
d.Inventory.Objects,
selectedObject: () => d.Selection.SelectedObjectId,
localPlayerId: () => d.PlayerIdentity.ServerGuid,
accountName: () => late.Session.AccountName
@ -530,7 +529,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
Magic: new MagicRuntimeBindings(
d.Spellbook,
magic.Casting,
d.Objects,
d.Inventory.Objects,
() => d.PlayerIdentity.ServerGuid,
d.MagicCatalog.Components,
iconComposer.GetIcon,
@ -571,7 +570,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
late.SelectionCamera.UiSnapshot),
Indicators: new IndicatorRuntimeBindings(
d.Spellbook,
d.Objects,
d.Inventory.Objects,
() => d.PlayerIdentity.ServerGuid,
() => d.LocalPlayer.GetAttribute(
LocalPlayerState.AttributeKind.Strength)
@ -581,13 +580,13 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
() => late.Session.CurrentSession?.RequestLinkStatusPing(),
d.Window.Close),
Toolbar: new ToolbarRuntimeBindings(
d.Objects,
() => d.Shortcuts.Items,
d.Inventory.Objects,
() => d.Inventory.Shortcuts.Items,
iconComposer.GetIcon,
iconComposer.GetDragIcon,
guid => late.Session.TryUseItem(guid, d.Log),
d.Combat,
d.ItemMana,
d.Inventory.ItemMana,
d.CombatModeCommands.Toggle,
itemInteraction,
entry => late.Session.CurrentSession?.SendAddShortcut(entry),
@ -596,10 +595,11 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
handler => d.Combat.HealthChanged += handler,
handler => d.Combat.HealthChanged -= handler,
late.Selection.ShouldShowHealth,
guid => d.Objects.Get(guid)?.GetAppropriateName(),
guid => d.Inventory.Objects.Get(guid)?.GetAppropriateName(),
d.Combat.GetHealthPercent,
d.Combat.HasHealth,
guid => (uint)(d.Objects.Get(guid)?.StackSize ?? 0),
guid =>
(uint)(d.Inventory.Objects.Get(guid)?.StackSize ?? 0),
guid => late.Session.CurrentSession?.SendQueryHealth(guid),
guid => late.Session.CurrentSession?.SendQueryItemMana(guid),
() => d.PlayerIdentity.ServerGuid,
@ -610,7 +610,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
placement)),
Character: new CharacterRuntimeBindings(characterSheet),
Inventory: new InventoryRuntimeBindings(
d.Objects,
d.Inventory.Objects,
() => d.PlayerIdentity.ServerGuid,
iconComposer.GetIcon,
iconComposer.GetDragIcon,
@ -637,8 +637,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
itemInteraction,
d.Selection),
ExternalContainer: new ExternalContainerRuntimeBindings(
d.ExternalContainers,
d.Objects,
d.Inventory.ExternalContainers,
d.Inventory.Objects,
iconComposer.GetIcon,
iconComposer.GetDragIcon,
itemInteraction,

View file

@ -6,6 +6,7 @@ using AcDream.App.Rendering;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.App.UI.Testing;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.UI.Abstractions;
@ -406,32 +407,6 @@ internal sealed class PlayerCharacterOptionsState
public void Reset() => Options = PlayerDescriptionParser.CharacterOptions1.Default;
}
internal sealed class ShortcutSnapshotState
{
private IReadOnlyList<AcDream.Core.Items.ShortcutEntry> _items =
Array.Empty<AcDream.Core.Items.ShortcutEntry>();
public IReadOnlyList<AcDream.Core.Items.ShortcutEntry> Items
{
get => _items;
set => _items = value ?? Array.Empty<AcDream.Core.Items.ShortcutEntry>();
}
}
internal sealed class DesiredComponentSnapshotState
{
private IReadOnlyList<(uint Id, uint Amount)> _items =
Array.Empty<(uint Id, uint Amount)>();
public IReadOnlyList<(uint Id, uint Amount)> Items
{
get => _items;
set => _items = value ?? Array.Empty<(uint Id, uint Amount)>();
}
public void Clear() => _items = Array.Empty<(uint Id, uint Amount)>();
}
/// <summary>
/// Probe scripts are mounted in Phase 5; reveal/resource facts become valid in
/// Phase 7. Calls remain inert and diagnostic until that exact owner binds.

View file

@ -72,8 +72,7 @@ internal sealed record SessionPlayerDependencies(
ViewportAspectState ViewportAspect,
PlayerApproachCompletionState PlayerApproachCompletions,
PlayerCharacterOptionsState CharacterOptions,
ShortcutSnapshotState Shortcuts,
DesiredComponentSnapshotState DesiredComponents,
RuntimeInventoryState Inventory,
PointerPositionState PointerPosition,
DispatcherMovementInputSource MovementInput,
IInputCaptureSource InputCapture,
@ -88,8 +87,6 @@ internal sealed record SessionPlayerDependencies(
RuntimeCommunicationState Communication,
LocalPlayerState LocalPlayer,
Spellbook Spellbook,
ItemManaState ItemMana,
ExternalContainerState ExternalContainers,
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
Action<string> Log);
@ -802,16 +799,13 @@ internal sealed class SessionPlayerCompositionPhase
d.PlayerController,
d.PlayerSkills,
d.WorldOrigin,
d.CharacterOptions,
d.Shortcuts,
d.DesiredComponents),
d.CharacterOptions),
new LiveSessionDomainRuntime(
d.EntityObjects,
d.LocalPlayer,
d.Spellbook,
d.Combat,
d.ItemMana,
d.ExternalContainers,
d.Inventory,
d.Communication),
new LiveSessionUiRuntime(
interaction.RetainedUi?.Runtime,