diff --git a/crates/benilla-ui/src/script/cursor.rs b/crates/benilla-ui/src/script/cursor.rs index 5c371459..4f5a3f62 100644 --- a/crates/benilla-ui/src/script/cursor.rs +++ b/crates/benilla-ui/src/script/cursor.rs @@ -223,8 +223,8 @@ pub(super) fn item_link_name(link: Option<&str>) -> String { .unwrap_or_default() } -/// `ClearCursor()`: drops any payload and frees an item's source slot; an empty cursor fires -/// nothing. +/// `ClearCursor()` (`0x495190`): drops any payload and frees an item's source slot, then signals +/// `CURSOR_UPDATE` from its shared tail (`0x49529b`) on every call, an empty cursor included. pub(crate) fn clear_cursor(model: &mut Model) { // Any clear first cancels an armed gift wrap, whatever its parameters (`0x495190` opens with // `0x5edf10`): the wrapping paper unlocks and the cursor mode returns to the base. @@ -233,25 +233,12 @@ pub(crate) fn clear_cursor(model: &mut Model) { model.ui_cursor = None; model.ui_cursor_dirty = true; } - match model.cursor.take() { - Some(CursorPayload::Item(item)) => { - queue_cursor_update(model); - queue_lock_changed(model, item.bag, item.slot); - } - Some( - CursorPayload::Spell(_) - | CursorPayload::Action(_) - | CursorPayload::Macro(_) - | CursorPayload::PetAction(_) - | CursorPayload::StablePet(_) - // Coins: the purse was never debited, so nothing goes back. - | CursorPayload::Money(_) - // A vendor row clears with no packet (`0x49525f`). - | CursorPayload::Merchant(_), - ) => { - queue_cursor_update(model); - } - None => {} + // Coins go back to nothing, as the purse was never debited; a vendor row clears with no + // packet (`0x49525f`). + let taken = model.cursor.take(); + queue_cursor_update(model); + if let Some(CursorPayload::Item(item)) = taken { + queue_lock_changed(model, item.bag, item.slot); } } @@ -742,6 +729,20 @@ mod tests { assert!(s.cursor_payload().is_none()); } + /// The clear's shared tail (`0x49529b`) signals on every call: with nothing held, one + /// `CURSOR_UPDATE` and no grid event. + #[test] + fn clear_cursor_with_nothing_held_still_signals() { + let s = UiScript::new().unwrap(); + let before = s.model_ref().pending_events.len(); + s.run("ClearCursor()").unwrap(); + let fired: Vec = s.model_ref().pending_events[before..] + .iter() + .map(|(e, _)| e.clone()) + .collect(); + assert_eq!(fired, vec!["CURSOR_UPDATE"]); + } + /// Clearing a spell on terrain is the deviation documented on `world_drop_click`. #[test] fn world_drop_terrain_and_nothing_both_clear_a_spell_payload() { diff --git a/crates/benilla-ui/src/script/cursor/bag_verbs.rs b/crates/benilla-ui/src/script/cursor/bag_verbs.rs index ad17b031..a56b34e3 100644 --- a/crates/benilla-ui/src/script/cursor/bag_verbs.rs +++ b/crates/benilla-ui/src/script/cursor/bag_verbs.rs @@ -142,15 +142,18 @@ pub(super) fn put_item_in_backpack(model: &mut Model) -> bool { true } -/// `PickupBagFromSlot(inventorySlot)` (`0x4c7b00`): clears the cursor before it looks at the slot, -/// where the reference clears only a held item (`0x4c7b70`, `0x4c7b79`); takes only a container +/// `PickupBagFromSlot(inventorySlot)` (`0x4c7b00`): clears a held payload before it looks at the +/// slot, where the reference clears only a held item (`0x4c7b70`, `0x4c7b79`); with nothing held it +/// skips the clear and its `CURSOR_UPDATE`, as the reference does. Takes only a container /// (`0x4c7bb3`), and never places or swaps. The client has no empty-bag rule: the server refuses /// with `EQUIP_ERR_CAN_ONLY_DO_WITH_EMPTY_BAGS`. pub(super) fn pickup_bag_from_slot(model: &mut Model, live: u32) { if !accepts(live) { return; } - clear_cursor(model); + if model.cursor.is_some() { + clear_cursor(model); + } let picked = usize::try_from(live) .ok() .and_then(|slot| model.inv_slot("player", slot)) @@ -227,6 +230,17 @@ mod tests { InventorySlots, UiScript, }; + /// With nothing held the reference skips the clear (`0x4c7b6a`-`0x4c7b70`), so a drag from an + /// empty bag slot signals nothing. + #[test] + fn an_empty_cursor_skips_the_clear_and_its_signal() { + let s = UiScript::new().unwrap(); + let before = s.model_ref().pending_events.len(); + s.run("PickupBagFromSlot(20)").unwrap(); + assert!(s.cursor_payload().is_none()); + assert_eq!(s.model_ref().pending_events.len(), before); + } + /// An item that is a bag: `equip_slots` is `find_equip_slot(INVTYPE_BAG)`. fn bag_slot_view(item_id: u32) -> InvSlotView { InvSlotView { diff --git a/crates/benilla-ui/src/script/merchant.rs b/crates/benilla-ui/src/script/merchant.rs index 0537d6e1..f0c5f0dd 100644 --- a/crates/benilla-ui/src/script/merchant.rs +++ b/crates/benilla-ui/src/script/merchant.rs @@ -1114,8 +1114,9 @@ mod tests { (n("updates"), n("shows"), n("hides")) } - /// The grab goes through the cursor setter (`0x4950f0`): `CURSOR_UPDATE` at `0x495159`, and - /// no `ACTIONBAR_SHOWGRID` since mode 5 is not mode 7. + /// The grab goes through the cursor setter (`0x4950f0`): its `ClearCursor(1,1)` (`0x495112`) + /// signals `CURSOR_UPDATE` from the clear's tail, the set signals it again (`0x495159`), and no + /// `ACTIONBAR_SHOWGRID` since mode 5 is not mode 7. #[test] fn a_vendor_grab_fires_cursor_update_but_not_the_bar_grid() { let mut s = UiScript::new().unwrap(); @@ -1124,12 +1125,12 @@ mod tests { s.run("PickupMerchantItem(1)").unwrap(); assert_eq!( cursor_event_counts(&mut s), - (1, 0, 0), + (2, 0, 0), "(CURSOR_UPDATE, ACTIONBAR_SHOWGRID, ACTIONBAR_HIDEGRID) after a vendor grab" ); // The toggle-off is a plain clear: one more CURSOR_UPDATE, no HIDEGRID. s.run("PickupMerchantItem(1)").unwrap(); - assert_eq!(cursor_event_counts(&mut s), (2, 0, 0)); + assert_eq!(cursor_event_counts(&mut s), (3, 0, 0)); } #[test]