using System; using System.Collections.Generic; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Combat; using AcDream.Core.Items; using AcDream.Core.Net.Messages; using Xunit; namespace AcDream.App.Tests.UI.Layout; public class ToolbarControllerTests { private static readonly uint[] Row1 = { 0x100001A7,0x100001A8,0x100001A9,0x100001AA,0x100001AB,0x100001AC,0x100001AD,0x100001AE,0x100001AF }; private static readonly uint[] Row2 = { 0x100006B7,0x100006B8,0x100006B9,0x100006BA,0x100006BB,0x100006BC,0x100006BD,0x100006BE,0x100006BF }; // The four mutually-exclusive combat-mode indicator element ids (must match ToolbarController's list). private static readonly uint[] CombatIds = { 0x10000192u, 0x10000193u, 0x10000194u, 0x10000195u }; private const uint CharacterButtonId = 0x10000199u; private const uint InventoryButtonId = 0x100001B1u; private const uint UseButtonId = 0x1000019Du; private const uint ExamineButtonId = 0x100001A5u; private static readonly (uint ElementId, uint PanelId)[] PanelButtons = { (0x10000197u, 12u), (0x10000198u, 13u), (CharacterButtonId, RetailPanelCatalog.Character), (0x1000055Au, 25u), (0x1000019Au, 16u), (0x1000019Bu, 10u), (InventoryButtonId, RetailPanelCatalog.Inventory), }; private static (ImportedLayout layout, Dictionary slots, Dictionary indicators) FakeToolbar() { var dict = new Dictionary(); var slots = new Dictionary(); var indicators = new Dictionary(); var root = new UiPanel(); foreach (var id in Row1) AddSlot(id); foreach (var id in Row2) AddSlot(id); // Combat indicators are retail UiButtons; one is visible at a time, // and every stance-specific instance dispatches the same toggle command. foreach (var id in CombatIds) { AddButton(id); indicators[id] = dict[id]; } foreach (var (elementId, panelId) in PanelButtons) AddButton(elementId, panelId); AddButton(UseButtonId); AddButton(ExamineButtonId); return (new ImportedLayout(root, dict), slots, indicators); void AddSlot(uint id) { var list = new UiItemList(_ => (0u, 0, 0)) { Width = 32, Height = 32 }; dict[id] = list; slots[id] = list; root.AddChild(list); } void AddButton(uint id, uint? panelId = null) { var info = new ElementInfo { Id = id, Type = 1, Width = 32, Height = 32, DefaultStateName = "Normal", }; info.StateMedia["Normal"] = (0x1u, 1); info.StateMedia["Highlight"] = (0x2u, 1); info.StateMedia["Ghosted"] = (0x3u, 1); if (panelId is { } value) { info.States[UiStateInfo.DirectStateId] = new UiStateInfo { Id = UiStateInfo.DirectStateId, Properties = new UiPropertyBag { Values = { [0x10000029u] = new UiPropertyValue { Kind = UiPropertyKind.Enum, UnsignedValue = value, }, }, }, }; } var button = new UiButton(info, _ => (0u, 0, 0)) { Width = 32, Height = 32 }; dict[id] = button; root.AddChild(button); } } [Fact] public void Populate_bindsShortcutToCorrectSlot() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u }); var shortcuts = new List { new(Index: 0, ObjectId: 0x5001u, SpellId: 0) }; ToolbarController.Bind(layout, repo, () => shortcuts, iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { }); Assert.Equal(0x5001u, slots[Row1[0]].Cell.ItemId); Assert.Equal(0x77u, slots[Row1[0]].Cell.IconTexture); Assert.Equal(0u, slots[Row1[1]].Cell.ItemId); // others empty } [Fact] public void SessionTableClearRemovesResolvedShortcutPresentation() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u, }); var shortcuts = new List { new(Index: 0, ObjectId: 0x5001u, SpellId: 0), }; ToolbarController.Bind( layout, repo, () => shortcuts, iconIds: (_, _, _, _, _) => 0x77u, useItem: _ => { }); Assert.Equal(0x5001u, slots[Row1[0]].Cell.ItemId); repo.Clear(); Assert.Equal(0u, slots[Row1[0]].Cell.ItemId); } [Fact] public void DeferredRebind_whenItemArrivesLate() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); // item NOT present yet var shortcuts = new List { new(Index: 2, ObjectId: 0x5002u, SpellId: 0) }; ToolbarController.Bind(layout, repo, () => shortcuts, iconIds: (_,_,_,_,_) => 0x88u, useItem: _ => { }); Assert.Equal(0u, slots[Row1[2]].Cell.ItemId); // not bound yet repo.AddOrUpdate(new ClientObject { ObjectId = 0x5002u, WeenieClassId = 1u, IconId = 0x06005678u }); Assert.Equal(0x5002u, slots[Row1[2]].Cell.ItemId); // rebound on ItemAdded } [Fact] public void Dispose_UnsubscribesDeferredRebind_AndIsIdempotent() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); var shortcuts = new List { new(Index: 2, ObjectId: 0x5002u, SpellId: 0) }; var controller = ToolbarController.Bind(layout, repo, () => shortcuts, iconIds: (_,_,_,_,_) => 0x88u, useItem: _ => { }); controller.Dispose(); controller.Dispose(); repo.AddOrUpdate(new ClientObject { ObjectId = 0x5002u, WeenieClassId = 1u, IconId = 0x06005678u, }); Assert.Equal(0u, slots[Row1[2]].Cell.ItemId); } [Fact] public void Click_emitsUseForBoundItem() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u }); var shortcuts = new List { new(Index: 0, ObjectId: 0x5001u, SpellId: 0) }; uint used = 0; ToolbarController.Bind(layout, repo, () => shortcuts, iconIds: (_,_,_,_,_) => 0x77u, useItem: g => used = g); // Use now fires on Click (mouse-up), not MouseDown — drag/click disambiguation. slots[Row1[0]].Cell.OnEvent(new UiEvent(0u, null, UiEventType.Click)); Assert.Equal(0x5001u, used); } [Fact] public void PanelButtons_bindByDatPanelId_andGhostUnavailablePanels() { var (layout, _, _) = FakeToolbar(); var repo = new ClientObjectTable(); var toggles = new List(); var ctrl = ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); ctrl.BindPanelButtons( panelId => panelId is RetailPanelCatalog.Inventory or RetailPanelCatalog.Character, toggles.Add); ((UiButton)layout.FindElement(InventoryButtonId)!).OnEvent(new UiEvent(0u, null, UiEventType.Click)); ((UiButton)layout.FindElement(CharacterButtonId)!).OnEvent(new UiEvent(0u, null, UiEventType.Click)); Assert.Equal(new[] { RetailPanelCatalog.Inventory, RetailPanelCatalog.Character }, toggles); foreach (var (elementId, panelId) in PanelButtons) { var button = (UiButton)layout.FindElement(elementId)!; Assert.Equal( panelId is RetailPanelCatalog.Inventory or RetailPanelCatalog.Character, button.Enabled); } } [Fact] public void InventoryButton_whenTargetModeActive_targetsPlayerInsteadOfTogglingInventory() { const uint player = 0x50000001u; const uint pack = 0x50000010u; const uint kit = 0x50000A01u; var (layout, _, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature }); repo.AddOrUpdate(new ClientObject { ObjectId = pack, Type = ItemType.Container, ItemsCapacity = 24 }); repo.MoveItem(pack, player, 0); repo.AddOrUpdate(new ClientObject { ObjectId = kit, Type = ItemType.Misc, Useability = 0x00220008u, // USEABLE_SOURCE_CONTAINED_TARGET_REMOTE_OR_SELF TargetType = (uint)ItemType.Creature, // retail kind gate }); repo.MoveItem(kit, pack, 0); var useWithTarget = new List<(uint Source, uint Target)>(); var interaction = new ItemInteractionController( repo, playerGuid: () => player, sendUse: null, sendUseWithTarget: (source, target) => useWithTarget.Add((source, target)), sendWield: null, sendDrop: null, nowMs: () => 1_000); int inventoryClicks = 0; var ctrl = ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, itemInteraction: interaction); ctrl.BindPanelButtons( panelId => panelId == RetailPanelCatalog.Inventory, _ => inventoryClicks++); interaction.ActivateItem(kit); ((UiButton)layout.FindElement(InventoryButtonId)!).OnEvent(new UiEvent(0u, null, UiEventType.Click)); Assert.Equal(0, inventoryClicks); Assert.Equal(new[] { (kit, player) }, useWithTarget); Assert.False(interaction.IsTargetModeActive); } [Fact] public void InventoryButton_freshItemDrop_acceptsAndPlacesInPlayerBackpack() { const uint player = 0x5000u; const uint item = 0x5001u; var (layout, _, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature }); repo.AddOrUpdate(new ClientObject { ObjectId = item, Type = ItemType.Misc }); var puts = new List<(uint Item, uint Container, int Placement)>(); ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, playerGuid: () => player, sendPutItemInContainer: (i, c, p) => puts.Add((i, c, p))); var button = (UiButton)layout.FindElement(InventoryButtonId)!; var payload = new ItemDragPayload(item, ItemDragSource.Inventory, 12, new UiItemSlot()); Assert.True(button.OnEvent(new UiEvent(0u, button, UiEventType.DragEnter, Payload: payload))); Assert.Equal(ItemDragAcceptance.Accept, button.ItemDragAcceptanceForTest); Assert.Equal(0x060011F7u, button.ItemDragAcceptSprite); Assert.True(button.OnEvent(new UiEvent(0u, button, UiEventType.DropReleased, Payload: payload))); Assert.Equal(ItemDragAcceptance.None, button.ItemDragAcceptanceForTest); Assert.Equal(new[] { (item, player, 0) }, puts); } [Fact] public void InventoryButton_shortcutAliasDrop_staysNeutralAndDoesNotMovePhysicalItem() { const uint player = 0x5000u; const uint item = 0x5001u; var (layout, _, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = item, Type = ItemType.Misc }); var puts = new List<(uint Item, uint Container, int Placement)>(); ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, playerGuid: () => player, sendPutItemInContainer: (i, c, p) => puts.Add((i, c, p))); var button = (UiButton)layout.FindElement(InventoryButtonId)!; var payload = new ItemDragPayload(item, ItemDragSource.ShortcutBar, 3, new UiItemSlot()); button.OnEvent(new UiEvent(0u, button, UiEventType.DragEnter, Payload: payload)); Assert.Equal(ItemDragAcceptance.None, button.ItemDragAcceptanceForTest); button.OnEvent(new UiEvent(0u, button, UiEventType.DropReleased, Payload: payload)); Assert.Empty(puts); } [Fact] public void WindowButtonState_highlightsWhenOpen() { var (layout, _, _) = FakeToolbar(); var repo = new ClientObjectTable(); var ctrl = ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); var inventoryButton = (UiButton)layout.FindElement(InventoryButtonId)!; var characterButton = (UiButton)layout.FindElement(CharacterButtonId)!; ctrl.BindPanelButtons(_ => true, _ => { }); ctrl.SetPanelOpen(RetailPanelCatalog.Inventory, true); ctrl.SetPanelOpen(RetailPanelCatalog.Character, true); Assert.Equal("Highlight", inventoryButton.ActiveState); Assert.Equal("Highlight", characterButton.ActiveState); ctrl.SetPanelOpen(RetailPanelCatalog.Inventory, false); ctrl.SetPanelOpen(RetailPanelCatalog.Character, false); Assert.Equal("Normal", inventoryButton.ActiveState); Assert.Equal("Normal", characterButton.ActiveState); } [Fact] public void RetailFixture_panelButtonsExposeExactDatPanelIds() { ImportedLayout layout = FixtureLoader.LoadToolbar(); foreach (var (elementId, panelId) in PanelButtons) { var button = Assert.IsType(layout.FindElement(elementId)); Assert.True(button.TryGetEnumAttribute(0x10000029u, out uint actual)); Assert.Equal(panelId, actual); } } [Fact] public void RetailFixture_ammoNumberUsesAuthoredMissileIndicatorElement() { ImportedLayout layout = FixtureLoader.LoadToolbar(); var ammoIndicator = Assert.IsType(layout.FindElement(0x10000194u)); Assert.Equal(55f, ammoIndicator.Width); Assert.Equal(58f, ammoIndicator.Height); } [Fact] public void UseAndExamineButtons_dispatchSelectedObjectPolicy() { const uint player = 0x50000001u; const uint pack = 0x50000002u; const uint item = 0x50000003u; var (layout, _, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature }); repo.AddOrUpdate(new ClientObject { ObjectId = pack, Type = ItemType.Container }); repo.MoveItem(pack, player, 0); repo.AddOrUpdate(new ClientObject { ObjectId = item, Type = ItemType.Misc, Useability = ItemUseability.Contained, }); repo.MoveItem(item, pack, 0); var uses = new List(); var examines = new List(); uint selected = item; var interaction = new ItemInteractionController( repo, () => player, sendUse: uses.Add, sendUseWithTarget: null, sendWield: null, sendDrop: null, sendExamine: examines.Add); ToolbarController.Bind( layout, repo, () => Array.Empty(), iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, itemInteraction: interaction, selectedObjectId: () => selected); ((UiButton)layout.FindElement(UseButtonId)!).OnClick!.Invoke(); ((UiButton)layout.FindElement(ExamineButtonId)!).OnClick!.Invoke(); Assert.Equal(new[] { item }, uses); Assert.Equal(new[] { item }, examines); selected = 0; ((UiButton)layout.FindElement(ExamineButtonId)!).OnClick!.Invoke(); Assert.Equal(InteractionModeKind.Examine, interaction.InteractionState.Current.Kind); } // ── C1: combat-mode indicator tests ───────────────────────────────────── [Fact] public void AmmoNumber_usesStackableMissileWeapon_andTracksStackChanges() { const uint player = 0x50000001u; const uint thrownWeapon = 0x50000002u; const uint separateAmmo = 0x50000003u; var (layout, _, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature }); repo.AddOrUpdate(new ClientObject { ObjectId = thrownWeapon, StackSize = 7, StackSizeMax = 25, }); repo.MoveItem(thrownWeapon, player, newEquipLocation: EquipMask.MissileWeapon); repo.AddOrUpdate(new ClientObject { ObjectId = separateAmmo, StackSize = 80, StackSizeMax = 100, }); repo.MoveItem(separateAmmo, player, newEquipLocation: EquipMask.MissileAmmo); ToolbarController.Bind( layout, repo, () => Array.Empty(), iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, playerGuid: () => player); var ammoButton = (UiButton)layout.FindElement(0x10000194u)!; Assert.Equal("7", ammoButton.Label); Assert.True(repo.UpdateStackSize(thrownWeapon, 6, value: 0)); Assert.Equal("6", ammoButton.Label); } [Fact] public void AmmoNumber_forLauncher_usesAmmoSlot_andClearsOnUnwield() { const uint player = 0x50000001u; const uint pack = 0x50000010u; const uint bow = 0x50000002u; const uint arrows = 0x50000003u; var (layout, _, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature }); repo.AddOrUpdate(new ClientObject { ObjectId = pack, Type = ItemType.Container }); repo.AddOrUpdate(new ClientObject { ObjectId = bow, StackSize = 1, StackSizeMax = 1 }); repo.MoveItem(bow, player, newEquipLocation: EquipMask.MissileWeapon); repo.AddOrUpdate(new ClientObject { ObjectId = arrows, StackSize = 42, StackSizeMax = 100 }); repo.MoveItem(arrows, player, newEquipLocation: EquipMask.MissileAmmo); ToolbarController.Bind( layout, repo, () => Array.Empty(), iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, playerGuid: () => player); var ammoButton = (UiButton)layout.FindElement(0x10000194u)!; Assert.Equal("42", ammoButton.Label); Assert.True(repo.MoveItem(arrows, pack)); Assert.Null(ammoButton.Label); } [Fact] public void CombatIndicators_allDispatchSharedRetailToggleCommand() { var (layout, _, indicators) = FakeToolbar(); var repo = new ClientObjectTable(); int toggles = 0; ToolbarController.Bind( layout, repo, () => Array.Empty(), iconIds: (_, _, _, _, _) => 0u, useItem: _ => { }, toggleCombat: () => toggles++); foreach (uint id in CombatIds) ((UiButton)indicators[id]).OnClick?.Invoke(); Assert.Equal(CombatIds.Length, toggles); } [Fact] public void UseShortcut_usesOrSelectsAccordingToRetailIntent() { const uint itemId = 0x50001001u; var (layout, _, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = itemId, Type = ItemType.Misc }); var shortcuts = new[] { new ShortcutEntry(0, itemId, 0), }; uint used = 0; uint selected = 0; var controller = ToolbarController.Bind( layout, repo, () => shortcuts, iconIds: (_, _, _, _, _) => 1u, useItem: id => used = id, selectItem: id => selected = id); Assert.True(controller.UseShortcut(0, use: true)); Assert.Equal(itemId, used); Assert.Equal(0u, selected); Assert.True(controller.UseShortcut(0, use: false)); Assert.Equal(itemId, selected); } [Fact] public void UseShortcut_targetModePrecedesUseAndClearsOneShotMode() { const uint player = 0x50000001u; const uint pack = 0x50000002u; const uint source = 0x50000003u; const uint target = 0x50000004u; var (layout, _, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature }); repo.AddOrUpdate(new ClientObject { ObjectId = pack, Type = ItemType.Container }); repo.MoveItem(pack, player, 0); repo.AddOrUpdate(new ClientObject { ObjectId = source, Type = ItemType.Misc, Useability = 0x00220008u, TargetType = (uint)ItemType.Creature, }); repo.MoveItem(source, pack, 0); repo.AddOrUpdate(new ClientObject { ObjectId = target, Type = ItemType.Creature }); uint sentSource = 0; uint sentTarget = 0; var interaction = new ItemInteractionController( repo, () => player, sendUse: null, sendUseWithTarget: (s, t) => (sentSource, sentTarget) = (s, t), sendWield: null, sendDrop: null, nowMs: () => 1_000); var shortcuts = new[] { new ShortcutEntry(0, target, 0), }; var controller = ToolbarController.Bind( layout, repo, () => shortcuts, iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, itemInteraction: interaction); Assert.True(interaction.ActivateItem(source)); Assert.True(controller.UseShortcut(0, use: false)); Assert.Equal(source, sentSource); Assert.Equal(target, sentTarget); Assert.False(interaction.IsTargetModeActive); } [Fact] public void CreateShortcutToItem_addsFirstEmptyOwnedEligibleItemOnce() { const uint player = 0x50000001u; const uint pack = 0x50000002u; const uint item = 0x50000003u; var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature }); repo.AddOrUpdate(new ClientObject { ObjectId = pack, Type = ItemType.Container }); repo.MoveItem(pack, player, 0); repo.AddOrUpdate(new ClientObject { ObjectId = item, Type = ItemType.Misc }); repo.MoveItem(item, pack, 0); var interaction = new ItemInteractionController( repo, () => player, sendUse: null, sendUseWithTarget: null, sendWield: null, sendDrop: null); var sends = new List<(uint Slot, uint Item)>(); var controller = ToolbarController.Bind( layout, repo, () => Array.Empty(), iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, itemInteraction: interaction, sendAddShortcut: entry => sends.Add(((uint)entry.Index, entry.ObjectId))); Assert.True(controller.CreateShortcutToItem(item)); Assert.Equal(item, slots[Row1[0]].Cell.ItemId); Assert.Equal(new[] { (0u, item) }, sends); Assert.False(controller.CreateShortcutToItem(item)); Assert.Single(sends); } /// /// At bind time (default NonCombat), only the peace indicator (0x10000192) is visible; /// the melee/missile/magic indicators (0x10000193/4/5) are hidden. /// Port of gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669). /// [Fact] public void CombatIndicator_defaultNonCombat_onlyPeaceVisible() { var (layout, _, indicators) = FakeToolbar(); var repo = new ClientObjectTable(); ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) =>0u, useItem: _ => { }); // Only peace indicator (index 0 = 0x10000192) is visible. Assert.True (indicators[0x10000192u].Visible, "peace indicator should be visible after bind"); Assert.False(indicators[0x10000193u].Visible, "melee indicator should be hidden after bind"); Assert.False(indicators[0x10000194u].Visible, "missile indicator should be hidden after bind"); Assert.False(indicators[0x10000195u].Visible, "magic indicator should be hidden after bind"); } /// /// SetCombatMode(Melee) hides peace/missile/magic and shows only the melee indicator. /// [Fact] public void CombatIndicator_setCombatModeMelee_onlyMeleeVisible() { var (layout, _, indicators) = FakeToolbar(); var repo = new ClientObjectTable(); var ctrl = ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) =>0u, useItem: _ => { }); ctrl.SetCombatMode(CombatMode.Melee); Assert.False(indicators[0x10000192u].Visible, "peace indicator should be hidden in melee mode"); Assert.True (indicators[0x10000193u].Visible, "melee indicator should be visible in melee mode"); Assert.False(indicators[0x10000194u].Visible, "missile indicator should be hidden in melee mode"); Assert.False(indicators[0x10000195u].Visible, "magic indicator should be hidden in melee mode"); } /// /// CombatModeChanged event on CombatState automatically updates the indicator. /// [Fact] public void CombatIndicator_liveSignal_updatesWhenCombatStateChanges() { var (layout, _, indicators) = FakeToolbar(); var repo = new ClientObjectTable(); var combat = new CombatState(); ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) =>0u, useItem: _ => { }, combatState: combat); // Initially NonCombat after bind. Assert.True(indicators[0x10000192u].Visible, "peace should be visible initially"); // Server fires CombatModeChanged → Magic. combat.SetCombatMode(CombatMode.Magic); Assert.False(indicators[0x10000192u].Visible, "peace should be hidden in magic mode"); Assert.False(indicators[0x10000193u].Visible, "melee should be hidden in magic mode"); Assert.False(indicators[0x10000194u].Visible, "missile should be hidden in magic mode"); Assert.True (indicators[0x10000195u].Visible, "magic indicator should be visible"); } // ── D1: Shortcut number (slot label) tests ─────────────────────────────── // Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465); // gmToolbarUI::RecvNotice_SetCombatMode (196610-196621). // Fake digit arrays: 9 regular entries (0x10..0x18), 9 ghosted entries (0x20..0x28), // 9 empty (background) entries (0x30..0x38). private static readonly uint[] FakeRegular = { 0x10u,0x11u,0x12u,0x13u,0x14u,0x15u,0x16u,0x17u,0x18u }; private static readonly uint[] FakeGhosted = { 0x20u,0x21u,0x22u,0x23u,0x24u,0x25u,0x26u,0x27u,0x28u }; private static readonly uint[] FakeEmpty = { 0x30u,0x31u,0x32u,0x33u,0x34u,0x35u,0x36u,0x37u,0x38u }; /// /// After Bind with regular/ghosted digit arrays, top-row cells (indices 0–8) have /// ShortcutNum == i (the slot position) and ShortcutGhosted == false (default NonCombat). /// Bottom-row cells (indices 9–17) have ShortcutNum == -1 (no label). /// Retail: numbers are slot LABELS — shown on ALL top-row slots including empty ones. /// [Fact] public void ShortcutNumbers_afterBind_topRowHasNumbers_bottomRowEmpty() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted); // Top row: ShortcutNum == slot index, ghosted == false. for (int i = 0; i < Row1.Length; i++) { var cell = slots[Row1[i]].Cell; Assert.Equal(i, cell.ShortcutNum); Assert.False(cell.ShortcutGhosted, $"top-row slot {i} should be regular at NonCombat"); } // Bottom row: no shortcut number. foreach (var id in Row2) Assert.Equal(-1, slots[id].Cell.ShortcutNum); } /// /// Retail keeps physical shortcut labels regular in NonCombat, Melee, and Missile. /// gmToolbarUI::RecvNotice_SetCombatMode @ 0x004BD610 passes ghosted=true only for Magic. /// [Theory] [InlineData(CombatMode.NonCombat)] [InlineData(CombatMode.Melee)] [InlineData(CombatMode.Missile)] public void ShortcutNumbers_nonMagicModes_useRegularDigits(CombatMode mode) { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); var ctrl = ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted); ctrl.SetCombatMode(mode); for (int i = 0; i < Row1.Length; i++) { var cell = slots[Row1[i]].Cell; Assert.Equal(i, cell.ShortcutNum); Assert.False(cell.ShortcutGhosted, $"top-row slot {i} should be regular in {mode}"); cell.SetItem((uint)(0x5000 + i), 0x99u); Assert.Same(FakeRegular, cell.ActiveDigitArray()); } // Bottom row still has no number. foreach (var id in Row2) Assert.Equal(-1, slots[id].Cell.ShortcutNum); } /// /// Magic mode ghosts physical shortcut labels, and returning to NonCombat restores them. /// [Fact] public void ShortcutNumbers_magicGhosts_thenNonCombatRestoresRegularDigits() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); var ctrl = ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted); ctrl.SetCombatMode(CombatMode.Magic); foreach (var id in Row1) { slots[id].Cell.SetItem(id, 0x99u); Assert.True(slots[id].Cell.ShortcutGhosted); Assert.Same(FakeGhosted, slots[id].Cell.ActiveDigitArray()); } ctrl.SetCombatMode(CombatMode.NonCombat); for (int i = 0; i < Row1.Length; i++) { Assert.False(slots[Row1[i]].Cell.ShortcutGhosted, $"top-row slot {i} should be regular after returning to NonCombat"); Assert.Same(FakeRegular, slots[Row1[i]].Cell.ActiveDigitArray()); } } /// /// Digit arrays are correctly injected into each cell. /// [Fact] public void ShortcutNumbers_digitArraysInjected() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted); foreach (var id in Row1) { Assert.Same(FakeRegular, slots[id].Cell.RegularDigits); Assert.Same(FakeGhosted, slots[id].Cell.GhostedDigits); } } /// /// EmptyDigits (0x1000005e background digit) is injected into every slot cell. /// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) — empty-slot branch. /// [Fact] public void ShortcutNumbers_emptyDigitArrayInjected() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted, emptyDigits: FakeEmpty); foreach (var id in Row1) Assert.Same(FakeEmpty, slots[id].Cell.EmptyDigits); foreach (var id in Row2) Assert.Same(FakeEmpty, slots[id].Cell.EmptyDigits); } /// /// When emptyDigits is null, cells have EmptyDigits == null (no digit on empty slots). /// This is the safe fallback when the dat property 0x1000005e is absent. /// [Fact] public void ShortcutNumbers_nullEmptyDigits_cellsHaveNullEmptyDigits() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, regularDigits: FakeRegular, ghostedDigits: FakeGhosted, emptyDigits: null); foreach (var id in Row1) Assert.Null(slots[id].Cell.EmptyDigits); } // ── E1: Guid filter + ObjectRemoved tests (D.5.4) ─────────────────────── /// /// ObjectAdded for a guid NOT in the shortcut list does NOT call iconIds again /// (no spurious Populate on creature/NPC spawns in a busy zone). /// D.5.4: ToolbarController filters to shortcut guids only. /// The iconIds spy lets us count how many times Populate actually ran. /// [Fact] public void ObjectAdded_nonShortcutGuid_doesNotCallIconIds() { var (layout, _, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u }); var shortcuts = new List { new(Index: 0, ObjectId: 0x5001u, SpellId: 0) }; int iconCallCount = 0; ToolbarController.Bind(layout, repo, () => shortcuts, iconIds: (_,_,_,_,_) => { iconCallCount++; return 0x77u; }, useItem: _ => { }); int callsAfterBind = iconCallCount; // 1 call from initial Populate // Fire ObjectAdded with a completely unrelated guid (a creature, NOT a shortcut). repo.AddOrUpdate(new ClientObject { ObjectId = 0xDEADBEEFu, WeenieClassId = 42u, IconId = 0u }); // iconIds must NOT have been called again — the filter blocked Populate. Assert.Equal(callsAfterBind, iconCallCount); } /// /// ObjectAdded for a guid that IS in the shortcut list calls iconIds again (deferred bind). /// This is the filtered-path counterpart of DeferredRebind_whenItemArrivesLate. /// [Fact] public void ObjectAdded_shortcutGuid_callsIconIds() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); // item NOT present yet var shortcuts = new List { new(Index: 1, ObjectId: 0x5003u, SpellId: 0) }; int iconCallCount = 0; ToolbarController.Bind(layout, repo, () => shortcuts, iconIds: (_,_,_,_,_) => { iconCallCount++; return 0x99u; }, useItem: _ => { }); Assert.Equal(0, iconCallCount); // not called — item absent during initial Populate Assert.Equal(0u, slots[Row1[1]].Cell.ItemId); // Now the shortcut item arrives — filter must PASS and Populate re-run. repo.AddOrUpdate(new ClientObject { ObjectId = 0x5003u, WeenieClassId = 1u, IconId = 0x06005678u }); Assert.Equal(1, iconCallCount); // iconIds called exactly once for the deferred bind Assert.Equal(0x5003u, slots[Row1[1]].Cell.ItemId); } /// /// ObjectRemoved for a guid that IS in the shortcut list clears the slot. /// D.5.4: subscribes to ObjectRemoved so a removed item evicts its icon. /// [Fact] public void ObjectRemoved_shortcutGuid_clearsSlot() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = 0x5004u, WeenieClassId = 1u, IconId = 0x06001234u }); var shortcuts = new List { new(Index: 3, ObjectId: 0x5004u, SpellId: 0) }; ToolbarController.Bind(layout, repo, () => shortcuts, iconIds: (_,_,_,_,_) => 0xAAu, useItem: _ => { }); Assert.Equal(0x5004u, slots[Row1[3]].Cell.ItemId); // bound // Remove the item from the session (server despawn / trade away). // Populate re-runs: item is gone from repo → slot clears. repo.Remove(0x5004u); Assert.Equal(0u, slots[Row1[3]].Cell.ItemId); } /// /// ObjectRemoved for a guid NOT in the shortcut list does NOT call iconIds again. /// D.5.4: the ObjectRemoved subscription also filters to shortcut guids. /// [Fact] public void ObjectRemoved_nonShortcutGuid_doesNotCallIconIds() { var (layout, _, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = 0x5005u, WeenieClassId = 1u, IconId = 0x06001234u }); repo.AddOrUpdate(new ClientObject { ObjectId = 0xCAFEBABEu, WeenieClassId = 99u, IconId = 0u }); var shortcuts = new List { new(Index: 4, ObjectId: 0x5005u, SpellId: 0) }; int iconCallCount = 0; ToolbarController.Bind(layout, repo, () => shortcuts, iconIds: (_,_,_,_,_) => { iconCallCount++; return 0xBBu; }, useItem: _ => { }); int callsAfterBind = iconCallCount; // 1 call for the shortcut item // Remove an unrelated object — filter must block Populate. repo.Remove(0xCAFEBABEu); Assert.Equal(callsAfterBind, iconCallCount); // unchanged } // ── B.1: drag-drop spine wiring ────────────────────────────────────────── /// Bind registers the controller as each slot list's drag handler and /// stamps every cell's SlotIndex + SourceKind=ShortcutBar. [Fact] public void Bind_registersDragHandler_andStampsSlots() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); var ctrl = ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); for (int i = 0; i < Row1.Length; i++) { Assert.Same(ctrl, slots[Row1[i]].DragHandler); Assert.Equal(i, slots[Row1[i]].Cell.SlotIndex); Assert.Equal(ItemDragSource.ShortcutBar, slots[Row1[i]].Cell.SourceKind); } // Bottom row slots are indices 9..17; same handler + source kind as the top row. for (int j = 0; j < Row2.Length; j++) { Assert.Same(ctrl, slots[Row2[j]].DragHandler); Assert.Equal(9 + j, slots[Row2[j]].Cell.SlotIndex); Assert.Equal(ItemDragSource.ShortcutBar, slots[Row2[j]].Cell.SourceKind); } } /// OnDragOver accepts a real item (ObjId != 0). Eligibility (IsShortcutEligible) /// is Stream B.2; the stub accepts any non-empty payload. [Fact] public void OnDragOver_acceptsRealItem() { var (layout, slots, _) = FakeToolbar(); var ctrl = ToolbarController.Bind(layout, new ClientObjectTable(), () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); var list = slots[Row1[0]]; var payload = new ItemDragPayload(0x5001u, ItemDragSource.Inventory, 0, new UiItemSlot()); Assert.Equal(ItemDragAcceptance.Accept, ctrl.OnDragOver(list, list.Cell, payload)); } /// OnDragOver rejects a ghost payload with ObjId 0 (the guard against an /// empty cell that somehow produced a payload). Complements OnDragOver_acceptsRealItem. [Fact] public void OnDragOver_rejectsZeroObjId() { var (layout, slots, _) = FakeToolbar(); var ctrl = ToolbarController.Bind(layout, new ClientObjectTable(), () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); var list = slots[Row1[0]]; var payload = new ItemDragPayload(0u, ItemDragSource.Inventory, 0, new UiItemSlot()); Assert.Equal(ItemDragAcceptance.None, ctrl.OnDragOver(list, list.Cell, payload)); } // ── B.2: live drag handler (store + reorder/remove + wire) ─────────────── private static (System.Collections.Generic.List adds, System.Collections.Generic.List removes) NewSpies(out System.Action add, out System.Action rem) { var adds = new System.Collections.Generic.List(); var removes = new System.Collections.Generic.List(); add = entry => adds.Add(entry); rem = i => removes.Add(i); return (adds, removes); } [Fact] public void OnDragLift_removesSourceSlot_sendsRemove_emptiesCell() { 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 { new(Index: 3, ObjectId: 0x5001u, SpellId: 0) }; var (adds, removes) = NewSpies(out var add, out var rem); uint selected = 0; var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts, iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { }, sendAddShortcut: add, sendRemoveShortcut: rem, selectItem: item => selected = item, selectedObjectId: () => selected); 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.Equal(0x5001u, selected); 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 { new(Index: 3, ObjectId: 0x5001u, SpellId: 0), new(Index: 5, ObjectId: 0x5002u, SpellId: 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(new ShortcutEntry(5, 0x5001u, 0u), adds); Assert.Contains(new ShortcutEntry(3, 0x5002u, 0u), adds); } [Fact] public void HandleDropRelease_reindexesWithoutLosingRawShortcutFields() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, IconId = 1u }); repo.AddOrUpdate(new ClientObject { ObjectId = 0x5002u, IconId = 2u }); var shortcuts = new[] { new ShortcutEntry(3, 0x5001u, 0x11223344u), new ShortcutEntry(5, 0x5002u, 0xA5C31234u), }; var (adds, _) = NewSpies(out var add, out var remove); var controller = ToolbarController.Bind( layout, repo, () => shortcuts, iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, sendAddShortcut: add, sendRemoveShortcut: remove); var payload = Assert.IsType(slots[Row1[3]].Cell.GetDragPayload()); controller.OnDragLift(slots[Row1[3]], slots[Row1[3]].Cell, payload); controller.HandleDropRelease(slots[Row1[5]], slots[Row1[5]].Cell, payload); Assert.Contains(new ShortcutEntry(5, 0x5001u, 0x11223344u), adds); Assert.Contains(new ShortcutEntry(3, 0x5002u, 0xA5C31234u), 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 { new(Index: 3, ObjectId: 0x5001u, SpellId: 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(new ShortcutEntry(7, 0x5001u, 0u), adds); Assert.DoesNotContain(adds, a => a.Index == 3); } [Fact] public void HandleDropRelease_freshInventoryToOccupied_usesCyclicRightSlotNotInventoryIndex() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, IconId = 1u }); repo.AddOrUpdate(new ClientObject { ObjectId = 0x5002u, IconId = 2u }); var shortcuts = new[] { new ShortcutEntry(5, 0x5002u, 0xA5C31234u), }; var wire = new List(); var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts, iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, sendAddShortcut: entry => wire.Add($"add:{entry.Index}:{entry.ObjectId:X8}:{entry.SpellId:X8}"), sendRemoveShortcut: slot => wire.Add($"remove:{slot}")); var payload = new ItemDragPayload( 0x5001u, ItemDragSource.Inventory, SourceSlot: 12, new UiItemSlot()); ctrl.HandleDropRelease(slots[Row1[5]], slots[Row1[5]].Cell, payload); Assert.Equal(0x5001u, slots[Row1[5]].Cell.ItemId); Assert.Equal(0x5002u, slots[Row1[6]].Cell.ItemId); Assert.Equal(0u, slots[Row2[3]].Cell.ItemId); // inventory grid index 12 is irrelevant Assert.Equal(new[] { "remove:5", "add:5:00005001:00000000", "add:6:00005002:A5C31234", }, wire); } [Fact] public void ReplaceFullyMergedShortcut_rekeysEntryAndPreservesRawFields() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, IconId = 1u }); repo.AddOrUpdate(new ClientObject { ObjectId = 0x5002u, IconId = 2u }); var shortcuts = new[] { new ShortcutEntry(4, 0x5001u, 0x11223344u), }; var wire = new List(); var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts, iconIds: (_, _, _, _, _) => 1u, useItem: _ => { }, sendAddShortcut: entry => wire.Add($"add:{entry.Index}:{entry.ObjectId:X8}:{entry.SpellId:X8}"), sendRemoveShortcut: slot => wire.Add($"remove:{slot}")); ctrl.ReplaceFullyMergedShortcut(0x5001u, 0x5002u); Assert.Equal(0x5002u, slots[Row1[4]].Cell.ItemId); Assert.Equal(new[] { "remove:4", "add:4:00005002:11223344", }, wire); } [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 { new(Index: 3, ObjectId: 0x5001u, SpellId: 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(new ShortcutEntry(3, 0x5001u, 0u), adds); // AddShortcut(3, A) sent Assert.Single(removes); // exactly RemoveShortcut(3) [from the lift] Assert.Single(adds); // exactly AddShortcut(3, A) [the re-place] } [Fact] public void ToolbarSlots_useGreenCrossAcceptSprite() { var (layout, slots, _) = FakeToolbar(); ToolbarController.Bind(layout, new ClientObjectTable(), () => System.Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }); Assert.Equal(0x060011FAu, slots[Row1[0]].Cell.DragAcceptSprite); // green cross, not the ring F9 } }