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
|
|
@ -0,0 +1,154 @@
|
||||||
|
# Slice J4.5 — retail local gameplay-command ordering
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This note fixes the command/local-state ordering used while J4.5 removes the
|
||||||
|
last graphical-only shortcut and spellbook mirrors. It does not add a new
|
||||||
|
gameplay algorithm. It preserves the existing packet builders and moves the
|
||||||
|
retail `PlayerModule` mutation onto the one Runtime-owned state object.
|
||||||
|
|
||||||
|
Oracle:
|
||||||
|
|
||||||
|
- `docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
||||||
|
- `docs/research/named-retail/acclient.h`
|
||||||
|
|
||||||
|
The existing wire ports remain:
|
||||||
|
|
||||||
|
- `AcDream.Core.Net.Messages.ClientCommandRequests`
|
||||||
|
- `AcDream.Core.Net.WorldSession`
|
||||||
|
- `AcDream.App.Net.LiveSessionCommandRouter`
|
||||||
|
|
||||||
|
## Retail pseudocode
|
||||||
|
|
||||||
|
### Toolbar shortcut add
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- `gmToolbarUI::AddShortcut @ 0x004BD9A0`
|
||||||
|
- `PlayerModule::AddShortCut @ 0x005D29A0`
|
||||||
|
- `ShortCutManager::AddShortCut @ 0x005D5790`
|
||||||
|
|
||||||
|
```text
|
||||||
|
if slot is valid and the toolbar accepts the item:
|
||||||
|
create ShortCutData(slot, objectId, spellId)
|
||||||
|
send Event_AddShortCut(data)
|
||||||
|
playerModule.AddShortCut(data)
|
||||||
|
|
||||||
|
ShortCutManager.AddShortCut(data):
|
||||||
|
if data.index is outside [0, 18):
|
||||||
|
return false
|
||||||
|
if the slot already owns a record:
|
||||||
|
replace all three record fields in place
|
||||||
|
else:
|
||||||
|
allocate and install one record
|
||||||
|
return true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Toolbar shortcut remove
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- `gmToolbarUI::RemoveShortcut @ 0x004BD450`
|
||||||
|
- `PlayerModule::RemoveShortCut @ 0x005D29E0`
|
||||||
|
- `ShortCutManager::RemoveShortCut @ 0x005D5690`
|
||||||
|
|
||||||
|
```text
|
||||||
|
find the slot containing the selected shortcut
|
||||||
|
flush the visible slot
|
||||||
|
if this is a persisted removal:
|
||||||
|
send Event_RemoveShortCut(slot)
|
||||||
|
playerModule.RemoveShortCut(slot)
|
||||||
|
|
||||||
|
ShortCutManager.RemoveShortCut(slot):
|
||||||
|
if slot is outside [0, 18):
|
||||||
|
return
|
||||||
|
destroy the record if present
|
||||||
|
set the slot to empty
|
||||||
|
```
|
||||||
|
|
||||||
|
### Favorite spell add/remove
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- spell-menu handler at `0x004C64C0`
|
||||||
|
- `PlayerModule::AddSpellFavorite @ 0x005D43E0`
|
||||||
|
- `PlayerModule::RemoveSpellFavorite @ 0x005D4910`
|
||||||
|
|
||||||
|
```text
|
||||||
|
add:
|
||||||
|
if tab is in [0, 8):
|
||||||
|
insert spellId at position in that tab
|
||||||
|
send Event_AddSpellFavorite(spellId, position, tab)
|
||||||
|
|
||||||
|
remove:
|
||||||
|
if tab is in [0, 8):
|
||||||
|
remove spellId from that tab
|
||||||
|
send Event_RemoveSpellFavorite(spellId, tab)
|
||||||
|
```
|
||||||
|
|
||||||
|
Unlike toolbar shortcuts, favorite-spell state changes locally before the
|
||||||
|
outbound event.
|
||||||
|
|
||||||
|
### Spellbook filter
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- `gmSpellbookUI::UpdateFilter @ 0x0048B5E0`
|
||||||
|
|
||||||
|
```text
|
||||||
|
derive the changed school/level bit
|
||||||
|
derive the next filter bitfield from the button state
|
||||||
|
if next differs from current:
|
||||||
|
playerModule.SetSpellbookFilter(next)
|
||||||
|
send Event_SpellbookFilterEvent(next)
|
||||||
|
rebuild the visible spell list
|
||||||
|
scroll to row zero
|
||||||
|
```
|
||||||
|
|
||||||
|
### Desired component level
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- `gmSpellComponentUI::ListenToElementMessage @ 0x0048A420`
|
||||||
|
- `PlayerModule::SetDesiredCompLevel @ 0x005D4940`
|
||||||
|
|
||||||
|
```text
|
||||||
|
parse the edited amount
|
||||||
|
if amount is in [0, 5000]:
|
||||||
|
send Event_SetDesiredComponentLevel(componentId, amount)
|
||||||
|
playerModule.SetDesiredCompLevel(componentId, amount)
|
||||||
|
else:
|
||||||
|
restore the current value in the editor
|
||||||
|
|
||||||
|
PlayerModule.SetDesiredCompLevel(componentId, amount):
|
||||||
|
if amount is outside [0, 5000]:
|
||||||
|
return false
|
||||||
|
lazily create the desired-component table
|
||||||
|
replace an existing value or add a new value
|
||||||
|
return true
|
||||||
|
```
|
||||||
|
|
||||||
|
Retail retains an explicit zero-valued entry. J4.5 preserves that distinction;
|
||||||
|
only the clear-list command destroys the complete desired-component table.
|
||||||
|
|
||||||
|
### Clear desired components
|
||||||
|
|
||||||
|
Retail anchors:
|
||||||
|
|
||||||
|
- chat-command handler at `0x0056FE4A`
|
||||||
|
- `PlayerModule::ClearDesiredCompList @ 0x005D2A00`
|
||||||
|
|
||||||
|
```text
|
||||||
|
send Event_SetDesiredComponentLevel(INVALID_DID, -1)
|
||||||
|
destroy the complete desired-component table
|
||||||
|
set the table pointer to null
|
||||||
|
publish the spell-component UI update notice
|
||||||
|
```
|
||||||
|
|
||||||
|
## J4.5 integration consequence
|
||||||
|
|
||||||
|
The graphical toolbar, retained spell UI, plugins, and future no-window host
|
||||||
|
must all mutate the same Runtime-owned `ShortcutStore` and `Spellbook`.
|
||||||
|
Graphical controllers may initiate the operation, but they may not retain or
|
||||||
|
reconstruct another mutable slot/favorite/component collection. Commands stay
|
||||||
|
synchronous; no additional frame or background queue is introduced.
|
||||||
|
|
@ -285,6 +285,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
DeferredSelectionUiAuthority selection = late.Selection;
|
DeferredSelectionUiAuthority selection = late.Selection;
|
||||||
return new ItemInteractionController(
|
return new ItemInteractionController(
|
||||||
d.Inventory.Objects,
|
d.Inventory.Objects,
|
||||||
|
d.Inventory.Transactions,
|
||||||
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendExamine: guid => session.CurrentSession?.SendAppraise(guid),
|
sendExamine: guid => session.CurrentSession?.SendAppraise(guid),
|
||||||
|
|
@ -334,8 +335,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
{
|
{
|
||||||
d.Inventory.ExternalContainers.RequestOpen(guid);
|
d.Inventory.ExternalContainers.RequestOpen(guid);
|
||||||
},
|
},
|
||||||
requestUse: selection.RequestUse,
|
requestUse: selection.RequestUse);
|
||||||
transactions: d.Inventory.Transactions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public RetainedUiComposition CreateRetainedUi(
|
public RetainedUiComposition CreateRetainedUi(
|
||||||
|
|
@ -592,7 +592,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
d.Window.Close),
|
d.Window.Close),
|
||||||
Toolbar: new ToolbarRuntimeBindings(
|
Toolbar: new ToolbarRuntimeBindings(
|
||||||
d.Inventory.Objects,
|
d.Inventory.Objects,
|
||||||
() => d.Inventory.Shortcuts.Items,
|
d.Inventory.Shortcuts,
|
||||||
iconComposer.GetIcon,
|
iconComposer.GetIcon,
|
||||||
iconComposer.GetDragIcon,
|
iconComposer.GetDragIcon,
|
||||||
guid => late.Session.TryUseItem(guid, d.Log),
|
guid => late.Session.TryUseItem(guid, d.Log),
|
||||||
|
|
|
||||||
|
|
@ -254,7 +254,7 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
private LiveInventorySessionBindings CreateInventoryBindings() => new(
|
private LiveInventorySessionBindings CreateInventoryBindings() => new(
|
||||||
_domain.Inventory.Objects,
|
_domain.Inventory.Objects,
|
||||||
PlayerGuid: () => _player.Identity.ServerGuid,
|
PlayerGuid: () => _player.Identity.ServerGuid,
|
||||||
OnShortcuts: _domain.Inventory.Shortcuts.Replace,
|
OnShortcuts: _domain.Inventory.Shortcuts.Load,
|
||||||
OnUseDone: error =>
|
OnUseDone: error =>
|
||||||
{
|
{
|
||||||
_domain.Inventory.ExternalContainers.ApplyUseDone(error);
|
_domain.Inventory.ExternalContainers.ApplyUseDone(error);
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,8 @@ internal sealed class CurrentGameRuntimeAdapter
|
||||||
sessionHost,
|
sessionHost,
|
||||||
commands,
|
commands,
|
||||||
_view,
|
_view,
|
||||||
|
inventory,
|
||||||
|
character,
|
||||||
selectionState,
|
selectionState,
|
||||||
selection,
|
selection,
|
||||||
gameplayInput,
|
gameplayInput,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using AcDream.App.Net;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
using AcDream.UI.Abstractions;
|
using AcDream.UI.Abstractions;
|
||||||
using AcDream.UI.Abstractions.Input;
|
using AcDream.UI.Abstractions.Input;
|
||||||
|
|
@ -31,6 +32,8 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
private readonly LiveSessionHost _sessionHost;
|
private readonly LiveSessionHost _sessionHost;
|
||||||
private readonly ICommandBus _commands;
|
private readonly ICommandBus _commands;
|
||||||
private readonly CurrentGameRuntimeViewAdapter _view;
|
private readonly CurrentGameRuntimeViewAdapter _view;
|
||||||
|
private readonly RuntimeInventoryState _inventory;
|
||||||
|
private readonly RuntimeCharacterState _character;
|
||||||
private readonly SelectionState _selectionState;
|
private readonly SelectionState _selectionState;
|
||||||
private readonly SelectionInteractionController _selection;
|
private readonly SelectionInteractionController _selection;
|
||||||
private readonly GameplayInputFrameController _gameplayInput;
|
private readonly GameplayInputFrameController _gameplayInput;
|
||||||
|
|
@ -42,6 +45,8 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
LiveSessionHost sessionHost,
|
LiveSessionHost sessionHost,
|
||||||
ICommandBus commands,
|
ICommandBus commands,
|
||||||
CurrentGameRuntimeViewAdapter view,
|
CurrentGameRuntimeViewAdapter view,
|
||||||
|
RuntimeInventoryState inventory,
|
||||||
|
RuntimeCharacterState character,
|
||||||
SelectionState selectionState,
|
SelectionState selectionState,
|
||||||
SelectionInteractionController selection,
|
SelectionInteractionController selection,
|
||||||
GameplayInputFrameController gameplayInput,
|
GameplayInputFrameController gameplayInput,
|
||||||
|
|
@ -52,6 +57,8 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
_sessionHost = sessionHost ?? throw new ArgumentNullException(nameof(sessionHost));
|
_sessionHost = sessionHost ?? throw new ArgumentNullException(nameof(sessionHost));
|
||||||
_commands = commands ?? throw new ArgumentNullException(nameof(commands));
|
_commands = commands ?? throw new ArgumentNullException(nameof(commands));
|
||||||
_view = view ?? throw new ArgumentNullException(nameof(view));
|
_view = view ?? throw new ArgumentNullException(nameof(view));
|
||||||
|
_inventory = inventory ?? throw new ArgumentNullException(nameof(inventory));
|
||||||
|
_character = character ?? throw new ArgumentNullException(nameof(character));
|
||||||
_selectionState = selectionState
|
_selectionState = selectionState
|
||||||
?? throw new ArgumentNullException(nameof(selectionState));
|
?? throw new ArgumentNullException(nameof(selectionState));
|
||||||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||||
|
|
@ -288,8 +295,13 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||||
if (gate != RuntimeCommandStatus.Accepted)
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
return Result(gate);
|
return Result(gate);
|
||||||
if (command.Index < 0
|
var entry = new ShortcutEntry(
|
||||||
|| (command.ObjectId == 0u && command.SpellId == 0u))
|
command.Index,
|
||||||
|
command.ObjectId,
|
||||||
|
command.SpellId);
|
||||||
|
if (!_inventory.TryAddShortcut(
|
||||||
|
entry,
|
||||||
|
() => _commands.Publish(new AddShortcutRuntimeCmd(entry))))
|
||||||
{
|
{
|
||||||
return EmitResult(
|
return EmitResult(
|
||||||
RuntimeCommandDomain.InventoryState,
|
RuntimeCommandDomain.InventoryState,
|
||||||
|
|
@ -298,10 +310,6 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
command.ObjectId);
|
command.ObjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_commands.Publish(new AddShortcutRuntimeCmd(new ShortcutEntry(
|
|
||||||
command.Index,
|
|
||||||
command.ObjectId,
|
|
||||||
command.SpellId)));
|
|
||||||
return EmitResult(
|
return EmitResult(
|
||||||
RuntimeCommandDomain.InventoryState,
|
RuntimeCommandDomain.InventoryState,
|
||||||
operation: 0,
|
operation: 0,
|
||||||
|
|
@ -316,7 +324,10 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||||
if (gate != RuntimeCommandStatus.Accepted)
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
return Result(gate);
|
return Result(gate);
|
||||||
if (index < 0)
|
if (!_inventory.TryRemoveShortcut(
|
||||||
|
index,
|
||||||
|
() => _commands.Publish(
|
||||||
|
new RemoveShortcutRuntimeCmd((uint)index))))
|
||||||
{
|
{
|
||||||
return EmitResult(
|
return EmitResult(
|
||||||
RuntimeCommandDomain.InventoryState,
|
RuntimeCommandDomain.InventoryState,
|
||||||
|
|
@ -324,7 +335,6 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
RuntimeCommandStatus.Rejected);
|
RuntimeCommandStatus.Rejected);
|
||||||
}
|
}
|
||||||
|
|
||||||
_commands.Publish(new RemoveShortcutRuntimeCmd((uint)index));
|
|
||||||
return EmitResult(
|
return EmitResult(
|
||||||
RuntimeCommandDomain.InventoryState,
|
RuntimeCommandDomain.InventoryState,
|
||||||
operation: 1,
|
operation: 1,
|
||||||
|
|
@ -340,9 +350,14 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||||
if (gate != RuntimeCommandStatus.Accepted)
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
return Result(gate);
|
return Result(gate);
|
||||||
if ((uint)tabIndex >= 8u
|
if (!_character.TryAddFavorite(
|
||||||
|| position < 0
|
tabIndex,
|
||||||
|| spellId == 0u)
|
position,
|
||||||
|
spellId,
|
||||||
|
() => _commands.Publish(new AddFavoriteRuntimeCmd(
|
||||||
|
spellId,
|
||||||
|
position,
|
||||||
|
tabIndex))))
|
||||||
{
|
{
|
||||||
return EmitResult(
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Spellbook,
|
RuntimeCommandDomain.Spellbook,
|
||||||
|
|
@ -351,10 +366,6 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
spellId);
|
spellId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_commands.Publish(new AddFavoriteRuntimeCmd(
|
|
||||||
spellId,
|
|
||||||
position,
|
|
||||||
tabIndex));
|
|
||||||
return EmitResult(
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Spellbook,
|
RuntimeCommandDomain.Spellbook,
|
||||||
operation: 0,
|
operation: 0,
|
||||||
|
|
@ -370,7 +381,11 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||||
if (gate != RuntimeCommandStatus.Accepted)
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
return Result(gate);
|
return Result(gate);
|
||||||
if ((uint)tabIndex >= 8u || spellId == 0u)
|
if (!_character.TryRemoveFavorite(
|
||||||
|
tabIndex,
|
||||||
|
spellId,
|
||||||
|
() => _commands.Publish(
|
||||||
|
new RemoveFavoriteRuntimeCmd(spellId, tabIndex))))
|
||||||
{
|
{
|
||||||
return EmitResult(
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Spellbook,
|
RuntimeCommandDomain.Spellbook,
|
||||||
|
|
@ -379,7 +394,6 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
spellId);
|
spellId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_commands.Publish(new RemoveFavoriteRuntimeCmd(spellId, tabIndex));
|
|
||||||
return EmitResult(
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Spellbook,
|
RuntimeCommandDomain.Spellbook,
|
||||||
operation: 1,
|
operation: 1,
|
||||||
|
|
@ -394,7 +408,10 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||||
if (gate != RuntimeCommandStatus.Accepted)
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
return Result(gate);
|
return Result(gate);
|
||||||
_commands.Publish(new SetSpellbookFilterRuntimeCmd(filters));
|
_character.SetSpellbookFilter(
|
||||||
|
filters,
|
||||||
|
() => _commands.Publish(
|
||||||
|
new SetSpellbookFilterRuntimeCmd(filters)));
|
||||||
return EmitResult(
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Spellbook,
|
RuntimeCommandDomain.Spellbook,
|
||||||
operation: 2,
|
operation: 2,
|
||||||
|
|
@ -433,7 +450,12 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||||
if (gate != RuntimeCommandStatus.Accepted)
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
return Result(gate);
|
return Result(gate);
|
||||||
if (componentId == 0u)
|
if (!_character.TrySetDesiredComponent(
|
||||||
|
componentId,
|
||||||
|
amount,
|
||||||
|
() => _commands.Publish(new SetDesiredComponentRuntimeCmd(
|
||||||
|
componentId,
|
||||||
|
amount))))
|
||||||
{
|
{
|
||||||
return EmitResult(
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Spellbook,
|
RuntimeCommandDomain.Spellbook,
|
||||||
|
|
@ -442,9 +464,6 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
componentId);
|
componentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_commands.Publish(new SetDesiredComponentRuntimeCmd(
|
|
||||||
componentId,
|
|
||||||
amount));
|
|
||||||
return EmitResult(
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Spellbook,
|
RuntimeCommandDomain.Spellbook,
|
||||||
operation: 4,
|
operation: 4,
|
||||||
|
|
@ -458,7 +477,9 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
||||||
if (gate != RuntimeCommandStatus.Accepted)
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
return Result(gate);
|
return Result(gate);
|
||||||
_commands.Publish(new ClearDesiredComponentsRuntimeCmd());
|
_character.ClearDesiredComponents(
|
||||||
|
() => _commands.Publish(
|
||||||
|
new ClearDesiredComponentsRuntimeCmd()));
|
||||||
return EmitResult(
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Spellbook,
|
RuntimeCommandDomain.Spellbook,
|
||||||
operation: 5,
|
operation: 5,
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ public static class FixtureProvider
|
||||||
return ToolbarController.Bind(
|
return ToolbarController.Bind(
|
||||||
layout,
|
layout,
|
||||||
objects,
|
objects,
|
||||||
shortcuts: () => System.Array.Empty<ShortcutEntry>(),
|
shortcuts: new ShortcutStore(),
|
||||||
iconIds: MakeIconIds(stack),
|
iconIds: MakeIconIds(stack),
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
combatState: null,
|
combatState: null,
|
||||||
|
|
@ -141,6 +141,7 @@ public static class FixtureProvider
|
||||||
var selection = new AcDream.Core.Selection.SelectionState();
|
var selection = new AcDream.Core.Selection.SelectionState();
|
||||||
var itemInteraction = new ItemInteractionController(
|
var itemInteraction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
() => SampleData.PlayerGuid,
|
() => SampleData.PlayerGuid,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ public sealed class ItemInteractionController : IDisposable
|
||||||
private readonly AutoWieldController _autoWield;
|
private readonly AutoWieldController _autoWield;
|
||||||
private readonly Action<uint, ItemUseRequestReservation>? _requestUse;
|
private readonly Action<uint, ItemUseRequestReservation>? _requestUse;
|
||||||
private readonly InventoryTransactionState _transactions;
|
private readonly InventoryTransactionState _transactions;
|
||||||
private readonly bool _ownsTransactions;
|
|
||||||
|
|
||||||
private long _lastUseMs = long.MinValue / 2;
|
private long _lastUseMs = long.MinValue / 2;
|
||||||
private uint _consumedPrimaryClickTarget;
|
private uint _consumedPrimaryClickTarget;
|
||||||
|
|
@ -74,6 +73,7 @@ public sealed class ItemInteractionController : IDisposable
|
||||||
|
|
||||||
public ItemInteractionController(
|
public ItemInteractionController(
|
||||||
ClientObjectTable objects,
|
ClientObjectTable objects,
|
||||||
|
InventoryTransactionState transactions,
|
||||||
Func<uint> playerGuid,
|
Func<uint> playerGuid,
|
||||||
Action<uint>? sendUse,
|
Action<uint>? sendUse,
|
||||||
Action<uint, uint>? sendUseWithTarget,
|
Action<uint, uint>? sendUseWithTarget,
|
||||||
|
|
@ -103,8 +103,7 @@ public sealed class ItemInteractionController : IDisposable
|
||||||
Action<uint>? requestExternalContainer = null,
|
Action<uint>? requestExternalContainer = null,
|
||||||
CombatState? combatState = null,
|
CombatState? combatState = null,
|
||||||
Action<CombatMode>? sendChangeCombatMode = null,
|
Action<CombatMode>? sendChangeCombatMode = null,
|
||||||
Action<uint, ItemUseRequestReservation>? requestUse = null,
|
Action<uint, ItemUseRequestReservation>? requestUse = null)
|
||||||
InventoryTransactionState? transactions = null)
|
|
||||||
{
|
{
|
||||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||||
|
|
@ -135,18 +134,14 @@ public sealed class ItemInteractionController : IDisposable
|
||||||
_systemMessage = systemMessage;
|
_systemMessage = systemMessage;
|
||||||
_requestUse = requestUse;
|
_requestUse = requestUse;
|
||||||
_interactionState = interactionState ?? new InteractionState();
|
_interactionState = interactionState ?? new InteractionState();
|
||||||
_transactions = transactions ?? new InventoryTransactionState(_objects);
|
_transactions = transactions
|
||||||
_ownsTransactions = transactions is null;
|
?? throw new ArgumentNullException(nameof(transactions));
|
||||||
if (!ReferenceEquals(_transactions.Objects, _objects))
|
if (!ReferenceEquals(_transactions.Objects, _objects))
|
||||||
{
|
{
|
||||||
if (_ownsTransactions)
|
|
||||||
_transactions.Dispose();
|
|
||||||
throw new ArgumentException(
|
throw new ArgumentException(
|
||||||
"The inventory transaction owner must borrow the controller's exact object table.",
|
"The inventory transaction owner must borrow the controller's exact object table.",
|
||||||
nameof(transactions));
|
nameof(transactions));
|
||||||
}
|
}
|
||||||
try
|
|
||||||
{
|
|
||||||
_autoWield = new AutoWieldController(
|
_autoWield = new AutoWieldController(
|
||||||
_objects,
|
_objects,
|
||||||
_playerGuid,
|
_playerGuid,
|
||||||
|
|
@ -156,13 +151,6 @@ public sealed class ItemInteractionController : IDisposable
|
||||||
_systemMessage,
|
_systemMessage,
|
||||||
combatState,
|
combatState,
|
||||||
sendChangeCombatMode);
|
sendChangeCombatMode);
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
if (_ownsTransactions)
|
|
||||||
_transactions.Dispose();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
_interactionState.Changed += OnInteractionModeChanged;
|
_interactionState.Changed += OnInteractionModeChanged;
|
||||||
_transactions.StateChanged += OnTransactionStateChanged;
|
_transactions.StateChanged += OnTransactionStateChanged;
|
||||||
_transactions.RequestCompleted += OnInventoryRequestCompleted;
|
_transactions.RequestCompleted += OnInventoryRequestCompleted;
|
||||||
|
|
@ -1166,8 +1154,6 @@ public sealed class ItemInteractionController : IDisposable
|
||||||
_transactions.ObjectTableCleared -= OnInventoryObjectsCleared;
|
_transactions.ObjectTableCleared -= OnInventoryObjectsCleared;
|
||||||
_transactions.RequestCompleted -= OnInventoryRequestCompleted;
|
_transactions.RequestCompleted -= OnInventoryRequestCompleted;
|
||||||
_transactions.StateChanged -= OnTransactionStateChanged;
|
_transactions.StateChanged -= OnTransactionStateChanged;
|
||||||
if (_ownsTransactions)
|
|
||||||
_transactions.Dispose();
|
|
||||||
_autoWield.Dispose();
|
_autoWield.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,6 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
// gmSpellbookUI::UpdateFilter @ 0x0048B5E0 persists only the changed
|
// gmSpellbookUI::UpdateFilter @ 0x0048B5E0 persists only the changed
|
||||||
// filter bitfield, rebuilds from learned spells, and returns to row zero.
|
// filter bitfield, rebuilds from learned spells, and returns to row zero.
|
||||||
uint filters = _spellbook.SpellbookFilters ^ mask;
|
uint filters = _spellbook.SpellbookFilters ^ mask;
|
||||||
_spellbook.SetSpellbookFilters(filters);
|
|
||||||
_sendFilter(filters);
|
_sendFilter(filters);
|
||||||
_spellList.Scroll.SetScrollY(0);
|
_spellList.Scroll.SetScrollY(0);
|
||||||
}
|
}
|
||||||
|
|
@ -383,7 +382,6 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
}
|
}
|
||||||
if (parsed == committed) return;
|
if (parsed == committed) return;
|
||||||
committed = parsed;
|
committed = parsed;
|
||||||
_spellbook.SetDesiredComponent(componentId, parsed);
|
|
||||||
_setDesiredComponent(componentId, parsed);
|
_setDesiredComponent(componentId, parsed);
|
||||||
}
|
}
|
||||||
desiredField.OnSubmit = Commit;
|
desiredField.OnSubmit = Commit;
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,6 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
||||||
}
|
}
|
||||||
int position = spells.Count;
|
int position = spells.Count;
|
||||||
_addFavorite?.Invoke(_activeTab, position, spellId);
|
_addFavorite?.Invoke(_activeTab, position, spellId);
|
||||||
_spellbook.SetFavorite(_activeTab, position, spellId);
|
|
||||||
SelectSpell(spellId);
|
SelectSpell(spellId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,13 +423,15 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
||||||
private void BeginFavoriteDrag(SpellFavoriteDragPayload payload)
|
private void BeginFavoriteDrag(SpellFavoriteDragPayload payload)
|
||||||
=> _removeFavorite?.Invoke(payload.SourceTab, payload.SpellId);
|
=> _removeFavorite?.Invoke(payload.SourceTab, payload.SpellId);
|
||||||
|
|
||||||
private void EndFavoriteDrag(SpellFavoriteDragPayload payload)
|
private static void EndFavoriteDrag(SpellFavoriteDragPayload payload)
|
||||||
=> _spellbook.RemoveFavorite(payload.SourceTab, payload.SpellId);
|
{
|
||||||
|
// 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)
|
private void DropFavorite(SpellFavoriteDragPayload payload, int targetTab, int targetPosition)
|
||||||
{
|
{
|
||||||
_addFavorite?.Invoke(targetTab, targetPosition, payload.SpellId);
|
_addFavorite?.Invoke(targetTab, targetPosition, payload.SpellId);
|
||||||
_spellbook.SetFavorite(targetTab, targetPosition, payload.SpellId);
|
|
||||||
_selected[targetTab] = payload.SpellId;
|
_selected[targetTab] = payload.SpellId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -443,7 +444,6 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
||||||
// CM_Magic::SendNotice_AddSpellShortcut; the open spellcasting menu
|
// CM_Magic::SendNotice_AddSpellShortcut; the open spellcasting menu
|
||||||
// chooses the favorite tab/position and persists it.
|
// chooses the favorite tab/position and persists it.
|
||||||
_addFavorite?.Invoke(targetTab, targetPosition, payload.SpellId);
|
_addFavorite?.Invoke(targetTab, targetPosition, payload.SpellId);
|
||||||
_spellbook.SetFavorite(targetTab, targetPosition, payload.SpellId);
|
|
||||||
_selected[targetTab] = 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 List<(uint PanelId, UiButton Button)> _panelButtons = new();
|
||||||
private readonly ClientObjectTable _repo;
|
private readonly ClientObjectTable _repo;
|
||||||
private readonly CombatState? _combatState;
|
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> _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex
|
||||||
private readonly Func<ItemType, uint, uint, uint, uint, uint>? _dragIconIds;
|
private readonly Func<ItemType, uint, uint, uint, uint, uint>? _dragIconIds;
|
||||||
private readonly Action<uint> _useItem; // guid → fire UseObject
|
private readonly Action<uint> _useItem; // guid → fire UseObject
|
||||||
private readonly ShortcutStore _store = new();
|
|
||||||
private bool _storeLoaded;
|
|
||||||
private readonly Action<ShortcutEntry>? _sendAddShortcut;
|
private readonly Action<ShortcutEntry>? _sendAddShortcut;
|
||||||
private readonly Action<uint>? _sendRemoveShortcut; // (index)
|
private readonly Action<uint>? _sendRemoveShortcut; // (index)
|
||||||
private readonly ItemInteractionController? _itemInteraction;
|
private readonly ItemInteractionController? _itemInteraction;
|
||||||
|
|
@ -95,7 +93,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
private ToolbarController(
|
private ToolbarController(
|
||||||
ImportedLayout layout,
|
ImportedLayout layout,
|
||||||
ClientObjectTable repo,
|
ClientObjectTable repo,
|
||||||
Func<IReadOnlyList<ShortcutEntry>> shortcuts,
|
ShortcutStore shortcuts,
|
||||||
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
||||||
Action<uint> useItem,
|
Action<uint> useItem,
|
||||||
CombatState? combatState,
|
CombatState? combatState,
|
||||||
|
|
@ -116,7 +114,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_combatState = combatState;
|
_combatState = combatState;
|
||||||
_shortcuts = shortcuts;
|
_store = shortcuts ?? throw new ArgumentNullException(nameof(shortcuts));
|
||||||
_iconIds = iconIds;
|
_iconIds = iconIds;
|
||||||
_dragIconIds = dragIconIds;
|
_dragIconIds = dragIconIds;
|
||||||
_useItem = useItem;
|
_useItem = useItem;
|
||||||
|
|
@ -213,6 +211,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
repo.ObjectRemoved += OnRepositoryObjectChanged;
|
repo.ObjectRemoved += OnRepositoryObjectChanged;
|
||||||
repo.ObjectMoved += OnRepositoryObjectMoved;
|
repo.ObjectMoved += OnRepositoryObjectMoved;
|
||||||
repo.Cleared += OnRepositoryCleared;
|
repo.Cleared += OnRepositoryCleared;
|
||||||
|
_store.Changed += Populate;
|
||||||
RefreshAmmo();
|
RefreshAmmo();
|
||||||
RefreshUseButton();
|
RefreshUseButton();
|
||||||
}
|
}
|
||||||
|
|
@ -296,21 +295,13 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
/// Returns true if <paramref name="guid"/> is one of the currently-active shortcut guids
|
/// 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"/>).
|
/// (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.
|
/// 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>
|
/// </summary>
|
||||||
private bool IsShortcutGuid(uint guid)
|
private bool IsShortcutGuid(uint guid)
|
||||||
{
|
{
|
||||||
if (_storeLoaded)
|
for (int s = 0; s < ShortcutStore.SlotCount; s++)
|
||||||
{
|
|
||||||
for (int s = 0; s < AcDream.Core.Items.ShortcutStore.SlotCount; s++)
|
|
||||||
if (_store.Get(s) == guid) return true;
|
if (_store.Get(s) == guid) return true;
|
||||||
return false;
|
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;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create and bind a <see cref="ToolbarController"/> to <paramref name="layout"/>.
|
/// Create and bind a <see cref="ToolbarController"/> to <paramref name="layout"/>.
|
||||||
|
|
@ -320,7 +311,10 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="layout">Imported toolbar layout (LayoutDesc 0x21000016).</param>
|
/// <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="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="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="useItem">Callback fired when a bound slot is clicked; receives the item guid.</param>
|
||||||
/// <param name="combatState">
|
/// <param name="combatState">
|
||||||
|
|
@ -345,7 +339,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
public static ToolbarController Bind(
|
public static ToolbarController Bind(
|
||||||
ImportedLayout layout,
|
ImportedLayout layout,
|
||||||
ClientObjectTable repo,
|
ClientObjectTable repo,
|
||||||
Func<IReadOnlyList<ShortcutEntry>> shortcuts,
|
ShortcutStore shortcuts,
|
||||||
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
||||||
Action<uint> useItem,
|
Action<uint> useItem,
|
||||||
CombatState? combatState = null,
|
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
|
/// 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
|
/// <c>ObjectAdded</c> event re-fires this method when the item arrives
|
||||||
/// (matching retail's <c>SetDelayedShortcutNum</c> deferred-rebind path).
|
/// (matching retail's <c>SetDelayedShortcutNum</c> deferred-rebind path).
|
||||||
/// As of B.2: the <see cref="AcDream.Core.Items.ShortcutStore"/> is the
|
/// The Runtime-owned <see cref="AcDream.Core.Items.ShortcutStore"/> is the
|
||||||
/// authoritative in-session slot map; <c>_shortcuts()</c> seeds it on first call.
|
/// authoritative in-session slot map.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Populate()
|
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();
|
foreach (var list in _slots) list?.Cell.Clear();
|
||||||
|
|
||||||
for (int slot = 0; slot < _slots.Length; slot++)
|
for (int slot = 0; slot < _slots.Length; slot++)
|
||||||
|
|
@ -654,7 +640,6 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
if (_itemInteraction?.IsOwnedByPlayer(itemId) != true)
|
if (_itemInteraction?.IsOwnedByPlayer(itemId) != true)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
EnsureStoreLoadedForMutation();
|
|
||||||
for (int slot = 0; slot < _slots.Length; slot++)
|
for (int slot = 0; slot < _slots.Length; slot++)
|
||||||
if (_store.Get(slot) == itemId)
|
if (_store.Get(slot) == itemId)
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -672,20 +657,11 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var entry = new ShortcutEntry(empty, itemId, 0u);
|
var entry = new ShortcutEntry(empty, itemId, 0u);
|
||||||
_store.Set(entry);
|
|
||||||
_sendAddShortcut?.Invoke(entry);
|
_sendAddShortcut?.Invoke(entry);
|
||||||
Populate();
|
_store.Set(entry);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureStoreLoadedForMutation()
|
|
||||||
{
|
|
||||||
if (_storeLoaded)
|
|
||||||
return;
|
|
||||||
_store.Load(_shortcuts());
|
|
||||||
_storeLoaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── IItemListDragHandler (B.2 live handler) ──────────────────────────────
|
// ── IItemListDragHandler (B.2 live handler) ──────────────────────────────
|
||||||
// Retail: gmToolbarUI is the m_dragHandler for every shortcut slot list.
|
// Retail: gmToolbarUI is the m_dragHandler for every shortcut slot list.
|
||||||
// Retail model (remove-on-lift / place-on-drop / no-restore):
|
// 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
|
// 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
|
// 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.
|
// drop onto a slot. Off-bar release leaves it removed.
|
||||||
_store.Remove(payload.SourceSlot);
|
|
||||||
_sendRemoveShortcut?.Invoke((uint)payload.SourceSlot);
|
_sendRemoveShortcut?.Invoke((uint)payload.SourceSlot);
|
||||||
Populate();
|
_store.Remove(payload.SourceSlot);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|
@ -717,7 +692,6 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||||
{
|
{
|
||||||
EnsureStoreLoadedForMutation();
|
|
||||||
ShortcutDropSource source = payload.SourceKind == ItemDragSource.ShortcutBar
|
ShortcutDropSource source = payload.SourceKind == ItemDragSource.ShortcutBar
|
||||||
? ShortcutDropSource.ShortcutAlias
|
? ShortcutDropSource.ShortcutAlias
|
||||||
: ShortcutDropSource.FreshItem;
|
: ShortcutDropSource.FreshItem;
|
||||||
|
|
@ -727,7 +701,6 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
_store.Snapshot(), source, payload.SourceSlot, targetCell.SlotIndex, dragged);
|
_store.Snapshot(), source, payload.SourceSlot, targetCell.SlotIndex, dragged);
|
||||||
|
|
||||||
ApplyShortcutPlan(plan);
|
ApplyShortcutPlan(plan);
|
||||||
Populate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -736,32 +709,29 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ReplaceFullyMergedShortcut(uint oldObjectId, uint newObjectId)
|
public void ReplaceFullyMergedShortcut(uint oldObjectId, uint newObjectId)
|
||||||
{
|
{
|
||||||
EnsureStoreLoadedForMutation();
|
|
||||||
ShortcutMutation[] plan = ShortcutDropPlanner.PlanFullStackMerge(
|
ShortcutMutation[] plan = ShortcutDropPlanner.PlanFullStackMerge(
|
||||||
_store.Snapshot(), oldObjectId, newObjectId);
|
_store.Snapshot(), oldObjectId, newObjectId);
|
||||||
ApplyShortcutPlan(plan);
|
ApplyShortcutPlan(plan);
|
||||||
if (plan.Length > 0)
|
|
||||||
Populate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyShortcutPlan(IReadOnlyList<ShortcutMutation> plan)
|
private void ApplyShortcutPlan(IReadOnlyList<ShortcutMutation> plan)
|
||||||
{
|
{
|
||||||
// Apply the already-validated transaction locally first, then reproduce the
|
// Retail sends each shortcut event immediately before applying the same
|
||||||
// planner's exact Remove/Add wire order. Shortcut events have no rejection reply.
|
// 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)
|
foreach (var mutation in plan)
|
||||||
{
|
{
|
||||||
if (mutation.Kind == ShortcutMutationKind.Remove)
|
if (mutation.Kind == ShortcutMutationKind.Remove)
|
||||||
|
{
|
||||||
|
_sendRemoveShortcut?.Invoke((uint)mutation.Slot);
|
||||||
_store.Remove(mutation.Slot);
|
_store.Remove(mutation.Slot);
|
||||||
|
}
|
||||||
else if (mutation.Entry is { } entry)
|
else if (mutation.Entry is { } entry)
|
||||||
|
{
|
||||||
|
_sendAddShortcut?.Invoke(entry);
|
||||||
_store.Set(entry);
|
_store.Set(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var mutation in plan)
|
|
||||||
{
|
|
||||||
if (mutation.Kind == ShortcutMutationKind.Remove)
|
|
||||||
_sendRemoveShortcut?.Invoke((uint)mutation.Slot);
|
|
||||||
else if (mutation.Entry is { } entry)
|
|
||||||
_sendAddShortcut?.Invoke(entry);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -800,5 +770,6 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
_repo.ObjectRemoved -= OnRepositoryObjectChanged;
|
_repo.ObjectRemoved -= OnRepositoryObjectChanged;
|
||||||
_repo.ObjectMoved -= OnRepositoryObjectMoved;
|
_repo.ObjectMoved -= OnRepositoryObjectMoved;
|
||||||
_repo.Cleared -= OnRepositoryCleared;
|
_repo.Cleared -= OnRepositoryCleared;
|
||||||
|
_store.Changed -= Populate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ public sealed record IndicatorRuntimeBindings(
|
||||||
|
|
||||||
public sealed record ToolbarRuntimeBindings(
|
public sealed record ToolbarRuntimeBindings(
|
||||||
ClientObjectTable Objects,
|
ClientObjectTable Objects,
|
||||||
Func<IReadOnlyList<ShortcutEntry>> Shortcuts,
|
ShortcutStore Shortcuts,
|
||||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
||||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
||||||
Action<uint> UseItem,
|
Action<uint> UseItem,
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,15 @@ public sealed class ChatCommandTargetState : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsDisposed
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
return _disposed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Forget character-scoped command targets without clearing visible
|
/// Forget character-scoped command targets without clearing visible
|
||||||
/// transcript history.
|
/// transcript history.
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,40 @@ namespace AcDream.Core.Items;
|
||||||
/// <see cref="ShortcutEntry"/> fields, including non-object records that the object-only
|
/// <see cref="ShortcutEntry"/> fields, including non-object records that the object-only
|
||||||
/// <c>gmToolbarUI</c> does not render.
|
/// <c>gmToolbarUI</c> does not render.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ShortcutStore
|
public sealed class ShortcutStore : IDisposable
|
||||||
{
|
{
|
||||||
public const int SlotCount = 18;
|
public const int SlotCount = 18;
|
||||||
private readonly ShortcutEntry?[] _entries = new ShortcutEntry?[SlotCount];
|
private readonly ShortcutEntry?[] _entries = new ShortcutEntry?[SlotCount];
|
||||||
|
private ShortcutEntry[] _items = [];
|
||||||
|
private Action? _changed;
|
||||||
|
private long _revision;
|
||||||
|
private long _dispatchFailureCount;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised synchronously after the canonical slot map changes. One failing
|
||||||
|
/// observer cannot prevent another host from seeing the same mutation.
|
||||||
|
/// </summary>
|
||||||
|
public event Action? Changed
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
_changed += value;
|
||||||
|
}
|
||||||
|
remove => _changed -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Stable packed-order view of all present shortcut records.</summary>
|
||||||
|
public IReadOnlyList<ShortcutEntry> Items => Volatile.Read(ref _items);
|
||||||
|
|
||||||
|
public int Count => Volatile.Read(ref _items).Length;
|
||||||
|
public long Revision => Interlocked.Read(ref _revision);
|
||||||
|
public int SubscriberCount => _changed?.GetInvocationList().Length ?? 0;
|
||||||
|
public long DispatchFailureCount =>
|
||||||
|
Interlocked.Read(ref _dispatchFailureCount);
|
||||||
|
public Exception? LastDispatchFailure { get; private set; }
|
||||||
|
public bool IsDisposed => _disposed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Replace all slots from packed entries. Invalid signed indices are rejected exactly
|
/// Replace all slots from packed entries. Invalid signed indices are rejected exactly
|
||||||
|
|
@ -21,9 +51,12 @@ public sealed class ShortcutStore
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Load(IEnumerable<ShortcutEntry> entries)
|
public void Load(IEnumerable<ShortcutEntry> entries)
|
||||||
{
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
ArgumentNullException.ThrowIfNull(entries);
|
||||||
Array.Clear(_entries);
|
Array.Clear(_entries);
|
||||||
foreach (var entry in entries)
|
foreach (var entry in entries)
|
||||||
Set(entry);
|
SetCore(entry);
|
||||||
|
PublishChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Raw entry at <paramref name="slot"/>, or null for absent/out-of-range.</summary>
|
/// <summary>Raw entry at <paramref name="slot"/>, or null for absent/out-of-range.</summary>
|
||||||
|
|
@ -44,8 +77,9 @@ public sealed class ShortcutStore
|
||||||
|
|
||||||
public void Set(ShortcutEntry entry)
|
public void Set(ShortcutEntry entry)
|
||||||
{
|
{
|
||||||
if ((uint)entry.Index < SlotCount)
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
_entries[entry.Index] = entry;
|
if (SetCore(entry))
|
||||||
|
PublishChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Create/replace a retail object shortcut; toolbar-created records use spell word zero.</summary>
|
/// <summary>Create/replace a retail object shortcut; toolbar-created records use spell word zero.</summary>
|
||||||
|
|
@ -53,7 +87,79 @@ public sealed class ShortcutStore
|
||||||
|
|
||||||
public void Remove(int slot)
|
public void Remove(int slot)
|
||||||
{
|
{
|
||||||
if ((uint)slot < SlotCount)
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
if ((uint)slot >= SlotCount || _entries[slot] is null)
|
||||||
|
return;
|
||||||
_entries[slot] = null;
|
_entries[slot] = null;
|
||||||
|
PublishChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear the complete retail shortcut manager while retaining this owner
|
||||||
|
/// for the next session.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
Array.Clear(_entries);
|
||||||
|
PublishChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
_disposed = true;
|
||||||
|
Array.Clear(_entries);
|
||||||
|
RebuildItems();
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
|
DispatchChanged();
|
||||||
|
_changed = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool SetCore(ShortcutEntry entry)
|
||||||
|
{
|
||||||
|
if ((uint)entry.Index >= SlotCount
|
||||||
|
|| _entries[entry.Index] == entry)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_entries[entry.Index] = entry;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PublishChanged()
|
||||||
|
{
|
||||||
|
RebuildItems();
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
|
DispatchChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RebuildItems()
|
||||||
|
{
|
||||||
|
var items = new List<ShortcutEntry>(SlotCount);
|
||||||
|
for (int i = 0; i < _entries.Length; i++)
|
||||||
|
{
|
||||||
|
if (_entries[i] is ShortcutEntry entry)
|
||||||
|
items.Add(entry);
|
||||||
|
}
|
||||||
|
Volatile.Write(ref _items, items.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DispatchChanged()
|
||||||
|
{
|
||||||
|
Delegate[] observers = _changed?.GetInvocationList() ?? [];
|
||||||
|
foreach (Delegate observer in observers)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
((Action)observer)();
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _dispatchFailureCount);
|
||||||
|
LastDispatchFailure = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -401,11 +401,25 @@ public sealed class Spellbook
|
||||||
|
|
||||||
public void SetDesiredComponent(uint componentId, uint amount)
|
public void SetDesiredComponent(uint componentId, uint amount)
|
||||||
{
|
{
|
||||||
uint current = _desiredComponents.TryGetValue(componentId, out uint existing)
|
bool exists = _desiredComponents.TryGetValue(
|
||||||
? existing : 0u;
|
componentId,
|
||||||
if (current == amount) return;
|
out uint current);
|
||||||
if (amount == 0) _desiredComponents.Remove(componentId);
|
if (exists && current == amount) return;
|
||||||
else _desiredComponents[componentId] = amount;
|
// PlayerModule::SetDesiredCompLevel @ 0x005D4940 retains zero as a
|
||||||
|
// real table value; only ClearDesiredCompList destroys the entry set.
|
||||||
|
_desiredComponents[componentId] = amount;
|
||||||
|
DesiredComponentsChanged?.Invoke();
|
||||||
|
StateChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Destroy the complete desired-component list, matching
|
||||||
|
/// <c>PlayerModule::ClearDesiredCompList @ 0x005D2A00</c>.
|
||||||
|
/// </summary>
|
||||||
|
public void ClearDesiredComponents()
|
||||||
|
{
|
||||||
|
if (_desiredComponents.Count == 0) return;
|
||||||
|
_desiredComponents.Clear();
|
||||||
DesiredComponentsChanged?.Invoke();
|
DesiredComponentsChanged?.Invoke();
|
||||||
StateChanged?.Invoke();
|
StateChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,41 @@
|
||||||
using AcDream.Core.Player;
|
using AcDream.Core.Player;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
|
using AcDream.Core.Items;
|
||||||
|
|
||||||
namespace AcDream.Runtime.Gameplay;
|
namespace AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
|
public readonly record struct RuntimeCharacterOwnershipSnapshot(
|
||||||
|
bool IsDisposed,
|
||||||
|
bool InternalSubscriptionsAttached,
|
||||||
|
int LearnedSpellCount,
|
||||||
|
int ActiveEnchantmentCount,
|
||||||
|
int DesiredComponentCount,
|
||||||
|
int FavoriteSpellCount,
|
||||||
|
int VitalCount,
|
||||||
|
int AttributeCount,
|
||||||
|
int SkillCount,
|
||||||
|
int PositionCount,
|
||||||
|
int PropertyCount,
|
||||||
|
bool OptionsAreDefaults,
|
||||||
|
bool MovementSkillsAreReset)
|
||||||
|
{
|
||||||
|
public bool IsConverged =>
|
||||||
|
IsDisposed
|
||||||
|
&& !InternalSubscriptionsAttached
|
||||||
|
&& LearnedSpellCount == 0
|
||||||
|
&& ActiveEnchantmentCount == 0
|
||||||
|
&& DesiredComponentCount == 0
|
||||||
|
&& FavoriteSpellCount == 0
|
||||||
|
&& VitalCount == 0
|
||||||
|
&& AttributeCount == 0
|
||||||
|
&& SkillCount == 0
|
||||||
|
&& PositionCount == 0
|
||||||
|
&& PropertyCount == 0
|
||||||
|
&& OptionsAreDefaults
|
||||||
|
&& MovementSkillsAreReset;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Canonical presentation-independent owner for the local character's magic
|
/// Canonical presentation-independent owner for the local character's magic
|
||||||
/// and player-sheet state. The two objects form one lifetime group because
|
/// and player-sheet state. The two objects form one lifetime group because
|
||||||
|
|
@ -14,6 +46,7 @@ public sealed class RuntimeCharacterState : IDisposable
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private long _characterRevision;
|
private long _characterRevision;
|
||||||
private long _spellbookRevision;
|
private long _spellbookRevision;
|
||||||
|
private bool _internalSubscriptionsAttached;
|
||||||
|
|
||||||
public RuntimeCharacterState(SpellTable? spellTable = null)
|
public RuntimeCharacterState(SpellTable? spellTable = null)
|
||||||
{
|
{
|
||||||
|
|
@ -26,6 +59,7 @@ public sealed class RuntimeCharacterState : IDisposable
|
||||||
LocalPlayer.Changed += OnVitalChanged;
|
LocalPlayer.Changed += OnVitalChanged;
|
||||||
LocalPlayer.AttributeChanged += OnAttributeChanged;
|
LocalPlayer.AttributeChanged += OnAttributeChanged;
|
||||||
LocalPlayer.CharacterChanged += OnCharacterChanged;
|
LocalPlayer.CharacterChanged += OnCharacterChanged;
|
||||||
|
_internalSubscriptionsAttached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Spellbook Spellbook { get; }
|
public Spellbook Spellbook { get; }
|
||||||
|
|
@ -35,6 +69,57 @@ public sealed class RuntimeCharacterState : IDisposable
|
||||||
public IRuntimeCharacterView View { get; }
|
public IRuntimeCharacterView View { get; }
|
||||||
public bool IsDisposed => _disposed;
|
public bool IsDisposed => _disposed;
|
||||||
|
|
||||||
|
public RuntimeCharacterOwnershipSnapshot CaptureOwnership()
|
||||||
|
{
|
||||||
|
int favoriteCount = 0;
|
||||||
|
for (int tab = 0; tab < 8; tab++)
|
||||||
|
favoriteCount += Spellbook.GetFavorites(tab).Count;
|
||||||
|
|
||||||
|
int vitalCount = 0;
|
||||||
|
foreach (LocalPlayerState.VitalKind kind
|
||||||
|
in Enum.GetValues<LocalPlayerState.VitalKind>())
|
||||||
|
{
|
||||||
|
if (LocalPlayer.Get(kind) is not null)
|
||||||
|
vitalCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int attributeCount = 0;
|
||||||
|
foreach (LocalPlayerState.AttributeKind kind
|
||||||
|
in Enum.GetValues<LocalPlayerState.AttributeKind>())
|
||||||
|
{
|
||||||
|
if (LocalPlayer.GetAttribute(kind) is not null)
|
||||||
|
attributeCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyBundle properties = LocalPlayer.Properties;
|
||||||
|
int propertyCount =
|
||||||
|
properties.Bools.Count
|
||||||
|
+ properties.Ints.Count
|
||||||
|
+ properties.Int64s.Count
|
||||||
|
+ properties.Floats.Count
|
||||||
|
+ properties.Strings.Count
|
||||||
|
+ properties.DataIds.Count
|
||||||
|
+ properties.InstanceIds.Count;
|
||||||
|
RuntimeCharacterOptionsSnapshot options = Options.Snapshot;
|
||||||
|
return new RuntimeCharacterOwnershipSnapshot(
|
||||||
|
_disposed,
|
||||||
|
_internalSubscriptionsAttached,
|
||||||
|
Spellbook.LearnedCount,
|
||||||
|
Spellbook.ActiveCount,
|
||||||
|
Spellbook.DesiredComponents.Count,
|
||||||
|
favoriteCount,
|
||||||
|
vitalCount,
|
||||||
|
attributeCount,
|
||||||
|
LocalPlayer.Skills.Count,
|
||||||
|
LocalPlayer.Positions.Count,
|
||||||
|
propertyCount,
|
||||||
|
options.Options1 == RuntimeCharacterOptionsState.DefaultOptions1
|
||||||
|
&& options.Options2
|
||||||
|
== RuntimeCharacterOptionsState.DefaultOptions2,
|
||||||
|
MovementSkills.RunSkill == -1
|
||||||
|
&& MovementSkills.JumpSkill == -1);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Installs immutable DAT metadata without transferring its ownership to
|
/// Installs immutable DAT metadata without transferring its ownership to
|
||||||
/// Runtime. The content host may install one table after portal.dat opens.
|
/// Runtime. The content host may install one table after portal.dat opens.
|
||||||
|
|
@ -57,6 +142,93 @@ public sealed class RuntimeCharacterState : IDisposable
|
||||||
LocalPlayer.Clear();
|
LocalPlayer.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply retail's local favorite insertion before sending the matching
|
||||||
|
/// character event.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryAddFavorite(
|
||||||
|
int tabIndex,
|
||||||
|
int position,
|
||||||
|
uint spellId,
|
||||||
|
Action publishOutbound)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
ArgumentNullException.ThrowIfNull(publishOutbound);
|
||||||
|
if ((uint)tabIndex >= 8u || position < 0 || spellId == 0u)
|
||||||
|
return false;
|
||||||
|
Spellbook.SetFavorite(tabIndex, position, spellId);
|
||||||
|
publishOutbound();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryRemoveFavorite(
|
||||||
|
int tabIndex,
|
||||||
|
uint spellId,
|
||||||
|
Action publishOutbound)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
ArgumentNullException.ThrowIfNull(publishOutbound);
|
||||||
|
if ((uint)tabIndex >= 8u || spellId == 0u)
|
||||||
|
return false;
|
||||||
|
Spellbook.RemoveFavorite(tabIndex, spellId);
|
||||||
|
publishOutbound();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply and publish a spellbook filter only when it differs, matching
|
||||||
|
/// <c>gmSpellbookUI::UpdateFilter @ 0x0048B5E0</c>.
|
||||||
|
/// </summary>
|
||||||
|
public void SetSpellbookFilter(
|
||||||
|
uint filters,
|
||||||
|
Action publishOutbound)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
ArgumentNullException.ThrowIfNull(publishOutbound);
|
||||||
|
if (Spellbook.SpellbookFilters == filters)
|
||||||
|
return;
|
||||||
|
Spellbook.SetSpellbookFilters(filters);
|
||||||
|
publishOutbound();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retail publishes the desired-component event before changing its local
|
||||||
|
/// PlayerModule table.
|
||||||
|
/// </summary>
|
||||||
|
public bool TrySetDesiredComponent(
|
||||||
|
uint componentId,
|
||||||
|
uint amount,
|
||||||
|
Action publishOutbound)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
ArgumentNullException.ThrowIfNull(publishOutbound);
|
||||||
|
if (componentId == 0u || amount > 5000u)
|
||||||
|
return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
publishOutbound();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Spellbook.SetDesiredComponent(componentId, amount);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearDesiredComponents(Action publishOutbound)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
ArgumentNullException.ThrowIfNull(publishOutbound);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
publishOutbound();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Spellbook.ClearDesiredComponents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears both coupled owners while retaining every failed suffix for a
|
/// Clears both coupled owners while retaining every failed suffix for a
|
||||||
/// retry. State mutation happens before the existing synchronous
|
/// retry. State mutation happens before the existing synchronous
|
||||||
|
|
@ -82,13 +254,33 @@ public sealed class RuntimeCharacterState : IDisposable
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
ResetSession();
|
List<Exception>? failures = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Do not call ResetSession as one opaque step here. Terminal
|
||||||
|
// disposal must run every suffix even when an external UI observer
|
||||||
|
// throws from one Core owner's synchronous clear notification.
|
||||||
|
Try(Spellbook.Clear, ref failures);
|
||||||
|
Try(LocalPlayer.Clear, ref failures);
|
||||||
|
Try(Options.ResetSession, ref failures);
|
||||||
|
Try(MovementSkills.ResetSession, ref failures);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
Spellbook.StateChanged -= OnSpellbookChanged;
|
Spellbook.StateChanged -= OnSpellbookChanged;
|
||||||
LocalPlayer.Changed -= OnVitalChanged;
|
LocalPlayer.Changed -= OnVitalChanged;
|
||||||
LocalPlayer.AttributeChanged -= OnAttributeChanged;
|
LocalPlayer.AttributeChanged -= OnAttributeChanged;
|
||||||
LocalPlayer.CharacterChanged -= OnCharacterChanged;
|
LocalPlayer.CharacterChanged -= OnCharacterChanged;
|
||||||
|
_internalSubscriptionsAttached = false;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
if (failures is not null)
|
||||||
|
{
|
||||||
|
throw new AggregateException(
|
||||||
|
"Runtime character state did not converge during disposal.",
|
||||||
|
failures);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnSpellbookChanged() =>
|
private void OnSpellbookChanged() =>
|
||||||
Interlocked.Increment(ref _spellbookRevision);
|
Interlocked.Increment(ref _spellbookRevision);
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,36 @@ public interface IRuntimeCommunicationEventSource
|
||||||
IDisposable Subscribe(IRuntimeCommunicationObserver observer);
|
IDisposable Subscribe(IRuntimeCommunicationObserver observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public readonly record struct RuntimeCommunicationOwnershipSnapshot(
|
||||||
|
bool IsDisposed,
|
||||||
|
bool CommandTargetsDisposed,
|
||||||
|
int StreamSubscriberCount,
|
||||||
|
int PendingDispatchCount,
|
||||||
|
bool IsDispatching,
|
||||||
|
int FriendCount,
|
||||||
|
int SquelchAccountCount,
|
||||||
|
int SquelchCharacterCount,
|
||||||
|
int SquelchGlobalTypeCount,
|
||||||
|
int NegotiatedRoomCount,
|
||||||
|
bool HasReplyTarget,
|
||||||
|
bool HasRetellTarget,
|
||||||
|
long DispatchFailureCount)
|
||||||
|
{
|
||||||
|
public bool IsConverged =>
|
||||||
|
IsDisposed
|
||||||
|
&& CommandTargetsDisposed
|
||||||
|
&& StreamSubscriberCount == 0
|
||||||
|
&& PendingDispatchCount == 0
|
||||||
|
&& !IsDispatching
|
||||||
|
&& FriendCount == 0
|
||||||
|
&& SquelchAccountCount == 0
|
||||||
|
&& SquelchCharacterCount == 0
|
||||||
|
&& SquelchGlobalTypeCount == 0
|
||||||
|
&& NegotiatedRoomCount == 0
|
||||||
|
&& !HasReplyTarget
|
||||||
|
&& !HasRetellTarget;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Canonical presentation-independent owner for communication and social
|
/// Canonical presentation-independent owner for communication and social
|
||||||
/// state. Graphical, headless, plugin, and bot hosts borrow these exact
|
/// state. Graphical, headless, plugin, and bot hosts borrow these exact
|
||||||
|
|
@ -59,6 +89,25 @@ public sealed class RuntimeCommunicationState : IDisposable
|
||||||
public long DispatchFailureCount => _events.DispatchFailureCount;
|
public long DispatchFailureCount => _events.DispatchFailureCount;
|
||||||
public Exception? LastDispatchFailure => _events.LastDispatchFailure;
|
public Exception? LastDispatchFailure => _events.LastDispatchFailure;
|
||||||
|
|
||||||
|
public RuntimeCommunicationOwnershipSnapshot CaptureOwnership()
|
||||||
|
{
|
||||||
|
SquelchDatabase squelch = Squelch.Snapshot();
|
||||||
|
return new RuntimeCommunicationOwnershipSnapshot(
|
||||||
|
_disposed,
|
||||||
|
CommandTargets.IsDisposed,
|
||||||
|
SubscriberCount,
|
||||||
|
PendingDispatchCount,
|
||||||
|
IsDispatching,
|
||||||
|
Friends.Count,
|
||||||
|
squelch.Accounts.Count,
|
||||||
|
squelch.Characters.Count,
|
||||||
|
squelch.Global.MessageTypes.Count,
|
||||||
|
CountRooms(TurbineChat),
|
||||||
|
CommandTargets.LastIncomingTellSender is not null,
|
||||||
|
CommandTargets.LastOutgoingTellTarget is not null,
|
||||||
|
DispatchFailureCount);
|
||||||
|
}
|
||||||
|
|
||||||
public void ResetCommandTargets() => CommandTargets.ResetSession();
|
public void ResetCommandTargets() => CommandTargets.ResetSession();
|
||||||
public void ResetChatIdentity() => Chat.ResetSessionIdentity();
|
public void ResetChatIdentity() => Chat.ResetSessionIdentity();
|
||||||
public void ResetNegotiatedChannels() => TurbineChat.Reset();
|
public void ResetNegotiatedChannels() => TurbineChat.Reset();
|
||||||
|
|
@ -126,6 +175,8 @@ public sealed class RuntimeCommunicationState : IDisposable
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private static int CountRooms(TurbineChatState state)
|
private static int CountRooms(TurbineChatState state)
|
||||||
{
|
{
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
|
@ -139,7 +190,6 @@ public sealed class RuntimeCommunicationState : IDisposable
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class RuntimeCommunicationEventStream
|
internal sealed class RuntimeCommunicationEventStream
|
||||||
: IRuntimeCommunicationEventSource,
|
: IRuntimeCommunicationEventSource,
|
||||||
|
|
|
||||||
34
src/AcDream.Runtime/Gameplay/RuntimeGameplayOwnership.cs
Normal file
34
src/AcDream.Runtime/Gameplay/RuntimeGameplayOwnership.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
namespace AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One allocation-free ledger over the complete J4 gameplay-state lifetime
|
||||||
|
/// group. It contains no state of its own; graphical and no-window hosts
|
||||||
|
/// capture the same three canonical owners.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct RuntimeGameplayOwnershipSnapshot(
|
||||||
|
RuntimeInventoryOwnershipSnapshot Inventory,
|
||||||
|
RuntimeCharacterOwnershipSnapshot Character,
|
||||||
|
RuntimeCommunicationOwnershipSnapshot Communication)
|
||||||
|
{
|
||||||
|
public bool IsConverged =>
|
||||||
|
Inventory.IsConverged
|
||||||
|
&& Character.IsConverged
|
||||||
|
&& Communication.IsConverged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RuntimeGameplayOwnership
|
||||||
|
{
|
||||||
|
public static RuntimeGameplayOwnershipSnapshot Capture(
|
||||||
|
RuntimeInventoryState inventory,
|
||||||
|
RuntimeCharacterState character,
|
||||||
|
RuntimeCommunicationState communication)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(inventory);
|
||||||
|
ArgumentNullException.ThrowIfNull(character);
|
||||||
|
ArgumentNullException.ThrowIfNull(communication);
|
||||||
|
return new RuntimeGameplayOwnershipSnapshot(
|
||||||
|
inventory.CaptureOwnership(),
|
||||||
|
character.CaptureOwnership(),
|
||||||
|
communication.CaptureOwnership());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,33 @@ using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
namespace AcDream.Runtime.Gameplay;
|
namespace AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
|
public readonly record struct RuntimeInventoryOwnershipSnapshot(
|
||||||
|
bool IsDisposed,
|
||||||
|
bool TransactionsDisposed,
|
||||||
|
bool ShortcutsDisposed,
|
||||||
|
int BusyCount,
|
||||||
|
bool HasPendingRequest,
|
||||||
|
uint RequestedContainerId,
|
||||||
|
uint CurrentContainerId,
|
||||||
|
int ItemManaCount,
|
||||||
|
int ShortcutCount,
|
||||||
|
int ShortcutSubscriberCount,
|
||||||
|
long ShortcutDispatchFailureCount,
|
||||||
|
long TransactionDispatchFailureCount)
|
||||||
|
{
|
||||||
|
public bool IsConverged =>
|
||||||
|
IsDisposed
|
||||||
|
&& TransactionsDisposed
|
||||||
|
&& ShortcutsDisposed
|
||||||
|
&& BusyCount == 0
|
||||||
|
&& !HasPendingRequest
|
||||||
|
&& RequestedContainerId == 0u
|
||||||
|
&& CurrentContainerId == 0u
|
||||||
|
&& ItemManaCount == 0
|
||||||
|
&& ShortcutCount == 0
|
||||||
|
&& ShortcutSubscriberCount == 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Canonical presentation-independent owner for inventory gameplay state.
|
/// Canonical presentation-independent owner for inventory gameplay state.
|
||||||
/// The object collection is borrowed directly from J3's entity/object
|
/// The object collection is borrowed directly from J3's entity/object
|
||||||
|
|
@ -19,7 +46,7 @@ public sealed class RuntimeInventoryState : IDisposable
|
||||||
?? throw new ArgumentNullException(nameof(entityObjects));
|
?? throw new ArgumentNullException(nameof(entityObjects));
|
||||||
ExternalContainers = new ExternalContainerState();
|
ExternalContainers = new ExternalContainerState();
|
||||||
ItemMana = new ItemManaState();
|
ItemMana = new ItemManaState();
|
||||||
Shortcuts = new ShortcutState();
|
Shortcuts = new ShortcutStore();
|
||||||
Transactions = new InventoryTransactionState(_entityObjects.Objects);
|
Transactions = new InventoryTransactionState(_entityObjects.Objects);
|
||||||
View = new InventoryStateView(this);
|
View = new InventoryStateView(this);
|
||||||
}
|
}
|
||||||
|
|
@ -27,11 +54,25 @@ public sealed class RuntimeInventoryState : IDisposable
|
||||||
public ClientObjectTable Objects => _entityObjects.Objects;
|
public ClientObjectTable Objects => _entityObjects.Objects;
|
||||||
public ExternalContainerState ExternalContainers { get; }
|
public ExternalContainerState ExternalContainers { get; }
|
||||||
public ItemManaState ItemMana { get; }
|
public ItemManaState ItemMana { get; }
|
||||||
public ShortcutState Shortcuts { get; }
|
public ShortcutStore Shortcuts { get; }
|
||||||
public InventoryTransactionState Transactions { get; }
|
public InventoryTransactionState Transactions { get; }
|
||||||
public IRuntimeInventoryStateView View { get; }
|
public IRuntimeInventoryStateView View { get; }
|
||||||
public bool IsDisposed => _disposed;
|
public bool IsDisposed => _disposed;
|
||||||
|
|
||||||
|
public RuntimeInventoryOwnershipSnapshot CaptureOwnership() => new(
|
||||||
|
_disposed,
|
||||||
|
Transactions.IsDisposed,
|
||||||
|
Shortcuts.IsDisposed,
|
||||||
|
Transactions.BusyCount,
|
||||||
|
Transactions.HasPendingRequest,
|
||||||
|
ExternalContainers.RequestedContainerId,
|
||||||
|
ExternalContainers.CurrentContainerId,
|
||||||
|
ItemMana.Count,
|
||||||
|
Shortcuts.Count,
|
||||||
|
Shortcuts.SubscriberCount,
|
||||||
|
Shortcuts.DispatchFailureCount,
|
||||||
|
Transactions.DispatchFailureCount);
|
||||||
|
|
||||||
public void ResetExternalContainer() => ExternalContainers.Reset();
|
public void ResetExternalContainer() => ExternalContainers.Reset();
|
||||||
public void ResetTransactions() => Transactions.ResetSession();
|
public void ResetTransactions() => Transactions.ResetSession();
|
||||||
public void ResetItemMana() => ItemMana.Clear();
|
public void ResetItemMana() => ItemMana.Clear();
|
||||||
|
|
@ -41,20 +82,82 @@ public sealed class RuntimeInventoryState : IDisposable
|
||||||
Shortcuts.Clear();
|
Shortcuts.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send and apply one retail toolbar add on the canonical shortcut
|
||||||
|
/// manager. Retail emits <c>Event_AddShortCut</c> before mutating
|
||||||
|
/// <c>PlayerModule</c>.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryAddShortcut(
|
||||||
|
ShortcutEntry entry,
|
||||||
|
Action publishOutbound)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
ArgumentNullException.ThrowIfNull(publishOutbound);
|
||||||
|
if ((uint)entry.Index >= ShortcutStore.SlotCount
|
||||||
|
|| (entry.ObjectId == 0u && entry.SpellId == 0u))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
publishOutbound();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Shortcuts.Set(entry);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send and apply one retail toolbar removal on the canonical shortcut
|
||||||
|
/// manager.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryRemoveShortcut(
|
||||||
|
int index,
|
||||||
|
Action publishOutbound)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
ArgumentNullException.ThrowIfNull(publishOutbound);
|
||||||
|
if ((uint)index >= ShortcutStore.SlotCount)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
publishOutbound();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Shortcuts.Remove(index);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
List<Exception>? failures = null;
|
List<Exception>? failures = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
Try(() => ExternalContainers.Reset(), ref failures);
|
Try(() => ExternalContainers.Reset(), ref failures);
|
||||||
Try(ItemMana.Clear, ref failures);
|
Try(ItemMana.Clear, ref failures);
|
||||||
Try(Shortcuts.Clear, ref failures);
|
Try(Shortcuts.Dispose, ref failures);
|
||||||
Try(Transactions.Dispose, ref failures);
|
Try(Transactions.Dispose, ref failures);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Disposal is terminal even when a borrowed presentation observer
|
||||||
|
// fails. Every owned leaf above mutates before dispatching, so the
|
||||||
|
// full suffix is safe to run and the Runtime root must never remain
|
||||||
|
// half-alive over already-disposed children.
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
if (failures is not null)
|
if (failures is not null)
|
||||||
throw new AggregateException(
|
throw new AggregateException(
|
||||||
"Runtime inventory state did not converge during disposal.",
|
"Runtime inventory state did not converge during disposal.",
|
||||||
failures);
|
failures);
|
||||||
_disposed = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Try(Action action, ref List<Exception>? failures)
|
private static void Try(Action action, ref List<Exception>? failures)
|
||||||
|
|
@ -93,7 +196,7 @@ public sealed class RuntimeInventoryState : IDisposable
|
||||||
owner.Transactions.BusyCount,
|
owner.Transactions.BusyCount,
|
||||||
owner.Transactions.CanBeginRequest,
|
owner.Transactions.CanBeginRequest,
|
||||||
pending,
|
pending,
|
||||||
owner.Shortcuts.Items.Count,
|
owner.Shortcuts.Count,
|
||||||
owner.Shortcuts.Revision,
|
owner.Shortcuts.Revision,
|
||||||
owner.ItemMana.Count,
|
owner.ItemMana.Count,
|
||||||
owner.ItemMana.Revision);
|
owner.ItemMana.Revision);
|
||||||
|
|
@ -124,22 +227,3 @@ public sealed class RuntimeInventoryState : IDisposable
|
||||||
owner.ItemMana.TryGetManaPercent(objectId, out fraction);
|
owner.ItemMana.TryGetManaPercent(objectId, out fraction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ShortcutState
|
|
||||||
{
|
|
||||||
private IReadOnlyList<ShortcutEntry> _items = Array.Empty<ShortcutEntry>();
|
|
||||||
private long _revision;
|
|
||||||
|
|
||||||
public IReadOnlyList<ShortcutEntry> Items => Volatile.Read(ref _items);
|
|
||||||
public long Revision => Interlocked.Read(ref _revision);
|
|
||||||
|
|
||||||
public void Replace(IReadOnlyList<ShortcutEntry>? items)
|
|
||||||
{
|
|
||||||
Volatile.Write(
|
|
||||||
ref _items,
|
|
||||||
items ?? Array.Empty<ShortcutEntry>());
|
|
||||||
Interlocked.Increment(ref _revision);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear() => Replace(Array.Empty<ShortcutEntry>());
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,7 @@ public sealed class SelectionInteractionControllerTests
|
||||||
});
|
});
|
||||||
Items = new ItemInteractionController(
|
Items = new ItemInteractionController(
|
||||||
Objects,
|
Objects,
|
||||||
|
new InventoryTransactionState(Objects),
|
||||||
() => Player,
|
() => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
|
||||||
|
|
@ -371,7 +371,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
harness.Character.Options.Replace(0x04000000u, 0x00948700u);
|
harness.Character.Options.Replace(0x04000000u, 0x00948700u);
|
||||||
harness.Character.MovementSkills.Update(200, 175);
|
harness.Character.MovementSkills.Update(200, 175);
|
||||||
harness.Character.Spellbook.OnSpellLearned(42u);
|
harness.Character.Spellbook.OnSpellLearned(42u);
|
||||||
harness.InventoryState.Shortcuts.Replace(
|
harness.InventoryState.Shortcuts.Load(
|
||||||
[
|
[
|
||||||
new ShortcutEntry(1, 0x80000001u, 0u),
|
new ShortcutEntry(1, 0x80000001u, 0u),
|
||||||
]);
|
]);
|
||||||
|
|
@ -506,6 +506,20 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
static entry => entry.Kind == RuntimeTraceKind.Command
|
static entry => entry.Kind == RuntimeTraceKind.Command
|
||||||
&& (entry.Code >> 16)
|
&& (entry.Code >> 16)
|
||||||
== (int)RuntimeCommandDomain.Social);
|
== (int)RuntimeCommandDomain.Social);
|
||||||
|
Assert.True(harness.InventoryState.View.TryGetShortcut(
|
||||||
|
2,
|
||||||
|
out RuntimeShortcutSnapshot storedShortcut));
|
||||||
|
Assert.Equal(0x80000001u, storedShortcut.ObjectId);
|
||||||
|
Assert.True(harness.Character.View.TryGetFavorite(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
out uint storedFavorite));
|
||||||
|
Assert.Equal(42u, storedFavorite);
|
||||||
|
Assert.Equal(0x3FFEu, harness.Character.Spellbook.SpellbookFilters);
|
||||||
|
Assert.True(harness.Character.View.TryGetDesiredComponent(
|
||||||
|
0x68000001u,
|
||||||
|
out uint storedDesired));
|
||||||
|
Assert.Equal(10u, storedDesired);
|
||||||
|
|
||||||
int published = harness.Commands.Published.Count;
|
int published = harness.Commands.Published.Count;
|
||||||
RuntimeCommandResult stale = commands.Character.SetOptions1(
|
RuntimeCommandResult stale = commands.Character.SetOptions1(
|
||||||
|
|
@ -515,6 +529,101 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
Assert.Equal(published, harness.Commands.Published.Count);
|
Assert.Equal(published, harness.Commands.Published.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GraphicalAndNoWindowJ4CommandsProduceIdenticalCanonicalState()
|
||||||
|
{
|
||||||
|
using var directEntities = new RuntimeEntityObjectLifetime();
|
||||||
|
using var directInventory =
|
||||||
|
new RuntimeInventoryState(directEntities);
|
||||||
|
using var directCharacter = new RuntimeCharacterState();
|
||||||
|
using var harness = new Harness();
|
||||||
|
_ = harness.Runtime.Session.Start(harness.Runtime.Generation);
|
||||||
|
RuntimeGenerationToken generation = harness.Runtime.Generation;
|
||||||
|
IGameRuntimeCommands graphical = harness.Runtime;
|
||||||
|
var directOutbound = new List<string>();
|
||||||
|
var shortcut = new ShortcutEntry(2, 0x80000001u, 0u);
|
||||||
|
|
||||||
|
Assert.True(directInventory.TryAddShortcut(
|
||||||
|
shortcut,
|
||||||
|
() => directOutbound.Add("add-shortcut")));
|
||||||
|
Assert.True(directCharacter.TryAddFavorite(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
42u,
|
||||||
|
() => directOutbound.Add("add-favorite")));
|
||||||
|
directCharacter.SetSpellbookFilter(
|
||||||
|
0x3FFEu,
|
||||||
|
() => directOutbound.Add("filter"));
|
||||||
|
Assert.True(directCharacter.TrySetDesiredComponent(
|
||||||
|
0x68000001u,
|
||||||
|
10u,
|
||||||
|
() => directOutbound.Add("desired")));
|
||||||
|
|
||||||
|
Assert.True(graphical.InventoryState.AddShortcut(
|
||||||
|
generation,
|
||||||
|
new RuntimeShortcutCommand(
|
||||||
|
shortcut.Index,
|
||||||
|
shortcut.ObjectId,
|
||||||
|
shortcut.SpellId)).Accepted);
|
||||||
|
Assert.True(graphical.Spellbook.AddFavorite(
|
||||||
|
generation,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
42u).Accepted);
|
||||||
|
Assert.True(graphical.Spellbook.SetFilter(
|
||||||
|
generation,
|
||||||
|
0x3FFEu).Accepted);
|
||||||
|
Assert.True(graphical.Spellbook.SetDesiredComponent(
|
||||||
|
generation,
|
||||||
|
0x68000001u,
|
||||||
|
10u).Accepted);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
directInventory.View.Snapshot,
|
||||||
|
harness.InventoryState.View.Snapshot);
|
||||||
|
Assert.Equal(
|
||||||
|
directCharacter.View.Snapshot,
|
||||||
|
harness.Character.View.Snapshot);
|
||||||
|
Assert.Equal(
|
||||||
|
directInventory.Shortcuts.Items,
|
||||||
|
harness.InventoryState.Shortcuts.Items);
|
||||||
|
Assert.Equal(
|
||||||
|
directCharacter.Spellbook.GetFavorites(0),
|
||||||
|
harness.Character.Spellbook.GetFavorites(0));
|
||||||
|
Assert.Equal(
|
||||||
|
["add-shortcut", "add-favorite", "filter", "desired"],
|
||||||
|
directOutbound);
|
||||||
|
|
||||||
|
Assert.True(directInventory.TryRemoveShortcut(
|
||||||
|
2,
|
||||||
|
() => directOutbound.Add("remove-shortcut")));
|
||||||
|
Assert.True(directCharacter.TryRemoveFavorite(
|
||||||
|
0,
|
||||||
|
42u,
|
||||||
|
() => directOutbound.Add("remove-favorite")));
|
||||||
|
directCharacter.ClearDesiredComponents(
|
||||||
|
() => directOutbound.Add("clear-desired"));
|
||||||
|
Assert.True(graphical.InventoryState.RemoveShortcut(
|
||||||
|
generation,
|
||||||
|
2).Accepted);
|
||||||
|
Assert.True(graphical.Spellbook.RemoveFavorite(
|
||||||
|
generation,
|
||||||
|
0,
|
||||||
|
42u).Accepted);
|
||||||
|
Assert.True(graphical.Spellbook.ClearDesiredComponents(
|
||||||
|
generation).Accepted);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
directInventory.View.Snapshot,
|
||||||
|
harness.InventoryState.View.Snapshot);
|
||||||
|
Assert.Equal(
|
||||||
|
directCharacter.View.Snapshot,
|
||||||
|
harness.Character.View.Snapshot);
|
||||||
|
Assert.Empty(harness.InventoryState.Shortcuts.Items);
|
||||||
|
Assert.Empty(harness.Character.Spellbook.GetFavorites(0));
|
||||||
|
Assert.Empty(harness.Character.Spellbook.DesiredComponents);
|
||||||
|
}
|
||||||
|
|
||||||
private static ClientObject Object(WorldSession.EntitySpawn spawn) => new()
|
private static ClientObject Object(WorldSession.EntitySpawn spawn) => new()
|
||||||
{
|
{
|
||||||
ObjectId = spawn.Guid,
|
ObjectId = spawn.Guid,
|
||||||
|
|
@ -587,6 +696,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
|
|
||||||
_items = new ItemInteractionController(
|
_items = new ItemInteractionController(
|
||||||
Objects,
|
Objects,
|
||||||
|
InventoryState.Transactions,
|
||||||
() => PlayerGuid,
|
() => PlayerGuid,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
|
||||||
|
|
@ -41,13 +41,32 @@ public sealed class RuntimeInventoryOwnershipTests
|
||||||
"InteractionRetainedUiComposition.cs");
|
"InteractionRetainedUiComposition.cs");
|
||||||
string session = ReadSource("Net", "LiveSessionRuntimeFactory.cs");
|
string session = ReadSource("Net", "LiveSessionRuntimeFactory.cs");
|
||||||
string shutdown = ReadSource("Rendering", "GameWindowLifetime.cs");
|
string shutdown = ReadSource("Rendering", "GameWindowLifetime.cs");
|
||||||
|
string itemInteraction = ReadSource(
|
||||||
|
"UI",
|
||||||
|
"ItemInteractionController.cs");
|
||||||
|
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"transactions: d.Inventory.Transactions",
|
"d.Inventory.Transactions",
|
||||||
ui,
|
ui,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"new InventoryTransactionState(d.Inventory.Objects)",
|
||||||
|
ui,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"new InventoryTransactionState",
|
||||||
|
itemInteraction,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"_ownsTransactions",
|
||||||
|
itemInteraction,
|
||||||
|
StringComparison.Ordinal);
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"OnShortcuts: _domain.Inventory.Shortcuts.Replace",
|
"InventoryTransactionState transactions,",
|
||||||
|
itemInteraction,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"OnShortcuts: _domain.Inventory.Shortcuts.Load",
|
||||||
session,
|
session,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
|
|
@ -64,6 +83,73 @@ public sealed class RuntimeInventoryOwnershipTests
|
||||||
"\"runtime entity/object lifetime\"");
|
"\"runtime entity/object lifetime\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToolbarBorrowsRuntimeShortcutManagerWithoutAProviderOrMirror()
|
||||||
|
{
|
||||||
|
string toolbar = ReadSource(
|
||||||
|
"UI",
|
||||||
|
"Layout",
|
||||||
|
"ToolbarController.cs");
|
||||||
|
string ui = ReadSource(
|
||||||
|
"Composition",
|
||||||
|
"InteractionRetainedUiComposition.cs");
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
"private readonly ShortcutStore _store;",
|
||||||
|
toolbar,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"_store = shortcuts ?? throw",
|
||||||
|
toolbar,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"new ShortcutStore()",
|
||||||
|
toolbar,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"Func<IReadOnlyList<ShortcutEntry>>",
|
||||||
|
toolbar,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"_storeLoaded",
|
||||||
|
toolbar,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"d.Inventory.Shortcuts,",
|
||||||
|
ui,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SpellUiDoesNotApplyASecondLocalCommandMutation()
|
||||||
|
{
|
||||||
|
string spellbook = ReadSource(
|
||||||
|
"UI",
|
||||||
|
"Layout",
|
||||||
|
"SpellbookWindowController.cs");
|
||||||
|
string spellcasting = ReadSource(
|
||||||
|
"UI",
|
||||||
|
"Layout",
|
||||||
|
"SpellcastingUiController.cs");
|
||||||
|
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"_spellbook.SetSpellbookFilters(filters);",
|
||||||
|
spellbook,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"_spellbook.SetDesiredComponent(componentId, parsed);",
|
||||||
|
spellbook,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"_spellbook.SetFavorite(",
|
||||||
|
spellcasting,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"_spellbook.RemoveFavorite(",
|
||||||
|
spellcasting,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
private static string ReadSource(params string[] relative)
|
private static string ReadSource(params string[] relative)
|
||||||
{
|
{
|
||||||
string root = FindRepositoryRoot();
|
string root = FindRepositoryRoot();
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,7 @@ public sealed class CursorFeedbackControllerTests
|
||||||
var objects = SeedTargetObjects();
|
var objects = SeedTargetObjects();
|
||||||
var interaction = new ItemInteractionController(
|
var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -188,6 +189,7 @@ public sealed class CursorFeedbackControllerTests
|
||||||
objects.Get(Source)!.TargetType = (uint)ItemType.Misc; // a tool that targets items
|
objects.Get(Source)!.TargetType = (uint)ItemType.Misc; // a tool that targets items
|
||||||
var interaction = new ItemInteractionController(
|
var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -226,6 +228,7 @@ public sealed class CursorFeedbackControllerTests
|
||||||
var objects = SeedTargetObjects();
|
var objects = SeedTargetObjects();
|
||||||
var interaction = new ItemInteractionController(
|
var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -254,6 +257,7 @@ public sealed class CursorFeedbackControllerTests
|
||||||
var objects = SeedTargetObjects();
|
var objects = SeedTargetObjects();
|
||||||
var interaction = new ItemInteractionController(
|
var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ public sealed class ItemInteractionControllerTests
|
||||||
public readonly List<CombatMode> CombatModeRequests = new();
|
public readonly List<CombatMode> CombatModeRequests = new();
|
||||||
public readonly CombatState Combat = new();
|
public readonly CombatState Combat = new();
|
||||||
public readonly StackSplitQuantityState SplitQuantity = new();
|
public readonly StackSplitQuantityState SplitQuantity = new();
|
||||||
public readonly InventoryTransactionState? SharedTransactions;
|
public readonly InventoryTransactionState SharedTransactions;
|
||||||
public uint SelectedObject;
|
public uint SelectedObject;
|
||||||
public bool NonCombatMode;
|
public bool NonCombatMode;
|
||||||
public bool DragOnPlayerOpensSecureTrade = true;
|
public bool DragOnPlayerOpensSecureTrade = true;
|
||||||
|
|
@ -39,8 +39,7 @@ public sealed class ItemInteractionControllerTests
|
||||||
public long Now = 1_000;
|
public long Now = 1_000;
|
||||||
|
|
||||||
public Harness(
|
public Harness(
|
||||||
Action<uint, ItemUseRequestReservation>? requestUse = null,
|
Action<uint, ItemUseRequestReservation>? requestUse = null)
|
||||||
bool sharedTransactions = false)
|
|
||||||
{
|
{
|
||||||
Objects.AddOrUpdate(new ClientObject
|
Objects.AddOrUpdate(new ClientObject
|
||||||
{
|
{
|
||||||
|
|
@ -56,12 +55,11 @@ public sealed class ItemInteractionControllerTests
|
||||||
ItemsCapacity = 24,
|
ItemsCapacity = 24,
|
||||||
});
|
});
|
||||||
Objects.MoveItem(Pack, Player, 0);
|
Objects.MoveItem(Pack, Player, 0);
|
||||||
SharedTransactions = sharedTransactions
|
SharedTransactions = new InventoryTransactionState(Objects);
|
||||||
? new InventoryTransactionState(Objects)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
Controller = new ItemInteractionController(
|
Controller = new ItemInteractionController(
|
||||||
Objects,
|
Objects,
|
||||||
|
SharedTransactions,
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: requestUse is null ? Uses.Add : null,
|
sendUse: requestUse is null ? Uses.Add : null,
|
||||||
sendExamine: Examines.Add,
|
sendExamine: Examines.Add,
|
||||||
|
|
@ -91,8 +89,7 @@ public sealed class ItemInteractionControllerTests
|
||||||
},
|
},
|
||||||
combatState: Combat,
|
combatState: Combat,
|
||||||
sendChangeCombatMode: CombatModeRequests.Add,
|
sendChangeCombatMode: CombatModeRequests.Add,
|
||||||
requestUse: requestUse,
|
requestUse: requestUse);
|
||||||
transactions: SharedTransactions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ItemInteractionController Controller { get; }
|
public ItemInteractionController Controller { get; }
|
||||||
|
|
@ -321,11 +318,11 @@ public sealed class ItemInteractionControllerTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void BorrowedTransactionOwnerIsExactAndOutlivesController()
|
public void BorrowedTransactionOwnerIsExactAndOutlivesController()
|
||||||
{
|
{
|
||||||
var h = new Harness(sharedTransactions: true);
|
var h = new Harness();
|
||||||
|
|
||||||
h.Controller.IncrementBusyCount();
|
h.Controller.IncrementBusyCount();
|
||||||
|
|
||||||
Assert.Equal(1, h.SharedTransactions!.BusyCount);
|
Assert.Equal(1, h.SharedTransactions.BusyCount);
|
||||||
h.Controller.Dispose();
|
h.Controller.Dispose();
|
||||||
Assert.False(h.SharedTransactions.IsDisposed);
|
Assert.False(h.SharedTransactions.IsDisposed);
|
||||||
|
|
||||||
|
|
@ -343,12 +340,12 @@ public sealed class ItemInteractionControllerTests
|
||||||
|
|
||||||
Assert.Throws<ArgumentException>(() => new ItemInteractionController(
|
Assert.Throws<ArgumentException>(() => new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
transactions,
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
sendWield: null,
|
sendWield: null,
|
||||||
sendDrop: null,
|
sendDrop: null));
|
||||||
transactions: transactions));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -858,6 +858,7 @@ public sealed class AppraisalUiControllerTests
|
||||||
List<uint> sent)
|
List<uint> sent)
|
||||||
=> new(
|
=> new(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => 0x50000002u,
|
playerGuid: () => 0x50000002u,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ public sealed class ExternalContainerControllerTests
|
||||||
|
|
||||||
Interaction = new ItemInteractionController(
|
Interaction = new ItemInteractionController(
|
||||||
Objects,
|
Objects,
|
||||||
|
new InventoryTransactionState(Objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: Uses.Add,
|
sendUse: Uses.Add,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
|
||||||
|
|
@ -508,6 +508,7 @@ public class InventoryControllerTests
|
||||||
var appraisals = new List<uint>();
|
var appraisals = new List<uint>();
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -544,6 +545,7 @@ public class InventoryControllerTests
|
||||||
objects.Get(0xA)!.Useability = 0x000A0008u;
|
objects.Get(0xA)!.Useability = 0x000A0008u;
|
||||||
var interaction = new ItemInteractionController(
|
var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -712,6 +714,7 @@ public class InventoryControllerTests
|
||||||
var pickups = new List<(uint item, uint container, int placement)>();
|
var pickups = new List<(uint item, uint container, int placement)>();
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -756,6 +759,7 @@ public class InventoryControllerTests
|
||||||
var eventOrder = new List<(string Kind, uint Item, ulong Token)>();
|
var eventOrder = new List<(string Kind, uint Item, ulong Token)>();
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -807,6 +811,7 @@ public class InventoryControllerTests
|
||||||
var eventOrder = new List<(string Kind, uint Item)>();
|
var eventOrder = new List<(string Kind, uint Item)>();
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -864,6 +869,7 @@ public class InventoryControllerTests
|
||||||
var messages = new List<string>();
|
var messages = new List<string>();
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -932,6 +938,7 @@ public class InventoryControllerTests
|
||||||
var messages = new List<string>();
|
var messages = new List<string>();
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -994,6 +1001,7 @@ public class InventoryControllerTests
|
||||||
var merges = new List<(uint Source, uint Target, uint Amount)>();
|
var merges = new List<(uint Source, uint Target, uint Amount)>();
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -1053,6 +1061,7 @@ public class InventoryControllerTests
|
||||||
var splits = new List<(uint Item, uint Container, uint Placement, uint Amount)>();
|
var splits = new List<(uint Item, uint Container, uint Placement, uint Amount)>();
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -1099,6 +1108,7 @@ public class InventoryControllerTests
|
||||||
var puts = new List<(uint Item, uint Container, int Placement)>();
|
var puts = new List<(uint Item, uint Container, int Placement)>();
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -1138,6 +1148,7 @@ public class InventoryControllerTests
|
||||||
SeedContained(objects, loot, chest, slot: 0, type: ItemType.Misc);
|
SeedContained(objects, loot, chest, slot: 0, type: ItemType.Misc);
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -1176,6 +1187,7 @@ public class InventoryControllerTests
|
||||||
SeedContained(objects, loot, chest, slot: 0, type: ItemType.Misc);
|
SeedContained(objects, loot, chest, slot: 0, type: ItemType.Misc);
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ public class PaperdollControllerTests
|
||||||
{
|
{
|
||||||
var itemInteraction = new ItemInteractionController(
|
var itemInteraction = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
() => Player,
|
() => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
|
||||||
|
|
@ -413,10 +413,18 @@ public sealed class SpellbookWindowControllerTests
|
||||||
spellId => spellId == 103u ? 8 : 1,
|
spellId => spellId == 103u ? 8 : 1,
|
||||||
selectObject ?? (_ => { }),
|
selectObject ?? (_ => { }),
|
||||||
addFavorite ?? (_ => { }),
|
addFavorite ?? (_ => { }),
|
||||||
sendFilter ?? (_ => { }),
|
filters =>
|
||||||
|
{
|
||||||
|
spellbook.SetSpellbookFilters(filters);
|
||||||
|
sendFilter?.Invoke(filters);
|
||||||
|
},
|
||||||
removeSpell ?? (_ => { }),
|
removeSpell ?? (_ => { }),
|
||||||
showConfirmation ?? ((_, _) => { }),
|
showConfirmation ?? ((_, _) => { }),
|
||||||
setDesiredComponent ?? ((_, _) => { }),
|
(componentId, amount) =>
|
||||||
|
{
|
||||||
|
spellbook.SetDesiredComponent(componentId, amount);
|
||||||
|
setDesiredComponent?.Invoke(componentId, amount);
|
||||||
|
},
|
||||||
close ?? (() => { }),
|
close ?? (() => { }),
|
||||||
new ComponentBookTemplateFactory(
|
new ComponentBookTemplateFactory(
|
||||||
FixtureLoader.LoadComponentCategoryTemplateInfos(),
|
FixtureLoader.LoadComponentCategoryTemplateInfos(),
|
||||||
|
|
|
||||||
|
|
@ -338,8 +338,12 @@ public sealed class SpellcastingUiControllerTests
|
||||||
item => item.ObjectId,
|
item => item.ObjectId,
|
||||||
useItem,
|
useItem,
|
||||||
selection ?? new SelectionState(),
|
selection ?? new SelectionState(),
|
||||||
addFavorite ?? ((_, _, _) => { }),
|
(tab, position, spellId) =>
|
||||||
(_, _) => { },
|
{
|
||||||
|
spellbook.SetFavorite(tab, position, spellId);
|
||||||
|
addFavorite?.Invoke(tab, position, spellId);
|
||||||
|
},
|
||||||
|
(tab, spellId) => spellbook.RemoveFavorite(tab, spellId),
|
||||||
shortcutDigits,
|
shortcutDigits,
|
||||||
emptySlotSprite,
|
emptySlotSprite,
|
||||||
examineSpell);
|
examineSpell);
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,14 @@ public class ToolbarControllerTests
|
||||||
(InventoryButtonId, RetailPanelCatalog.Inventory),
|
(InventoryButtonId, RetailPanelCatalog.Inventory),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static ShortcutStore Store(
|
||||||
|
IEnumerable<ShortcutEntry> shortcuts)
|
||||||
|
{
|
||||||
|
var store = new ShortcutStore();
|
||||||
|
store.Load(shortcuts);
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
private static (ImportedLayout layout, Dictionary<uint, UiItemList> slots,
|
private static (ImportedLayout layout, Dictionary<uint, UiItemList> slots,
|
||||||
Dictionary<uint, UiElement> indicators) FakeToolbar()
|
Dictionary<uint, UiElement> indicators) FakeToolbar()
|
||||||
{
|
{
|
||||||
|
|
@ -108,7 +116,7 @@ public class ToolbarControllerTests
|
||||||
var shortcuts = new List<ShortcutEntry>
|
var shortcuts = new List<ShortcutEntry>
|
||||||
{ new(Index: 0, ObjectId: 0x5001u, SpellId: 0) };
|
{ new(Index: 0, ObjectId: 0x5001u, SpellId: 0) };
|
||||||
|
|
||||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
ToolbarController.Bind(layout, repo, Store(shortcuts),
|
||||||
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { });
|
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { });
|
||||||
|
|
||||||
Assert.Equal(0x5001u, slots[Row1[0]].Cell.ItemId);
|
Assert.Equal(0x5001u, slots[Row1[0]].Cell.ItemId);
|
||||||
|
|
@ -134,7 +142,7 @@ public class ToolbarControllerTests
|
||||||
ToolbarController.Bind(
|
ToolbarController.Bind(
|
||||||
layout,
|
layout,
|
||||||
repo,
|
repo,
|
||||||
() => shortcuts,
|
Store(shortcuts),
|
||||||
iconIds: (_, _, _, _, _) => 0x77u,
|
iconIds: (_, _, _, _, _) => 0x77u,
|
||||||
useItem: _ => { });
|
useItem: _ => { });
|
||||||
Assert.Equal(0x5001u, slots[Row1[0]].Cell.ItemId);
|
Assert.Equal(0x5001u, slots[Row1[0]].Cell.ItemId);
|
||||||
|
|
@ -152,7 +160,7 @@ public class ToolbarControllerTests
|
||||||
var shortcuts = new List<ShortcutEntry>
|
var shortcuts = new List<ShortcutEntry>
|
||||||
{ new(Index: 2, ObjectId: 0x5002u, SpellId: 0) };
|
{ new(Index: 2, ObjectId: 0x5002u, SpellId: 0) };
|
||||||
|
|
||||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
ToolbarController.Bind(layout, repo, Store(shortcuts),
|
||||||
iconIds: (_,_,_,_,_) => 0x88u, useItem: _ => { });
|
iconIds: (_,_,_,_,_) => 0x88u, useItem: _ => { });
|
||||||
Assert.Equal(0u, slots[Row1[2]].Cell.ItemId); // not bound yet
|
Assert.Equal(0u, slots[Row1[2]].Cell.ItemId); // not bound yet
|
||||||
|
|
||||||
|
|
@ -168,7 +176,7 @@ public class ToolbarControllerTests
|
||||||
var repo = new ClientObjectTable();
|
var repo = new ClientObjectTable();
|
||||||
var shortcuts = new List<ShortcutEntry>
|
var shortcuts = new List<ShortcutEntry>
|
||||||
{ new(Index: 2, ObjectId: 0x5002u, SpellId: 0) };
|
{ new(Index: 2, ObjectId: 0x5002u, SpellId: 0) };
|
||||||
var controller = ToolbarController.Bind(layout, repo, () => shortcuts,
|
var controller = ToolbarController.Bind(layout, repo, Store(shortcuts),
|
||||||
iconIds: (_,_,_,_,_) => 0x88u, useItem: _ => { });
|
iconIds: (_,_,_,_,_) => 0x88u, useItem: _ => { });
|
||||||
|
|
||||||
controller.Dispose();
|
controller.Dispose();
|
||||||
|
|
@ -194,7 +202,7 @@ public class ToolbarControllerTests
|
||||||
uint used = 0;
|
uint used = 0;
|
||||||
var selection = new SelectionState();
|
var selection = new SelectionState();
|
||||||
|
|
||||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
ToolbarController.Bind(layout, repo, Store(shortcuts),
|
||||||
iconIds: (_,_,_,_,_) => 0x77u,
|
iconIds: (_,_,_,_,_) => 0x77u,
|
||||||
useItem: g => used = g,
|
useItem: g => used = g,
|
||||||
selection: selection);
|
selection: selection);
|
||||||
|
|
@ -217,7 +225,7 @@ public class ToolbarControllerTests
|
||||||
var toggles = new List<uint>();
|
var toggles = new List<uint>();
|
||||||
|
|
||||||
var ctrl = ToolbarController.Bind(layout, repo,
|
var ctrl = ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||||
ctrl.BindPanelButtons(
|
ctrl.BindPanelButtons(
|
||||||
panelId => panelId is RetailPanelCatalog.Inventory or RetailPanelCatalog.Character,
|
panelId => panelId is RetailPanelCatalog.Inventory or RetailPanelCatalog.Character,
|
||||||
|
|
@ -257,6 +265,7 @@ public class ToolbarControllerTests
|
||||||
var useWithTarget = new List<(uint Source, uint Target)>();
|
var useWithTarget = new List<(uint Source, uint Target)>();
|
||||||
var interaction = new ItemInteractionController(
|
var interaction = new ItemInteractionController(
|
||||||
repo,
|
repo,
|
||||||
|
new InventoryTransactionState(repo),
|
||||||
playerGuid: () => player,
|
playerGuid: () => player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: (source, target) => useWithTarget.Add((source, target)),
|
sendUseWithTarget: (source, target) => useWithTarget.Add((source, target)),
|
||||||
|
|
@ -266,7 +275,7 @@ public class ToolbarControllerTests
|
||||||
int inventoryClicks = 0;
|
int inventoryClicks = 0;
|
||||||
|
|
||||||
var ctrl = ToolbarController.Bind(layout, repo,
|
var ctrl = ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) => 0u,
|
iconIds: (_,_,_,_,_) => 0u,
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
itemInteraction: interaction);
|
itemInteraction: interaction);
|
||||||
|
|
@ -293,7 +302,7 @@ public class ToolbarControllerTests
|
||||||
repo.AddOrUpdate(new ClientObject { ObjectId = item, Type = ItemType.Misc });
|
repo.AddOrUpdate(new ClientObject { ObjectId = item, Type = ItemType.Misc });
|
||||||
var puts = new List<(uint Item, uint Container, int Placement)>();
|
var puts = new List<(uint Item, uint Container, int Placement)>();
|
||||||
ToolbarController.Bind(layout, repo,
|
ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_, _, _, _, _) => 0u,
|
iconIds: (_, _, _, _, _) => 0u,
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
playerGuid: () => player,
|
playerGuid: () => player,
|
||||||
|
|
@ -322,6 +331,7 @@ public class ToolbarControllerTests
|
||||||
var directPuts = new List<(uint Item, uint Container, int Placement)>();
|
var directPuts = new List<(uint Item, uint Container, int Placement)>();
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
repo,
|
repo,
|
||||||
|
new InventoryTransactionState(repo),
|
||||||
playerGuid: () => player,
|
playerGuid: () => player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -331,7 +341,7 @@ public class ToolbarControllerTests
|
||||||
ToolbarController.Bind(
|
ToolbarController.Bind(
|
||||||
layout,
|
layout,
|
||||||
repo,
|
repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: static (_, _, _, _, _) => 0u,
|
iconIds: static (_, _, _, _, _) => 0u,
|
||||||
useItem: static _ => { },
|
useItem: static _ => { },
|
||||||
itemInteraction: interaction,
|
itemInteraction: interaction,
|
||||||
|
|
@ -370,6 +380,7 @@ public class ToolbarControllerTests
|
||||||
var messages = new List<string>();
|
var messages = new List<string>();
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
repo,
|
repo,
|
||||||
|
new InventoryTransactionState(repo),
|
||||||
playerGuid: () => player,
|
playerGuid: () => player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -379,7 +390,7 @@ public class ToolbarControllerTests
|
||||||
ToolbarController.Bind(
|
ToolbarController.Bind(
|
||||||
layout,
|
layout,
|
||||||
repo,
|
repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: static (_, _, _, _, _) => 0u,
|
iconIds: static (_, _, _, _, _) => 0u,
|
||||||
useItem: static _ => { },
|
useItem: static _ => { },
|
||||||
itemInteraction: interaction,
|
itemInteraction: interaction,
|
||||||
|
|
@ -411,7 +422,7 @@ public class ToolbarControllerTests
|
||||||
repo.AddOrUpdate(new ClientObject { ObjectId = item, Type = ItemType.Misc });
|
repo.AddOrUpdate(new ClientObject { ObjectId = item, Type = ItemType.Misc });
|
||||||
var puts = new List<(uint Item, uint Container, int Placement)>();
|
var puts = new List<(uint Item, uint Container, int Placement)>();
|
||||||
ToolbarController.Bind(layout, repo,
|
ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_, _, _, _, _) => 0u,
|
iconIds: (_, _, _, _, _) => 0u,
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
playerGuid: () => player,
|
playerGuid: () => player,
|
||||||
|
|
@ -432,7 +443,7 @@ public class ToolbarControllerTests
|
||||||
var (layout, _, _) = FakeToolbar();
|
var (layout, _, _) = FakeToolbar();
|
||||||
var repo = new ClientObjectTable();
|
var repo = new ClientObjectTable();
|
||||||
var ctrl = ToolbarController.Bind(layout, repo,
|
var ctrl = ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||||
var inventoryButton = (UiButton)layout.FindElement(InventoryButtonId)!;
|
var inventoryButton = (UiButton)layout.FindElement(InventoryButtonId)!;
|
||||||
var characterButton = (UiButton)layout.FindElement(CharacterButtonId)!;
|
var characterButton = (UiButton)layout.FindElement(CharacterButtonId)!;
|
||||||
|
|
@ -498,6 +509,7 @@ public class ToolbarControllerTests
|
||||||
uint selected = item;
|
uint selected = item;
|
||||||
var interaction = new ItemInteractionController(
|
var interaction = new ItemInteractionController(
|
||||||
repo,
|
repo,
|
||||||
|
new InventoryTransactionState(repo),
|
||||||
() => player,
|
() => player,
|
||||||
sendUse: uses.Add,
|
sendUse: uses.Add,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -507,7 +519,7 @@ public class ToolbarControllerTests
|
||||||
ToolbarController.Bind(
|
ToolbarController.Bind(
|
||||||
layout,
|
layout,
|
||||||
repo,
|
repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_, _, _, _, _) => 0u,
|
iconIds: (_, _, _, _, _) => 0u,
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
itemInteraction: interaction,
|
itemInteraction: interaction,
|
||||||
|
|
@ -555,6 +567,7 @@ public class ToolbarControllerTests
|
||||||
var selection = new SelectionState();
|
var selection = new SelectionState();
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
repo,
|
repo,
|
||||||
|
new InventoryTransactionState(repo),
|
||||||
() => player,
|
() => player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -563,7 +576,7 @@ public class ToolbarControllerTests
|
||||||
using var controller = ToolbarController.Bind(
|
using var controller = ToolbarController.Bind(
|
||||||
layout,
|
layout,
|
||||||
repo,
|
repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_, _, _, _, _) => 0u,
|
iconIds: (_, _, _, _, _) => 0u,
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
itemInteraction: interaction,
|
itemInteraction: interaction,
|
||||||
|
|
@ -619,6 +632,7 @@ public class ToolbarControllerTests
|
||||||
var wields = new List<(uint Item, uint Location)>();
|
var wields = new List<(uint Item, uint Location)>();
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
repo,
|
repo,
|
||||||
|
new InventoryTransactionState(repo),
|
||||||
() => player,
|
() => player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -627,7 +641,7 @@ public class ToolbarControllerTests
|
||||||
using var controller = ToolbarController.Bind(
|
using var controller = ToolbarController.Bind(
|
||||||
layout,
|
layout,
|
||||||
repo,
|
repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_, _, _, _, _) => 0u,
|
iconIds: (_, _, _, _, _) => 0u,
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
itemInteraction: interaction,
|
itemInteraction: interaction,
|
||||||
|
|
@ -670,7 +684,7 @@ public class ToolbarControllerTests
|
||||||
ToolbarController.Bind(
|
ToolbarController.Bind(
|
||||||
layout,
|
layout,
|
||||||
repo,
|
repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_, _, _, _, _) => 0u,
|
iconIds: (_, _, _, _, _) => 0u,
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
playerGuid: () => player);
|
playerGuid: () => player);
|
||||||
|
|
@ -709,7 +723,7 @@ public class ToolbarControllerTests
|
||||||
ToolbarController.Bind(
|
ToolbarController.Bind(
|
||||||
layout,
|
layout,
|
||||||
repo,
|
repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_, _, _, _, _) => 0u,
|
iconIds: (_, _, _, _, _) => 0u,
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
playerGuid: () => player);
|
playerGuid: () => player);
|
||||||
|
|
@ -731,7 +745,7 @@ public class ToolbarControllerTests
|
||||||
ToolbarController.Bind(
|
ToolbarController.Bind(
|
||||||
layout,
|
layout,
|
||||||
repo,
|
repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_, _, _, _, _) => 0u,
|
iconIds: (_, _, _, _, _) => 0u,
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
toggleCombat: () => toggles++);
|
toggleCombat: () => toggles++);
|
||||||
|
|
@ -758,7 +772,7 @@ public class ToolbarControllerTests
|
||||||
var controller = ToolbarController.Bind(
|
var controller = ToolbarController.Bind(
|
||||||
layout,
|
layout,
|
||||||
repo,
|
repo,
|
||||||
() => shortcuts,
|
Store(shortcuts),
|
||||||
iconIds: (_, _, _, _, _) => 1u,
|
iconIds: (_, _, _, _, _) => 1u,
|
||||||
useItem: id => used = id,
|
useItem: id => used = id,
|
||||||
selectItem: id => selected = id);
|
selectItem: id => selected = id);
|
||||||
|
|
@ -784,6 +798,7 @@ public class ToolbarControllerTests
|
||||||
var appraisals = new List<uint>();
|
var appraisals = new List<uint>();
|
||||||
using var interaction = new ItemInteractionController(
|
using var interaction = new ItemInteractionController(
|
||||||
repo,
|
repo,
|
||||||
|
new InventoryTransactionState(repo),
|
||||||
playerGuid: () => player,
|
playerGuid: () => player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -793,7 +808,7 @@ public class ToolbarControllerTests
|
||||||
using var controller = ToolbarController.Bind(
|
using var controller = ToolbarController.Bind(
|
||||||
layout,
|
layout,
|
||||||
repo,
|
repo,
|
||||||
() => shortcuts,
|
Store(shortcuts),
|
||||||
iconIds: (_, _, _, _, _) => 1u,
|
iconIds: (_, _, _, _, _) => 1u,
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
itemInteraction: interaction,
|
itemInteraction: interaction,
|
||||||
|
|
@ -832,6 +847,7 @@ public class ToolbarControllerTests
|
||||||
uint sentTarget = 0;
|
uint sentTarget = 0;
|
||||||
var interaction = new ItemInteractionController(
|
var interaction = new ItemInteractionController(
|
||||||
repo,
|
repo,
|
||||||
|
new InventoryTransactionState(repo),
|
||||||
() => player,
|
() => player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: (s, t) => (sentSource, sentTarget) = (s, t),
|
sendUseWithTarget: (s, t) => (sentSource, sentTarget) = (s, t),
|
||||||
|
|
@ -845,7 +861,7 @@ public class ToolbarControllerTests
|
||||||
var controller = ToolbarController.Bind(
|
var controller = ToolbarController.Bind(
|
||||||
layout,
|
layout,
|
||||||
repo,
|
repo,
|
||||||
() => shortcuts,
|
Store(shortcuts),
|
||||||
iconIds: (_, _, _, _, _) => 1u,
|
iconIds: (_, _, _, _, _) => 1u,
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
itemInteraction: interaction);
|
itemInteraction: interaction);
|
||||||
|
|
@ -873,6 +889,7 @@ public class ToolbarControllerTests
|
||||||
repo.MoveItem(item, pack, 0);
|
repo.MoveItem(item, pack, 0);
|
||||||
var interaction = new ItemInteractionController(
|
var interaction = new ItemInteractionController(
|
||||||
repo,
|
repo,
|
||||||
|
new InventoryTransactionState(repo),
|
||||||
() => player,
|
() => player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -882,7 +899,7 @@ public class ToolbarControllerTests
|
||||||
var controller = ToolbarController.Bind(
|
var controller = ToolbarController.Bind(
|
||||||
layout,
|
layout,
|
||||||
repo,
|
repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_, _, _, _, _) => 1u,
|
iconIds: (_, _, _, _, _) => 1u,
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
itemInteraction: interaction,
|
itemInteraction: interaction,
|
||||||
|
|
@ -908,7 +925,7 @@ public class ToolbarControllerTests
|
||||||
var repo = new ClientObjectTable();
|
var repo = new ClientObjectTable();
|
||||||
|
|
||||||
ToolbarController.Bind(layout, repo,
|
ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) =>0u, useItem: _ => { });
|
iconIds: (_,_,_,_,_) =>0u, useItem: _ => { });
|
||||||
|
|
||||||
// Only peace indicator (index 0 = 0x10000192) is visible.
|
// Only peace indicator (index 0 = 0x10000192) is visible.
|
||||||
|
|
@ -928,7 +945,7 @@ public class ToolbarControllerTests
|
||||||
var repo = new ClientObjectTable();
|
var repo = new ClientObjectTable();
|
||||||
|
|
||||||
var ctrl = ToolbarController.Bind(layout, repo,
|
var ctrl = ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) =>0u, useItem: _ => { });
|
iconIds: (_,_,_,_,_) =>0u, useItem: _ => { });
|
||||||
|
|
||||||
ctrl.SetCombatMode(CombatMode.Melee);
|
ctrl.SetCombatMode(CombatMode.Melee);
|
||||||
|
|
@ -950,7 +967,7 @@ public class ToolbarControllerTests
|
||||||
var combat = new CombatState();
|
var combat = new CombatState();
|
||||||
|
|
||||||
ToolbarController.Bind(layout, repo,
|
ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) =>0u, useItem: _ => { },
|
iconIds: (_,_,_,_,_) =>0u, useItem: _ => { },
|
||||||
combatState: combat);
|
combatState: combat);
|
||||||
|
|
||||||
|
|
@ -989,7 +1006,7 @@ public class ToolbarControllerTests
|
||||||
var repo = new ClientObjectTable();
|
var repo = new ClientObjectTable();
|
||||||
|
|
||||||
ToolbarController.Bind(layout, repo,
|
ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
||||||
regularDigits: FakeRegular, ghostedDigits: FakeGhosted);
|
regularDigits: FakeRegular, ghostedDigits: FakeGhosted);
|
||||||
|
|
||||||
|
|
@ -1018,7 +1035,7 @@ public class ToolbarControllerTests
|
||||||
var (layout, slots, _) = FakeToolbar();
|
var (layout, slots, _) = FakeToolbar();
|
||||||
var repo = new ClientObjectTable();
|
var repo = new ClientObjectTable();
|
||||||
var ctrl = ToolbarController.Bind(layout, repo,
|
var ctrl = ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
||||||
regularDigits: FakeRegular, ghostedDigits: FakeGhosted);
|
regularDigits: FakeRegular, ghostedDigits: FakeGhosted);
|
||||||
|
|
||||||
|
|
@ -1046,7 +1063,7 @@ public class ToolbarControllerTests
|
||||||
var (layout, slots, _) = FakeToolbar();
|
var (layout, slots, _) = FakeToolbar();
|
||||||
var repo = new ClientObjectTable();
|
var repo = new ClientObjectTable();
|
||||||
var ctrl = ToolbarController.Bind(layout, repo,
|
var ctrl = ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
||||||
regularDigits: FakeRegular, ghostedDigits: FakeGhosted);
|
regularDigits: FakeRegular, ghostedDigits: FakeGhosted);
|
||||||
|
|
||||||
|
|
@ -1078,7 +1095,7 @@ public class ToolbarControllerTests
|
||||||
var repo = new ClientObjectTable();
|
var repo = new ClientObjectTable();
|
||||||
|
|
||||||
ToolbarController.Bind(layout, repo,
|
ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
||||||
regularDigits: FakeRegular, ghostedDigits: FakeGhosted);
|
regularDigits: FakeRegular, ghostedDigits: FakeGhosted);
|
||||||
|
|
||||||
|
|
@ -1100,7 +1117,7 @@ public class ToolbarControllerTests
|
||||||
var repo = new ClientObjectTable();
|
var repo = new ClientObjectTable();
|
||||||
|
|
||||||
ToolbarController.Bind(layout, repo,
|
ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
||||||
regularDigits: FakeRegular, ghostedDigits: FakeGhosted, emptyDigits: FakeEmpty);
|
regularDigits: FakeRegular, ghostedDigits: FakeGhosted, emptyDigits: FakeEmpty);
|
||||||
|
|
||||||
|
|
@ -1121,7 +1138,7 @@ public class ToolbarControllerTests
|
||||||
var repo = new ClientObjectTable();
|
var repo = new ClientObjectTable();
|
||||||
|
|
||||||
ToolbarController.Bind(layout, repo,
|
ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
||||||
regularDigits: FakeRegular, ghostedDigits: FakeGhosted, emptyDigits: null);
|
regularDigits: FakeRegular, ghostedDigits: FakeGhosted, emptyDigits: null);
|
||||||
|
|
||||||
|
|
@ -1147,7 +1164,7 @@ public class ToolbarControllerTests
|
||||||
{ new(Index: 0, ObjectId: 0x5001u, SpellId: 0) };
|
{ new(Index: 0, ObjectId: 0x5001u, SpellId: 0) };
|
||||||
|
|
||||||
int iconCallCount = 0;
|
int iconCallCount = 0;
|
||||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
ToolbarController.Bind(layout, repo, Store(shortcuts),
|
||||||
iconIds: (_,_,_,_,_) => { iconCallCount++; return 0x77u; }, useItem: _ => { });
|
iconIds: (_,_,_,_,_) => { iconCallCount++; return 0x77u; }, useItem: _ => { });
|
||||||
|
|
||||||
int callsAfterBind = iconCallCount; // 1 call from initial Populate
|
int callsAfterBind = iconCallCount; // 1 call from initial Populate
|
||||||
|
|
@ -1172,7 +1189,7 @@ public class ToolbarControllerTests
|
||||||
{ new(Index: 1, ObjectId: 0x5003u, SpellId: 0) };
|
{ new(Index: 1, ObjectId: 0x5003u, SpellId: 0) };
|
||||||
|
|
||||||
int iconCallCount = 0;
|
int iconCallCount = 0;
|
||||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
ToolbarController.Bind(layout, repo, Store(shortcuts),
|
||||||
iconIds: (_,_,_,_,_) => { iconCallCount++; return 0x99u; }, useItem: _ => { });
|
iconIds: (_,_,_,_,_) => { iconCallCount++; return 0x99u; }, useItem: _ => { });
|
||||||
|
|
||||||
Assert.Equal(0, iconCallCount); // not called — item absent during initial Populate
|
Assert.Equal(0, iconCallCount); // not called — item absent during initial Populate
|
||||||
|
|
@ -1198,7 +1215,7 @@ public class ToolbarControllerTests
|
||||||
var shortcuts = new List<ShortcutEntry>
|
var shortcuts = new List<ShortcutEntry>
|
||||||
{ new(Index: 3, ObjectId: 0x5004u, SpellId: 0) };
|
{ new(Index: 3, ObjectId: 0x5004u, SpellId: 0) };
|
||||||
|
|
||||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
ToolbarController.Bind(layout, repo, Store(shortcuts),
|
||||||
iconIds: (_,_,_,_,_) => 0xAAu, useItem: _ => { });
|
iconIds: (_,_,_,_,_) => 0xAAu, useItem: _ => { });
|
||||||
|
|
||||||
Assert.Equal(0x5004u, slots[Row1[3]].Cell.ItemId); // bound
|
Assert.Equal(0x5004u, slots[Row1[3]].Cell.ItemId); // bound
|
||||||
|
|
@ -1225,7 +1242,7 @@ public class ToolbarControllerTests
|
||||||
{ new(Index: 4, ObjectId: 0x5005u, SpellId: 0) };
|
{ new(Index: 4, ObjectId: 0x5005u, SpellId: 0) };
|
||||||
|
|
||||||
int iconCallCount = 0;
|
int iconCallCount = 0;
|
||||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
ToolbarController.Bind(layout, repo, Store(shortcuts),
|
||||||
iconIds: (_,_,_,_,_) => { iconCallCount++; return 0xBBu; }, useItem: _ => { });
|
iconIds: (_,_,_,_,_) => { iconCallCount++; return 0xBBu; }, useItem: _ => { });
|
||||||
|
|
||||||
int callsAfterBind = iconCallCount; // 1 call for the shortcut item
|
int callsAfterBind = iconCallCount; // 1 call for the shortcut item
|
||||||
|
|
@ -1247,7 +1264,7 @@ public class ToolbarControllerTests
|
||||||
var repo = new ClientObjectTable();
|
var repo = new ClientObjectTable();
|
||||||
|
|
||||||
var ctrl = ToolbarController.Bind(layout, repo,
|
var ctrl = ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||||
|
|
||||||
for (int i = 0; i < Row1.Length; i++)
|
for (int i = 0; i < Row1.Length; i++)
|
||||||
|
|
@ -1272,7 +1289,7 @@ public class ToolbarControllerTests
|
||||||
{
|
{
|
||||||
var (layout, slots, _) = FakeToolbar();
|
var (layout, slots, _) = FakeToolbar();
|
||||||
var ctrl = ToolbarController.Bind(layout, new ClientObjectTable(),
|
var ctrl = ToolbarController.Bind(layout, new ClientObjectTable(),
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||||
|
|
||||||
var list = slots[Row1[0]];
|
var list = slots[Row1[0]];
|
||||||
|
|
@ -1287,7 +1304,7 @@ public class ToolbarControllerTests
|
||||||
{
|
{
|
||||||
var (layout, slots, _) = FakeToolbar();
|
var (layout, slots, _) = FakeToolbar();
|
||||||
var ctrl = ToolbarController.Bind(layout, new ClientObjectTable(),
|
var ctrl = ToolbarController.Bind(layout, new ClientObjectTable(),
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||||
var list = slots[Row1[0]];
|
var list = slots[Row1[0]];
|
||||||
var payload = new ItemDragPayload(0u, ItemDragSource.Inventory, 0, new UiItemSlot());
|
var payload = new ItemDragPayload(0u, ItemDragSource.Inventory, 0, new UiItemSlot());
|
||||||
|
|
@ -1316,7 +1333,7 @@ public class ToolbarControllerTests
|
||||||
var (adds, removes) = NewSpies(out var add, out var rem);
|
var (adds, removes) = NewSpies(out var add, out var rem);
|
||||||
uint selected = 0;
|
uint selected = 0;
|
||||||
|
|
||||||
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
|
var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts),
|
||||||
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
|
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
|
||||||
sendAddShortcut: add, sendRemoveShortcut: rem,
|
sendAddShortcut: add, sendRemoveShortcut: rem,
|
||||||
selectItem: item => selected = item, selectedObjectId: () => selected);
|
selectItem: item => selected = item, selectedObjectId: () => selected);
|
||||||
|
|
@ -1341,7 +1358,7 @@ public class ToolbarControllerTests
|
||||||
{ new(Index: 3, ObjectId: 0x5001u, SpellId: 0),
|
{ new(Index: 3, ObjectId: 0x5001u, SpellId: 0),
|
||||||
new(Index: 5, ObjectId: 0x5002u, SpellId: 0) };
|
new(Index: 5, ObjectId: 0x5002u, SpellId: 0) };
|
||||||
var (adds, removes) = NewSpies(out var add, out var rem);
|
var (adds, removes) = NewSpies(out var add, out var rem);
|
||||||
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
|
var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts),
|
||||||
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
|
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
|
||||||
sendAddShortcut: add, sendRemoveShortcut: rem);
|
sendAddShortcut: add, sendRemoveShortcut: rem);
|
||||||
|
|
||||||
|
|
@ -1370,7 +1387,7 @@ public class ToolbarControllerTests
|
||||||
};
|
};
|
||||||
var (adds, _) = NewSpies(out var add, out var remove);
|
var (adds, _) = NewSpies(out var add, out var remove);
|
||||||
var controller = ToolbarController.Bind(
|
var controller = ToolbarController.Bind(
|
||||||
layout, repo, () => shortcuts,
|
layout, repo, Store(shortcuts),
|
||||||
iconIds: (_, _, _, _, _) => 1u,
|
iconIds: (_, _, _, _, _) => 1u,
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
sendAddShortcut: add,
|
sendAddShortcut: add,
|
||||||
|
|
@ -1393,7 +1410,7 @@ public class ToolbarControllerTests
|
||||||
var shortcuts = new System.Collections.Generic.List<ShortcutEntry>
|
var shortcuts = new System.Collections.Generic.List<ShortcutEntry>
|
||||||
{ new(Index: 3, ObjectId: 0x5001u, SpellId: 0) };
|
{ new(Index: 3, ObjectId: 0x5001u, SpellId: 0) };
|
||||||
var (adds, removes) = NewSpies(out var add, out var rem);
|
var (adds, removes) = NewSpies(out var add, out var rem);
|
||||||
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
|
var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts),
|
||||||
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
|
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
|
||||||
sendAddShortcut: add, sendRemoveShortcut: rem);
|
sendAddShortcut: add, sendRemoveShortcut: rem);
|
||||||
|
|
||||||
|
|
@ -1419,7 +1436,7 @@ public class ToolbarControllerTests
|
||||||
new ShortcutEntry(5, 0x5002u, 0xA5C31234u),
|
new ShortcutEntry(5, 0x5002u, 0xA5C31234u),
|
||||||
};
|
};
|
||||||
var wire = new List<string>();
|
var wire = new List<string>();
|
||||||
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
|
var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts),
|
||||||
iconIds: (_, _, _, _, _) => 1u,
|
iconIds: (_, _, _, _, _) => 1u,
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
sendAddShortcut: entry => wire.Add($"add:{entry.Index}:{entry.ObjectId:X8}:{entry.SpellId:X8}"),
|
sendAddShortcut: entry => wire.Add($"add:{entry.Index}:{entry.ObjectId:X8}:{entry.SpellId:X8}"),
|
||||||
|
|
@ -1452,7 +1469,7 @@ public class ToolbarControllerTests
|
||||||
new ShortcutEntry(4, 0x5001u, 0x11223344u),
|
new ShortcutEntry(4, 0x5001u, 0x11223344u),
|
||||||
};
|
};
|
||||||
var wire = new List<string>();
|
var wire = new List<string>();
|
||||||
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
|
var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts),
|
||||||
iconIds: (_, _, _, _, _) => 1u,
|
iconIds: (_, _, _, _, _) => 1u,
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
sendAddShortcut: entry => wire.Add($"add:{entry.Index}:{entry.ObjectId:X8}:{entry.SpellId:X8}"),
|
sendAddShortcut: entry => wire.Add($"add:{entry.Index}:{entry.ObjectId:X8}:{entry.SpellId:X8}"),
|
||||||
|
|
@ -1477,7 +1494,7 @@ public class ToolbarControllerTests
|
||||||
var shortcuts = new System.Collections.Generic.List<ShortcutEntry>
|
var shortcuts = new System.Collections.Generic.List<ShortcutEntry>
|
||||||
{ new(Index: 3, ObjectId: 0x5001u, SpellId: 0) };
|
{ new(Index: 3, ObjectId: 0x5001u, SpellId: 0) };
|
||||||
var (adds, removes) = NewSpies(out var add, out var rem);
|
var (adds, removes) = NewSpies(out var add, out var rem);
|
||||||
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
|
var ctrl = ToolbarController.Bind(layout, repo, Store(shortcuts),
|
||||||
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
|
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
|
||||||
sendAddShortcut: add, sendRemoveShortcut: rem);
|
sendAddShortcut: add, sendRemoveShortcut: rem);
|
||||||
|
|
||||||
|
|
@ -1496,7 +1513,7 @@ public class ToolbarControllerTests
|
||||||
{
|
{
|
||||||
var (layout, slots, _) = FakeToolbar();
|
var (layout, slots, _) = FakeToolbar();
|
||||||
ToolbarController.Bind(layout, new ClientObjectTable(),
|
ToolbarController.Bind(layout, new ClientObjectTable(),
|
||||||
() => System.Array.Empty<ShortcutEntry>(),
|
new ShortcutStore(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||||
Assert.Equal(0x060011FAu, slots[Row1[0]].Cell.DragAcceptSprite); // green cross, not the ring F9
|
Assert.Equal(0x060011FAu, slots[Row1[0]].Cell.DragAcceptSprite); // green cross, not the ring F9
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ public sealed class RetailItemConfirmationControllerTests
|
||||||
var uses = new List<uint>();
|
var uses = new List<uint>();
|
||||||
var items = new ItemInteractionController(
|
var items = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: uses.Add,
|
sendUse: uses.Add,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -52,6 +53,7 @@ public sealed class RetailItemConfirmationControllerTests
|
||||||
var uses = new List<uint>();
|
var uses = new List<uint>();
|
||||||
var items = new ItemInteractionController(
|
var items = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: uses.Add,
|
sendUse: uses.Add,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
@ -80,6 +82,7 @@ public sealed class RetailItemConfirmationControllerTests
|
||||||
var messages = new List<string>();
|
var messages = new List<string>();
|
||||||
var items = new ItemInteractionController(
|
var items = new ItemInteractionController(
|
||||||
objects,
|
objects,
|
||||||
|
new InventoryTransactionState(objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: uses.Add,
|
sendUse: uses.Add,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,7 @@ public sealed class RetailUiInteractionFlowTests
|
||||||
{
|
{
|
||||||
var interaction = new ItemInteractionController(
|
var interaction = new ItemInteractionController(
|
||||||
Objects,
|
Objects,
|
||||||
|
new InventoryTransactionState(Objects),
|
||||||
playerGuid: () => Player,
|
playerGuid: () => Player,
|
||||||
sendUse: Uses.Add,
|
sendUse: Uses.Add,
|
||||||
sendUseWithTarget: (source, target) => UseWithTarget.Add((source, target)),
|
sendUseWithTarget: (source, target) => UseWithTarget.Add((source, target)),
|
||||||
|
|
@ -197,6 +198,7 @@ public sealed class RetailUiInteractionFlowTests
|
||||||
{
|
{
|
||||||
itemInteraction ??= new ItemInteractionController(
|
itemInteraction ??= new ItemInteractionController(
|
||||||
Objects,
|
Objects,
|
||||||
|
new InventoryTransactionState(Objects),
|
||||||
() => Player,
|
() => Player,
|
||||||
sendUse: null,
|
sendUse: null,
|
||||||
sendUseWithTarget: null,
|
sendUseWithTarget: null,
|
||||||
|
|
|
||||||
|
|
@ -41,4 +41,41 @@ public class ShortcutStoreTests
|
||||||
store.SetItem(18, 1u); // no-op, no throw
|
store.SetItem(18, 1u); // no-op, no throw
|
||||||
store.Remove(-1); // no-op, no throw
|
store.Remove(-1); // no-op, no throw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RevisionObserversAndDisposalTrackTheOneCanonicalSlotMap()
|
||||||
|
{
|
||||||
|
var store = new ShortcutStore();
|
||||||
|
int observed = 0;
|
||||||
|
store.Changed += () => throw new InvalidOperationException("observer");
|
||||||
|
store.Changed += () => observed++;
|
||||||
|
|
||||||
|
store.Load([new ShortcutEntry(2, 0x5001u, 0u)]);
|
||||||
|
|
||||||
|
Assert.Equal(1, observed);
|
||||||
|
Assert.Equal(1, store.Revision);
|
||||||
|
Assert.Equal(1, store.DispatchFailureCount);
|
||||||
|
Assert.Equal(2, store.SubscriberCount);
|
||||||
|
Assert.Equal(1, store.Count);
|
||||||
|
|
||||||
|
store.Set(new ShortcutEntry(2, 0x5001u, 0u));
|
||||||
|
Assert.Equal(1, store.Revision);
|
||||||
|
Assert.Equal(1, observed);
|
||||||
|
|
||||||
|
store.Set(new ShortcutEntry(2, 0x5002u, 0u));
|
||||||
|
Assert.Equal(2, store.Revision);
|
||||||
|
Assert.Equal(2, observed);
|
||||||
|
|
||||||
|
store.Dispose();
|
||||||
|
store.Dispose();
|
||||||
|
|
||||||
|
Assert.True(store.IsDisposed);
|
||||||
|
Assert.Empty(store.Items);
|
||||||
|
Assert.Equal(0, store.SubscriberCount);
|
||||||
|
Assert.Equal(3, observed);
|
||||||
|
Assert.Equal(3, store.Revision);
|
||||||
|
Assert.Equal(3, store.DispatchFailureCount);
|
||||||
|
Assert.Throws<ObjectDisposedException>(
|
||||||
|
() => store.SetItem(1, 1u));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ public sealed class RuntimeCharacterStateTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DisposalRetriesObserverFailuresAndClearsBothOwners()
|
public void DisposalReportsObserverFailuresAfterTerminalConvergence()
|
||||||
{
|
{
|
||||||
var state = new RuntimeCharacterState();
|
var state = new RuntimeCharacterState();
|
||||||
state.Spellbook.OnSpellLearned(7u);
|
state.Spellbook.OnSpellLearned(7u);
|
||||||
|
|
@ -143,13 +143,13 @@ public sealed class RuntimeCharacterStateTests
|
||||||
state.Dispose);
|
state.Dispose);
|
||||||
|
|
||||||
Assert.Equal(2, error.InnerExceptions.Count);
|
Assert.Equal(2, error.InnerExceptions.Count);
|
||||||
Assert.False(state.IsDisposed);
|
Assert.True(state.IsDisposed);
|
||||||
Assert.False(state.Spellbook.Knows(7u));
|
Assert.False(state.Spellbook.Knows(7u));
|
||||||
Assert.Null(state.LocalPlayer.Get(
|
Assert.Null(state.LocalPlayer.Get(
|
||||||
AcDream.Core.Player.LocalPlayerState.VitalKind.Health));
|
AcDream.Core.Player.LocalPlayerState.VitalKind.Health));
|
||||||
|
Assert.True(state.CaptureOwnership().IsConverged);
|
||||||
|
|
||||||
state.Dispose();
|
state.Dispose();
|
||||||
state.Dispose();
|
|
||||||
|
|
||||||
Assert.True(state.IsDisposed);
|
Assert.True(state.IsDisposed);
|
||||||
}
|
}
|
||||||
|
|
@ -254,4 +254,77 @@ public sealed class RuntimeCharacterStateTests
|
||||||
Assert.True(first.View.Snapshot.SpellbookRevision > 0);
|
Assert.True(first.View.Snapshot.SpellbookRevision > 0);
|
||||||
Assert.Equal(0, second.View.Snapshot.SpellbookRevision);
|
Assert.Equal(0, second.View.Snapshot.SpellbookRevision);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SpellbookCommandsFollowRetailLocalAndOutboundOrder()
|
||||||
|
{
|
||||||
|
using var state = new RuntimeCharacterState();
|
||||||
|
var order = new List<string>();
|
||||||
|
state.Spellbook.SpellbookChanged += () => order.Add("local");
|
||||||
|
state.Spellbook.DesiredComponentsChanged += () => order.Add("local");
|
||||||
|
|
||||||
|
Assert.True(state.TryAddFavorite(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
42u,
|
||||||
|
() => order.Add("send")));
|
||||||
|
Assert.Equal(["local", "send"], order);
|
||||||
|
Assert.Equal([42u], state.Spellbook.GetFavorites(0));
|
||||||
|
|
||||||
|
order.Clear();
|
||||||
|
state.SetSpellbookFilter(0x3FFEu, () => order.Add("send"));
|
||||||
|
Assert.Equal(["local", "send"], order);
|
||||||
|
Assert.Equal(0x3FFEu, state.Spellbook.SpellbookFilters);
|
||||||
|
|
||||||
|
order.Clear();
|
||||||
|
Assert.True(state.TrySetDesiredComponent(
|
||||||
|
0x68000001u,
|
||||||
|
0u,
|
||||||
|
() => order.Add("send")));
|
||||||
|
Assert.Equal(["send", "local"], order);
|
||||||
|
Assert.True(state.Spellbook.DesiredComponents.ContainsKey(
|
||||||
|
0x68000001u));
|
||||||
|
Assert.Equal(0u, state.Spellbook.DesiredComponents[0x68000001u]);
|
||||||
|
|
||||||
|
order.Clear();
|
||||||
|
state.ClearDesiredComponents(() => order.Add("send"));
|
||||||
|
Assert.Equal(["send", "local"], order);
|
||||||
|
Assert.Empty(state.Spellbook.DesiredComponents);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(
|
||||||
|
() => state.TrySetDesiredComponent(
|
||||||
|
0x68000002u,
|
||||||
|
10u,
|
||||||
|
() => throw new InvalidOperationException("transport")));
|
||||||
|
Assert.Equal(10u, state.Spellbook.DesiredComponents[0x68000002u]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidSpellbookCommandsDoNotPublishOrMutate()
|
||||||
|
{
|
||||||
|
using var state = new RuntimeCharacterState();
|
||||||
|
int sends = 0;
|
||||||
|
|
||||||
|
Assert.False(state.TryAddFavorite(
|
||||||
|
8,
|
||||||
|
0,
|
||||||
|
42u,
|
||||||
|
() => sends++));
|
||||||
|
Assert.False(state.TryRemoveFavorite(
|
||||||
|
-1,
|
||||||
|
42u,
|
||||||
|
() => sends++));
|
||||||
|
Assert.False(state.TrySetDesiredComponent(
|
||||||
|
0u,
|
||||||
|
1u,
|
||||||
|
() => sends++));
|
||||||
|
Assert.False(state.TrySetDesiredComponent(
|
||||||
|
1u,
|
||||||
|
5001u,
|
||||||
|
() => sends++));
|
||||||
|
|
||||||
|
Assert.Equal(0, sends);
|
||||||
|
Assert.Empty(state.Spellbook.GetFavorites(0));
|
||||||
|
Assert.Empty(state.Spellbook.DesiredComponents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
using AcDream.Core.Chat;
|
||||||
|
using AcDream.Core.Items;
|
||||||
|
using AcDream.Core.Social;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
|
namespace AcDream.Runtime.Tests.Gameplay;
|
||||||
|
|
||||||
|
public sealed class RuntimeGameplayOwnershipTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CombinedLedgerConvergesEveryJ4OwnerAndSubscription()
|
||||||
|
{
|
||||||
|
using var entities = new RuntimeEntityObjectLifetime();
|
||||||
|
var inventory = new RuntimeInventoryState(entities);
|
||||||
|
var character = new RuntimeCharacterState();
|
||||||
|
var communication = new RuntimeCommunicationState();
|
||||||
|
inventory.Shortcuts.Changed += static () => { };
|
||||||
|
inventory.Shortcuts.Load([new ShortcutEntry(1, 2u, 3u)]);
|
||||||
|
inventory.ItemMana.OnQueryItemManaResponse(2u, 0.5f, true);
|
||||||
|
inventory.ExternalContainers.RequestOpen(3u);
|
||||||
|
inventory.ExternalContainers.ApplyViewContents(3u);
|
||||||
|
inventory.Transactions.IncrementBusyCount();
|
||||||
|
|
||||||
|
character.Spellbook.OnSpellLearned(4u);
|
||||||
|
character.Spellbook.SetFavorite(0, 0, 4u);
|
||||||
|
character.Spellbook.SetDesiredComponent(5u, 6u);
|
||||||
|
character.LocalPlayer.OnVitalUpdate(7u, 1u, 2u, 3u, 4u);
|
||||||
|
character.LocalPlayer.OnAttributeUpdate(1u, 2u, 3u, 4u);
|
||||||
|
character.LocalPlayer.OnSkillUpdate(
|
||||||
|
6u, 1u, 2u, 3u, 4u, 5u, 6d, 7u);
|
||||||
|
character.Options.Replace(1u, 2u);
|
||||||
|
character.MovementSkills.Update(3, 4);
|
||||||
|
|
||||||
|
communication.Chat.OnTellReceived(
|
||||||
|
"Sender",
|
||||||
|
"hello",
|
||||||
|
0x50000001u);
|
||||||
|
communication.Chat.OnSelfSent(
|
||||||
|
ChatKind.Tell,
|
||||||
|
"reply",
|
||||||
|
"Recipient");
|
||||||
|
communication.TurbineChat.OnChannelsReceived(
|
||||||
|
1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u);
|
||||||
|
communication.Friends.Apply(new FriendsUpdate(
|
||||||
|
FriendsUpdateType.Full,
|
||||||
|
[new FriendEntry(1u, "Friend", true, false, [], [])]));
|
||||||
|
communication.Squelch.Replace(new SquelchDatabase(
|
||||||
|
new Dictionary<string, uint> { ["account"] = 1u },
|
||||||
|
new Dictionary<uint, SquelchInfo>
|
||||||
|
{
|
||||||
|
[2u] = new SquelchInfo(
|
||||||
|
"Character",
|
||||||
|
false,
|
||||||
|
new HashSet<uint> { 3u }),
|
||||||
|
},
|
||||||
|
new SquelchInfo(
|
||||||
|
string.Empty,
|
||||||
|
false,
|
||||||
|
new HashSet<uint> { 4u })));
|
||||||
|
IDisposable subscription =
|
||||||
|
communication.Events.Subscribe(new NoopObserver());
|
||||||
|
|
||||||
|
RuntimeGameplayOwnershipSnapshot populated =
|
||||||
|
RuntimeGameplayOwnership.Capture(
|
||||||
|
inventory,
|
||||||
|
character,
|
||||||
|
communication);
|
||||||
|
|
||||||
|
Assert.False(populated.IsConverged);
|
||||||
|
Assert.Equal(1, populated.Inventory.ShortcutCount);
|
||||||
|
Assert.Equal(1, populated.Character.LearnedSpellCount);
|
||||||
|
Assert.Equal(1, populated.Communication.StreamSubscriberCount);
|
||||||
|
Assert.True(populated.Communication.HasReplyTarget);
|
||||||
|
Assert.True(populated.Communication.HasRetellTarget);
|
||||||
|
|
||||||
|
communication.Dispose();
|
||||||
|
character.Dispose();
|
||||||
|
inventory.Dispose();
|
||||||
|
subscription.Dispose();
|
||||||
|
|
||||||
|
RuntimeGameplayOwnershipSnapshot retired =
|
||||||
|
RuntimeGameplayOwnership.Capture(
|
||||||
|
inventory,
|
||||||
|
character,
|
||||||
|
communication);
|
||||||
|
|
||||||
|
Assert.True(retired.IsConverged);
|
||||||
|
Assert.Equal(0, retired.Inventory.ShortcutSubscriberCount);
|
||||||
|
Assert.False(retired.Character.InternalSubscriptionsAttached);
|
||||||
|
Assert.Equal(0, retired.Communication.StreamSubscriberCount);
|
||||||
|
Assert.Equal(0, retired.Communication.PendingDispatchCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BorrowerFailuresCannotStrandAnyJ4OwnerDuringTerminalDisposal()
|
||||||
|
{
|
||||||
|
using var entities = new RuntimeEntityObjectLifetime();
|
||||||
|
var inventory = new RuntimeInventoryState(entities);
|
||||||
|
var character = new RuntimeCharacterState();
|
||||||
|
var communication = new RuntimeCommunicationState();
|
||||||
|
|
||||||
|
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
||||||
|
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||||
|
inventory.ExternalContainers.Changed += static _ =>
|
||||||
|
throw new InvalidOperationException("inventory observer");
|
||||||
|
|
||||||
|
character.Spellbook.OnSpellLearned(42u);
|
||||||
|
character.LocalPlayer.OnVitalUpdate(7u, 1u, 2u, 3u, 4u);
|
||||||
|
character.Spellbook.SpellbookChanged += static () =>
|
||||||
|
throw new InvalidOperationException("spellbook observer");
|
||||||
|
character.LocalPlayer.CharacterChanged += static () =>
|
||||||
|
throw new InvalidOperationException("character observer");
|
||||||
|
|
||||||
|
_ = communication.Events.Subscribe(new ThrowingObserver());
|
||||||
|
communication.Chat.OnSystemMessage("failure-isolated event", 0u);
|
||||||
|
Assert.Equal(1, communication.DispatchFailureCount);
|
||||||
|
|
||||||
|
Assert.Throws<AggregateException>(inventory.Dispose);
|
||||||
|
Assert.Throws<AggregateException>(character.Dispose);
|
||||||
|
communication.Dispose();
|
||||||
|
|
||||||
|
RuntimeGameplayOwnershipSnapshot retired =
|
||||||
|
RuntimeGameplayOwnership.Capture(
|
||||||
|
inventory,
|
||||||
|
character,
|
||||||
|
communication);
|
||||||
|
|
||||||
|
Assert.True(retired.IsConverged);
|
||||||
|
Assert.Equal(1, retired.Communication.DispatchFailureCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NoopObserver : IRuntimeCommunicationObserver
|
||||||
|
{
|
||||||
|
public void OnChat(in RuntimeCommunicationEvent delta) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ThrowingObserver : IRuntimeCommunicationObserver
|
||||||
|
{
|
||||||
|
public void OnChat(in RuntimeCommunicationEvent delta) =>
|
||||||
|
throw new InvalidOperationException("communication observer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ public sealed class RuntimeInventoryStateTests
|
||||||
{
|
{
|
||||||
using var entities = new RuntimeEntityObjectLifetime();
|
using var entities = new RuntimeEntityObjectLifetime();
|
||||||
using var inventory = new RuntimeInventoryState(entities);
|
using var inventory = new RuntimeInventoryState(entities);
|
||||||
inventory.Shortcuts.Replace(
|
inventory.Shortcuts.Load(
|
||||||
[
|
[
|
||||||
new AcDream.Core.Items.ShortcutEntry(3, 0x80000001u, 0u),
|
new AcDream.Core.Items.ShortcutEntry(3, 0x80000001u, 0u),
|
||||||
new AcDream.Core.Items.ShortcutEntry(4, 0u, 42u),
|
new AcDream.Core.Items.ShortcutEntry(4, 0u, 42u),
|
||||||
|
|
@ -55,12 +55,12 @@ public sealed class RuntimeInventoryStateTests
|
||||||
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
||||||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||||
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.75f, true);
|
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.75f, true);
|
||||||
inventory.Shortcuts.Replace(shortcuts);
|
inventory.Shortcuts.Load(shortcuts);
|
||||||
|
|
||||||
Assert.Same(entities.Objects, inventory.Objects);
|
Assert.Same(entities.Objects, inventory.Objects);
|
||||||
Assert.Equal(0x70000001u, inventory.ExternalContainers.CurrentContainerId);
|
Assert.Equal(0x70000001u, inventory.ExternalContainers.CurrentContainerId);
|
||||||
Assert.Equal(0.75f, inventory.ItemMana.GetManaPercent(0x50000001u));
|
Assert.Equal(0.75f, inventory.ItemMana.GetManaPercent(0x50000001u));
|
||||||
Assert.Same(shortcuts, inventory.Shortcuts.Items);
|
Assert.Equal(shortcuts, inventory.Shortcuts.Items);
|
||||||
Assert.Equal(1, inventory.Shortcuts.Revision);
|
Assert.Equal(1, inventory.Shortcuts.Revision);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ public sealed class RuntimeInventoryStateTests
|
||||||
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
||||||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||||
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.75f, true);
|
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.75f, true);
|
||||||
inventory.Shortcuts.Replace([new ShortcutEntry(1, 2u, 3u)]);
|
inventory.Shortcuts.Load([new ShortcutEntry(1, 2u, 3u)]);
|
||||||
inventory.Transactions.IncrementBusyCount();
|
inventory.Transactions.IncrementBusyCount();
|
||||||
|
|
||||||
inventory.ResetExternalContainer();
|
inventory.ResetExternalContainer();
|
||||||
|
|
@ -98,7 +98,7 @@ public sealed class RuntimeInventoryStateTests
|
||||||
using var first = new RuntimeInventoryState(firstEntities);
|
using var first = new RuntimeInventoryState(firstEntities);
|
||||||
using var second = new RuntimeInventoryState(secondEntities);
|
using var second = new RuntimeInventoryState(secondEntities);
|
||||||
|
|
||||||
first.Shortcuts.Replace([new ShortcutEntry(1, 2u, 3u)]);
|
first.Shortcuts.Load([new ShortcutEntry(1, 2u, 3u)]);
|
||||||
first.Transactions.IncrementBusyCount();
|
first.Transactions.IncrementBusyCount();
|
||||||
|
|
||||||
Assert.NotSame(first.Objects, second.Objects);
|
Assert.NotSame(first.Objects, second.Objects);
|
||||||
|
|
@ -116,7 +116,7 @@ public sealed class RuntimeInventoryStateTests
|
||||||
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
||||||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||||
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.5f, true);
|
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.5f, true);
|
||||||
inventory.Shortcuts.Replace([new ShortcutEntry(1, 2u, 3u)]);
|
inventory.Shortcuts.Load([new ShortcutEntry(1, 2u, 3u)]);
|
||||||
|
|
||||||
inventory.Dispose();
|
inventory.Dispose();
|
||||||
inventory.Dispose();
|
inventory.Dispose();
|
||||||
|
|
@ -129,7 +129,7 @@ public sealed class RuntimeInventoryStateTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DisposalRetriesATransientOwnerObserverFailureToConvergence()
|
public void DisposalFailureIsReportedAfterTerminalOwnerConvergence()
|
||||||
{
|
{
|
||||||
using var entities = new RuntimeEntityObjectLifetime();
|
using var entities = new RuntimeEntityObjectLifetime();
|
||||||
var inventory = new RuntimeInventoryState(entities);
|
var inventory = new RuntimeInventoryState(entities);
|
||||||
|
|
@ -146,12 +146,72 @@ public sealed class RuntimeInventoryStateTests
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.Throws<AggregateException>(inventory.Dispose);
|
Assert.Throws<AggregateException>(inventory.Dispose);
|
||||||
Assert.False(inventory.IsDisposed);
|
Assert.True(inventory.IsDisposed);
|
||||||
Assert.True(inventory.Transactions.IsDisposed);
|
Assert.True(inventory.Transactions.IsDisposed);
|
||||||
|
inventory.Dispose();
|
||||||
|
|
||||||
|
Assert.True(inventory.CaptureOwnership().IsConverged);
|
||||||
|
Assert.Equal(0u, inventory.ExternalContainers.CurrentContainerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShortcutCommandsUseRetailSendThenLocalOrderOnTheExactOwner()
|
||||||
|
{
|
||||||
|
using var entities = new RuntimeEntityObjectLifetime();
|
||||||
|
using var inventory = new RuntimeInventoryState(entities);
|
||||||
|
var order = new List<string>();
|
||||||
|
inventory.Shortcuts.Changed += () => order.Add("local");
|
||||||
|
var entry = new ShortcutEntry(3, 0x80000001u, 0u);
|
||||||
|
|
||||||
|
Assert.True(inventory.TryAddShortcut(
|
||||||
|
entry,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
Assert.Null(inventory.Shortcuts.GetEntry(3));
|
||||||
|
order.Add("send");
|
||||||
|
}));
|
||||||
|
Assert.Equal(["send", "local"], order);
|
||||||
|
Assert.Equal(entry, inventory.Shortcuts.GetEntry(3));
|
||||||
|
|
||||||
|
order.Clear();
|
||||||
|
Assert.True(inventory.TryRemoveShortcut(
|
||||||
|
3,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
Assert.Equal(entry, inventory.Shortcuts.GetEntry(3));
|
||||||
|
order.Add("send");
|
||||||
|
}));
|
||||||
|
Assert.Equal(["send", "local"], order);
|
||||||
|
Assert.Null(inventory.Shortcuts.GetEntry(3));
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(
|
||||||
|
() => inventory.TryAddShortcut(
|
||||||
|
entry,
|
||||||
|
() => throw new InvalidOperationException("transport")));
|
||||||
|
Assert.Equal(entry, inventory.Shortcuts.GetEntry(3));
|
||||||
|
|
||||||
|
int sends = 0;
|
||||||
|
Assert.False(inventory.TryAddShortcut(
|
||||||
|
new ShortcutEntry(ShortcutStore.SlotCount, 1u, 0u),
|
||||||
|
() => sends++));
|
||||||
|
Assert.False(inventory.TryRemoveShortcut(-1, () => sends++));
|
||||||
|
Assert.Equal(0, sends);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DisposalRetiresShortcutObserversAndTransactionSubscriptions()
|
||||||
|
{
|
||||||
|
using var entities = new RuntimeEntityObjectLifetime();
|
||||||
|
var inventory = new RuntimeInventoryState(entities);
|
||||||
|
inventory.Shortcuts.Changed += static () => { };
|
||||||
|
inventory.Shortcuts.Load([new ShortcutEntry(1, 2u, 3u)]);
|
||||||
|
|
||||||
inventory.Dispose();
|
inventory.Dispose();
|
||||||
|
|
||||||
Assert.True(inventory.IsDisposed);
|
Assert.True(inventory.IsDisposed);
|
||||||
Assert.Equal(0u, inventory.ExternalContainers.CurrentContainerId);
|
Assert.True(inventory.Shortcuts.IsDisposed);
|
||||||
|
Assert.True(inventory.Transactions.IsDisposed);
|
||||||
|
Assert.Equal(0, inventory.Shortcuts.SubscriberCount);
|
||||||
|
Assert.Empty(inventory.Shortcuts.Items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue