Lua verbs fire their events before returning, in the reference's order (#1015)
In 1.12.1 an event a Lua verb raises runs its handlers before the verb returns: `SignalEvent` (`0x703e50`) walks the listeners in place. About 35 benilla binding sites queued theirs for a later drain instead. - Verbs now raise inside `tick::verb` steps, and the events fire as each step's model borrow drops. A step follows the reference's split wherever it signals before changing state a getter reads: the money pickup, bag pickup and place, merchant grab, repair cursor, item use, action and pet-bar pickup and drop. - The grid events fire before `CURSOR_UPDATE`, and a clear's unlock before both, as at `0x4952b3`/`0x495455`. `ITEM_LOCK_CHANGED` carries no arguments. - `pending_events` is host-only now. Tests pin handler order per family, nesting from a handler, and the order changes. Fixes #999 Fixes #582 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
24c7cab983
commit
aba120cdcb
@@ -3,9 +3,10 @@
|
||||
use mlua::{Lua, MultiValue, Value};
|
||||
|
||||
use crate::script::container::ContainerMove;
|
||||
use crate::script::tick::verb;
|
||||
use crate::script::Model;
|
||||
|
||||
use super::{clear_cursor, queue_cursor_update, queue_lock_changed, CursorItem, CursorPayload};
|
||||
use super::{clear_cursor, raise_cursor_update, raise_lock_changed, CursorItem, CursorPayload};
|
||||
use super::{doll::pickup_inventory_item, EQUIPMENT_BAG};
|
||||
|
||||
/// The ids `PutItemInBag` and `PickupBagFromSlot` take: what their argument reader (`0x4c8520`)
|
||||
@@ -80,9 +81,10 @@ pub(super) fn put_item_in_bag(model: &mut Model, live: u32) -> bool {
|
||||
|
||||
// The held bag is this slot's own: a cancel that sends nothing and answers 1.
|
||||
if held.bag == EQUIPMENT_BAG && held.slot == live {
|
||||
// `ClearCursor(1,1)`: the unlock, then `CURSOR_UPDATE`.
|
||||
model.cursor = None;
|
||||
queue_cursor_update(model);
|
||||
queue_lock_changed(model, held.bag, held.slot);
|
||||
raise_lock_changed(model);
|
||||
raise_cursor_update(model);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -98,8 +100,8 @@ pub(super) fn put_item_in_bag(model: &mut Model, live: u32) -> bool {
|
||||
dst_slot: live,
|
||||
count: None,
|
||||
});
|
||||
queue_cursor_update(model);
|
||||
queue_lock_changed(model, held.bag, held.slot);
|
||||
raise_cursor_update(model);
|
||||
raise_lock_changed(model);
|
||||
return true;
|
||||
}
|
||||
model.cursor = Some(CursorPayload::Item(held));
|
||||
@@ -118,8 +120,8 @@ pub(super) fn put_item_in_bag(model: &mut Model, live: u32) -> bool {
|
||||
dst_bag,
|
||||
count: held.count,
|
||||
});
|
||||
queue_cursor_update(model);
|
||||
queue_lock_changed(model, held.bag, held.slot);
|
||||
raise_cursor_update(model);
|
||||
raise_lock_changed(model);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -137,23 +139,31 @@ pub(super) fn put_item_in_backpack(model: &mut Model) -> bool {
|
||||
dst_bag: 0,
|
||||
count: held.count,
|
||||
});
|
||||
queue_cursor_update(model);
|
||||
queue_lock_changed(model, held.bag, held.slot);
|
||||
raise_cursor_update(model);
|
||||
raise_lock_changed(model);
|
||||
true
|
||||
}
|
||||
|
||||
/// `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
|
||||
/// skips the clear and its `CURSOR_UPDATE`, as the reference does. The clear's events run before
|
||||
/// the slot is read, so its `CURSOR_UPDATE` handler sees the empty cursor. 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) {
|
||||
pub(super) fn pickup_bag_from_slot(lua: &Lua, live: u32) {
|
||||
if !accepts(live) {
|
||||
return;
|
||||
}
|
||||
if model.cursor.is_some() {
|
||||
clear_cursor(model);
|
||||
}
|
||||
verb(lua, |model| {
|
||||
if model.cursor.is_some() {
|
||||
clear_cursor(model);
|
||||
}
|
||||
});
|
||||
verb(lua, |model| grab_bag(model, live));
|
||||
}
|
||||
|
||||
/// `PickupBagFromSlot`'s grab of an unlocked container in `live`.
|
||||
fn grab_bag(model: &mut Model, live: u32) {
|
||||
let picked = usize::try_from(live)
|
||||
.ok()
|
||||
.and_then(|slot| model.inv_slot("player", slot))
|
||||
@@ -172,8 +182,8 @@ pub(super) fn pickup_bag_from_slot(model: &mut Model, live: u32) {
|
||||
.filter(is_container);
|
||||
if let Some(item) = picked {
|
||||
model.cursor = Some(CursorPayload::Item(item));
|
||||
queue_cursor_update(model);
|
||||
queue_lock_changed(model, EQUIPMENT_BAG, live);
|
||||
raise_cursor_update(model);
|
||||
raise_lock_changed(model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,28 +203,17 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
g.set(
|
||||
"PutItemInBag",
|
||||
lua.create_function(|lua, live: u32| {
|
||||
let yes = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
put_item_in_bag(&mut model, live)
|
||||
};
|
||||
consumed(yes)
|
||||
consumed(verb(lua, |model| put_item_in_bag(model, live)))
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"PutItemInBackpack",
|
||||
lua.create_function(|lua, ()| {
|
||||
let yes = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
put_item_in_backpack(&mut model)
|
||||
};
|
||||
consumed(yes)
|
||||
})?,
|
||||
lua.create_function(|lua, ()| consumed(verb(lua, put_item_in_backpack)))?,
|
||||
)?;
|
||||
g.set(
|
||||
"PickupBagFromSlot",
|
||||
lua.create_function(|lua, live: u32| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
pickup_bag_from_slot(&mut model, live);
|
||||
pickup_bag_from_slot(lua, live);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -225,6 +224,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::script::cursor::EQUIPMENT_BAG;
|
||||
use crate::script::tick::heard;
|
||||
use crate::script::{
|
||||
BagAutoStore, BankBagSlots, ContainerMove, ContainerSlot, ContainerState, InvSlotView,
|
||||
InventorySlots, UiScript,
|
||||
@@ -235,10 +235,10 @@ mod tests {
|
||||
#[test]
|
||||
fn an_empty_cursor_skips_the_clear_and_its_signal() {
|
||||
let s = UiScript::new().unwrap();
|
||||
let before = s.model_ref().pending_events.len();
|
||||
heard::listen(&s, &["CURSOR_UPDATE"], None);
|
||||
s.run("PickupBagFromSlot(20)").unwrap();
|
||||
assert!(s.cursor_payload().is_none());
|
||||
assert_eq!(s.model_ref().pending_events.len(), before);
|
||||
assert!(heard::take(&s).is_empty());
|
||||
}
|
||||
|
||||
/// An item that is a bag: `equip_slots` is `find_equip_slot(INVTYPE_BAG)`.
|
||||
@@ -454,6 +454,32 @@ mod tests {
|
||||
assert!(s.take_container_moves().is_empty(), "no place path exists");
|
||||
}
|
||||
|
||||
/// The clear's events (the unlock `0x495455`, then `0x4952cc`) run before the grab reads the
|
||||
/// slot (`0x4c7be2`), so its handlers see the bag still in place and only the grab's
|
||||
/// (`0x494c54`, then `0x495415`) see it held and locked.
|
||||
#[test]
|
||||
fn pickup_bag_from_slot_fires_the_clear_before_the_grab() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_container(0, Some(backpack()));
|
||||
s.set_bank_bag_slots(one_bank_bag());
|
||||
s.run("PickupContainerItem(0, 2)").unwrap();
|
||||
heard::listen(
|
||||
&s,
|
||||
&["CURSOR_UPDATE", "ITEM_LOCK_CHANGED"],
|
||||
Some("IsInventoryItemLocked(64)"),
|
||||
);
|
||||
s.run("PickupBagFromSlot(64)").unwrap();
|
||||
assert_eq!(
|
||||
heard::take(&s),
|
||||
[
|
||||
"ITEM_LOCK_CHANGED nil",
|
||||
"CURSOR_UPDATE nil",
|
||||
"CURSOR_UPDATE 1",
|
||||
"ITEM_LOCK_CHANGED 1"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pickup_bag_from_slot_only_takes_containers_and_never_a_locked_one() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
use mlua::Lua;
|
||||
|
||||
use crate::script::action::{ACTION_KIND_ITEM, ACTION_KIND_MACRO, ACTION_KIND_SPELL};
|
||||
use crate::script::tick::verb;
|
||||
use crate::script::{ActionSlot, Model};
|
||||
|
||||
use super::{queue_cursor_update, CursorAction, CursorPayload};
|
||||
use super::{raise_cursor_update, CursorAction, CursorPayload};
|
||||
|
||||
/// The passive-spell refusal's error, errorId `0x9e`: entry `0xb4c0f0` of the table at `0xb4b498`
|
||||
/// (stride `0x14`) names this key (`0x84133c`).
|
||||
@@ -20,47 +21,84 @@ fn pack(kind: u8, action: u32) -> u32 {
|
||||
}
|
||||
|
||||
/// `PickupAction(id)`, the slot's shift-click and drag start (`ActionBarFrame.xml:12-38`). With a
|
||||
/// payload held it places instead; a pickup also clears the slot on the wire at once. Returns
|
||||
/// whether to repaint.
|
||||
pub(super) fn pickup_action(model: &mut Model, id: u32) -> bool {
|
||||
if model.cursor.is_some() {
|
||||
return place_action(model, id);
|
||||
/// payload held it places instead; a pickup also clears the slot on the wire at once. The cursor
|
||||
/// goes first and the slot after, as `0x4e6130` runs its setter before the slot clear `0x4e5db0`,
|
||||
/// so the pickup's `CURSOR_UPDATE` handler still reads the action in its slot. Returns whether to
|
||||
/// repaint.
|
||||
pub(super) fn pickup_action(lua: &Lua, id: u32) -> bool {
|
||||
let held = lua
|
||||
.app_data_ref::<Model>()
|
||||
.expect("model app_data")
|
||||
.cursor
|
||||
.is_some();
|
||||
if held {
|
||||
return place_action(lua, id);
|
||||
}
|
||||
let Some(slot) = model.actions.get(&id) else {
|
||||
return false;
|
||||
};
|
||||
let payload = CursorAction {
|
||||
src_slot: id,
|
||||
kind: slot.kind,
|
||||
action: slot.action,
|
||||
texture: slot.texture.clone(),
|
||||
};
|
||||
model.actions.remove(&id);
|
||||
model.cursor = Some(CursorPayload::Action(payload));
|
||||
model.action_sets.push((id, 0));
|
||||
queue_cursor_update(model);
|
||||
true
|
||||
let picked = verb(lua, |model| {
|
||||
let Some(slot) = model.actions.get(&id) else {
|
||||
return false;
|
||||
};
|
||||
let payload = CursorAction {
|
||||
src_slot: id,
|
||||
kind: slot.kind,
|
||||
action: slot.action,
|
||||
texture: slot.texture.clone(),
|
||||
};
|
||||
model.cursor = Some(CursorPayload::Action(payload));
|
||||
raise_cursor_update(model);
|
||||
true
|
||||
});
|
||||
if picked {
|
||||
verb(lua, |model| {
|
||||
model.actions.remove(&id);
|
||||
model.action_sets.push((id, 0));
|
||||
});
|
||||
}
|
||||
picked
|
||||
}
|
||||
|
||||
/// `PlaceAction(id)`, the slot's `OnReceiveDrag` and `UseAction(id, 1)`'s place fork. An occupied
|
||||
/// slot's action hops onto the cursor with `id` as its source; an item stays in its bag, since a
|
||||
/// bar item is a reference, not a move. Returns whether to repaint.
|
||||
pub(crate) fn place_action(model: &mut Model, id: u32) -> bool {
|
||||
/// bar item is a reference, not a move. The cursor goes first and the slot after, as `0x4e62e0`
|
||||
/// puts the displaced action on the cursor (or clears it) before it writes the slot, so the
|
||||
/// cursor's events read the slot as it was. Returns whether to repaint.
|
||||
pub(crate) fn place_action(lua: &Lua, id: u32) -> bool {
|
||||
let Some((kind, action, texture)) = verb(lua, |model| take_for_place(model, id)) else {
|
||||
return false;
|
||||
};
|
||||
verb(lua, |model| {
|
||||
model.actions.insert(
|
||||
id,
|
||||
ActionSlot {
|
||||
texture,
|
||||
kind,
|
||||
action,
|
||||
// Count and consumable wait for the app's next-frame re-feed, which knows the item.
|
||||
count: 0,
|
||||
consumable: false,
|
||||
},
|
||||
);
|
||||
model.action_sets.push((id, pack(kind, action)));
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
/// `PlaceAction`'s cursor half: the accept filters, then the held payload swapped for the slot's
|
||||
/// occupant. Returns what the slot takes, or `None` for a refusal that leaves the payload held.
|
||||
fn take_for_place(model: &mut Model, id: u32) -> Option<(u8, u32, Option<String>)> {
|
||||
// The two accept filters (`0x4e62e0`) refuse with a bare return: nothing stored or sent, and
|
||||
// the payload stays held. An item needs an on-use spell or an equip slot (`0x4e6571`), so a
|
||||
// bag is placeable; a passive spell (`Attributes & 0x40`) also raises errorId `0x9e` through
|
||||
// `DisplayError` (`0x4e63ad`, `0x496720`), where the item refusal is mute.
|
||||
match &model.cursor {
|
||||
Some(CursorPayload::Item(i)) if !i.bar_placeable => return false,
|
||||
Some(CursorPayload::Item(i)) if !i.bar_placeable => return None,
|
||||
Some(CursorPayload::Spell(s)) if s.passive => {
|
||||
model.ui_errors.push(PASSIVE_ON_BAR_ERROR);
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let Some(held) = model.cursor.take() else {
|
||||
return false;
|
||||
};
|
||||
let held = model.cursor.take()?;
|
||||
let placeable = match &held {
|
||||
CursorPayload::Action(a) => Some((a.kind, a.action, a.texture.clone())),
|
||||
CursorPayload::Item(i) => Some((ACTION_KIND_ITEM, i.item_id, i.texture.clone())),
|
||||
@@ -74,32 +112,20 @@ pub(crate) fn place_action(model: &mut Model, id: u32) -> bool {
|
||||
CursorPayload::Money(_) => None,
|
||||
CursorPayload::Merchant(_) => None,
|
||||
};
|
||||
let Some((kind, action, texture)) = placeable else {
|
||||
let Some(placed) = placeable else {
|
||||
model.cursor = Some(held);
|
||||
return false;
|
||||
return None;
|
||||
};
|
||||
let displaced = model.actions.insert(
|
||||
id,
|
||||
ActionSlot {
|
||||
texture,
|
||||
kind,
|
||||
action,
|
||||
// Count and consumable wait for the app's next-frame re-feed, which knows the item.
|
||||
count: 0,
|
||||
consumable: false,
|
||||
},
|
||||
);
|
||||
model.action_sets.push((id, pack(kind, action)));
|
||||
model.cursor = displaced.map(|d| {
|
||||
model.cursor = model.actions.get(&id).map(|d| {
|
||||
CursorPayload::Action(CursorAction {
|
||||
src_slot: id,
|
||||
kind: d.kind,
|
||||
action: d.action,
|
||||
texture: d.texture,
|
||||
texture: d.texture.clone(),
|
||||
})
|
||||
});
|
||||
queue_cursor_update(model);
|
||||
true
|
||||
raise_cursor_update(model);
|
||||
Some(placed)
|
||||
}
|
||||
|
||||
/// Register `PickupAction` and `PlaceAction` as globals.
|
||||
@@ -109,16 +135,14 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
g.set(
|
||||
"PickupAction",
|
||||
lua.create_function(|lua, id: u32| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
pickup_action(&mut model, id);
|
||||
pickup_action(lua, id);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"PlaceAction",
|
||||
lua.create_function(|lua, id: u32| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
place_action(&mut model, id);
|
||||
place_action(lua, id);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -147,7 +171,7 @@ mod tests {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_action(1, Some(action_slot("Interface\\Icons\\Spell_A", 0x00, 133)));
|
||||
|
||||
assert!(pickup_action(&mut s.model_mut(), 1));
|
||||
assert!(pickup_action(&s.lua, 1));
|
||||
assert!(
|
||||
!s.eval::<bool>("return HasAction(1)").unwrap(),
|
||||
"removed from the engine's optimistic mirror"
|
||||
@@ -160,10 +184,40 @@ mod tests {
|
||||
assert_eq!(s.take_action_sets(), vec![(1, 0)]);
|
||||
}
|
||||
|
||||
/// The cursor's events run before the slot write, as `0x4e6130` runs its setter before the
|
||||
/// slot clear `0x4e5db0` and `0x4e62e0` hops the displaced action before it writes: a handler
|
||||
/// reads each slot as it was.
|
||||
#[test]
|
||||
fn the_cursor_events_fire_before_the_slot_write() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_action(1, Some(action_slot("Interface\\Icons\\Spell_A", 0x00, 111)));
|
||||
s.set_action(2, Some(action_slot("Interface\\Icons\\Spell_B", 0x00, 222)));
|
||||
crate::script::tick::heard::listen(
|
||||
&s,
|
||||
&["CURSOR_UPDATE"],
|
||||
Some("tostring(GetActionTexture(1)) .. ' ' .. tostring(GetActionTexture(2))"),
|
||||
);
|
||||
s.run("PickupAction(1)").unwrap();
|
||||
assert_eq!(
|
||||
crate::script::tick::heard::take(&s),
|
||||
["CURSOR_UPDATE Interface\\Icons\\Spell_A Interface\\Icons\\Spell_B"]
|
||||
);
|
||||
assert!(!s.eval::<bool>("return HasAction(1)").unwrap());
|
||||
s.run("PlaceAction(2)").unwrap();
|
||||
assert_eq!(
|
||||
crate::script::tick::heard::take(&s),
|
||||
["CURSOR_UPDATE nil Interface\\Icons\\Spell_B"]
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GetActionTexture(2)").unwrap(),
|
||||
"Interface\\Icons\\Spell_A"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pickup_action_on_an_empty_slot_is_a_no_op() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
assert!(!pickup_action(&mut s.model_mut(), 5));
|
||||
assert!(!pickup_action(&s.lua, 5));
|
||||
assert!(s.cursor_payload().is_none());
|
||||
assert!(s.take_action_sets().is_empty());
|
||||
}
|
||||
@@ -178,7 +232,7 @@ mod tests {
|
||||
texture: Some("Interface\\Icons\\Spell_A".into()),
|
||||
}));
|
||||
|
||||
assert!(place_action(&mut s.model_mut(), 7));
|
||||
assert!(place_action(&s.lua, 7));
|
||||
assert!(s.cursor_payload().is_none(), "empty destination clears");
|
||||
assert!(s.eval::<bool>("return HasAction(7)").unwrap());
|
||||
assert_eq!(
|
||||
@@ -195,10 +249,10 @@ mod tests {
|
||||
s.set_action(1, Some(action_slot("Interface\\Icons\\Spell_A", 0x00, 111)));
|
||||
s.set_action(2, Some(action_slot("Interface\\Icons\\Spell_B", 0x00, 222)));
|
||||
|
||||
assert!(pickup_action(&mut s.model_mut(), 1));
|
||||
assert!(pickup_action(&s.lua, 1));
|
||||
assert_eq!(s.take_action_sets(), vec![(1, 0)]);
|
||||
|
||||
assert!(place_action(&mut s.model_mut(), 2));
|
||||
assert!(place_action(&s.lua, 2));
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GetActionTexture(2)").unwrap(),
|
||||
"Interface\\Icons\\Spell_A"
|
||||
@@ -230,7 +284,7 @@ mod tests {
|
||||
equip_slots: Vec::new(),
|
||||
}));
|
||||
|
||||
assert!(place_action(&mut s.model_mut(), 4));
|
||||
assert!(place_action(&s.lua, 4));
|
||||
assert!(s.cursor_payload().is_none());
|
||||
// 0x80<<24 | 117.
|
||||
assert_eq!(s.take_action_sets(), vec![(4, 0x8000_0000 | 117)]);
|
||||
@@ -251,7 +305,7 @@ mod tests {
|
||||
texture: Some("Interface\\Icons\\Spell_A".into()),
|
||||
}));
|
||||
|
||||
assert!(place_action(&mut s.model_mut(), 9));
|
||||
assert!(place_action(&s.lua, 9));
|
||||
assert_eq!(s.take_action_sets(), vec![(9, 133)]); // 0x00<<24 | 133
|
||||
}
|
||||
|
||||
@@ -259,7 +313,7 @@ mod tests {
|
||||
fn place_action_with_an_empty_cursor_is_a_no_op() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_action(1, Some(action_slot("Interface\\Icons\\Spell_A", 0x00, 111)));
|
||||
assert!(!place_action(&mut s.model_mut(), 1));
|
||||
assert!(!place_action(&s.lua, 1));
|
||||
assert!(s.eval::<bool>("return HasAction(1)").unwrap(), "untouched");
|
||||
assert!(s.take_action_sets().is_empty());
|
||||
}
|
||||
@@ -273,7 +327,7 @@ mod tests {
|
||||
action: 111,
|
||||
texture: Some("Interface\\Icons\\Spell_A".into()),
|
||||
}));
|
||||
assert!(pickup_action(&mut s.model_mut(), 5));
|
||||
assert!(pickup_action(&s.lua, 5));
|
||||
assert!(s.cursor_payload().is_none(), "placed onto the empty slot 5");
|
||||
assert!(s.eval::<bool>("return HasAction(5)").unwrap());
|
||||
assert_eq!(s.take_action_sets(), vec![(5, 111)]);
|
||||
@@ -329,7 +383,7 @@ mod tests {
|
||||
equip_slots: Vec::new(),
|
||||
}));
|
||||
|
||||
assert!(!place_action(&mut s.model_mut(), 4));
|
||||
assert!(!place_action(&s.lua, 4));
|
||||
assert!(!s.eval::<bool>("return HasAction(4)").unwrap(), "no store");
|
||||
assert!(s.take_action_sets().is_empty(), "no packet");
|
||||
assert!(
|
||||
@@ -354,7 +408,7 @@ mod tests {
|
||||
passive: true,
|
||||
}));
|
||||
|
||||
assert!(!place_action(&mut s.model_mut(), 4));
|
||||
assert!(!place_action(&s.lua, 4));
|
||||
assert!(!s.eval::<bool>("return HasAction(4)").unwrap());
|
||||
assert!(s.take_action_sets().is_empty());
|
||||
assert!(matches!(s.cursor_payload(), Some(CursorPayload::Spell(_))));
|
||||
@@ -380,7 +434,7 @@ mod tests {
|
||||
passive: false,
|
||||
}));
|
||||
|
||||
assert!(place_action(&mut s.model_mut(), 4));
|
||||
assert!(place_action(&s.lua, 4));
|
||||
assert!(s.eval::<bool>("return HasAction(4)").unwrap());
|
||||
assert!(s.take_ui_errors().is_empty());
|
||||
}
|
||||
@@ -400,7 +454,7 @@ mod tests {
|
||||
equip_slots: vec![20, 21, 22, 23],
|
||||
}));
|
||||
|
||||
assert!(place_action(&mut s.model_mut(), 4));
|
||||
assert!(place_action(&s.lua, 4));
|
||||
assert_eq!(s.take_action_sets(), vec![(4, 0x8000_0000u32 | 4496)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ use mlua::Lua;
|
||||
|
||||
use crate::script::binding_abi::flag;
|
||||
use crate::script::container::ContainerMove;
|
||||
use crate::script::tick::verb;
|
||||
use crate::script::Model;
|
||||
|
||||
use super::{queue_cursor_update, queue_lock_changed, CursorItem, CursorPayload, EQUIPMENT_BAG};
|
||||
use super::{raise_cursor_update, raise_lock_changed, CursorItem, CursorPayload, EQUIPMENT_BAG};
|
||||
|
||||
/// `PickupInventoryItem(id)` (`0x4c7300`), the doll slot's click, on the bag click's model: a
|
||||
/// fitting whole item moves in and the cursor clears, with no hop; a split or a misfit stays
|
||||
@@ -57,16 +58,17 @@ pub(super) fn pickup_inventory_item(model: &mut Model, id: u32) -> bool {
|
||||
match picked {
|
||||
Some(item) => {
|
||||
model.cursor = Some(CursorPayload::Item(item));
|
||||
queue_cursor_update(model);
|
||||
queue_lock_changed(model, EQUIPMENT_BAG, id);
|
||||
raise_cursor_update(model);
|
||||
raise_lock_changed(model);
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
// The cancel is `ClearCursor(1,1)` (`0x4c794d`): the unlock, then `CURSOR_UPDATE`.
|
||||
Some(CursorPayload::Item(held)) if held.bag == EQUIPMENT_BAG && held.slot == id => {
|
||||
queue_cursor_update(model);
|
||||
queue_lock_changed(model, held.bag, held.slot);
|
||||
raise_lock_changed(model);
|
||||
raise_cursor_update(model);
|
||||
true
|
||||
}
|
||||
Some(CursorPayload::Item(held)) => {
|
||||
@@ -82,8 +84,8 @@ pub(super) fn pickup_inventory_item(model: &mut Model, id: u32) -> bool {
|
||||
dst_slot: id,
|
||||
count: None,
|
||||
});
|
||||
queue_cursor_update(model);
|
||||
queue_lock_changed(model, held.bag, held.slot);
|
||||
raise_cursor_update(model);
|
||||
raise_lock_changed(model);
|
||||
true
|
||||
}
|
||||
// Mode 5, a buy into this slot: `0x4c78fd` sends `CMSG_BUY_ITEM_IN_SLOT` (`0x1a3`) and the
|
||||
@@ -99,7 +101,7 @@ pub(super) fn pickup_inventory_item(model: &mut Model, id: u32) -> bool {
|
||||
if let Some(entry) = entry {
|
||||
model.merchant_slot_buys.push((EQUIPMENT_BAG, id, entry));
|
||||
}
|
||||
queue_cursor_update(model);
|
||||
raise_cursor_update(model);
|
||||
true
|
||||
}
|
||||
Some(
|
||||
@@ -144,8 +146,8 @@ pub(super) fn auto_equip_cursor_item(model: &mut Model) -> bool {
|
||||
match model.cursor.take() {
|
||||
Some(CursorPayload::Item(item)) if item.bag >= 0 && item.count.is_none() => {
|
||||
model.container_autoequips.push((item.bag, item.slot));
|
||||
queue_cursor_update(model);
|
||||
queue_lock_changed(model, item.bag, item.slot);
|
||||
raise_cursor_update(model);
|
||||
raise_lock_changed(model);
|
||||
true
|
||||
}
|
||||
other => {
|
||||
@@ -156,11 +158,17 @@ pub(super) fn auto_equip_cursor_item(model: &mut Model) -> bool {
|
||||
}
|
||||
|
||||
/// `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);
|
||||
/// clears before the slot is read (`0x4c79a9`), so a held payload goes back, never placed, its
|
||||
/// events running first; 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(lua: &Lua, id: u32) {
|
||||
verb(lua, super::clear_cursor);
|
||||
verb(lua, |model| use_cleared(model, id));
|
||||
}
|
||||
|
||||
/// `UseInventoryItem` past its clear.
|
||||
fn use_cleared(model: &mut Model, id: u32) {
|
||||
if !model.repair_mode {
|
||||
model
|
||||
.script_calls
|
||||
@@ -195,16 +203,14 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
g.set(
|
||||
"PickupInventoryItem",
|
||||
lua.create_function(|lua, id: u32| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
pickup_inventory_item(&mut model, id);
|
||||
verb(lua, |model| pickup_inventory_item(model, id));
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"EquipCursorItem",
|
||||
lua.create_function(|lua, id: u32| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
equip_cursor_item(&mut model, id);
|
||||
verb(lua, |model| equip_cursor_item(model, id));
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -218,16 +224,14 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
g.set(
|
||||
"AutoEquipCursorItem",
|
||||
lua.create_function(|lua, ()| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
auto_equip_cursor_item(&mut model);
|
||||
verb(lua, auto_equip_cursor_item);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"UseInventoryItem",
|
||||
lua.create_function(|lua, id: u32| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
use_inventory_item(&mut model, id);
|
||||
use_inventory_item(lua, id);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
//! The money cursor, payload mode 2: coins taken off a money frame; the purse is never debited.
|
||||
//! Deviation: a `CURSOR_UPDATE` handler already sees the coins, because events are queued; the
|
||||
//! reference runs it inside the clear, before they land.
|
||||
|
||||
use mlua::{Lua, Value};
|
||||
|
||||
use super::{queue_cursor_update, CursorMoney, CursorPayload};
|
||||
use super::{raise_cursor_update, CursorMoney, CursorPayload};
|
||||
use crate::script::binding_abi::{flag, number_arg};
|
||||
use crate::script::tick::verb;
|
||||
use crate::script::Model;
|
||||
|
||||
/// The coin icon for an amount of copper: `GetCoinIcon`'s table (`0x48d4e0`), thresholds signed.
|
||||
@@ -33,32 +32,38 @@ pub(crate) fn cursor_money(model: &Model) -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue `PLAYER_MONEY`, which repaints the money frames less the held coins.
|
||||
fn queue_player_money(model: &mut Model) {
|
||||
model
|
||||
.pending_events
|
||||
.push(("PLAYER_MONEY".to_string(), Vec::new()));
|
||||
/// Raise `PLAYER_MONEY`, which repaints the money frames less the held coins.
|
||||
fn raise_player_money(model: &mut Model) {
|
||||
model.raise("PLAYER_MONEY", Vec::new());
|
||||
}
|
||||
|
||||
/// Put coins on the cursor as `0x494cc0` does: the inner clear's `CURSOR_UPDATE`, after its own
|
||||
/// `PLAYER_MONEY` if coins were already held, then the payload and `PLAYER_MONEY`.
|
||||
fn install_money(model: &mut Model, copper: u32) {
|
||||
if matches!(model.cursor, Some(CursorPayload::Money(_))) {
|
||||
queue_player_money(model);
|
||||
}
|
||||
model.cursor = None;
|
||||
queue_cursor_update(model);
|
||||
model.cursor = Some(CursorPayload::Money(CursorMoney { copper }));
|
||||
queue_player_money(model);
|
||||
/// Put coins on the cursor as `0x494cc0` does, in two steps. Its inner `ClearCursor(1,1)`
|
||||
/// (`0x494cd9`) drops coins already held with their `PLAYER_MONEY` (`0x495243`) and fires
|
||||
/// `CURSOR_UPDATE` (`0x4952cc`) on the empty cursor, so that handler sees no coins; then the coins
|
||||
/// land and `PLAYER_MONEY` fires (`0x494d0a`), with no second `CURSOR_UPDATE`.
|
||||
fn install_money(lua: &Lua, copper: u32) {
|
||||
verb(lua, |model| {
|
||||
if matches!(model.cursor.take(), Some(CursorPayload::Money(_))) {
|
||||
raise_player_money(model);
|
||||
}
|
||||
raise_cursor_update(model);
|
||||
});
|
||||
verb(lua, |model| {
|
||||
model.cursor = Some(CursorPayload::Money(CursorMoney { copper }));
|
||||
raise_player_money(model);
|
||||
});
|
||||
}
|
||||
|
||||
/// `PickupPlayerMoney` after the argument gate; each refusal is silent.
|
||||
pub(crate) fn pickup_player_money(model: &mut Model, copper: u32) -> bool {
|
||||
if copper == 0 || !model.player_control || u64::from(copper) > model.money {
|
||||
return false;
|
||||
pub(crate) fn pickup_player_money(lua: &Lua, copper: u32) -> bool {
|
||||
let allowed = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
copper != 0 && model.player_control && u64::from(copper) <= model.money
|
||||
};
|
||||
if allowed {
|
||||
install_money(lua, copper);
|
||||
}
|
||||
install_money(model, copper);
|
||||
true
|
||||
allowed
|
||||
}
|
||||
|
||||
/// `DropCursorMoney`: `PLAYER_MONEY`, then the clear's `CURSOR_UPDATE`, and no packet.
|
||||
@@ -67,8 +72,8 @@ pub(crate) fn drop_cursor_money(model: &mut Model) -> bool {
|
||||
return false;
|
||||
}
|
||||
model.cursor = None;
|
||||
queue_player_money(model);
|
||||
queue_cursor_update(model);
|
||||
raise_player_money(model);
|
||||
raise_cursor_update(model);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -91,24 +96,29 @@ pub(crate) fn add_trade_money(model: &mut Model) -> bool {
|
||||
}
|
||||
// `ClearCursor(1, 0)`: no PLAYER_MONEY, the clear's own CURSOR_UPDATE.
|
||||
model.cursor = None;
|
||||
queue_cursor_update(model);
|
||||
raise_cursor_update(model);
|
||||
true
|
||||
}
|
||||
|
||||
/// `PickupTradeMoney` after the argument gate: signed gates, then `CMSG_SET_TRADE_GOLD` (`0x11F`)
|
||||
/// with the trimmed offer, then the coins onto the cursor.
|
||||
pub(crate) fn pickup_trade_money(model: &mut Model, amount: i32) -> bool {
|
||||
let offer = model.trade.as_ref().map_or(0, |t| t.player.gold) as i32;
|
||||
if amount <= 0 || amount > offer {
|
||||
return false;
|
||||
pub(crate) fn pickup_trade_money(lua: &Lua, amount: i32) -> bool {
|
||||
let trimmed = verb(lua, |model| {
|
||||
let offer = model.trade.as_ref().map_or(0, |t| t.player.gold) as i32;
|
||||
if amount <= 0 || amount > offer {
|
||||
return false;
|
||||
}
|
||||
let left = (offer - amount) as u32;
|
||||
model.trade_set_money = Some(left);
|
||||
if let Some(t) = model.trade.as_mut() {
|
||||
t.player.gold = left;
|
||||
}
|
||||
true
|
||||
});
|
||||
if trimmed {
|
||||
install_money(lua, amount as u32);
|
||||
}
|
||||
let left = (offer - amount) as u32;
|
||||
model.trade_set_money = Some(left);
|
||||
if let Some(t) = model.trade.as_mut() {
|
||||
t.player.gold = left;
|
||||
}
|
||||
install_money(model, amount as u32);
|
||||
true
|
||||
trimmed
|
||||
}
|
||||
|
||||
impl crate::script::UiScript {
|
||||
@@ -139,24 +149,21 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"PickupPlayerMoney",
|
||||
lua.create_function(|lua, amount: Value| {
|
||||
let n = number_arg(lua, amount, "Usage: PickupPlayerMoney(amount)")? as u32;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
pickup_player_money(&mut model, n);
|
||||
pickup_player_money(lua, n);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"DropCursorMoney",
|
||||
lua.create_function(|lua, ()| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
drop_cursor_money(&mut model);
|
||||
verb(lua, drop_cursor_money);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"AddTradeMoney",
|
||||
lua.create_function(|lua, ()| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
add_trade_money(&mut model);
|
||||
verb(lua, add_trade_money);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -164,8 +171,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"PickupTradeMoney",
|
||||
lua.create_function(|lua, amount: Value| {
|
||||
let n = number_arg(lua, amount, "Usage: PickupTradeMoney(amount)")?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
pickup_trade_money(&mut model, n);
|
||||
pickup_trade_money(lua, n);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -183,18 +189,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
mod tests {
|
||||
use super::coin_icon;
|
||||
use crate::script::cursor::CursorPayload;
|
||||
use crate::script::tick::heard;
|
||||
use crate::script::UiScript;
|
||||
|
||||
fn events(s: &mut UiScript) -> Vec<String> {
|
||||
s.run(
|
||||
r#"EV = {} F = F or CreateFrame("Frame") F:RegisterEvent("PLAYER_MONEY")
|
||||
F:RegisterEvent("CURSOR_UPDATE") F:SetScript("OnEvent", function() table.insert(EV, event) end)"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.tick(0.0);
|
||||
s.eval::<Vec<String>>("return EV").unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coins_ride_the_cursor_without_leaving_the_purse() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
@@ -251,25 +248,75 @@ mod tests {
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The events fire inside each verb, in `0x494cc0`'s order: the inner clear's `CURSOR_UPDATE`
|
||||
/// reads an empty cursor, and only the install's `PLAYER_MONEY` reads the new coins.
|
||||
#[test]
|
||||
fn the_pickup_and_drop_fire_player_money_around_the_clear() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_money(1_000);
|
||||
let _ = events(&mut s);
|
||||
heard::listen(
|
||||
&s,
|
||||
&["PLAYER_MONEY", "CURSOR_UPDATE"],
|
||||
Some("GetCursorMoney()"),
|
||||
);
|
||||
s.run("PickupPlayerMoney(10)").unwrap();
|
||||
assert_eq!(
|
||||
events(&mut s),
|
||||
vec!["CURSOR_UPDATE", "PLAYER_MONEY"],
|
||||
heard::take(&s),
|
||||
["CURSOR_UPDATE 0", "PLAYER_MONEY 10"],
|
||||
"the inner clear's CURSOR_UPDATE, then the money's PLAYER_MONEY"
|
||||
);
|
||||
s.run("PickupPlayerMoney(20)").unwrap();
|
||||
assert_eq!(
|
||||
events(&mut s),
|
||||
vec!["PLAYER_MONEY", "CURSOR_UPDATE", "PLAYER_MONEY"],
|
||||
heard::take(&s),
|
||||
["PLAYER_MONEY 0", "CURSOR_UPDATE 0", "PLAYER_MONEY 20"],
|
||||
"coins already held: the mode-2 arm's PLAYER_MONEY first"
|
||||
);
|
||||
s.run("DropCursorMoney()").unwrap();
|
||||
assert_eq!(events(&mut s), vec!["PLAYER_MONEY", "CURSOR_UPDATE"]);
|
||||
assert_eq!(heard::take(&s), ["PLAYER_MONEY 0", "CURSOR_UPDATE 0"]);
|
||||
assert!(s.model_ref().pending_events.is_empty());
|
||||
}
|
||||
|
||||
/// A handler that calls another verb runs it to completion inside the dispatch, as the
|
||||
/// reference's synchronous `SignalEvent` nests: the mail verb's event lands between the
|
||||
/// pickup's two steps, and a pickup called from the install's `PLAYER_MONEY` runs both of its
|
||||
/// own steps before the outer verb returns. No step waits on a held model borrow.
|
||||
#[test]
|
||||
fn a_verb_called_from_a_handler_nests_inside_the_dispatch() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_money(1_000);
|
||||
let log: Vec<String> = s
|
||||
.eval(
|
||||
r#"LOG = {} NEST = 0
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("CURSOR_UPDATE")
|
||||
f:RegisterEvent("PLAYER_MONEY")
|
||||
f:RegisterEvent("SEND_MAIL_MONEY_CHANGED")
|
||||
f:SetScript("OnEvent", function()
|
||||
table.insert(LOG, event .. " " .. GetCursorMoney() .. " " .. GetSendMailMoney())
|
||||
if event == "CURSOR_UPDATE" and NEST == 0 then
|
||||
NEST = 1 SetSendMailMoney(5)
|
||||
elseif event == "PLAYER_MONEY" and NEST == 1 then
|
||||
NEST = 2 PickupPlayerMoney(20)
|
||||
end
|
||||
end)
|
||||
PickupPlayerMoney(10)
|
||||
return LOG"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
log,
|
||||
[
|
||||
"CURSOR_UPDATE 0 0",
|
||||
"SEND_MAIL_MONEY_CHANGED 0 5",
|
||||
"PLAYER_MONEY 10 5",
|
||||
"PLAYER_MONEY 0 5",
|
||||
"CURSOR_UPDATE 0 5",
|
||||
"PLAYER_MONEY 20 5",
|
||||
]
|
||||
);
|
||||
assert_eq!(s.eval::<i64>("return GetCursorMoney()").unwrap(), 20);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
assert!(s.model_ref().pending_events.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
|
||||
use mlua::Lua;
|
||||
|
||||
use crate::script::tick::verb;
|
||||
use crate::script::Model;
|
||||
|
||||
use super::{queue_cursor_update, CursorPayload, CursorPetAction};
|
||||
use super::{raise_cursor_update, CursorPayload, CursorPetAction};
|
||||
|
||||
/// Pet bar slots (`NUM_PET_ACTION_SLOTS`, vmangos `MAX_UNIT_ACTION_BAR_INDEX`). Deviation: slot
|
||||
/// 11 is refused, because the reference's gate (`cmp esi,0xa; jbe`) admits it and neither
|
||||
@@ -96,22 +97,64 @@ fn assign(slots: &mut [u32], target: usize, source: u32, passive: bool) -> Optio
|
||||
/// (`0x4be1f7`) or is `UNIT_FLAG_POSSESSED` (`0x4be20a`) blocks both ends, the gate sitting above
|
||||
/// the cursor fork, so the cursor is left as it was. Another payload stays held here, where the
|
||||
/// reference clears it (`0x4be220`) and picks the slot up. A pickup blanks only a spell slot
|
||||
/// (`0x4be268`), so a dragged token is copied, not moved.
|
||||
pub(super) fn pickup_pet_action(model: &mut Model, slot: u32) -> bool {
|
||||
/// (`0x4be268`), so a dragged token is copied, not moved; the blank comes after the setter's
|
||||
/// events (`0x494e20`), so they read the slot as it was. Returns whether to repaint.
|
||||
pub(super) fn pickup_pet_action(lua: &Lua, slot: u32) -> bool {
|
||||
let pick = verb(lua, |model| {
|
||||
let pick = pick_or_place(model, slot);
|
||||
// Both the pickup and a drop's displaced occupant can put a token on the cursor.
|
||||
resolve_token_icon(lua, model);
|
||||
pick
|
||||
});
|
||||
match pick {
|
||||
PetPick::Refused => false,
|
||||
PetPick::Dropping(held) => verb(lua, |model| {
|
||||
let wrote = place_pet_action(model, slot, &held);
|
||||
resolve_token_icon(lua, model);
|
||||
wrote
|
||||
}),
|
||||
PetPick::Picked(blank) => {
|
||||
if let Some((index, word)) = blank {
|
||||
verb(lua, |model| {
|
||||
write_slot(model, index, word, false);
|
||||
});
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What [`pickup_pet_action`]'s cursor half did.
|
||||
enum PetPick {
|
||||
Refused,
|
||||
/// A held pet action left the cursor, its clear signalled; the drop still owes its write.
|
||||
Dropping(CursorPetAction),
|
||||
/// The slot's word went on the cursor; a spell slot still owes its blank, `(index, word)`.
|
||||
Picked(Option<(usize, u32)>),
|
||||
}
|
||||
|
||||
fn pick_or_place(model: &mut Model, slot: u32) -> PetPick {
|
||||
if !model.pet_bar.pickup_allowed {
|
||||
return false;
|
||||
return PetPick::Refused;
|
||||
}
|
||||
match &model.cursor {
|
||||
Some(CursorPayload::PetAction(_)) => return place_pet_action(model, slot),
|
||||
Some(_) => return false,
|
||||
Some(CursorPayload::PetAction(held)) => {
|
||||
// `ClearCursor(1,1)` (`0x4be220`), unconditionally, before anything is decided: its
|
||||
// `PET_BAR_HIDEGRID` and `CURSOR_UPDATE` run before the bar write.
|
||||
let held = held.clone();
|
||||
model.cursor = None;
|
||||
raise_cursor_update(model);
|
||||
return PetPick::Dropping(held);
|
||||
}
|
||||
Some(_) => return PetPick::Refused,
|
||||
None => {}
|
||||
}
|
||||
let Some(index) = slot_index(model, slot) else {
|
||||
return false;
|
||||
return PetPick::Refused;
|
||||
};
|
||||
let view = model.pet_bar.slots[index].view.clone();
|
||||
let Some(packed) = payload_word(view.packed) else {
|
||||
return false;
|
||||
return PetPick::Refused;
|
||||
};
|
||||
|
||||
model.cursor = Some(CursorPayload::PetAction(CursorPetAction {
|
||||
@@ -120,43 +163,32 @@ pub(super) fn pickup_pet_action(model: &mut Model, slot: u32) -> bool {
|
||||
passive: view.passive,
|
||||
texture: view.texture.clone(),
|
||||
}));
|
||||
raise_cursor_update(model);
|
||||
// A spell slot keeps its type and autocast bits with id 0, and the server is told. `false` is
|
||||
// right: the passive gate reads the source's spell record, and id 0 has none.
|
||||
if kind(packed) == 1 {
|
||||
write_slot(model, index, packed & 0xFFFF_0000, false);
|
||||
}
|
||||
queue_cursor_update(model);
|
||||
true
|
||||
PetPick::Picked((kind(packed) == 1).then_some((index, packed & 0xFFFF_0000)))
|
||||
}
|
||||
|
||||
/// The drop: the cursor always empties, and only a homeless occupant refills it (`0x4bce3d`).
|
||||
/// Returns whether anything was written; the cursor empties on `false` too.
|
||||
fn place_pet_action(model: &mut Model, slot: u32) -> bool {
|
||||
let Some(CursorPayload::PetAction(held)) = model.cursor.clone() else {
|
||||
return false;
|
||||
};
|
||||
// `ClearCursor(1,1)`, unconditionally, before anything is decided.
|
||||
model.cursor = None;
|
||||
/// The drop past its clear: the bar write (`0x4bce00`), and only a homeless occupant refills the
|
||||
/// cursor (`0x4bce3d`), signalling as a pickup does. Returns whether anything was written.
|
||||
fn place_pet_action(model: &mut Model, slot: u32, held: &CursorPetAction) -> bool {
|
||||
let Some(index) = slot_index(model, slot) else {
|
||||
queue_cursor_update(model);
|
||||
return false;
|
||||
};
|
||||
// Read before the write: a displaced word's passive bit and icon are its slot's.
|
||||
let occupant_view = model.pet_bar.slots[index].view.clone();
|
||||
let Some(assigned) = write_slot(model, index, held.packed, held.passive) else {
|
||||
queue_cursor_update(model);
|
||||
return false;
|
||||
};
|
||||
|
||||
model.cursor = assigned.displaced.map(|packed| {
|
||||
CursorPayload::PetAction(CursorPetAction {
|
||||
if let Some(packed) = assigned.displaced {
|
||||
model.cursor = Some(CursorPayload::PetAction(CursorPetAction {
|
||||
src_slot: slot,
|
||||
packed,
|
||||
passive: occupant_view.passive,
|
||||
texture: occupant_view.texture.clone(),
|
||||
})
|
||||
});
|
||||
queue_cursor_update(model);
|
||||
}));
|
||||
raise_cursor_update(model);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -197,23 +229,19 @@ fn write_slot(model: &mut Model, target: usize, source: u32, passive: bool) -> O
|
||||
}
|
||||
|
||||
/// A token's texture is a global's name (`GetPetActionInfo`, `0x4bdc50`), which the bar resolves
|
||||
/// with `getglobal` (`PetActionBarFrame.lua:102`); the cursor needs the path, resolved here.
|
||||
fn resolve_token_icon(lua: &Lua) {
|
||||
let name = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
match &model.cursor {
|
||||
Some(CursorPayload::PetAction(p)) if is_token(p.packed) => p.texture.clone(),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let Some(name) = name else {
|
||||
/// with `getglobal` (`PetActionBarFrame.lua:102`); the cursor needs the path, resolved here. A raw
|
||||
/// global read touches no model state, so it runs inside the verb's segment, before its events.
|
||||
fn resolve_token_icon(lua: &Lua, model: &mut Model) {
|
||||
let Some(CursorPayload::PetAction(p)) = &mut model.cursor else {
|
||||
return;
|
||||
};
|
||||
let path: Option<String> = lua.globals().get(name.as_str()).unwrap_or_default();
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
if let Some(CursorPayload::PetAction(p)) = &mut model.cursor {
|
||||
p.texture = path;
|
||||
if !is_token(p.packed) {
|
||||
return;
|
||||
}
|
||||
let Some(name) = p.texture.clone() else {
|
||||
return;
|
||||
};
|
||||
p.texture = lua.globals().raw_get(name.as_str()).unwrap_or_default();
|
||||
}
|
||||
|
||||
/// Register the pet bar's one cursor global.
|
||||
@@ -221,12 +249,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
lua.globals().set(
|
||||
"PickupPetAction",
|
||||
lua.create_function(|lua, slot: u32| {
|
||||
{
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
pickup_pet_action(&mut model, slot);
|
||||
}
|
||||
// Both the pickup and a drop's displaced occupant can put a token on the cursor.
|
||||
resolve_token_icon(lua);
|
||||
pickup_pet_action(lua, slot);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -338,10 +361,44 @@ mod tests {
|
||||
s
|
||||
}
|
||||
|
||||
/// A drop clears first (`0x4be220`): its grid hide and `CURSOR_UPDATE` read the bar before the
|
||||
/// write (`0x4bce00`), and the displaced occupant's events after it. The written word is
|
||||
/// engine state, read here through a test global.
|
||||
#[test]
|
||||
fn a_drop_signals_its_clear_before_the_bar_write() {
|
||||
let s = bar_script();
|
||||
let word = s
|
||||
.lua
|
||||
.create_function(|lua, ()| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(model.pet_bar.slots[4].view.packed == CLAW)
|
||||
})
|
||||
.unwrap();
|
||||
s.lua.globals().set("SlotFiveIsClaw", word).unwrap();
|
||||
s.run("PickupPetAction(4)").unwrap();
|
||||
crate::script::tick::heard::listen(
|
||||
&s,
|
||||
&["PET_BAR_HIDEGRID", "CURSOR_UPDATE", "PET_BAR_SHOWGRID"],
|
||||
Some("SlotFiveIsClaw()"),
|
||||
);
|
||||
// Growl is displaced and rides the cursor out, signalling after the write.
|
||||
s.run("PickupPetAction(5)").unwrap();
|
||||
assert_eq!(
|
||||
crate::script::tick::heard::take(&s),
|
||||
[
|
||||
"PET_BAR_HIDEGRID false",
|
||||
"CURSOR_UPDATE false",
|
||||
"PET_BAR_SHOWGRID true",
|
||||
"CURSOR_UPDATE true"
|
||||
]
|
||||
);
|
||||
assert!(s.eval::<bool>("return SlotFiveIsClaw()").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picking_up_a_spell_blanks_its_slot_and_sends() {
|
||||
let mut s = bar_script();
|
||||
assert!(pickup_pet_action(&mut s.model_mut(), 4));
|
||||
assert!(pickup_pet_action(&s.lua, 4));
|
||||
assert!(
|
||||
matches!(s.cursor_payload(), Some(crate::script::CursorPayload::PetAction(c)) if c.src_slot == 4),
|
||||
"{:?}",
|
||||
@@ -389,7 +446,7 @@ mod tests {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_pet_actions(true, false, true, hunter_bar());
|
||||
|
||||
assert!(pickup_pet_action(&mut s.model_mut(), 4));
|
||||
assert!(pickup_pet_action(&s.lua, 4));
|
||||
assert!(
|
||||
matches!(s.cursor_payload(), Some(crate::script::CursorPayload::PetAction(c))
|
||||
if c.src_slot == 4 && c.packed == CLAW),
|
||||
@@ -441,7 +498,7 @@ mod tests {
|
||||
#[test]
|
||||
fn picking_up_a_token_leaves_the_slot_alone() {
|
||||
let mut s = bar_script();
|
||||
assert!(pickup_pet_action(&mut s.model_mut(), 1));
|
||||
assert!(pickup_pet_action(&s.lua, 1));
|
||||
assert!(
|
||||
s.take_pet_set_actions().is_empty(),
|
||||
"nothing was written, so nothing is sent"
|
||||
@@ -507,8 +564,8 @@ mod tests {
|
||||
#[test]
|
||||
fn slot_eleven_is_refused_rather_than_written_past_the_array() {
|
||||
let mut s = bar_script();
|
||||
assert!(!pickup_pet_action(&mut s.model_mut(), 11));
|
||||
assert!(!pickup_pet_action(&mut s.model_mut(), 0));
|
||||
assert!(!pickup_pet_action(&s.lua, 11));
|
||||
assert!(!pickup_pet_action(&s.lua, 0));
|
||||
assert!(s.take_pet_set_actions().is_empty());
|
||||
assert!(s.cursor_payload().is_none());
|
||||
}
|
||||
@@ -518,7 +575,7 @@ mod tests {
|
||||
fn a_possessed_bar_takes_no_drag_at_all() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_pet_actions(true, true, false, hunter_bar());
|
||||
assert!(!pickup_pet_action(&mut s.model_mut(), 4));
|
||||
assert!(!pickup_pet_action(&s.lua, 4));
|
||||
assert!(s.take_pet_set_actions().is_empty());
|
||||
}
|
||||
|
||||
@@ -559,7 +616,7 @@ mod tests {
|
||||
"the action bar's grid stays down — nothing can land there"
|
||||
);
|
||||
// PlaceAction refuses it outright and leaves it held.
|
||||
assert!(!place_action(&mut s.model_mut(), 1));
|
||||
assert!(!place_action(&s.lua, 1));
|
||||
assert!(matches!(
|
||||
s.cursor_payload(),
|
||||
Some(crate::script::CursorPayload::PetAction(_))
|
||||
|
||||
Reference in New Issue
Block a user