RegisterEvent ignores case; a frame's first resize precedes OnLoad (#1001)
- `RegisterEvent` and `UnregisterEvent` match a name against the client's event table case-insensitively and drop a name that isn't in it (`0x702140`). A new gate checks that every event name the code fires is in the table. - A visible XML frame resolves its layout at the post-load hook (`0x76a310` → `0x7680e0(1)`). So its first `OnSizeChanged` fires before its `<Frames>`, `OnLoad` and `OnShow`, as in 1.12.1. A hidden frame still waits for its show. - `pending_events`' doc now says what the queue is: `SignalEvent` (`0x703e50`) runs handlers in place, and the Lua verbs that still queue are a gap. - #890 was already fixed by #907; two stale comments around it are corrected. Fixes #959 Fixes #972 Fixes #560 Closes #890 🤖 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
720dcf8188
commit
0f505a2ebf
@@ -90,9 +90,9 @@ fn error_message_is_the_installed_handler_and_the_host_channel_stays_sighted() {
|
||||
"reported"
|
||||
);
|
||||
// An engine-caught error still lands in `errors()`; the dispatch adds the dialog on top.
|
||||
s.run("BrokenProbe = CreateFrame('Frame') BrokenProbe:RegisterEvent('ERROR_PROBE') BrokenProbe:SetScript('OnEvent', function() error('fault') end)")
|
||||
s.run("BrokenProbe = CreateFrame('Frame') BrokenProbe:RegisterEvent('PLAYER_ALIVE') BrokenProbe:SetScript('OnEvent', function() error('fault') end)")
|
||||
.unwrap();
|
||||
s.fire_event("ERROR_PROBE", vec![]);
|
||||
s.fire_event("PLAYER_ALIVE", vec![]);
|
||||
assert!(
|
||||
s.errors().iter().any(|e| e.contains("fault")),
|
||||
"engine-caught errors stay on the host channel: {:?}",
|
||||
|
||||
@@ -253,6 +253,38 @@ fn every_event_we_fire_carries_the_reference_s_arguments() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The in-game event table (`0x703d90`, name array `0xbe1198`) holds the names of
|
||||
/// `reference/1.12-event-catalog.tsv` and the glue table the 26 of `GLUE_EVENTS`; `RegisterEvent`
|
||||
/// drops any other name (`0x702140`), so an event fired under a name off both reaches no frame
|
||||
/// that registered it by name.
|
||||
#[test]
|
||||
fn every_event_we_fire_is_in_the_reference_table() {
|
||||
let tsv = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../reference/1.12-event-catalog.tsv"
|
||||
);
|
||||
let text = std::fs::read_to_string(tsv).expect("reference/1.12-event-catalog.tsv");
|
||||
let names: std::collections::BTreeSet<&str> = text
|
||||
.lines()
|
||||
.filter(|l| !l.starts_with('#'))
|
||||
.filter_map(|l| l.split_once('\t').map(|(_, n)| n))
|
||||
.chain(benilla_ui::script::GLUE_EVENTS)
|
||||
.collect();
|
||||
assert!(names.len() > 390, "the catalog stopped being read");
|
||||
let mut off: Vec<String> = fire_sites()
|
||||
.into_iter()
|
||||
.filter(|(name, _, _)| !names.contains(name.as_str()))
|
||||
.map(|(name, _, at)| format!("{name} from {at}"))
|
||||
.collect();
|
||||
off.sort();
|
||||
off.dedup();
|
||||
assert!(
|
||||
off.is_empty(),
|
||||
"fired under a name the reference's event table lacks:\n {}",
|
||||
off.join("\n ")
|
||||
);
|
||||
}
|
||||
|
||||
/// `0x51bbb0` registers one watch per named unit-window field, so each of the 25 named event ids
|
||||
/// below `0xb6` comes from that bridge with the unit token, and each needs a producer here.
|
||||
#[test]
|
||||
|
||||
@@ -2775,6 +2775,87 @@ mod post_load_show_tests {
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The pre-load hook defers the frame's layout (`0x7697cc`) and the post-load hook undefers a
|
||||
/// visible one first of all (`0x76a310` → `0x768060(0)` → `0x7680e0(1)`), so its rect applies
|
||||
/// and fires `OnSizeChanged` (`0x76b580`) before its `<Frames>` are built (`0x76a36f`), its
|
||||
/// `OnLoad` (`0x76a387`) and its `OnShow` (`0x76a3b3`); a child's own hook does the same
|
||||
/// inside, so the order is parent size, child size, child load, parent load.
|
||||
#[test]
|
||||
fn a_visible_frames_first_onsizechanged_fires_before_its_children_and_onload() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.run("LOG = {} function Log(tag) table.insert(LOG, tag) end")
|
||||
.unwrap();
|
||||
let doc = parse(
|
||||
r#"<Ui>
|
||||
<Frame name="SizedBorn">
|
||||
<Size><AbsDimension x="100" y="50"/></Size>
|
||||
<Anchors><Anchor point="CENTER"/></Anchors>
|
||||
<Scripts>
|
||||
<OnSizeChanged>Log(SizedBornChild and "size+child" or "size")</OnSizeChanged>
|
||||
<OnLoad>Log("load")</OnLoad>
|
||||
<OnShow>Log("show")</OnShow>
|
||||
</Scripts>
|
||||
<Frames>
|
||||
<Frame name="SizedBornChild">
|
||||
<Size><AbsDimension x="10" y="10"/></Size>
|
||||
<Anchors><Anchor point="CENTER"/></Anchors>
|
||||
<Scripts>
|
||||
<OnSizeChanged>Log("child-size")</OnSizeChanged>
|
||||
<OnLoad>Log("child-load")</OnLoad>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Frames>
|
||||
</Frame>
|
||||
</Ui>"#,
|
||||
);
|
||||
let report = load_in(&s, &doc, "Test.xml", &no_files);
|
||||
assert!(report.errors.is_empty(), "{:?}", report.errors);
|
||||
let at_load = s.eval::<String>("return table.concat(LOG, ',')").unwrap();
|
||||
s.resolve();
|
||||
let after_drain = s.eval::<String>("return table.concat(LOG, ',')").unwrap();
|
||||
assert_eq!(
|
||||
at_load, "size,child-size,child-load,load,show",
|
||||
"fired inside the load, not at the next drain"
|
||||
);
|
||||
assert_eq!(after_drain, at_load, "and not again");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A frame hidden at the end of its load stays deferred (`0x76a2f0` undefers only a visible
|
||||
/// one), so its first `OnSizeChanged` is its first show's, ahead of its `OnShow`.
|
||||
#[test]
|
||||
fn a_hidden_frames_first_onsizechanged_waits_for_its_show() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.run("LOG = {} function Log(tag) table.insert(LOG, tag) end")
|
||||
.unwrap();
|
||||
let doc = parse(
|
||||
r#"<Ui>
|
||||
<Frame name="SizedHidden" hidden="true">
|
||||
<Size><AbsDimension x="100" y="50"/></Size>
|
||||
<Anchors><Anchor point="CENTER"/></Anchors>
|
||||
<Scripts>
|
||||
<OnSizeChanged>Log("size")</OnSizeChanged>
|
||||
<OnLoad>Log("load")</OnLoad>
|
||||
<OnShow>Log("show")</OnShow>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>"#,
|
||||
);
|
||||
let report = load_in(&s, &doc, "Test.xml", &no_files);
|
||||
assert!(report.errors.is_empty(), "{:?}", report.errors);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return table.concat(LOG, ',')").unwrap(),
|
||||
"load"
|
||||
);
|
||||
s.run("SizedHidden:Show()").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return table.concat(LOG, ',')").unwrap(),
|
||||
"load,size,show"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The post-load OnShow is keyed on the frame's visibility at `0x76a38c`, and nothing records
|
||||
/// an OnShow already given: a frame first shown by its own OnLoad gets that `Show()`'s OnShow
|
||||
/// and then the hook's, while a frame still hidden gets none.
|
||||
|
||||
@@ -57,21 +57,45 @@ pub const GLUE_EVENTS: [&str; 26] = [
|
||||
"PLAYER_ENTER_PIN",
|
||||
];
|
||||
|
||||
/// The in-game event table: the 549-slot name array `UI_Init` loads (`0x703d90` at `0x48feab`),
|
||||
/// `reference/1.12-event-catalog.tsv`. The 130 slots the client never writes stay nameless, and
|
||||
/// `0x703d90` skips a null name (`0x703e0c`), so the 419 names here are all it holds.
|
||||
const IN_GAME_EVENT_TABLE: &str = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../reference/1.12-event-catalog.tsv"
|
||||
));
|
||||
|
||||
/// The in-game table's distinct names, in id order.
|
||||
pub(crate) fn in_game_events() -> &'static [&'static str] {
|
||||
static NAMES: std::sync::LazyLock<Vec<&'static str>> = std::sync::LazyLock::new(|| {
|
||||
let mut names: Vec<&'static str> = Vec::new();
|
||||
for line in IN_GAME_EVENT_TABLE.lines() {
|
||||
// `id<TAB>NAME`; a name two slots share (`UNIT_PET`) is one event to a name match.
|
||||
let Some((_, name)) = line.split_once('\t') else {
|
||||
continue;
|
||||
};
|
||||
if !line.starts_with('#') && !names.contains(&name) {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
names
|
||||
});
|
||||
&NAMES
|
||||
}
|
||||
|
||||
impl VmKind {
|
||||
/// The event table's own spelling of `name` for `RegisterEvent`/`UnregisterEvent` (`0x702140`,
|
||||
/// `0x702280`), or `None` for a name the table lacks, which registers nothing.
|
||||
///
|
||||
/// The in-game VM answers every name as given: benilla's in-game events register by name and
|
||||
/// carry no table, so a name off the reference's table registers and never fires, and only the
|
||||
/// exact spelling matches.
|
||||
pub(crate) fn event_key(self, name: &str) -> Option<&str> {
|
||||
match self {
|
||||
VmKind::InGame => Some(name),
|
||||
VmKind::Glue => GLUE_EVENTS
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|e| e.eq_ignore_ascii_case(name)),
|
||||
}
|
||||
/// The reference compares the name with every table entry's, ASCII case folded (`0x64a4c0` →
|
||||
/// `0x414310`), and a miss returns without an error. A name two slots share registers on both,
|
||||
/// which a name-keyed listener list is one entry for.
|
||||
pub(crate) fn event_key(self, name: &str) -> Option<&'static str> {
|
||||
let table: &'static [&'static str] = match self {
|
||||
VmKind::InGame => in_game_events(),
|
||||
VmKind::Glue => &GLUE_EVENTS,
|
||||
};
|
||||
table.iter().copied().find(|e| e.eq_ignore_ascii_case(name))
|
||||
}
|
||||
|
||||
/// Whether the host may fire `event` into this VM. The reference fires by id
|
||||
|
||||
@@ -1019,8 +1019,21 @@ pub(crate) struct Model {
|
||||
pub(crate) chat_bubbles: super::chat_bubble::ChatBubbles,
|
||||
/// The world-state readout's rows, gated and resolved by the app.
|
||||
pub(crate) worldstate: super::worldstate::WorldStateUiState,
|
||||
/// Events bindings raise. Deviation: fired at the next tick, not synchronously as in the
|
||||
/// reference, because a binding runs inside Lua and cannot re-enter handler dispatch.
|
||||
/// Events raised outside a dispatch, fired at the start of the next tick ahead of its `OnUpdate`
|
||||
/// pass (`UiScript::tick`). The reference queues nothing here: a fire site calls
|
||||
/// `SignalEvent` (`0x703e50`, `0x703f50`), which runs every handler before it returns, so a
|
||||
/// verb's event reaches its listeners inside the call.
|
||||
///
|
||||
/// A binding can do the same: `tick::fire_event_into` re-enters dispatch from inside Lua, as
|
||||
/// `SetCVar`'s `CVAR_UPDATE` does, once the binding holds no model borrow. So the queue is no
|
||||
/// limit, and a Lua verb that pushes here instead is a gap against the reference: the cursor
|
||||
/// verbs' `CURSOR_UPDATE`, grid, `ITEM_LOCK_CHANGED`, `DELETE_ITEM_CONFIRM` and
|
||||
/// `PLAYER_MONEY`, the send-mail verbs' events, `SetGuildRecruitmentMode`'s
|
||||
/// `UPDATE_CHAT_WINDOWS`, `SetBattlefieldScoreFaction`'s `UPDATE_BATTLEFIELD_SCORE`,
|
||||
/// `UpdateInventoryAlertStatus`'s `UPDATE_INVENTORY_ALERTS`, `CancelAreaSpiritHeal`'s
|
||||
/// `AREA_SPIRIT_HEALER_OUT_OF_RANGE` and the trainer verbs' `TRAINER_UPDATE`. The host's own
|
||||
/// setters (the world map's, the world-state rows, a dropped mail attachment) and
|
||||
/// `UiScript::queue_event` have no verb to fire inside and queue by design.
|
||||
pub(crate) pending_events: Vec<(String, Vec<ScriptValue>)>,
|
||||
/// The last cursor position in UI space (logical px, y-up), behind `GetCursorPosition()`.
|
||||
pub(crate) cursor_pos: (f32, f32),
|
||||
@@ -2082,24 +2095,35 @@ impl Model {
|
||||
|
||||
/// The post-load hook's step (`0x76a2f0`): a visible frame undefers, with its title region and
|
||||
/// each region it draws (`0x76a310`, `0x76a324`, `0x76a35b`). A hidden one stays deferred.
|
||||
pub(crate) fn layout_load_end(&mut self, h: FrameHandle) {
|
||||
/// Whether any owes a resolve now, as [`Self::layout_on_show_frame`]; the caller runs it
|
||||
/// before the frame's `<Frames>` are built.
|
||||
pub(crate) fn layout_load_end(&mut self, h: FrameHandle) -> bool {
|
||||
let Some(f) = self.arena.frame(h) else {
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
if !f.effective_visible {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
let title = f.title_region;
|
||||
let regions: Vec<RegionHandle> = f.regions.clone();
|
||||
self.undefer_frame(h);
|
||||
let mut owed = self.undefer_frame(h);
|
||||
if let Some(t) = title {
|
||||
self.undefer_region(t);
|
||||
owed |= self.undefer_region(t);
|
||||
}
|
||||
for rh in regions {
|
||||
if Some(rh) != title && self.region_draws(rh) {
|
||||
self.undefer_region(rh);
|
||||
owed |= self.undefer_region(rh);
|
||||
}
|
||||
}
|
||||
owed || !self.layout_pass.is_empty()
|
||||
}
|
||||
|
||||
/// The layout ids [`Self::layout_load_end`] undeferred: the frame, its title region and the
|
||||
/// regions it draws.
|
||||
pub(crate) fn load_end_roots(&self, h: FrameHandle) -> Vec<u32> {
|
||||
let mut out = self.show_frame_roots(h);
|
||||
out.extend(self.show_region_roots(h));
|
||||
out
|
||||
}
|
||||
|
||||
/// The region's own shown bit (`+0xc4`): not hidden, and linked to its owner.
|
||||
|
||||
@@ -643,7 +643,8 @@ pub(super) fn create_frame(
|
||||
// The builder's pre-load and post-load hooks (`0x6ee4d6`, `0x6ee4ea`): a frame born hidden
|
||||
// is deferred, as a hidden parent leaves it. A template's load opens its own window.
|
||||
model.layout_load_begin(h);
|
||||
model.layout_load_end(h);
|
||||
// No handler is bound yet, so the resolve this owes fires nothing.
|
||||
let _ = model.layout_load_end(h);
|
||||
id
|
||||
};
|
||||
|
||||
|
||||
@@ -47,12 +47,12 @@ fn globals_are_restored_after_firing_nesting_safe() {
|
||||
r#"
|
||||
this, event, arg1 = "outer_this", "outer_event", "outer_arg1"
|
||||
local f = CreateFrame("Frame", "NF")
|
||||
f:RegisterEvent("E")
|
||||
f:RegisterEvent("PLAYER_LOGIN")
|
||||
f:SetScript("OnEvent", function() end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.fire_event("E", vec![ScriptValue::Str("x".into())]);
|
||||
s.fire_event("PLAYER_LOGIN", vec![ScriptValue::Str("x".into())]);
|
||||
// `0x704f10` sets the globals for the handler and restores the prior values after.
|
||||
let (t, e, a): (String, String, String) = s.eval("return this, event, arg1").unwrap();
|
||||
assert_eq!(
|
||||
@@ -67,12 +67,12 @@ fn handler_errors_are_collected_not_panicked() {
|
||||
s.run(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "BoomF")
|
||||
f:RegisterEvent("E")
|
||||
f:RegisterEvent("PLAYER_LOGIN")
|
||||
f:SetScript("OnEvent", function() error("boom") end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.fire_event("E", vec![]);
|
||||
s.fire_event("PLAYER_LOGIN", vec![]);
|
||||
let errs = s.errors();
|
||||
assert_eq!(errs.len(), 1, "{errs:?}");
|
||||
assert!(errs[0].contains("boom"), "{errs:?}");
|
||||
@@ -89,28 +89,67 @@ fn events_fire_in_registration_order_fifo() {
|
||||
local a = CreateFrame("Frame", "FA")
|
||||
local b = CreateFrame("Frame", "FB")
|
||||
local c = CreateFrame("Frame", "FC")
|
||||
a:RegisterEvent("E"); b:RegisterEvent("E"); c:RegisterEvent("E")
|
||||
a:RegisterEvent("PLAYER_LOGIN"); b:RegisterEvent("PLAYER_LOGIN"); c:RegisterEvent("PLAYER_LOGIN")
|
||||
a:SetScript("OnEvent", function() order = order .. "A" end)
|
||||
b:SetScript("OnEvent", function() order = order .. "B" end)
|
||||
c:SetScript("OnEvent", function() order = order .. "C" end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.fire_event("E", vec![]);
|
||||
s.fire_event("PLAYER_LOGIN", vec![]);
|
||||
assert_eq!(s.eval::<String>("return order").unwrap(), "ABC");
|
||||
|
||||
// A duplicate registration keeps A's position.
|
||||
s.run("FA:RegisterEvent('E'); order = ''").unwrap();
|
||||
s.fire_event("E", vec![]);
|
||||
s.run("FA:RegisterEvent('PLAYER_LOGIN'); order = ''")
|
||||
.unwrap();
|
||||
s.fire_event("PLAYER_LOGIN", vec![]);
|
||||
assert_eq!(s.eval::<String>("return order").unwrap(), "ABC");
|
||||
|
||||
// Unregistering frees B's node, so re-registering appends it at the tail.
|
||||
s.run("FB:UnregisterEvent('E'); FB:RegisterEvent('E'); order = ''")
|
||||
s.run("FB:UnregisterEvent('PLAYER_LOGIN'); FB:RegisterEvent('PLAYER_LOGIN'); order = ''")
|
||||
.unwrap();
|
||||
s.fire_event("E", vec![]);
|
||||
s.fire_event("PLAYER_LOGIN", vec![]);
|
||||
assert_eq!(s.eval::<String>("return order").unwrap(), "ACB");
|
||||
}
|
||||
|
||||
/// `RegisterEvent` (`0x774a40` → `0x702140`) walks the in-game event table with an ASCII
|
||||
/// case-folding compare (`0x64a4c0` → `0x414310`): a name in any case registers that event, the
|
||||
/// handler's `event` is the table's spelling, and a name the table lacks matches nothing and
|
||||
/// raises nothing. `UnregisterEvent` (`0x702280`) compares the same way.
|
||||
#[test]
|
||||
fn register_event_matches_the_table_case_insensitively_and_drops_unknown_names() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
heard = {}
|
||||
f = CreateFrame("Frame", "CaseF")
|
||||
f:SetScript("OnEvent", function() table.insert(heard, event) end)
|
||||
f:RegisterEvent("player_login")
|
||||
f:RegisterEvent("Not_An_Event")
|
||||
g = CreateFrame("Frame", "CaseG")
|
||||
g:SetScript("OnEvent", function() table.insert(heard, "g:" .. event) end)
|
||||
g:RegisterEvent("UNIT_health")
|
||||
"#,
|
||||
)
|
||||
.expect("a name off the table raises nothing");
|
||||
s.fire_event("PLAYER_LOGIN", vec![]);
|
||||
s.fire_event("Not_An_Event", vec![]);
|
||||
s.fire_event("UNIT_HEALTH", vec![]);
|
||||
assert_eq!(
|
||||
s.eval::<Vec<String>>("return heard").unwrap(),
|
||||
["PLAYER_LOGIN", "g:UNIT_HEALTH"],
|
||||
"the table's spelling registers and is what the handler reads; the unknown name never fires"
|
||||
);
|
||||
|
||||
s.run(r#"heard = {} f:UnregisterEvent("Player_Login")"#)
|
||||
.unwrap();
|
||||
s.fire_event("PLAYER_LOGIN", vec![]);
|
||||
assert!(
|
||||
s.eval::<Vec<String>>("return heard").unwrap().is_empty(),
|
||||
"UnregisterEvent matches in any case too"
|
||||
);
|
||||
}
|
||||
|
||||
/// Callers ask before hooking, when nothing is set yet (`Tablet-2.0.lua:2409`). Our kind table is
|
||||
/// flat where the reference's is per widget type, so a Frame also answers true for `OnClick`.
|
||||
#[test]
|
||||
@@ -163,16 +202,16 @@ fn a_self_unregistering_handler_does_not_rob_its_successor() {
|
||||
log = {}
|
||||
for _, n in ipairs({"WalkA", "WalkB", "WalkC"}) do
|
||||
local f = CreateFrame("Frame", n)
|
||||
f:RegisterEvent("E")
|
||||
f:RegisterEvent("PLAYER_LOGIN")
|
||||
f:SetScript("OnEvent", function()
|
||||
table.insert(log, n)
|
||||
if n == "WalkB" then WalkB:UnregisterEvent("E") end
|
||||
if n == "WalkB" then WalkB:UnregisterEvent("PLAYER_LOGIN") end
|
||||
end)
|
||||
end
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.fire_event("E", vec![]);
|
||||
s.fire_event("PLAYER_LOGIN", vec![]);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
let log: Vec<String> = s.eval("return log").unwrap();
|
||||
assert_eq!(
|
||||
@@ -180,7 +219,7 @@ fn a_self_unregistering_handler_does_not_rob_its_successor() {
|
||||
vec!["WalkA", "WalkB", "WalkC"],
|
||||
"the once-idiom's self-removal must not skip the next listener"
|
||||
);
|
||||
s.fire_event("E", vec![]);
|
||||
s.fire_event("PLAYER_LOGIN", vec![]);
|
||||
let log: Vec<String> = s.eval("return log").unwrap();
|
||||
assert_eq!(log, vec!["WalkA", "WalkB", "WalkC", "WalkA", "WalkC"]);
|
||||
}
|
||||
@@ -195,19 +234,19 @@ fn mid_dispatch_removal_of_the_next_stops_and_append_is_visited() {
|
||||
log = {}
|
||||
local function reg(n, body)
|
||||
local f = CreateFrame("Frame", n)
|
||||
f:RegisterEvent("E2")
|
||||
f:RegisterEvent("PLAYER_LOGOUT")
|
||||
f:SetScript("OnEvent", function() table.insert(log, n); if body then body() end end)
|
||||
return f
|
||||
end
|
||||
reg("NxA", function()
|
||||
NxB:UnregisterEvent("E2") -- kill the walk's saved next
|
||||
NxB:UnregisterEvent("PLAYER_LOGOUT") -- kill the walk's saved next
|
||||
end)
|
||||
reg("NxB")
|
||||
reg("NxC")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.fire_event("E2", vec![]);
|
||||
s.fire_event("PLAYER_LOGOUT", vec![]);
|
||||
let log: Vec<String> = s.eval("return log").unwrap();
|
||||
assert_eq!(
|
||||
log,
|
||||
@@ -220,19 +259,19 @@ fn mid_dispatch_removal_of_the_next_stops_and_append_is_visited() {
|
||||
log = {}
|
||||
local function reg(n, body)
|
||||
local f = CreateFrame("Frame", n)
|
||||
f:RegisterEvent("E3")
|
||||
f:RegisterEvent("PLAYER_ALIVE")
|
||||
f:SetScript("OnEvent", function() table.insert(log, n); if body then body() end end)
|
||||
end
|
||||
reg("ApA", function()
|
||||
local f = CreateFrame("Frame", "ApLate")
|
||||
f:RegisterEvent("E3")
|
||||
f:RegisterEvent("PLAYER_ALIVE")
|
||||
f:SetScript("OnEvent", function() table.insert(log, "ApLate") end)
|
||||
end)
|
||||
reg("ApB")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.fire_event("E3", vec![]);
|
||||
s.fire_event("PLAYER_ALIVE", vec![]);
|
||||
let log: Vec<String> = s.eval("return log").unwrap();
|
||||
assert_eq!(
|
||||
log,
|
||||
|
||||
@@ -359,7 +359,8 @@ fn glue_events_register_by_the_glue_table() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The in-game VM keeps its by-name registry: an in-game event registers and fires there.
|
||||
/// The in-game VM registers by its own event table (`0x48feab`): an in-game event registers and
|
||||
/// fires there.
|
||||
#[test]
|
||||
fn the_in_game_vm_keeps_its_events() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -261,11 +261,11 @@ fn the_engine_reads_the_handler_slot_not_the_geterrorhandler_global() {
|
||||
seterrorhandler(function(m) table.insert(Heard, m) end) \
|
||||
geterrorhandler = function() return function(m) table.insert(Decoy, m) end end \
|
||||
local f = CreateFrame('Frame') \
|
||||
f:RegisterEvent('ERROR_PROBE') \
|
||||
f:RegisterEvent('PLAYER_ALIVE') \
|
||||
f:SetScript('OnEvent', function() error('slot probe') end)",
|
||||
)
|
||||
.unwrap();
|
||||
s.fire_event("ERROR_PROBE", vec![]);
|
||||
s.fire_event("PLAYER_ALIVE", vec![]);
|
||||
s.dispatch_script_errors_to_handler();
|
||||
assert_eq!(
|
||||
s.eval::<(i64, i64)>("return table.getn(Heard), table.getn(Decoy)")
|
||||
|
||||
@@ -188,13 +188,13 @@ fn a_chosen_error_handler_hears_engine_caught_errors_and_none_leaves_the_host_ch
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.run(
|
||||
"local f = CreateFrame('Frame') \
|
||||
f:RegisterEvent('ERROR_PROBE') \
|
||||
f:RegisterEvent('PLAYER_ALIVE') \
|
||||
f:SetScript('OnEvent', function() error('boom from a handler') end)",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// With no handler set, as before FrameXML sets one, the dispatch adds nothing.
|
||||
s.fire_event("ERROR_PROBE", vec![]);
|
||||
s.fire_event("PLAYER_ALIVE", vec![]);
|
||||
s.dispatch_script_errors_to_handler();
|
||||
let errors = s.take_errors();
|
||||
assert_eq!(
|
||||
@@ -206,7 +206,7 @@ fn a_chosen_error_handler_hears_engine_caught_errors_and_none_leaves_the_host_ch
|
||||
// A chosen handler gets the message, and the host channel still records it.
|
||||
s.run("caught = {} seterrorhandler(function(msg) table.insert(caught, msg) end)")
|
||||
.unwrap();
|
||||
s.fire_event("ERROR_PROBE", vec![]);
|
||||
s.fire_event("PLAYER_ALIVE", vec![]);
|
||||
s.dispatch_script_errors_to_handler();
|
||||
assert!(
|
||||
s.eval::<String>("return caught[1]")
|
||||
@@ -257,7 +257,7 @@ fn the_error_handler_receives_the_bare_lua_message() {
|
||||
s.run(
|
||||
"caught = {} seterrorhandler(function(m) table.insert(caught, m) end) \
|
||||
local f = CreateFrame('Frame') \
|
||||
f:RegisterEvent('ERROR_PROBE') \
|
||||
f:RegisterEvent('PLAYER_ALIVE') \
|
||||
f:SetScript('OnEvent', function() this:NoSuchMethod() end) \
|
||||
local b = CreateFrame('Button') \
|
||||
b:SetScript('OnClick', function() local t = nil; t.x = 1 end) \
|
||||
@@ -267,7 +267,7 @@ fn the_error_handler_receives_the_bare_lua_message() {
|
||||
c:Click()",
|
||||
)
|
||||
.unwrap();
|
||||
s.fire_event("ERROR_PROBE", vec![]);
|
||||
s.fire_event("PLAYER_ALIVE", vec![]);
|
||||
s.dispatch_script_errors_to_handler();
|
||||
let caught: Vec<String> = s.eval("return caught").unwrap();
|
||||
assert!(caught.len() >= 3, "{caught:?}");
|
||||
@@ -316,11 +316,11 @@ fn an_error_handler_that_errors_does_not_recurse() {
|
||||
s.run(
|
||||
"seterrorhandler(function() error('the handler is broken too') end) \
|
||||
local f = CreateFrame('Frame') \
|
||||
f:RegisterEvent('ERROR_PROBE') \
|
||||
f:RegisterEvent('PLAYER_ALIVE') \
|
||||
f:SetScript('OnEvent', function() error('original fault') end)",
|
||||
)
|
||||
.unwrap();
|
||||
s.fire_event("ERROR_PROBE", vec![]);
|
||||
s.fire_event("PLAYER_ALIVE", vec![]);
|
||||
s.dispatch_script_errors_to_handler();
|
||||
let errors = s.take_errors();
|
||||
assert!(
|
||||
|
||||
@@ -30,7 +30,8 @@ pub(super) fn set_shown(lua: &Lua, h: FrameHandle, shown: bool) {
|
||||
|
||||
/// The show slot (`+0x88`): the kind's override, else the base [`base_show`]. GameTooltip's is
|
||||
/// `0x530a80`. CharacterModelBase's (`0x505d00`, which rebuilds the doll from its unit or file)
|
||||
/// is not modelled.
|
||||
/// is the app's: the script model holds no doll instance, and the app re-takes a body pane's
|
||||
/// snapshot on its hidden-to-drawn edge, however the show came.
|
||||
pub(super) fn show(lua: &Lua, h: FrameHandle) {
|
||||
match kind(lua, h) {
|
||||
Some(FrameKind::GameTooltip) => super::tooltip::show_override(lua, h),
|
||||
@@ -39,8 +40,8 @@ pub(super) fn show(lua: &Lua, h: FrameHandle) {
|
||||
}
|
||||
|
||||
/// The hide slot (`+0x84`): the kind's override, else the base [`base_hide`]. GameTooltip's is
|
||||
/// `0x530a60`. CharacterModelBase's (`0x505ce0`, which releases the doll's model) is not
|
||||
/// modelled.
|
||||
/// `0x530a60`. CharacterModelBase's (`0x505ce0`, which releases the doll's model) is the app's
|
||||
/// too: a hidden pane is not drawn, and its snapshot re-takes on the next show.
|
||||
pub(super) fn hide(lua: &Lua, h: FrameHandle) {
|
||||
match kind(lua, h) {
|
||||
Some(FrameKind::GameTooltip) => super::tooltip::hide_override(lua, h),
|
||||
@@ -278,20 +279,31 @@ fn notify(lua: &Lua, h: FrameHandle, visible: bool) {
|
||||
|
||||
/// Open or close a frame's load window: the pre-load hook's `+0x114 = 1` (`0x7697c2`) and the
|
||||
/// post-load hook's first write, `+0x114 = 0` (`0x76a2ff`).
|
||||
///
|
||||
/// The pre-load hook defers the frame's layout (`0x7697c2`–`0x769807`). The post-load hook, once
|
||||
/// the window is down, undefers a visible one (`0x76a2ff`–`0x76a35b`), whose resolve runs there
|
||||
/// (`0x76a310` → `0x768060(0)` → `0x7680e0(1)`), so its `OnSizeChanged` fires ahead of its
|
||||
/// `<Frames>` (`0x76a36f`), its `OnLoad` (`0x76a387`) and its `OnShow` (`0x76a3b3`), as a show's
|
||||
/// does ([`base_show`]).
|
||||
pub(crate) fn set_load_window(lua: &Lua, wrapper: &Table, open: bool) {
|
||||
let Ok(h) = super::object::frame_handle_of(lua, wrapper) else {
|
||||
return;
|
||||
};
|
||||
let mut model = model_mut(lua);
|
||||
if let Some(f) = model.arena.frame_mut(h) {
|
||||
f.load_window = open;
|
||||
}
|
||||
// The pre-load hook defers the frame's layout (`0x7697c2`–`0x769807`); the post-load hook,
|
||||
// once the window is down, undefers a visible one (`0x76a2ff`–`0x76a35b`).
|
||||
if open {
|
||||
model.layout_load_begin(h);
|
||||
} else {
|
||||
model.layout_load_end(h);
|
||||
let roots = {
|
||||
let mut model = model_mut(lua);
|
||||
if let Some(f) = model.arena.frame_mut(h) {
|
||||
f.load_window = open;
|
||||
}
|
||||
if open {
|
||||
model.layout_load_begin(h);
|
||||
return;
|
||||
}
|
||||
let owed = model.layout_load_end(h);
|
||||
let roots = model.load_end_roots(h);
|
||||
(owed && model.resolve_fires(&roots)).then_some(roots)
|
||||
};
|
||||
if let Some(roots) = roots {
|
||||
resolve_and_fire(lua, roots);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user