ClearCursor signals CURSOR_UPDATE on every call, an empty cursor included, as 1.12.1's shared tail does (#432)

Fixes #425

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-09-26 15:38:21 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent e26e234d03
commit 24a8fb274c
3 changed files with 44 additions and 28 deletions
+22 -21
View File
@@ -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<String> = 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() {
@@ -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 {
+5 -4
View File
@@ -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]