From e26e234d031183f87952ec88dbac335f2617472b Mon Sep 17 00:00:00 2001 From: Sam Sung <51686767+samwhosung@users.noreply.github.com> Date: Sat, 26 Sep 2026 15:25:41 +0700 Subject: [PATCH] Right-clicking a bag or worn item puts a held item back first, as 1.12.1 clears the cursor before any use (#431) - Right-clicking a bag or worn item puts a held item back first, as 1.12.1 clears the cursor before any use - The bag frame's end-to-end test: a right-click with a held item puts it back, then uses the slot Fixes #413 Fixes #421 Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/benilla-app/src/ui_items/drain.rs | 8 --- .../src/ui_script/action_bar_tests.rs | 11 ++-- crates/benilla-ui/src/script/container.rs | 52 +++++++++++++------ crates/benilla-ui/src/script/cursor/doll.rs | 29 +++++++++-- 4 files changed, 66 insertions(+), 34 deletions(-) diff --git a/crates/benilla-app/src/ui_items/drain.rs b/crates/benilla-app/src/ui_items/drain.rs index 6bb50950..96f2d1aa 100644 --- a/crates/benilla-app/src/ui_items/drain.rs +++ b/crates/benilla-app/src/ui_items/drain.rs @@ -272,14 +272,6 @@ pub(super) fn drain_container_uses( } } for (bag, slot) in script.take_container_uses() { - // Any right-click, a sell or a deposit included, cancels an armed gift wrap: the - // reference's use path clears the cursor first (`0x4fa198`). - if let Some(w) = script.cancel_gift_wrap() { - debug!( - "ui_items: right-click cancels the armed gift wrap on bag {} slot {}", - w.bag, w.slot - ); - } let slot0 = u8::try_from(slot.saturating_sub(1)).ok(); // With a merchant open, the click sells the whole stack (`CMSG_SELL_ITEM`, count 0). if let (true, Some(vendor)) = (merchant.is_open(), merchant.vendor) { diff --git a/crates/benilla-app/src/ui_script/action_bar_tests.rs b/crates/benilla-app/src/ui_script/action_bar_tests.rs index 755e6dec..e9f415c8 100644 --- a/crates/benilla-app/src/ui_script/action_bar_tests.rs +++ b/crates/benilla-app/src/ui_script/action_bar_tests.rs @@ -1143,20 +1143,19 @@ fn shipped_bag_frame_drives_end_to_end() { "a pickup alone queues no move" ); - // Right-click while holding: the right arm has no cursor test (ContainerFrame.lua:583-599). - // What the reference client does with a use while the cursor holds an item is untraced. + // Right-click while holding: the right arm has no cursor test (ContainerFrame.lua:583-599), + // and `UseContainerItem` clears the cursor before it reads the slot (`0x4fa198`). s.mouse_button(bx, by, "RightButton", true); s.mouse_button(bx, by, "RightButton", false); assert_eq!( s.take_container_uses(), vec![(0, 1)], - "the reference's right arm uses the slot even with a full cursor" + "the right arm uses the slot even with a full cursor" ); assert!( - s.cursor_item().is_some(), - "…and leaves the held item where it was" + s.cursor_item().is_none(), + "…after putting the held item back" ); - s.run("ClearCursor()").unwrap(); s.mouse_button(bx, by, "RightButton", true); s.mouse_button(bx, by, "RightButton", false); diff --git a/crates/benilla-ui/src/script/container.rs b/crates/benilla-ui/src/script/container.rs index 9eb98008..4985fc02 100644 --- a/crates/benilla-ui/src/script/container.rs +++ b/crates/benilla-ui/src/script/container.rs @@ -199,18 +199,6 @@ impl super::UiScript { cursor::queue_lock_changed(&mut model, bag, slot); } - /// Disarm a gift wrap, unlocking the paper and resetting the cursor, but keeping any held - /// payload, which `ClearCursor` would drop. How the reference's right-click cancels a wrap is - /// untraced. - pub fn cancel_gift_wrap(&mut self) -> Option { - let mut model = self.model_mut(); - let wrap = model.pending_wrap.take()?; - cursor::queue_lock_changed(&mut model, wrap.bag, wrap.slot); - model.ui_cursor = None; - model.ui_cursor_dirty = true; - Some(wrap) - } - /// The armed gift wrap, if any; the app reads it to tell whether a right-click cancels one. pub fn gift_wrap_armed(&self) -> Option { self.model_ref().pending_wrap @@ -623,10 +611,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { "UseContainerItem", lua.create_function(|lua, (bag, slot, _rest): (i64, u32, mlua::MultiValue)| { let mut model = lua.app_data_mut::().expect("model app_data"); + // Every right-click clears the cursor before it reads the slot (`0x4fa198`): a held + // payload goes back, never placed, and an armed gift wrap is cancelled. + cursor::clear_cursor(&mut model); if model.repair_mode { - // The right-click clears the cursor first (`0x4fa198`), then repairs the item in - // the slot (`0x4fa1a6`, `0x4fa1da`): a held payload goes back, never placed. - cursor::clear_cursor(&mut model); + // Then the item in the slot is repaired (`0x4fa1a6`, `0x4fa1da`). let occupied = model .containers .get(&bag) @@ -899,6 +888,39 @@ mod tests { assert!(s.take_container_uses().is_empty(), "drained"); } + /// A right-click clears the cursor before it reads the slot (`0x4fa198`): a held item goes + /// back to its slot, unlocked, and the clicked item is still used. + #[test] + fn right_click_puts_a_held_item_back_then_uses_the_clicked_one() { + let mut s = UiScript::new().unwrap(); + s.set_container(0, Some(backpack())); + s.run("PickupContainerItem(0, 1)").unwrap(); + assert!(s.cursor_item().is_some()); + + s.run("UseContainerItem(0, 4)").unwrap(); + assert!(s.cursor_payload().is_none(), "the held item went back"); + assert!( + !s.eval::("local _, _, locked = GetContainerItemInfo(0, 1) return locked") + .unwrap(), + "its slot unlocks" + ); + assert!(s.take_container_moves().is_empty(), "never placed"); + assert_eq!(s.take_container_uses(), vec![(0, 4)]); + } + + /// The same clear cancels an armed gift wrap (`0x495190` opens with `0x5edf10`), and the + /// clicked item is used, not wrapped. + #[test] + fn right_click_cancels_an_armed_gift_wrap_then_uses_the_clicked_one() { + let mut s = UiScript::new().unwrap(); + s.set_container(0, Some(backpack())); + s.arm_gift_wrap(0, 1); + s.run("UseContainerItem(0, 4)").unwrap(); + assert_eq!(s.gift_wrap_armed(), None); + assert!(s.take_container_wraps().is_empty(), "nothing wrapped"); + assert_eq!(s.take_container_uses(), vec![(0, 4)]); + } + #[test] fn pickup_then_place_swaps_and_clears_cursor() { let mut s = UiScript::new().unwrap(); diff --git a/crates/benilla-ui/src/script/cursor/doll.rs b/crates/benilla-ui/src/script/cursor/doll.rs index bf824fb7..d9af7591 100644 --- a/crates/benilla-ui/src/script/cursor/doll.rs +++ b/crates/benilla-ui/src/script/cursor/doll.rs @@ -155,16 +155,16 @@ pub(super) fn auto_equip_cursor_item(model: &mut Model) -> bool { } } -/// `UseInventoryItem(id)`, the doll slot's right-click (`PaperDollFrame.lua:658-659`): the app -/// sends `CMSG_USE_ITEM` with bag 255 and the 0-based slot, dropping an empty slot unsent. In -/// repair mode the cursor clears first (`0x4c79a9`) and a worn item is repaired, locked or not -/// (`0x4c79c4`): a held payload goes back, never placed. +/// `UseInventoryItem(id)`, the doll slot's right-click (`PaperDollFrame.lua:658-659`): the cursor +/// clears before the slot is read (`0x4c79a9`), so a held payload goes back, never placed; then +/// the app sends `CMSG_USE_ITEM` with bag 255 and the 0-based slot, dropping an empty slot unsent, +/// or in repair mode a worn item is repaired, locked or not (`0x4c79c4`). pub(super) fn use_inventory_item(model: &mut Model, id: u32) { + super::clear_cursor(model); if !model.repair_mode { model.inventory_uses.push(id); return; } - super::clear_cursor(model); if (1..=19).contains(&id) && model .inv_slot("player", id as usize) @@ -602,6 +602,25 @@ mod tests { assert!(s.take_inventory_uses().is_empty(), "drained"); } + /// A right-click clears the cursor before it reads the slot (`0x4c79a9`): a held item goes + /// back, and the worn item is still used. + #[test] + fn right_click_puts_a_held_item_back_then_uses_the_worn_one() { + let mut s = UiScript::new().unwrap(); + s.set_inventory_slots(doll_slots()); + s.run("PickupInventoryItem(11)").unwrap(); + assert!(s.cursor_item().is_some()); + + s.run("UseInventoryItem(19)").unwrap(); + assert!(s.cursor_payload().is_none(), "the ring went back"); + assert!( + !s.eval::("return IsInventoryItemLocked(11)").unwrap(), + "its slot unlocks" + ); + assert!(s.take_container_moves().is_empty(), "never placed"); + assert_eq!(s.take_inventory_uses(), vec![19]); + } + #[test] fn repair_mode_queues_an_equipped_slot_without_picking_it_up() { let mut s = UiScript::new().unwrap();