refactor(runtime): close canonical gameplay ownership
Unify the toolbar shortcut manager with Runtime inventory state, route retail-ordered shortcut and spellbook command effects through the canonical owners, and make retained controllers borrow those exact instances. Remove the item-interaction transaction fallback and add graphical/no-window parity plus failure-safe terminal ownership-ledger coverage. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
ce6fae7b38
commit
89e6b207f8
36 changed files with 1433 additions and 251 deletions
|
|
@ -285,6 +285,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
DeferredSelectionUiAuthority selection = late.Selection;
|
||||
return new ItemInteractionController(
|
||||
d.Inventory.Objects,
|
||||
d.Inventory.Transactions,
|
||||
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
||||
sendUse: null,
|
||||
sendExamine: guid => session.CurrentSession?.SendAppraise(guid),
|
||||
|
|
@ -334,8 +335,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
{
|
||||
d.Inventory.ExternalContainers.RequestOpen(guid);
|
||||
},
|
||||
requestUse: selection.RequestUse,
|
||||
transactions: d.Inventory.Transactions);
|
||||
requestUse: selection.RequestUse);
|
||||
}
|
||||
|
||||
public RetainedUiComposition CreateRetainedUi(
|
||||
|
|
@ -592,7 +592,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
d.Window.Close),
|
||||
Toolbar: new ToolbarRuntimeBindings(
|
||||
d.Inventory.Objects,
|
||||
() => d.Inventory.Shortcuts.Items,
|
||||
d.Inventory.Shortcuts,
|
||||
iconComposer.GetIcon,
|
||||
iconComposer.GetDragIcon,
|
||||
guid => late.Session.TryUseItem(guid, d.Log),
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
private LiveInventorySessionBindings CreateInventoryBindings() => new(
|
||||
_domain.Inventory.Objects,
|
||||
PlayerGuid: () => _player.Identity.ServerGuid,
|
||||
OnShortcuts: _domain.Inventory.Shortcuts.Replace,
|
||||
OnShortcuts: _domain.Inventory.Shortcuts.Load,
|
||||
OnUseDone: error =>
|
||||
{
|
||||
_domain.Inventory.ExternalContainers.ApplyUseDone(error);
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ internal sealed class CurrentGameRuntimeAdapter
|
|||
sessionHost,
|
||||
commands,
|
||||
_view,
|
||||
inventory,
|
||||
character,
|
||||
selectionState,
|
||||
selection,
|
||||
gameplayInput,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using AcDream.App.Net;
|
|||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Session;
|
||||
using AcDream.UI.Abstractions;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
|
@ -31,6 +32,8 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
private readonly LiveSessionHost _sessionHost;
|
||||
private readonly ICommandBus _commands;
|
||||
private readonly CurrentGameRuntimeViewAdapter _view;
|
||||
private readonly RuntimeInventoryState _inventory;
|
||||
private readonly RuntimeCharacterState _character;
|
||||
private readonly SelectionState _selectionState;
|
||||
private readonly SelectionInteractionController _selection;
|
||||
private readonly GameplayInputFrameController _gameplayInput;
|
||||
|
|
@ -42,6 +45,8 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
LiveSessionHost sessionHost,
|
||||
ICommandBus commands,
|
||||
CurrentGameRuntimeViewAdapter view,
|
||||
RuntimeInventoryState inventory,
|
||||
RuntimeCharacterState character,
|
||||
SelectionState selectionState,
|
||||
SelectionInteractionController selection,
|
||||
GameplayInputFrameController gameplayInput,
|
||||
|
|
@ -52,6 +57,8 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
_sessionHost = sessionHost ?? throw new ArgumentNullException(nameof(sessionHost));
|
||||
_commands = commands ?? throw new ArgumentNullException(nameof(commands));
|
||||
_view = view ?? throw new ArgumentNullException(nameof(view));
|
||||
_inventory = inventory ?? throw new ArgumentNullException(nameof(inventory));
|
||||
_character = character ?? throw new ArgumentNullException(nameof(character));
|
||||
_selectionState = selectionState
|
||||
?? throw new ArgumentNullException(nameof(selectionState));
|
||||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||
|
|
@ -288,8 +295,13 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
if (command.Index < 0
|
||||
|| (command.ObjectId == 0u && command.SpellId == 0u))
|
||||
var entry = new ShortcutEntry(
|
||||
command.Index,
|
||||
command.ObjectId,
|
||||
command.SpellId);
|
||||
if (!_inventory.TryAddShortcut(
|
||||
entry,
|
||||
() => _commands.Publish(new AddShortcutRuntimeCmd(entry))))
|
||||
{
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.InventoryState,
|
||||
|
|
@ -298,10 +310,6 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
command.ObjectId);
|
||||
}
|
||||
|
||||
_commands.Publish(new AddShortcutRuntimeCmd(new ShortcutEntry(
|
||||
command.Index,
|
||||
command.ObjectId,
|
||||
command.SpellId)));
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.InventoryState,
|
||||
operation: 0,
|
||||
|
|
@ -316,7 +324,10 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
if (index < 0)
|
||||
if (!_inventory.TryRemoveShortcut(
|
||||
index,
|
||||
() => _commands.Publish(
|
||||
new RemoveShortcutRuntimeCmd((uint)index))))
|
||||
{
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.InventoryState,
|
||||
|
|
@ -324,7 +335,6 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
RuntimeCommandStatus.Rejected);
|
||||
}
|
||||
|
||||
_commands.Publish(new RemoveShortcutRuntimeCmd((uint)index));
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.InventoryState,
|
||||
operation: 1,
|
||||
|
|
@ -340,9 +350,14 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
if ((uint)tabIndex >= 8u
|
||||
|| position < 0
|
||||
|| spellId == 0u)
|
||||
if (!_character.TryAddFavorite(
|
||||
tabIndex,
|
||||
position,
|
||||
spellId,
|
||||
() => _commands.Publish(new AddFavoriteRuntimeCmd(
|
||||
spellId,
|
||||
position,
|
||||
tabIndex))))
|
||||
{
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
|
|
@ -351,10 +366,6 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
spellId);
|
||||
}
|
||||
|
||||
_commands.Publish(new AddFavoriteRuntimeCmd(
|
||||
spellId,
|
||||
position,
|
||||
tabIndex));
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 0,
|
||||
|
|
@ -370,7 +381,11 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
if ((uint)tabIndex >= 8u || spellId == 0u)
|
||||
if (!_character.TryRemoveFavorite(
|
||||
tabIndex,
|
||||
spellId,
|
||||
() => _commands.Publish(
|
||||
new RemoveFavoriteRuntimeCmd(spellId, tabIndex))))
|
||||
{
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
|
|
@ -379,7 +394,6 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
spellId);
|
||||
}
|
||||
|
||||
_commands.Publish(new RemoveFavoriteRuntimeCmd(spellId, tabIndex));
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 1,
|
||||
|
|
@ -394,7 +408,10 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
_commands.Publish(new SetSpellbookFilterRuntimeCmd(filters));
|
||||
_character.SetSpellbookFilter(
|
||||
filters,
|
||||
() => _commands.Publish(
|
||||
new SetSpellbookFilterRuntimeCmd(filters)));
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 2,
|
||||
|
|
@ -433,7 +450,12 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
if (componentId == 0u)
|
||||
if (!_character.TrySetDesiredComponent(
|
||||
componentId,
|
||||
amount,
|
||||
() => _commands.Publish(new SetDesiredComponentRuntimeCmd(
|
||||
componentId,
|
||||
amount))))
|
||||
{
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
|
|
@ -442,9 +464,6 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
componentId);
|
||||
}
|
||||
|
||||
_commands.Publish(new SetDesiredComponentRuntimeCmd(
|
||||
componentId,
|
||||
amount));
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 4,
|
||||
|
|
@ -458,7 +477,9 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
_commands.Publish(new ClearDesiredComponentsRuntimeCmd());
|
||||
_character.ClearDesiredComponents(
|
||||
() => _commands.Publish(
|
||||
new ClearDesiredComponentsRuntimeCmd()));
|
||||
return EmitResult(
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 5,
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ public static class FixtureProvider
|
|||
return ToolbarController.Bind(
|
||||
layout,
|
||||
objects,
|
||||
shortcuts: () => System.Array.Empty<ShortcutEntry>(),
|
||||
shortcuts: new ShortcutStore(),
|
||||
iconIds: MakeIconIds(stack),
|
||||
useItem: _ => { },
|
||||
combatState: null,
|
||||
|
|
@ -141,6 +141,7 @@ public static class FixtureProvider
|
|||
var selection = new AcDream.Core.Selection.SelectionState();
|
||||
var itemInteraction = new ItemInteractionController(
|
||||
objects,
|
||||
new InventoryTransactionState(objects),
|
||||
() => SampleData.PlayerGuid,
|
||||
sendUse: null,
|
||||
sendUseWithTarget: null,
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ public sealed class ItemInteractionController : IDisposable
|
|||
private readonly AutoWieldController _autoWield;
|
||||
private readonly Action<uint, ItemUseRequestReservation>? _requestUse;
|
||||
private readonly InventoryTransactionState _transactions;
|
||||
private readonly bool _ownsTransactions;
|
||||
|
||||
private long _lastUseMs = long.MinValue / 2;
|
||||
private uint _consumedPrimaryClickTarget;
|
||||
|
|
@ -74,6 +73,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
|
||||
public ItemInteractionController(
|
||||
ClientObjectTable objects,
|
||||
InventoryTransactionState transactions,
|
||||
Func<uint> playerGuid,
|
||||
Action<uint>? sendUse,
|
||||
Action<uint, uint>? sendUseWithTarget,
|
||||
|
|
@ -103,8 +103,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
Action<uint>? requestExternalContainer = null,
|
||||
CombatState? combatState = null,
|
||||
Action<CombatMode>? sendChangeCombatMode = null,
|
||||
Action<uint, ItemUseRequestReservation>? requestUse = null,
|
||||
InventoryTransactionState? transactions = null)
|
||||
Action<uint, ItemUseRequestReservation>? requestUse = null)
|
||||
{
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
|
|
@ -135,34 +134,23 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_systemMessage = systemMessage;
|
||||
_requestUse = requestUse;
|
||||
_interactionState = interactionState ?? new InteractionState();
|
||||
_transactions = transactions ?? new InventoryTransactionState(_objects);
|
||||
_ownsTransactions = transactions is null;
|
||||
_transactions = transactions
|
||||
?? throw new ArgumentNullException(nameof(transactions));
|
||||
if (!ReferenceEquals(_transactions.Objects, _objects))
|
||||
{
|
||||
if (_ownsTransactions)
|
||||
_transactions.Dispose();
|
||||
throw new ArgumentException(
|
||||
"The inventory transaction owner must borrow the controller's exact object table.",
|
||||
nameof(transactions));
|
||||
}
|
||||
try
|
||||
{
|
||||
_autoWield = new AutoWieldController(
|
||||
_objects,
|
||||
_playerGuid,
|
||||
_sendWield,
|
||||
sendPutItemInContainer,
|
||||
_toast,
|
||||
_systemMessage,
|
||||
combatState,
|
||||
sendChangeCombatMode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (_ownsTransactions)
|
||||
_transactions.Dispose();
|
||||
throw;
|
||||
}
|
||||
_autoWield = new AutoWieldController(
|
||||
_objects,
|
||||
_playerGuid,
|
||||
_sendWield,
|
||||
sendPutItemInContainer,
|
||||
_toast,
|
||||
_systemMessage,
|
||||
combatState,
|
||||
sendChangeCombatMode);
|
||||
_interactionState.Changed += OnInteractionModeChanged;
|
||||
_transactions.StateChanged += OnTransactionStateChanged;
|
||||
_transactions.RequestCompleted += OnInventoryRequestCompleted;
|
||||
|
|
@ -1166,8 +1154,6 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_transactions.ObjectTableCleared -= OnInventoryObjectsCleared;
|
||||
_transactions.RequestCompleted -= OnInventoryRequestCompleted;
|
||||
_transactions.StateChanged -= OnTransactionStateChanged;
|
||||
if (_ownsTransactions)
|
||||
_transactions.Dispose();
|
||||
_autoWield.Dispose();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -251,7 +251,6 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
|||
// gmSpellbookUI::UpdateFilter @ 0x0048B5E0 persists only the changed
|
||||
// filter bitfield, rebuilds from learned spells, and returns to row zero.
|
||||
uint filters = _spellbook.SpellbookFilters ^ mask;
|
||||
_spellbook.SetSpellbookFilters(filters);
|
||||
_sendFilter(filters);
|
||||
_spellList.Scroll.SetScrollY(0);
|
||||
}
|
||||
|
|
@ -383,7 +382,6 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
|||
}
|
||||
if (parsed == committed) return;
|
||||
committed = parsed;
|
||||
_spellbook.SetDesiredComponent(componentId, parsed);
|
||||
_setDesiredComponent(componentId, parsed);
|
||||
}
|
||||
desiredField.OnSubmit = Commit;
|
||||
|
|
|
|||
|
|
@ -209,7 +209,6 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
}
|
||||
int position = spells.Count;
|
||||
_addFavorite?.Invoke(_activeTab, position, spellId);
|
||||
_spellbook.SetFavorite(_activeTab, position, spellId);
|
||||
SelectSpell(spellId);
|
||||
}
|
||||
|
||||
|
|
@ -424,13 +423,15 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
private void BeginFavoriteDrag(SpellFavoriteDragPayload payload)
|
||||
=> _removeFavorite?.Invoke(payload.SourceTab, payload.SpellId);
|
||||
|
||||
private void EndFavoriteDrag(SpellFavoriteDragPayload payload)
|
||||
=> _spellbook.RemoveFavorite(payload.SourceTab, payload.SpellId);
|
||||
private static void EndFavoriteDrag(SpellFavoriteDragPayload payload)
|
||||
{
|
||||
// The press-time command already performed retail's PlayerModule
|
||||
// removal on the one Runtime-owned Spellbook.
|
||||
}
|
||||
|
||||
private void DropFavorite(SpellFavoriteDragPayload payload, int targetTab, int targetPosition)
|
||||
{
|
||||
_addFavorite?.Invoke(targetTab, targetPosition, payload.SpellId);
|
||||
_spellbook.SetFavorite(targetTab, targetPosition, payload.SpellId);
|
||||
_selected[targetTab] = payload.SpellId;
|
||||
}
|
||||
|
||||
|
|
@ -443,7 +444,6 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
// CM_Magic::SendNotice_AddSpellShortcut; the open spellcasting menu
|
||||
// chooses the favorite tab/position and persists it.
|
||||
_addFavorite?.Invoke(targetTab, targetPosition, payload.SpellId);
|
||||
_spellbook.SetFavorite(targetTab, targetPosition, payload.SpellId);
|
||||
_selected[targetTab] = payload.SpellId;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,12 +63,10 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
private readonly List<(uint PanelId, UiButton Button)> _panelButtons = new();
|
||||
private readonly ClientObjectTable _repo;
|
||||
private readonly CombatState? _combatState;
|
||||
private readonly Func<IReadOnlyList<ShortcutEntry>> _shortcuts;
|
||||
private readonly ShortcutStore _store;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint>? _dragIconIds;
|
||||
private readonly Action<uint> _useItem; // guid → fire UseObject
|
||||
private readonly ShortcutStore _store = new();
|
||||
private bool _storeLoaded;
|
||||
private readonly Action<ShortcutEntry>? _sendAddShortcut;
|
||||
private readonly Action<uint>? _sendRemoveShortcut; // (index)
|
||||
private readonly ItemInteractionController? _itemInteraction;
|
||||
|
|
@ -95,7 +93,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
private ToolbarController(
|
||||
ImportedLayout layout,
|
||||
ClientObjectTable repo,
|
||||
Func<IReadOnlyList<ShortcutEntry>> shortcuts,
|
||||
ShortcutStore shortcuts,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
||||
Action<uint> useItem,
|
||||
CombatState? combatState,
|
||||
|
|
@ -116,7 +114,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
{
|
||||
_repo = repo;
|
||||
_combatState = combatState;
|
||||
_shortcuts = shortcuts;
|
||||
_store = shortcuts ?? throw new ArgumentNullException(nameof(shortcuts));
|
||||
_iconIds = iconIds;
|
||||
_dragIconIds = dragIconIds;
|
||||
_useItem = useItem;
|
||||
|
|
@ -213,6 +211,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
repo.ObjectRemoved += OnRepositoryObjectChanged;
|
||||
repo.ObjectMoved += OnRepositoryObjectMoved;
|
||||
repo.Cleared += OnRepositoryCleared;
|
||||
_store.Changed += Populate;
|
||||
RefreshAmmo();
|
||||
RefreshUseButton();
|
||||
}
|
||||
|
|
@ -296,19 +295,11 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
/// Returns true if <paramref name="guid"/> is one of the currently-active shortcut guids
|
||||
/// (i.e., present in the live <see cref="AcDream.Core.Items.ShortcutStore"/>).
|
||||
/// Used to gate repo-event subscriptions so we don't re-populate on every creature spawn.
|
||||
/// Falls back to scanning <c>_shortcuts()</c> before the store is loaded (pre-PD window).
|
||||
/// </summary>
|
||||
private bool IsShortcutGuid(uint guid)
|
||||
{
|
||||
if (_storeLoaded)
|
||||
{
|
||||
for (int s = 0; s < AcDream.Core.Items.ShortcutStore.SlotCount; s++)
|
||||
if (_store.Get(s) == guid) return true;
|
||||
return false;
|
||||
}
|
||||
// Store not yet loaded — fall back to the shortcuts provider (pre-PD window).
|
||||
foreach (var sc in _shortcuts())
|
||||
if (sc.ObjectId == guid) return true;
|
||||
for (int s = 0; s < ShortcutStore.SlotCount; s++)
|
||||
if (_store.Get(s) == guid) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -320,7 +311,10 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
/// </summary>
|
||||
/// <param name="layout">Imported toolbar layout (LayoutDesc 0x21000016).</param>
|
||||
/// <param name="repo">Live item repository — must stay alive for the controller's lifetime.</param>
|
||||
/// <param name="shortcuts">Provider for the current shortcut bar list.</param>
|
||||
/// <param name="shortcuts">
|
||||
/// Runtime-owned retail shortcut manager. The toolbar borrows this exact
|
||||
/// mutable owner; it never reconstructs another slot map.
|
||||
/// </param>
|
||||
/// <param name="iconIds">Resolves (itemType, iconId, underlayId, overlayId, effects) → GL texture handle.</param>
|
||||
/// <param name="useItem">Callback fired when a bound slot is clicked; receives the item guid.</param>
|
||||
/// <param name="combatState">
|
||||
|
|
@ -345,7 +339,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
public static ToolbarController Bind(
|
||||
ImportedLayout layout,
|
||||
ClientObjectTable repo,
|
||||
Func<IReadOnlyList<ShortcutEntry>> shortcuts,
|
||||
ShortcutStore shortcuts,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
||||
Action<uint> useItem,
|
||||
CombatState? combatState = null,
|
||||
|
|
@ -417,19 +411,11 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
/// Entries whose item is not yet in the repo are silently skipped here; the
|
||||
/// <c>ObjectAdded</c> event re-fires this method when the item arrives
|
||||
/// (matching retail's <c>SetDelayedShortcutNum</c> deferred-rebind path).
|
||||
/// As of B.2: the <see cref="AcDream.Core.Items.ShortcutStore"/> is the
|
||||
/// authoritative in-session slot map; <c>_shortcuts()</c> seeds it on first call.
|
||||
/// The Runtime-owned <see cref="AcDream.Core.Items.ShortcutStore"/> is the
|
||||
/// authoritative in-session slot map.
|
||||
/// </summary>
|
||||
public void Populate()
|
||||
{
|
||||
// Lazy-load the store from the login shortcut list the first time it's present (PD arrives
|
||||
// after Bind); thereafter the store is authoritative and drag ops mutate it directly.
|
||||
if (!_storeLoaded && _shortcuts().Count > 0)
|
||||
{
|
||||
_store.Load(_shortcuts());
|
||||
_storeLoaded = true;
|
||||
}
|
||||
|
||||
foreach (var list in _slots) list?.Cell.Clear();
|
||||
|
||||
for (int slot = 0; slot < _slots.Length; slot++)
|
||||
|
|
@ -654,7 +640,6 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
if (_itemInteraction?.IsOwnedByPlayer(itemId) != true)
|
||||
return false;
|
||||
|
||||
EnsureStoreLoadedForMutation();
|
||||
for (int slot = 0; slot < _slots.Length; slot++)
|
||||
if (_store.Get(slot) == itemId)
|
||||
return false;
|
||||
|
|
@ -672,20 +657,11 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
return false;
|
||||
|
||||
var entry = new ShortcutEntry(empty, itemId, 0u);
|
||||
_store.Set(entry);
|
||||
_sendAddShortcut?.Invoke(entry);
|
||||
Populate();
|
||||
_store.Set(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void EnsureStoreLoadedForMutation()
|
||||
{
|
||||
if (_storeLoaded)
|
||||
return;
|
||||
_store.Load(_shortcuts());
|
||||
_storeLoaded = true;
|
||||
}
|
||||
|
||||
// ── IItemListDragHandler (B.2 live handler) ──────────────────────────────
|
||||
// Retail: gmToolbarUI is the m_dragHandler for every shortcut slot list.
|
||||
// Retail model (remove-on-lift / place-on-drop / no-restore):
|
||||
|
|
@ -705,9 +681,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
// Retail RecvNotice_ItemListBeginDrag → RemoveShortcut (0x004bd930/0x004bd450): the lifted
|
||||
// shortcut leaves the bar (+ wire) the instant the drag starts; it's re-placed only on a
|
||||
// drop onto a slot. Off-bar release leaves it removed.
|
||||
_store.Remove(payload.SourceSlot);
|
||||
_sendRemoveShortcut?.Invoke((uint)payload.SourceSlot);
|
||||
Populate();
|
||||
_store.Remove(payload.SourceSlot);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -717,7 +692,6 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
/// <inheritdoc/>
|
||||
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||
{
|
||||
EnsureStoreLoadedForMutation();
|
||||
ShortcutDropSource source = payload.SourceKind == ItemDragSource.ShortcutBar
|
||||
? ShortcutDropSource.ShortcutAlias
|
||||
: ShortcutDropSource.FreshItem;
|
||||
|
|
@ -727,7 +701,6 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
_store.Snapshot(), source, payload.SourceSlot, targetCell.SlotIndex, dragged);
|
||||
|
||||
ApplyShortcutPlan(plan);
|
||||
Populate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -736,32 +709,29 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
/// </summary>
|
||||
public void ReplaceFullyMergedShortcut(uint oldObjectId, uint newObjectId)
|
||||
{
|
||||
EnsureStoreLoadedForMutation();
|
||||
ShortcutMutation[] plan = ShortcutDropPlanner.PlanFullStackMerge(
|
||||
_store.Snapshot(), oldObjectId, newObjectId);
|
||||
ApplyShortcutPlan(plan);
|
||||
if (plan.Length > 0)
|
||||
Populate();
|
||||
}
|
||||
|
||||
private void ApplyShortcutPlan(IReadOnlyList<ShortcutMutation> plan)
|
||||
{
|
||||
// Apply the already-validated transaction locally first, then reproduce the
|
||||
// planner's exact Remove/Add wire order. Shortcut events have no rejection reply.
|
||||
foreach (var mutation in plan)
|
||||
{
|
||||
if (mutation.Kind == ShortcutMutationKind.Remove)
|
||||
_store.Remove(mutation.Slot);
|
||||
else if (mutation.Entry is { } entry)
|
||||
_store.Set(entry);
|
||||
}
|
||||
|
||||
// Retail sends each shortcut event immediately before applying the same
|
||||
// PlayerModule mutation (gmToolbarUI @ 0x004BD450/0x004BD9A0).
|
||||
// Production commands mutate this exact Runtime-owned store; the
|
||||
// idempotent local call also keeps isolated controller fixtures useful.
|
||||
foreach (var mutation in plan)
|
||||
{
|
||||
if (mutation.Kind == ShortcutMutationKind.Remove)
|
||||
{
|
||||
_sendRemoveShortcut?.Invoke((uint)mutation.Slot);
|
||||
_store.Remove(mutation.Slot);
|
||||
}
|
||||
else if (mutation.Entry is { } entry)
|
||||
{
|
||||
_sendAddShortcut?.Invoke(entry);
|
||||
_store.Set(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -800,5 +770,6 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
_repo.ObjectRemoved -= OnRepositoryObjectChanged;
|
||||
_repo.ObjectMoved -= OnRepositoryObjectMoved;
|
||||
_repo.Cleared -= OnRepositoryCleared;
|
||||
_store.Changed -= Populate;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ public sealed record IndicatorRuntimeBindings(
|
|||
|
||||
public sealed record ToolbarRuntimeBindings(
|
||||
ClientObjectTable Objects,
|
||||
Func<IReadOnlyList<ShortcutEntry>> Shortcuts,
|
||||
ShortcutStore Shortcuts,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
||||
Action<uint> UseItem,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue