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
@@ -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