feat(ui): D.5.3/B.2 — toolbar reorder/remove via ShortcutStore + wire + green-cross overlay
ToolbarController is now the LIVE drag handler: - OnDragLift: removes the source slot from ShortcutStore, sends RemoveShortcut (0x019D) wire immediately (retail remove-on-lift model). - HandleDropRelease: evicts occupant from target (RemoveShortcut), places dragged item (AddShortcut 0x019C), bumps evicted item into the vacated source slot if empty (swap path); off-bar release leaves the lift's removal standing. - Populate() now lazy-loads ShortcutStore from the PD shortcut list on first call; store is authoritative thereafter. - IsShortcutGuid() uses the store (O(18)) when loaded, falls back to scanning _shortcuts() in the pre-PD window. - All 18 slot cells now get DragAcceptSprite=0x060011FA (green cross, distinct from the inventory ring 0x060011F9). - GameWindow.Bind wired: sendAddShortcut + sendRemoveShortcut lambdas forwarded to _liveSession?.Send*(). - Divergence register: AP-47 Divergence+Risk cells updated (opacity corrected, underlay-backing framing improved). TS-33 row deleted (stopgap retired). Section header 32→31 rows. - Tests: HandleDropRelease_isInertStub removed; 5 new B.2 tests added (lift removes + sends, swap sequence, empty-target place, self-drop re-adds, green-cross sprite). 24/24 ToolbarController tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
41bb70c11a
commit
250f7827be
4 changed files with 179 additions and 38 deletions
|
|
@ -146,11 +146,11 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| AP-42 | `UiMenu` item model is flat (label + opaque payload, single-level popup); retail `UIElement_Menu::MakePopup @0x46d310` supports hierarchical nested submenus via recursive popup chain | `src/AcDream.App/UI/UiMenu.cs` | The chat talk-focus menu is single-level (14 rows, 2 columns, no submenu); hierarchy is latent and unreachable through the chat window — no behavioral difference in the current usage | A future menu with nested submenus would render flat (only the top-level items drawn, no drill-down) | `UIElement_Menu::MakePopup` @0x46d310 |
|
||||
| AP-45 | `PublicUpdatePropertyInt (0x02CE)` sequence byte parsed-past but not honored; last update wins (no freshness check against sequence number) | `src/AcDream.Core.Net/Messages/PublicUpdatePropertyInt.cs` | Loopback ACE rarely reorders; latest-wins matches `PrivateUpdateVital`/`UpdatePosition`'s existing non-sequence behavior. Sequence tracking added when needed alongside TS-26. | A reordered 0x02CE on a real network could apply a stale UiEffects value — item icon temporarily shows the wrong effect state, corrected on next update | `PublicUpdatePropertyInt` sequence byte (ACE GameMessagePublicUpdatePropertyInt) |
|
||||
| AP-46 | Health-meter gate approximation: retail shows the health meter for `IsPlayer() || pet_owner || ClientCombatSystem::ObjectIsAttackable()` (full PK/faction logic); acdream's `GameWindow.IsHealthBarTarget` uses the server PWD bits `BF_ATTACKABLE (0x10)` OR `BF_PLAYER (0x8)` | `src/AcDream.App/Rendering/GameWindow.cs` (`IsHealthBarTarget`) → `SelectedObjectController` | The PWD `BF_ATTACKABLE`/`BF_PLAYER` bits distinguish monsters + players (bar) from friendly/vendor NPCs (name-only) for the M1.5 dev loop; the pet case and the full ObjectIsAttackable PK/faction refinement (free-PK, PK-vs-PK, PKLite) are not ported | A PK/faction edge (e.g. a hostile-flagged player whose `BF_ATTACKABLE` is unset, or a pet) could show/hide the bar where retail differs — no impact on the non-PK PvE dev loop | `ClientCombatSystem::ObjectIsAttackable` acclient_2013_pseudo_c.txt:375385; `BF_ATTACKABLE` acclient.h:6437 |
|
||||
| AP-47 | Cursor drag ghost reuses the full composited `m_pIcon` at reduced alpha (`GhostAlpha=0.6`) instead of retail's dedicated `m_pDragIcon` (base + custom-overlay, NO type-default underlay) | `src/AcDream.App/UI/UiRoot.cs` (`DrawDragGhost`/`GhostAlpha`) → `src/AcDream.App/UI/UiItemSlot.cs` (`GetDragGhost`) | Cosmetic only — the dragged item is still unambiguously identifiable; building the second underlay-less composite is deferred polish; the ghost is item-agnostic in UiRoot via `GetDragGhost()` | The ghost shows the opaque type-default underlay backing rather than retail's underlay-less translucent copy — a subtle look difference while dragging, no functional effect | `IconData::RenderIcons` acclient_2013_pseudo_c.txt:407594-407625 (m_pDragIcon path); deep-dive §3.2/§5.5 |
|
||||
| AP-47 | Cursor drag ghost reuses the full composited `m_pIcon` (incl. type-default underlay) instead of retail's dedicated `m_pDragIcon` (base + custom-overlay, NO type-default underlay). Opacity now matches retail (full). | `src/AcDream.App/UI/UiRoot.cs` (`DrawDragGhost`/`GhostAlpha`) → `src/AcDream.App/UI/UiItemSlot.cs` (`GetDragGhost`) | Cosmetic only — the dragged item is still unambiguously identifiable; building the second underlay-less composite is deferred polish; the ghost is item-agnostic in UiRoot via `GetDragGhost()` | The ghost carries the opaque type-default underlay backing rather than retail's underlay-less copy — a subtle look difference while dragging, no functional effect. | `IconData::RenderIcons` acclient_2013_pseudo_c.txt:407594-407625 (m_pDragIcon path); deep-dive §3.2/§5.5 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Temporary stopgap (TS) — 32 rows
|
||||
## 4. Temporary stopgap (TS) — 31 rows
|
||||
|
||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
@ -186,7 +186,6 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| TS-30 | Numbered chat tabs (element ids `0x10000522`–`0x10000525`) render as clickable buttons but do not switch channel filter or affect the transcript — tab state is a no-op | `src/AcDream.App/UI/Layout/ChatWindowController.cs:210` | Retail's tab switching routes transcript lines by chat channel (`gmMainChatUI::gmScrollWindow` sub-windows per tab); the tab wiring is D.5 scope | Tab clicks produce no visible transcript change; retail would filter to the selected channel — all chat always shows in all tabs | `gmMainChatUI::PostInit` tab setup @0x4ce2a0; holtburger chat tab handling |
|
||||
| TS-31 | Squelch toggle absent (no `/squelch` slash command, no clickable name-tags to silence); retail's squelch list filters incoming chat lines | `src/AcDream.Core/Chat/ChatLog.cs` | Squelch is a social / moderation feature deferred to post-M1.5; the data structure (`ChatLog`) has no squelch set today | Any player can spam all clients; clickable-name-tag contextual menu (used in retail to squelch, tell, add-to-friends) is absent | `ChatFilter::IsSquelched`; retail right-click player name → Squelch menu |
|
||||
| TS-32 | `ClientObjectTable` has no pre-queue for a child `CreateObject` that arrives before its parent (out-of-order PARENTED create); such objects are ingested as root objects and their `ContainerId` links a not-yet-known container. Retail's `null_object_table` + `null_weenie_object_table` hold unresolvable objects until the parent arrives | `src/AcDream.Core/Items/ClientObjectTable.cs` (`Ingest`) | PD↔`CreateObject` ordering is handled (upsert semantics); out-of-order PARENTED creates are observed only at high packet loss or in vendor/corpse multi-object bursts on non-loopback links; deferred to D.5.5+ | A container's child object arriving before the container is ingested as a root item — it won't appear in `GetContents` until the next `RecordMembership` or a move event corrects the parent link | `CObjectMaint::null_object_table` / `null_weenie_object_table` (acclient.h / named-retail pc) |
|
||||
| TS-33 | Toolbar `IItemListDragHandler.HandleDropRelease` is a logging stub — no `AddShortcut 0x019C` / `RemoveShortcut 0x019D` wire, no mutable `ShortcutStore`; a drop onto the bar logs and does nothing | `src/AcDream.App/UI/Layout/ToolbarController.cs` (`HandleDropRelease`) | The B.1 spine ships the drag machine (payload/ghost/overlay/dispatch) independent of the wire; the shortcut store + add/remove/reorder sends are Stream B.2, which needs the inventory window as a drag source too | A player dragging onto/within the hotbar sees the ghost + accept overlay but the drop is inert until B.2 — add/remove/reorder don't persist | `gmToolbarUI::HandleDropRelease` acclient_2013_pseudo_c.txt:197971; `AddShortcut`/`RemoveShortcut` 0x019C/0x019D |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -2018,7 +2018,9 @@ public sealed class GameWindow : IDisposable
|
|||
combatState: Combat,
|
||||
peaceDigits: toolbarPeaceDigits,
|
||||
warDigits: toolbarWarDigits,
|
||||
emptyDigits: toolbarEmptyDigits);
|
||||
emptyDigits: toolbarEmptyDigits,
|
||||
sendAddShortcut: (i, g) => _liveSession?.SendAddShortcut(i, g),
|
||||
sendRemoveShortcut: i => _liveSession?.SendRemoveShortcut(i));
|
||||
|
||||
// Phase D.5.3a — selected-object strip (name, overlay state, health meter).
|
||||
// Analogue of retail gmToolbarUI::HandleSelectionChanged
|
||||
|
|
|
|||
|
|
@ -60,6 +60,10 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
private readonly Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> _shortcuts;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex
|
||||
private readonly Action<uint> _useItem; // guid → fire UseObject
|
||||
private readonly AcDream.Core.Net.Items.ShortcutStore _store = new();
|
||||
private bool _storeLoaded;
|
||||
private readonly Action<uint, uint>? _sendAddShortcut; // (index, objectGuid)
|
||||
private readonly Action<uint>? _sendRemoveShortcut; // (index)
|
||||
|
||||
// Digit sprite DID arrays for slot labels (top row, numbers 1-9).
|
||||
// Read from LayoutDesc 0x21000037, element 0x1000034A under composite 0x10000346.
|
||||
|
|
@ -82,7 +86,9 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
CombatState? combatState,
|
||||
uint[]? peaceDigits,
|
||||
uint[]? warDigits,
|
||||
uint[]? emptyDigits)
|
||||
uint[]? emptyDigits,
|
||||
Action<uint, uint>? sendAddShortcut = null,
|
||||
Action<uint>? sendRemoveShortcut = null)
|
||||
{
|
||||
_repo = repo;
|
||||
_shortcuts = shortcuts;
|
||||
|
|
@ -91,6 +97,8 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
_peaceDigits = peaceDigits;
|
||||
_warDigits = warDigits;
|
||||
_emptyDigits = emptyDigits;
|
||||
_sendAddShortcut = sendAddShortcut;
|
||||
_sendRemoveShortcut = sendRemoveShortcut;
|
||||
|
||||
for (int i = 0; i < SlotIds.Length; i++)
|
||||
{
|
||||
|
|
@ -104,6 +112,7 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
list.RegisterDragHandler(this);
|
||||
list.Cell.SlotIndex = i;
|
||||
list.Cell.SourceKind = ItemDragSource.ShortcutBar;
|
||||
list.Cell.DragAcceptSprite = 0x060011FAu; // green cross (toolbar), not the ring 0x060011F9 (inventory)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,11 +142,20 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.Net.Items.ShortcutStore"/>).
|
||||
/// Used to gate repo-event subscriptions so we don't re-populate on every creature spawn.
|
||||
/// Falls back to scanning <c>_shortcuts()</c> before the store is loaded (pre-PD window).
|
||||
/// </summary>
|
||||
private bool IsShortcutGuid(uint guid)
|
||||
{
|
||||
if (_storeLoaded)
|
||||
{
|
||||
for (int s = 0; s < AcDream.Core.Net.Items.ShortcutStore.SlotCount; s++)
|
||||
if (_store.Get(s) == guid) return true;
|
||||
return false;
|
||||
}
|
||||
// Store not yet loaded — fall back to the shortcuts provider (pre-PD window).
|
||||
foreach (var sc in _shortcuts())
|
||||
if (sc.ObjectGuid == guid) return true;
|
||||
return false;
|
||||
|
|
@ -182,10 +200,13 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
CombatState? combatState = null,
|
||||
uint[]? peaceDigits = null,
|
||||
uint[]? warDigits = null,
|
||||
uint[]? emptyDigits = null)
|
||||
uint[]? emptyDigits = null,
|
||||
Action<uint, uint>? sendAddShortcut = null,
|
||||
Action<uint>? sendRemoveShortcut = null)
|
||||
{
|
||||
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
|
||||
peaceDigits, warDigits, emptyDigits);
|
||||
peaceDigits, warDigits, emptyDigits,
|
||||
sendAddShortcut, sendRemoveShortcut);
|
||||
c.Populate();
|
||||
return c;
|
||||
}
|
||||
|
|
@ -196,24 +217,27 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
/// Entries whose item is not yet in the repo are silently skipped here; the
|
||||
/// <c>ObjectAdded</c> event re-fires this method when the item arrives
|
||||
/// (matching retail's <c>SetDelayedShortcutNum</c> deferred-rebind path).
|
||||
/// As of B.2: the <see cref="AcDream.Core.Net.Items.ShortcutStore"/> is the
|
||||
/// authoritative in-session slot map; <c>_shortcuts()</c> seeds it on first call.
|
||||
/// </summary>
|
||||
public void Populate()
|
||||
{
|
||||
// Clear all slot cells first (flush).
|
||||
// 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 sc in _shortcuts())
|
||||
for (int slot = 0; slot < _slots.Length; slot++)
|
||||
{
|
||||
if (sc.ObjectGuid == 0) continue; // spell-only shortcut — inventory phase
|
||||
if (sc.Index >= (uint)_slots.Length) continue;
|
||||
var list = _slots[(int)sc.Index];
|
||||
uint guid = _store.Get(slot);
|
||||
if (guid == 0) continue;
|
||||
var list = _slots[slot];
|
||||
if (list is null) continue;
|
||||
|
||||
var item = _repo.Get(sc.ObjectGuid);
|
||||
if (item is null) continue; // deferred: ObjectAdded will re-call Populate
|
||||
|
||||
var item = _repo.Get(guid);
|
||||
if (item is null) continue; // deferred: ObjectAdded re-calls Populate
|
||||
uint tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
|
||||
list.Cell.SetItem(sc.ObjectGuid, tex);
|
||||
list.Cell.SetItem(guid, tex);
|
||||
}
|
||||
|
||||
// Re-stamp slot number labels after any item change.
|
||||
|
|
@ -296,13 +320,24 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
};
|
||||
}
|
||||
|
||||
// ── IItemListDragHandler (B.1 spine) ─────────────────────────────────────
|
||||
// ── IItemListDragHandler (B.2 live handler) ──────────────────────────────
|
||||
// Retail: gmToolbarUI is the m_dragHandler for every shortcut slot list.
|
||||
// The accept/reject gate (IsShortcutEligible) and the AddShortcut/RemoveShortcut
|
||||
// wire are Stream B.2; this stub accepts any real item and LOGS the drop (TS-33).
|
||||
// Retail model (remove-on-lift / place-on-drop / no-restore):
|
||||
// lift → RemoveShortcut (0x019D) + store.Remove (slot empties immediately)
|
||||
// drop → AddShortcut (0x019C) + optional swap of evicted item into source
|
||||
// off-bar release → the lift's removal stands (no restore)
|
||||
// Retail ref: gmToolbarUI::HandleDropRelease acclient_2013_pseudo_c.txt:197971
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload) { /* Task 3: remove + wire */ }
|
||||
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload)
|
||||
{
|
||||
// Retail RecvNotice_ItemListBeginDrag → RemoveShortcut (0x004bd930/0x004bd450): the lifted
|
||||
// shortcut leaves the bar (+ wire) the instant the drag starts; it's re-placed only on a
|
||||
// drop onto a slot. Off-bar release leaves it removed.
|
||||
_store.Remove(payload.SourceSlot);
|
||||
_sendRemoveShortcut?.Invoke((uint)payload.SourceSlot);
|
||||
Populate();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||
|
|
@ -311,9 +346,19 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
/// <inheritdoc/>
|
||||
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||
{
|
||||
// TS-33: no wire yet. Stream B.2 replaces this with the ShortcutStore mutation +
|
||||
// AddShortcut 0x019C / RemoveShortcut 0x019D sends (gmToolbarUI::HandleDropRelease :197971).
|
||||
Console.WriteLine($"[B.2 TODO] drop obj 0x{payload.ObjId:X8} from {payload.SourceKind} " +
|
||||
$"slot {payload.SourceSlot} → toolbar slot {targetCell.SlotIndex}");
|
||||
// Retail gmToolbarUI::HandleDropRelease (0x004be7c0) within-bar reorder branch (deep-dive §5):
|
||||
// evict the target's occupant, place the dragged item there, and bump the evicted item into
|
||||
// the (now-vacated) source slot if it's free.
|
||||
int target = targetCell.SlotIndex;
|
||||
uint evicted = _store.Get(target); // RemoveShortcutInSlotNum(target)
|
||||
if (evicted != 0) { _store.Remove(target); _sendRemoveShortcut?.Invoke((uint)target); }
|
||||
_store.Set(target, payload.ObjId); // AddShortcut(dragged, target)
|
||||
_sendAddShortcut?.Invoke((uint)target, payload.ObjId);
|
||||
if (evicted != 0 && evicted != payload.ObjId && _store.IsEmpty(payload.SourceSlot))
|
||||
{ // displaced → vacated source slot
|
||||
_store.Set(payload.SourceSlot, evicted);
|
||||
_sendAddShortcut?.Invoke((uint)payload.SourceSlot, evicted);
|
||||
}
|
||||
Populate();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -480,21 +480,116 @@ public class ToolbarControllerTests
|
|||
Assert.False(ctrl.OnDragOver(list, list.Cell, payload));
|
||||
}
|
||||
|
||||
/// <summary>HandleDropRelease is a logging stub (TS-33): it must not throw and must not
|
||||
/// mutate the target slot (no wire / no store yet).</summary>
|
||||
// ── B.2: live drag handler (store + reorder/remove + wire) ───────────────
|
||||
private static (System.Collections.Generic.List<(uint i,uint g)> adds,
|
||||
System.Collections.Generic.List<uint> removes) NewSpies(out System.Action<uint,uint> add, out System.Action<uint> rem)
|
||||
{
|
||||
var adds = new System.Collections.Generic.List<(uint,uint)>();
|
||||
var removes = new System.Collections.Generic.List<uint>();
|
||||
add = (i, g) => adds.Add((i, g));
|
||||
rem = i => removes.Add(i);
|
||||
return (adds, removes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandleDropRelease_isInertStub()
|
||||
public void OnDragLift_removesSourceSlot_sendsRemove_emptiesCell()
|
||||
{
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
var ctrl = ToolbarController.Bind(layout, new ClientObjectTable(),
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
|
||||
var shortcuts = new System.Collections.Generic.List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 3, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
|
||||
var (adds, removes) = NewSpies(out var add, out var rem);
|
||||
|
||||
var list = slots[Row1[2]];
|
||||
list.Cell.SetItem(0x5001u, 0x77u); // populate so "unchanged" is a meaningful assertion
|
||||
var payload = new ItemDragPayload(0x5001u, ItemDragSource.ShortcutBar, 0, new UiItemSlot());
|
||||
var ex = Record.Exception(() => ctrl.HandleDropRelease(list, list.Cell, payload));
|
||||
Assert.Null(ex);
|
||||
Assert.Equal(0x5001u, list.Cell.ItemId); // unchanged — inert stub did not clear/mutate
|
||||
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
|
||||
sendAddShortcut: add, sendRemoveShortcut: rem);
|
||||
Assert.Equal(0x5001u, slots[Row1[3]].Cell.ItemId);
|
||||
|
||||
var payload = new ItemDragPayload(0x5001u, ItemDragSource.ShortcutBar, 3, slots[Row1[3]].Cell);
|
||||
ctrl.OnDragLift(slots[Row1[3]], slots[Row1[3]].Cell, payload);
|
||||
|
||||
Assert.Contains(3u, removes);
|
||||
Assert.Equal(0u, slots[Row1[3]].Cell.ItemId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandleDropRelease_ontoOccupied_swaps_andSendsRetailSequence()
|
||||
{
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u }); // A
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5002u, WeenieClassId = 1u, IconId = 0x06005678u }); // B
|
||||
var shortcuts = new System.Collections.Generic.List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 3, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0),
|
||||
new(Index: 5, ObjectGuid: 0x5002u, SpellId: 0, Layer: 0) };
|
||||
var (adds, removes) = NewSpies(out var add, out var rem);
|
||||
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
|
||||
sendAddShortcut: add, sendRemoveShortcut: rem);
|
||||
|
||||
var payload = new ItemDragPayload(0x5001u, ItemDragSource.ShortcutBar, 3, slots[Row1[3]].Cell);
|
||||
ctrl.OnDragLift(slots[Row1[3]], slots[Row1[3]].Cell, payload);
|
||||
ctrl.HandleDropRelease(slots[Row1[5]], slots[Row1[5]].Cell, payload);
|
||||
|
||||
Assert.Equal(0x5001u, slots[Row1[5]].Cell.ItemId);
|
||||
Assert.Equal(0x5002u, slots[Row1[3]].Cell.ItemId);
|
||||
Assert.Equal(new[] { 3u, 5u }, removes.ToArray());
|
||||
Assert.Contains((5u, 0x5001u), adds);
|
||||
Assert.Contains((3u, 0x5002u), adds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandleDropRelease_ontoEmpty_placesOnly_sourceStaysEmpty()
|
||||
{
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
|
||||
var shortcuts = new System.Collections.Generic.List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 3, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
|
||||
var (adds, removes) = NewSpies(out var add, out var rem);
|
||||
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
|
||||
sendAddShortcut: add, sendRemoveShortcut: rem);
|
||||
|
||||
var payload = new ItemDragPayload(0x5001u, ItemDragSource.ShortcutBar, 3, slots[Row1[3]].Cell);
|
||||
ctrl.OnDragLift(slots[Row1[3]], slots[Row1[3]].Cell, payload);
|
||||
ctrl.HandleDropRelease(slots[Row1[7]], slots[Row1[7]].Cell, payload);
|
||||
|
||||
Assert.Equal(0x5001u, slots[Row1[7]].Cell.ItemId);
|
||||
Assert.Equal(0u, slots[Row1[3]].Cell.ItemId);
|
||||
Assert.Contains((7u, 0x5001u), adds);
|
||||
Assert.DoesNotContain(adds, a => a.i == 3u);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandleDropRelease_ontoSelf_reAddsToSource()
|
||||
{
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
|
||||
var shortcuts = new System.Collections.Generic.List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 3, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
|
||||
var (adds, removes) = NewSpies(out var add, out var rem);
|
||||
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
|
||||
sendAddShortcut: add, sendRemoveShortcut: rem);
|
||||
|
||||
var payload = new ItemDragPayload(0x5001u, ItemDragSource.ShortcutBar, 3, slots[Row1[3]].Cell);
|
||||
ctrl.OnDragLift(slots[Row1[3]], slots[Row1[3]].Cell, payload); // slot 3 emptied
|
||||
ctrl.HandleDropRelease(slots[Row1[3]], slots[Row1[3]].Cell, payload); // drop back on slot 3
|
||||
|
||||
Assert.Equal(0x5001u, slots[Row1[3]].Cell.ItemId); // re-added to source (net no-op)
|
||||
Assert.Contains((3u, 0x5001u), adds); // AddShortcut(3, A) sent
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToolbarSlots_useGreenCrossAcceptSprite()
|
||||
{
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
ToolbarController.Bind(layout, new ClientObjectTable(),
|
||||
() => System.Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||
Assert.Equal(0x060011FAu, slots[Row1[0]].Cell.DragAcceptSprite); // green cross, not the ring F9
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue