Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad4749ac26 | |||
| c0630f3d9d | |||
| 16affa8d45 | |||
| c9ddd80342 | |||
| f356e90fe6 | |||
| ff49eb5b8f | |||
| 45da0a0850 | |||
| 710eb8f531 | |||
| c2fc3cf304 | |||
| dc63a06178 | |||
| f0dfa86148 | |||
| 87706792cb | |||
| d0d1b5def9 | |||
| 208b1e0bb0 | |||
| 1c5a568191 | |||
| ea7e0d087a | |||
| 63e917965d | |||
| 80b0848628 | |||
| 2e82d34c1b | |||
| 85980939f5 | |||
| f8493febb0 | |||
| ffdc206ac2 | |||
| bb84f1f1fb | |||
| ab909df6d9 | |||
| 622e1c3e7a | |||
| aa103d5a65 | |||
| 4a389add47 | |||
| 9bc7276dd8 | |||
| cf891dc375 |
Generated
+10
@@ -778,6 +778,7 @@ version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd310426290cec560221f9750c2f4484be4a8eeea7de3483c423329b465c40e"
|
||||
dependencies = [
|
||||
"bevy_dylib",
|
||||
"bevy_internal",
|
||||
"tracing",
|
||||
]
|
||||
@@ -1097,6 +1098,15 @@ dependencies = [
|
||||
"sysinfo",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_dylib"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f142f72bbbc1a61338bf6da5dc7cf6dcf223683b9d53bcd635ebff4bfeb5308f"
|
||||
dependencies = [
|
||||
"bevy_internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_ecs"
|
||||
version = "0.18.1"
|
||||
|
||||
@@ -16,6 +16,9 @@ dev = ["benilla-assets/dev", "benilla-formats/dev", "benilla-world/dev", "dep:be
|
||||
tracy = ["bevy/trace_tracy"]
|
||||
# Writes every system's per-run span to `trace-<ts>.json` in the cwd.
|
||||
trace_chrome = ["bevy/trace_chrome"]
|
||||
# Links Bevy as a shared library so an incremental rebuild relinks faster. The binary then starts
|
||||
# only through `cargo run`, which puts the toolchain's libstd on the loader path.
|
||||
bevy_dynamic_linking = ["bevy/dynamic_linking"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
|
||||
@@ -158,11 +158,11 @@ impl Addon {
|
||||
// order, as `AddOn_Load 0x51f240` hands every listed file to `0x6edb90`.
|
||||
match script.run_chunk_named(&bytes, &self.chunk_name(file, &path)) {
|
||||
Ok(()) => info!("ui_script: {}/{file} ran", self.name),
|
||||
Err(e) => {
|
||||
let e = format!("{}/{file}: {e}", self.name);
|
||||
Err(err) => {
|
||||
let e = format!("{}/{file}: {err}", self.name);
|
||||
error!("ui_script: {e}");
|
||||
// A script error: it reaches the player through the Lua error handler.
|
||||
script.report_script_error(&e);
|
||||
script.report_script_error(&e, &benilla_ui::script::lua_message(&err));
|
||||
failures.push(e);
|
||||
}
|
||||
}
|
||||
@@ -210,11 +210,11 @@ impl Addon {
|
||||
script.report_load_failure(&e);
|
||||
failures.push(e);
|
||||
}
|
||||
for e in &report.errors {
|
||||
for (e, heard) in report.raised() {
|
||||
error!("ui_script({}/{file}): {e}", self.name);
|
||||
// A script error, as in the Lua arm; `_ERRORMESSAGE`'s IsVisible guard shows only
|
||||
// a burst's first, as in the reference.
|
||||
script.report_script_error(&format!("{}/{file}: {e}", self.name));
|
||||
script.report_script_error(&format!("{}/{file}: {e}", self.name), heard);
|
||||
failures.push(format!("{}/{file}: {e}", self.name));
|
||||
}
|
||||
info!(
|
||||
@@ -237,39 +237,18 @@ fn is_lua(entry: &str) -> bool {
|
||||
.is_some_and(|(_, ext)| ext.eq_ignore_ascii_case("lua"))
|
||||
}
|
||||
|
||||
/// `root/rel`, refusing to escape `root`. `rel` is already resolved by
|
||||
/// [`benilla_ui::loader::join_ref`], so an escape shows as a leading `..`, refused with anything
|
||||
/// absolute before any filesystem call; a symlinked addon folder still works.
|
||||
fn read_under(root: &Path, rel: &str) -> Option<Vec<u8>> {
|
||||
let rel = Path::new(rel);
|
||||
if rel
|
||||
.components()
|
||||
.any(|c| !matches!(c, std::path::Component::Normal(_)))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
std::fs::read(root.join(rel)).ok()
|
||||
}
|
||||
|
||||
/// The one file namespace, install-relative, as in the reference: `0x647e60` answers a name from
|
||||
/// the install tree's hash index, then the archive chain, after `0x6ede10` collapses `..`. So an
|
||||
/// addon's `..\Blizzard_AuctionUI\…` reaches the archive behind a `.pub`-only folder. Only a path
|
||||
/// under `Interface/AddOns/` touches the filesystem, and never above the AddOns root.
|
||||
/// under `Interface/AddOns/` touches the filesystem, never above the AddOns root, and each of its
|
||||
/// components matches case-insensitively, as on the reference's Windows filesystem: the resolver
|
||||
/// addon art and sound use ([`benilla_assets::loose_addon_file`]).
|
||||
pub(crate) fn read_addon_file(root: &Path, req: &str) -> Option<Vec<u8>> {
|
||||
under_addons(req)
|
||||
.and_then(|rel| read_under(root, rel))
|
||||
benilla_assets::loose_addon_file(root, &benilla_assets::normalize_path(req))
|
||||
.and_then(|file| std::fs::read(file).ok())
|
||||
.or_else(|| super::reference_ui::read(&req.replace('/', "\\")))
|
||||
}
|
||||
|
||||
/// `req` with the `Interface/AddOns/` prefix stripped, or `None` without it; matched
|
||||
/// case-insensitively, as the reference is a Windows client (`Interface\Addons\` is the same path).
|
||||
fn under_addons(req: &str) -> Option<&str> {
|
||||
let rest = req.get(ADDONS_PREFIX.len()..)?;
|
||||
req.get(..ADDONS_PREFIX.len())
|
||||
.filter(|p| p.eq_ignore_ascii_case(ADDONS_PREFIX))
|
||||
.map(|_| rest)
|
||||
}
|
||||
|
||||
/// A `Dir` addon's base before its folder name ([`Addon::prefix`]), `/`-separated as `join_ref` is.
|
||||
const ADDONS_PREFIX: &str = "Interface/AddOns/";
|
||||
|
||||
@@ -1244,6 +1223,44 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
/// A manifest entry names its file in whatever case its author typed on Windows: Questie's
|
||||
/// `.toc` lists `Astrolabe\Astrolabe.lua` from a folder shipped as `astrolabe`. Every
|
||||
/// component matches case-insensitively, the folder and the file alike, on any filesystem.
|
||||
#[test]
|
||||
fn an_addon_file_is_found_whatever_the_case_of_its_path() {
|
||||
let tmp =
|
||||
std::env::temp_dir().join(format!("benilla-addon-case-test-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
let root = tmp.join("AddOns");
|
||||
std::fs::create_dir_all(root.join("!Probe/astrolabe")).unwrap();
|
||||
std::fs::write(
|
||||
root.join("!Probe/astrolabe/astrolabe.lua"),
|
||||
"Astrolabe = {}",
|
||||
)
|
||||
.unwrap();
|
||||
let addon = Addon {
|
||||
name: "!Probe".into(),
|
||||
toc: Toc::default(),
|
||||
source: Source::Dir(root),
|
||||
};
|
||||
|
||||
for req in [
|
||||
"Interface/AddOns/!Probe/Astrolabe/Astrolabe.lua",
|
||||
"Interface/AddOns/!PROBE/ASTROLABE/ASTROLABE.LUA",
|
||||
"interface/addons/!probe/astrolabe/astrolabe.lua",
|
||||
] {
|
||||
assert_eq!(
|
||||
addon.read(req).as_deref(),
|
||||
Some(&b"Astrolabe = {}"[..]),
|
||||
"{req} names the file on disk as the reference's filesystem would"
|
||||
);
|
||||
}
|
||||
assert!(addon
|
||||
.read("Interface/AddOns/!Probe/astrolabe/missing.lua")
|
||||
.is_none());
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
/// A `.toc`-listed `Bindings.xml` is walked as FrameXML, each `<Binding>` an unknown frame
|
||||
/// type, which the reference logs and carries on from (`0x6ee356`).
|
||||
#[test]
|
||||
@@ -2874,4 +2891,74 @@ mod tests {
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(home.parent().unwrap());
|
||||
}
|
||||
|
||||
/// The error handler hears what 1.12's `lua_pcall` error function hands it (`0x703b40`, from
|
||||
/// `FS_DoBuffer 0x704ae0` for a file's top level and its syntax error, `0x704f10` for a
|
||||
/// handler): the bare Lua message, with no frame, `OnLoad: `, addon or file word and no
|
||||
/// traceback. The decorated text stays on the failures list and the host log.
|
||||
#[test]
|
||||
fn the_error_handler_hears_the_bare_message_of_every_load_failure() {
|
||||
let tmp = std::env::temp_dir().join(format!("benilla-addon-bare-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
let addon = dir_addon(
|
||||
&tmp,
|
||||
"Bare",
|
||||
"## Interface: 11200\ntop.lua\nsyntax.lua\nframe.xml\n",
|
||||
);
|
||||
std::fs::write(tmp.join("Bare/top.lua"), "local t = nil\nt.x = 1\n").unwrap();
|
||||
std::fs::write(tmp.join("Bare/syntax.lua"), "x = = 1\n").unwrap();
|
||||
std::fs::write(
|
||||
tmp.join("Bare/frame.xml"),
|
||||
r#"<Ui><Frame name="BareFrame"><Scripts><OnLoad>this:NoSuchMethod()</OnLoad></Scripts></Frame>
|
||||
<Script>local u = nil; u.y = 2</Script></Ui>"#,
|
||||
)
|
||||
.unwrap();
|
||||
let mut script = UiScript::new().unwrap();
|
||||
script.set_screen_size(1024.0, 768.0);
|
||||
script
|
||||
.run("Heard = {} seterrorhandler(function(m) table.insert(Heard, m) end)")
|
||||
.unwrap();
|
||||
let failures = addon.load(&script, &mut Status::default());
|
||||
assert_eq!(failures.len(), 4, "{failures:#?}");
|
||||
script.dispatch_script_errors_to_handler();
|
||||
let heard: Vec<String> = script.eval("return Heard").unwrap();
|
||||
assert_eq!(heard.len(), 4, "{heard:#?}");
|
||||
for m in &heard {
|
||||
assert!(
|
||||
!m.contains("runtime error")
|
||||
&& !m.contains("syntax error")
|
||||
&& !m.contains("stack traceback")
|
||||
&& !m.contains("OnLoad: ")
|
||||
&& !m.contains("Bare/")
|
||||
&& !m.contains('\n'),
|
||||
"{m:?}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
heard[0].ends_with("top.lua:2: attempt to index local `t' (a nil value)"),
|
||||
"{:?}",
|
||||
heard[0]
|
||||
);
|
||||
assert!(
|
||||
heard[0].starts_with("Interface\\AddOns\\Bare\\top.lua"),
|
||||
"{:?}",
|
||||
heard[0]
|
||||
);
|
||||
assert!(
|
||||
heard[1].contains("syntax.lua:1:") && heard[1].contains("unexpected symbol"),
|
||||
"{:?}",
|
||||
heard[1]
|
||||
);
|
||||
assert!(
|
||||
heard[2].ends_with("attempt to call method `NoSuchMethod' (a nil value)"),
|
||||
"{:?}",
|
||||
heard[2]
|
||||
);
|
||||
assert!(
|
||||
heard[3].ends_with("attempt to index local `u' (a nil value)"),
|
||||
"{:?}",
|
||||
heard[3]
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -958,6 +958,57 @@ fn an_exalted_npc_target_reads_revered_and_its_plate_is_green() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A target whose faction template the server changes live recolours its plate: template 35
|
||||
/// (friendly) to 14 (hostile) names no side either way, so only the template's own edge fires
|
||||
/// `UNIT_FACTION`. No edge, or another unit's, leaves the plate as it was.
|
||||
#[test]
|
||||
fn a_live_faction_template_change_recolours_the_target_plate() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
use crate::net::FieldEdges;
|
||||
use benilla_protocol::field::FIELD_UNIT_FACTIONTEMPLATE;
|
||||
|
||||
const MOB: u64 = 0xF130_0000_3796_0001;
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_unit_frames(&s);
|
||||
let mob = |reaction: u8| UnitState {
|
||||
exists: true,
|
||||
has_object: true,
|
||||
guid: MOB,
|
||||
name: Some("Pusillin".into()),
|
||||
health: 100,
|
||||
max_health: 100,
|
||||
level: 58,
|
||||
reaction,
|
||||
can_attack: reaction < 4,
|
||||
..UnitState::default()
|
||||
};
|
||||
let is_green = |c: [f32; 4]| c[0].abs() < 1e-6 && (c[1] - 1.0).abs() < 1e-6;
|
||||
let is_red = |c: [f32; 4]| (c[0] - 1.0).abs() < 1e-6 && c[1].abs() < 1e-6;
|
||||
|
||||
let friendly = mob(5);
|
||||
s.set_unit("target", Some(friendly.clone()));
|
||||
s.fire_event("PLAYER_TARGET_CHANGED", vec![]);
|
||||
let before = plate_color(&mut s);
|
||||
assert!(is_green(before), "template 35 reads green, got {before:?}");
|
||||
|
||||
let hostile = mob(2);
|
||||
s.set_unit("target", Some(hostile.clone()));
|
||||
let fire = |s: &mut UiScript, prev: &UnitState, edges: &[(u64, u16)]| {
|
||||
crate::ui_unit::fire_transitions(s, "target", Some(prev), &hostile, &FieldEdges::of(edges));
|
||||
};
|
||||
fire(&mut s, &friendly, &[]);
|
||||
let stale = plate_color(&mut s);
|
||||
assert!(is_green(stale), "no edge, no repaint, got {stale:?}");
|
||||
fire(&mut s, &hostile, &[(MOB + 1, FIELD_UNIT_FACTIONTEMPLATE)]);
|
||||
let other = plate_color(&mut s);
|
||||
assert!(is_green(other), "another unit's edge, got {other:?}");
|
||||
fire(&mut s, &hostile, &[(MOB, FIELD_UNIT_FACTIONTEMPLATE)]);
|
||||
let after = plate_color(&mut s);
|
||||
assert!(is_red(after), "template 14 reads red, got {after:?}");
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Stock `TargetFrame_CheckClassification` (`TargetFrame.lua:205-218`), asserted on the drawn
|
||||
/// quads: elite, rare-elite and world boss share the Elite art (1.12 ships no rare-elite border),
|
||||
/// and `UNIT_CLASSIFICATION_CHANGED` alone repaints it.
|
||||
|
||||
@@ -1202,11 +1202,19 @@ pub(crate) fn fire_transitions(
|
||||
}
|
||||
// `UNIT_FACTION`, the PvP-icon repaint, on the fields the icon reads and on the tapped bit: the
|
||||
// reference's dynamic-flags watcher (`0x600440`) fires event 29 on bit `0x4` (`0x6005a1` →
|
||||
// `0x6005b0`) and has no arm for `0x8`.
|
||||
if prev.is_none_or(|p| {
|
||||
(p.pvp, p.is_pvp_ffa, &p.faction_group, p.tapped)
|
||||
!= (cur.pvp, cur.is_pvp_ffa, &cur.faction_group, cur.tapped)
|
||||
}) {
|
||||
// `0x6005b0`) and has no arm for `0x8`. Also on any change of `UNIT_FIELD_FACTIONTEMPLATE`, the
|
||||
// watch bridge's id 29 (`0x51bbb0`, as for `UNIT_FLAGS` above): a template swap between two
|
||||
// that name no faction group moves nothing in the tuple, yet changes the reaction.
|
||||
let template_moved = edges.moved(
|
||||
cur.guid,
|
||||
benilla_protocol::field::FIELD_UNIT_FACTIONTEMPLATE,
|
||||
);
|
||||
if template_moved
|
||||
|| prev.is_none_or(|p| {
|
||||
(p.pvp, p.is_pvp_ffa, &p.faction_group, p.tapped)
|
||||
!= (cur.pvp, cur.is_pvp_ffa, &cur.faction_group, cur.tapped)
|
||||
})
|
||||
{
|
||||
script.fire_event("UNIT_FACTION", vec![tok()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,13 +187,17 @@ pub fn read_chain_or_loose(
|
||||
}
|
||||
|
||||
/// Map a normalized `interface\addons\…` path onto a file under the addon root, matching each
|
||||
/// component case-insensitively as the Windows reference does; a dot-component is refused, so a
|
||||
/// path cannot escape the root.
|
||||
/// component case-insensitively as the Windows reference does; a dot-component, or one the host
|
||||
/// reads as more than a plain name (a Windows `C:`), is refused, so a path cannot escape the root.
|
||||
pub fn loose_addon_file(root: &Path, candidate: &str) -> Option<PathBuf> {
|
||||
let rel = candidate.strip_prefix("interface\\addons\\")?;
|
||||
let mut at = root.to_path_buf();
|
||||
for comp in rel.split('\\') {
|
||||
if comp.is_empty() || comp.starts_with('.') {
|
||||
let plain = matches!(
|
||||
Path::new(comp).components().collect::<Vec<_>>()[..],
|
||||
[std::path::Component::Normal(_)]
|
||||
);
|
||||
if !plain || comp.starts_with('.') {
|
||||
return None;
|
||||
}
|
||||
let direct = at.join(comp);
|
||||
|
||||
@@ -15,7 +15,7 @@ impl Loader<'_> {
|
||||
let table = match self.build_backdrop_table(bd) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
self.report.errors.push(format!("{dbg}: <Backdrop>: {e}"));
|
||||
self.report.fault(format!("{dbg}: <Backdrop>: {e}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -146,9 +146,7 @@ impl Loader<'_> {
|
||||
let region: Table = match wrapper.call_method("CreateTitleRegion", ()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("{dbg}: CreateTitleRegion: {e}"));
|
||||
self.report.fault(format!("{dbg}: CreateTitleRegion: {e}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,6 +22,33 @@ mod regions;
|
||||
mod scripts;
|
||||
mod widgets;
|
||||
|
||||
impl LoadReport {
|
||||
/// A load fault that is no script raise: the handler hears the row's own text.
|
||||
pub(super) fn fault(&mut self, text: String) {
|
||||
self.handler_errors.push(text.clone());
|
||||
self.errors.push(text);
|
||||
}
|
||||
|
||||
/// A script raise: the row is `text`, the handler hears the bare Lua message of `e`.
|
||||
pub(super) fn raise(&mut self, text: String, e: &mlua::Error) {
|
||||
self.handler_errors.push(crate::script::lua_message(e));
|
||||
self.errors.push(text);
|
||||
}
|
||||
|
||||
/// Each of [`Self::errors`] with what the Lua error handler is called with for it.
|
||||
pub fn raised(&self) -> impl Iterator<Item = (&str, &str)> {
|
||||
debug_assert_eq!(
|
||||
self.errors.len(),
|
||||
self.handler_errors.len(),
|
||||
"every error row is written through `fault` or `raise`"
|
||||
);
|
||||
self.errors
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.zip(self.handler_errors.iter().map(String::as_str))
|
||||
}
|
||||
}
|
||||
|
||||
/// The outcome of a [`load`]; nothing aborts a load, as the reference logs and continues.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct LoadReport {
|
||||
@@ -31,6 +58,11 @@ pub struct LoadReport {
|
||||
/// Failures the reference raises on: a handler that fails to compile, a method call that
|
||||
/// errors, a malformed included document.
|
||||
pub errors: Vec<String>,
|
||||
/// What the Lua error handler is called with for each of [`Self::errors`], row for row
|
||||
/// ([`Self::raised`]): the bare Lua message of a raise, as `lua_pcall` leaves it for the
|
||||
/// reference's error function (`0x703b40`), and the row's own text for a fault that is no
|
||||
/// script raise.
|
||||
handler_errors: Vec<String>,
|
||||
/// `<Include file=>` and `<Script file=>` paths the provider lacks. The reference logs
|
||||
/// `"Couldn't open %s"` (`0x6edaa0`, `0x846ff4`) or `"Error loading %s"` (`0x872e50`) and goes
|
||||
/// on (`0x6ee00d`); kept apart from `warnings` so a load that resolved nothing is not clean.
|
||||
@@ -101,15 +133,17 @@ pub fn load_into_under(
|
||||
loader.report
|
||||
}
|
||||
|
||||
/// Apply a template to an existing frame, `CreateFrame`'s fourth argument. Returns the warnings,
|
||||
/// then the errors. The frame always survives: an unknown name leaves it bare, as in the
|
||||
/// reference, and only virtual elements are registered. The frame's own name is the `$parent`
|
||||
/// Apply a template to an existing frame, `CreateFrame`'s fourth argument. Returns the report:
|
||||
/// its errors are script errors for the error handler, as the reference's `CreateFrame` builds the
|
||||
/// template through the XML instantiator `0x6ee280`, whose OnLoad runs under the error function
|
||||
/// (`0x704d50`). The frame always survives an error, and only virtual elements are registered; an
|
||||
/// unknown name never gets here, since the binding raises first. The frame's own name is the `$parent`
|
||||
/// base (`MyButton` makes `$parentText` `MyButtonText`). Runs inside the `CreateFrame` binding, so
|
||||
/// it enters at [`Loader::decorate`]: [`Loader::materialize`] would recurse.
|
||||
pub fn apply_template(lua: &mlua::Lua, wrapper: &Table, kind: &str, template: &str) -> Vec<String> {
|
||||
pub fn apply_template(lua: &mlua::Lua, wrapper: &Table, kind: &str, template: &str) -> LoadReport {
|
||||
let template = template.trim();
|
||||
if template.is_empty() {
|
||||
return Vec::new();
|
||||
return LoadReport::default();
|
||||
}
|
||||
// A template never reads a file: `<Include>` and `<Script file=>` are top-level only.
|
||||
let no_files = |_: &str| -> Option<Vec<u8>> { None };
|
||||
@@ -179,9 +213,7 @@ pub fn apply_template(lua: &mlua::Lua, wrapper: &Table, kind: &str, template: &s
|
||||
|
||||
loader.decorate(&expanded, wrapper, &self_name, &parent_name, &dbg);
|
||||
|
||||
let mut out = loader.report.warnings;
|
||||
out.extend(loader.report.errors);
|
||||
out
|
||||
loader.report
|
||||
}
|
||||
|
||||
/// Join a FrameXML reference to its file's directory, lexically: `\` and `/` both separate, `..`
|
||||
@@ -352,8 +384,7 @@ impl Loader<'_> {
|
||||
Some(bytes) => {
|
||||
if let Err(e) = self.run(crate::source::chunk(&bytes), &joined) {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("<Script file=\"{path}\">: {e}"));
|
||||
.raise(format!("<Script file=\"{path}\">: {e}"), &e);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
@@ -377,7 +408,7 @@ impl Loader<'_> {
|
||||
// `[string "Interface\FrameXML\Foo.xml:<Scripts>"]`, not a file path.
|
||||
let name = format!("{}:<Scripts>", self.path.replace('/', "\\"));
|
||||
if let Err(e) = self.run_named(&chunk, &name) {
|
||||
self.report.errors.push(format!("inline <Script>: {e}"));
|
||||
self.report.raise(format!("inline <Script>: {e}"), &e);
|
||||
}
|
||||
}
|
||||
TopLevel::Font(el) => self.do_font(el),
|
||||
@@ -418,8 +449,7 @@ impl Loader<'_> {
|
||||
if status::runs_as_lua(&joined) {
|
||||
if let Err(e) = self.run(crate::source::chunk(&bytes), &joined) {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("<Include file=\"{path}\">: {e}"));
|
||||
.raise(format!("<Include file=\"{path}\">: {e}"), &e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -441,9 +471,7 @@ impl Loader<'_> {
|
||||
Err(e) => {
|
||||
let line = status::unparsed(&self.status_path(&joined));
|
||||
self.report.status.report(status::FAILURE, line);
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("<Include file=\"{path}\">: {e}"))
|
||||
self.report.fault(format!("<Include file=\"{path}\">: {e}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -600,8 +628,7 @@ impl Loader<'_> {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("CreateFrame global missing: {e}"));
|
||||
.fault(format!("CreateFrame global missing: {e}"));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
@@ -609,7 +636,7 @@ impl Loader<'_> {
|
||||
match create.call((el.tag.clone(), resolved_name.clone(), parent.cloned())) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
self.report.errors.push(format!(
|
||||
self.report.fault(format!(
|
||||
"CreateFrame(<{}>{}): {e}",
|
||||
el.tag,
|
||||
resolved_name
|
||||
@@ -699,7 +726,7 @@ impl Loader<'_> {
|
||||
// and gives the ScrollFrame its scroll range. An empty one errors in the reference.
|
||||
for sc in children_named(el, "ScrollChild") {
|
||||
let Some(child) = sc.children.first() else {
|
||||
self.report.errors.push(format!(
|
||||
self.report.fault(format!(
|
||||
"{dbg_name}: <ScrollChild> is empty — the reference errors here, and a \
|
||||
ScrollFrame with no child has no scroll range at all"
|
||||
));
|
||||
@@ -710,9 +737,7 @@ impl Loader<'_> {
|
||||
continue; // materialize already reported why
|
||||
};
|
||||
if let Err(e) = wrapper.call_method::<()>("SetScrollChild", child_wrapper) {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("{dbg_name}: <ScrollChild>: {e}"));
|
||||
self.report.fault(format!("{dbg_name}: <ScrollChild>: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,13 +53,13 @@ impl Loader<'_> {
|
||||
match wrapper.call_method(method, (rname, Some(level.clone()))) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
self.report.errors.push(format!("{dbg}: {method}: {e}"));
|
||||
self.report.fault(format!("{dbg}: {method}: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if region.attr_bool("hidden") {
|
||||
if let Err(e) = region_wrapper.call_method::<()>("Hide", ()) {
|
||||
self.report.errors.push(format!("{dbg}: region Hide: {e}"));
|
||||
self.report.fault(format!("{dbg}: region Hide: {e}"));
|
||||
}
|
||||
}
|
||||
if let Some(a) = region
|
||||
@@ -85,9 +85,7 @@ impl Loader<'_> {
|
||||
if let Err(e) =
|
||||
crate::script::implicit_creation_anchor_lua(self.lua, ®ion_wrapper)
|
||||
{
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("{dbg}: implicit anchor: {e}"));
|
||||
self.report.fault(format!("{dbg}: implicit anchor: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,8 +139,7 @@ impl Loader<'_> {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("{dbg}: CreateFontString (special): {e}"));
|
||||
.fault(format!("{dbg}: CreateFontString (special): {e}"));
|
||||
continue;
|
||||
}
|
||||
},
|
||||
@@ -152,9 +149,7 @@ impl Loader<'_> {
|
||||
self.apply_region_visual(region, ®ion_wrapper, false, dbg);
|
||||
// The implicit creation anchor, as in `apply_layers`; an EditBox's adopt replaces it.
|
||||
if let Err(e) = crate::script::implicit_creation_anchor_lua(self.lua, ®ion_wrapper) {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("{dbg}: implicit anchor: {e}"));
|
||||
self.report.fault(format!("{dbg}: implicit anchor: {e}"));
|
||||
}
|
||||
// It is the EditBox's text region by slot, never found by a search, which could take a
|
||||
// `<Layers>` string; the box's insets anchor it.
|
||||
@@ -163,8 +158,7 @@ impl Loader<'_> {
|
||||
crate::script::adopt_text_region(self.lua(), wrapper, ®ion_wrapper)
|
||||
{
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("{dbg}: adopt special FontString: {e}"));
|
||||
.fault(format!("{dbg}: adopt special FontString: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -383,9 +377,7 @@ impl Loader<'_> {
|
||||
height,
|
||||
outline,
|
||||
) {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("{dbg}: region font attrs: {e}"));
|
||||
self.report.fault(format!("{dbg}: region font attrs: {e}"));
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
|
||||
@@ -102,8 +102,7 @@ impl Loader<'_> {
|
||||
Ok(f) => Some(f),
|
||||
Err(e) => {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("{dbg}: compiling <{name}>: {e}"));
|
||||
.raise(format!("{dbg}: compiling <{name}>: {e}"), &e);
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -113,7 +112,7 @@ impl Loader<'_> {
|
||||
/// (`0x704d50`); an error is recorded, never propagated.
|
||||
pub(super) fn fire_onload(&mut self, wrapper: &Table, func: &Function, dbg: &str) {
|
||||
if let Err(e) = self.invoke_handler(wrapper, func) {
|
||||
self.report.errors.push(format!("{dbg}: OnLoad: {e}"));
|
||||
self.report.raise(format!("{dbg}: OnLoad: {e}"), &e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +125,7 @@ impl Loader<'_> {
|
||||
dbg: &str,
|
||||
) {
|
||||
if let Err(e) = wrapper.call_method::<()>(method, args) {
|
||||
self.report.errors.push(format!("{dbg}: {method}: {e}"));
|
||||
self.report.fault(format!("{dbg}: {method}: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,9 +138,7 @@ impl Loader<'_> {
|
||||
dbg: &str,
|
||||
) {
|
||||
if let Err(e) = region.call_method::<()>(method, args) {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("{dbg}: region {method}: {e}"));
|
||||
self.report.fault(format!("{dbg}: region {method}: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,9 +165,7 @@ impl Loader<'_> {
|
||||
self.call_region(®ion, "ClearAllPoints", (), dbg);
|
||||
self.apply_region_layout(bt, ®ion, self_name, dbg, font_attrs);
|
||||
if let Err(e) = crate::script::implicit_creation_anchor_lua(self.lua, ®ion) {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("{dbg}: implicit anchor: {e}"));
|
||||
self.report.fault(format!("{dbg}: implicit anchor: {e}"));
|
||||
}
|
||||
// Published both ways, like a named state texture.
|
||||
if let Some(rname) = bt_name {
|
||||
@@ -232,8 +230,7 @@ impl Loader<'_> {
|
||||
self.lua, wrapper, which, jh,
|
||||
) {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("{dbg}: <{}> justifyH: {e}", f.tag));
|
||||
.fault(format!("{dbg}: <{}> justifyH: {e}", f.tag));
|
||||
}
|
||||
}
|
||||
// Reported, not modelled: the reference clears the axis on a cross-axis
|
||||
@@ -561,8 +558,7 @@ impl Loader<'_> {
|
||||
outline,
|
||||
) {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("{dbg}: <SimpleHTML> {name} font attrs: {e}"));
|
||||
.fault(format!("{dbg}: <SimpleHTML> {name} font attrs: {e}"));
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
|
||||
@@ -525,7 +525,10 @@ fn load_bindings(lua: &Lua, name: &str, read: &Reader) {
|
||||
match crate::bindings_xml::parse(&crate::source::decode(&bytes)) {
|
||||
Ok(bindings) => super::keybind::register_addon_bindings(lua, name, &bindings),
|
||||
// Reported, not swallowed: otherwise a broken binding file leaves keys dead silently.
|
||||
Err(e) => log_error(lua, &format!("{name}/Bindings.xml: {e}")),
|
||||
Err(e) => {
|
||||
let msg = format!("{name}/Bindings.xml: {e}");
|
||||
log_error(lua, &msg, &msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,7 +558,8 @@ fn load_saved_variables(lua: &Lua, i: usize) {
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue, // the first-run case
|
||||
Err(e) => {
|
||||
// Present but unreadable is held, like a file that will not run.
|
||||
log_error(lua, &format!("{}: {e}", path.display()));
|
||||
let msg = format!("{}: {e}", path.display());
|
||||
log_error(lua, &msg, &msg);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
if !model.held_saved_files.contains(&path) {
|
||||
model.held_saved_files.push(path);
|
||||
@@ -570,7 +574,11 @@ fn load_saved_variables(lua: &Lua, i: usize) {
|
||||
) {
|
||||
// Deviation: the reference fails silently; we report it, as a player needs to know,
|
||||
// and hold the file so the shutdown write keeps it.
|
||||
log_error(lua, &format!("{}: {e}", path.display()));
|
||||
log_error(
|
||||
lua,
|
||||
&format!("{}: {e}", path.display()),
|
||||
&super::lua_message(&e),
|
||||
);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
if !model.held_saved_files.contains(&path) {
|
||||
model.held_saved_files.push(path);
|
||||
@@ -616,7 +624,7 @@ fn run_files(lua: &Lua, name: &str, read: &Reader, files: &[String], toc: &mut S
|
||||
};
|
||||
if lua_file {
|
||||
if let Err(e) = run_chunk(lua, &bytes, &crate::script::addon_chunk_name(name, file)) {
|
||||
log_error(lua, &format!("{name}/{file}: {e}"));
|
||||
log_error(lua, &format!("{name}/{file}: {e}"), &super::lua_message(&e));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -630,15 +638,15 @@ fn run_files(lua: &Lua, name: &str, read: &Reader, files: &[String], toc: &mut S
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let report = crate::loader::load_into_under(lua, &doc, &path, &provider, ADDONS_ROOT);
|
||||
let mut report = crate::loader::load_into_under(lua, &doc, &path, &provider, ADDONS_ROOT);
|
||||
let banner = crate::status::file_banner(&install);
|
||||
report.status.close_into(toc, framexml_debug(lua), banner);
|
||||
std::mem::take(&mut report.status).close_into(toc, framexml_debug(lua), banner);
|
||||
// Warnings take the `<Addon>/<file>` prefix; a bare loader warning names no document.
|
||||
for w in report.warnings {
|
||||
for w in &report.warnings {
|
||||
crate::script::diagnostics::record_warning(lua, &format!("{name}/{file}: {w}"));
|
||||
}
|
||||
for e in report.errors {
|
||||
log_error(lua, &format!("{name}/{file}: {e}"));
|
||||
for (e, heard) in report.raised() {
|
||||
log_error(lua, &format!("{name}/{file}: {e}"), heard);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -683,12 +691,13 @@ fn run_chunk(lua: &Lua, bytes: &[u8], name: &str) -> mlua::Result<()> {
|
||||
}
|
||||
|
||||
/// Record a demand-load error as the startup walk does ([`super::UiScript::report_script_error`]):
|
||||
/// a `Load` row, dispatched to the player's error handler and put on the host drain.
|
||||
fn log_error(lua: &Lua, msg: &str) {
|
||||
/// a `Load` row, dispatched to the player's error handler and put on the host drain. `handler` is
|
||||
/// the bare Lua message of a raise; a fault that is no raise passes its own text.
|
||||
fn log_error(lua: &Lua, msg: &str, handler: &str) {
|
||||
let msg = format!("LoadAddOn: {msg}");
|
||||
super::diagnostics::record_load_failure(lua, &msg);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.pending_error_dispatch.push(msg.clone());
|
||||
model.pending_error_dispatch.push(handler.to_string());
|
||||
model.errors.push(msg);
|
||||
}
|
||||
|
||||
|
||||
@@ -998,7 +998,7 @@ pub(super) fn click_button(lua: &Lua, id: u32, button: &str, down: bool, scripte
|
||||
if let Err(e) = event::fire_widget_handler(lua, id, "OnClick", vec![btn]) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
// A nameplate click selects its unit, from the pointer or `Click()` alike (the plate's
|
||||
// override `0x7cb910` chains the base), after the handler so an erroring hook cannot eat it. A
|
||||
|
||||
@@ -132,9 +132,10 @@ pub(super) fn install(lua: &Lua, locale: &str) -> mlua::Result<()> {
|
||||
.set_mode(mlua::ChunkMode::Text)
|
||||
.exec()
|
||||
{
|
||||
let bare = super::stdlib::lua_message(&e);
|
||||
lua.app_data_mut::<crate::script::Model>()
|
||||
.expect("model")
|
||||
.record_script_error(super::stdlib::lua_message(&e));
|
||||
.record_script_error(bare.clone(), bare);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
|
||||
@@ -268,7 +268,7 @@ fn fire_color_select(lua: &Lua, this: &Table, r: f64, g: f64, b: f64) -> mlua::R
|
||||
) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ fn insert(lua: &Lua, h: FrameHandle, ins: &str, fire_space: bool) {
|
||||
if let Err(e) = on_char {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
if fire_space {
|
||||
for _ in 0..out.spaces {
|
||||
@@ -519,7 +519,7 @@ fn fire_script(lua: &Lua, id: u32, name: &str) {
|
||||
if let Err(e) = event::fire_widget_handler(lua, id, name, Vec::new()) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ fn cursor_changed(lua: &Lua, h: FrameHandle) -> bool {
|
||||
) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -93,6 +93,19 @@ fn the_flush_follows_the_boxs_own_on_update_cursor_then_text() {
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The box's place in the walk is its stratum's, not its creation's: a BACKGROUND box updates and
|
||||
/// flushes before the MEDIUM frames made around it (`0x7656c8`).
|
||||
#[test]
|
||||
fn a_box_flushes_after_its_on_update_in_its_strata_place() {
|
||||
let mut s = script();
|
||||
logged_box(&mut s);
|
||||
s.run(r#"E:SetFrameStrata("BACKGROUND") E:SetText("abc")"#)
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "update cursor@21 text:abc early late");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// An edit a box makes in its own `OnUpdate` is flushed by the same update, so its events fire in
|
||||
/// the same tick.
|
||||
#[test]
|
||||
|
||||
@@ -38,7 +38,7 @@ pub(super) fn fire_all_event_listeners(lua: &Lua, event: &str, args: &[ScriptVal
|
||||
if let Err(e) = fire_event_handler(lua, id, event, args) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
}
|
||||
at = next;
|
||||
@@ -102,7 +102,7 @@ pub(super) fn fire_size_changes(lua: &Lua) {
|
||||
) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,14 +124,14 @@ fn fire(lua: &Lua, id: u32, script: &str, arg: &str) {
|
||||
Err(e) => {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
return;
|
||||
}
|
||||
});
|
||||
if let Err(e) = event::fire_widget_handler(lua, id, script, vec![val]) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,8 @@ mod spellbook;
|
||||
mod stable;
|
||||
mod statusbar;
|
||||
mod stdlib;
|
||||
/// An mlua error's bare Lua message, the one value the error handler is called with.
|
||||
pub use stdlib::lua_message;
|
||||
mod summon;
|
||||
mod talent;
|
||||
mod taxi;
|
||||
@@ -131,6 +133,7 @@ mod tooltip_spell;
|
||||
mod tooltip_unit;
|
||||
mod ui_errors;
|
||||
mod ui_scale;
|
||||
mod update_walk;
|
||||
pub use tooltip_unit::TooltipTint;
|
||||
mod trade;
|
||||
mod tradeskill;
|
||||
@@ -1199,14 +1202,16 @@ impl UiScript {
|
||||
|
||||
/// Report a script error the host caught outside the VM's dispatch (an addon file that failed
|
||||
/// to compile or raised at file scope): it joins the handler-dispatch queue and the retained
|
||||
/// log, not `errors`, since the caller has already logged it.
|
||||
pub fn report_script_error(&self, msg: &str) {
|
||||
/// log, not `errors`, since the caller has already logged it. `log` is the decorated text the
|
||||
/// log keeps; `handler` is the one value the error handler is called with, the bare Lua
|
||||
/// message as `lua_pcall` leaves it (`0x703b40`).
|
||||
pub fn report_script_error(&self, log: &str, handler: &str) {
|
||||
// A Load row, not an Error one: to the player a file scope that raised and a missing file
|
||||
// are the same fact, the addon is not running.
|
||||
diagnostics::record_load_failure(&self.lua, msg);
|
||||
diagnostics::record_load_failure(&self.lua, log);
|
||||
self.model_mut()
|
||||
.pending_error_dispatch
|
||||
.push(msg.to_string());
|
||||
.push(handler.to_string());
|
||||
}
|
||||
|
||||
/// Hand each queued script error to the Lua error handler, as the reference's catch closure
|
||||
@@ -1263,7 +1268,7 @@ impl UiScript {
|
||||
}
|
||||
|
||||
fn push_error(&self, e: mlua::Error) {
|
||||
self.model_mut().record_script_error(e.to_string());
|
||||
self.model_mut().record_lua_error(&e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -176,8 +176,11 @@ pub(crate) struct Model {
|
||||
|
||||
/// Which handlers each frame has; the closures themselves live in Lua's `REG_SCRIPTS` table.
|
||||
pub(crate) scripts: HashMap<FrameHandle, HashSet<&'static str>>,
|
||||
/// Frames with an `OnUpdate` script, kept by `SetScript`; the tick sorts them by frame id.
|
||||
/// Frames with an `OnUpdate` script, kept by `SetScript`, from which the update walk starts.
|
||||
pub(crate) on_update_frames: Vec<FrameHandle>,
|
||||
/// Bumped each time a frame joins [`Self::on_update_frames`], so the walk notices a frame that
|
||||
/// gains an `OnUpdate` before its turn.
|
||||
pub(crate) on_update_joins: u64,
|
||||
/// Frames with an `OnSizeChanged` script, kept by `SetScript`, for the resolve's size snapshot.
|
||||
pub(crate) on_size_changed_frames: Vec<FrameHandle>,
|
||||
/// Frames with an `OnUpdateModel` script, fired as a visible model pane paints (`0x76d1a0`).
|
||||
@@ -985,13 +988,21 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
/// Records a caught script error for the host, the retained log and the Lua error handler; an
|
||||
/// error raised by the handler itself goes to `errors` directly, which stops the recursion.
|
||||
pub(crate) fn record_script_error(&mut self, msg: String) {
|
||||
/// Records a caught script error for the host, the retained log and the Lua error handler: `log`
|
||||
/// is the decorated text the host and the retained log keep, `handler` the one value the error
|
||||
/// handler is called with. An error raised by the handler itself goes to `errors` directly,
|
||||
/// which stops the recursion.
|
||||
pub(crate) fn record_script_error(&mut self, log: String, handler: String) {
|
||||
self.diagnostics
|
||||
.record(super::diagnostics::DiagnosticKind::Error, &msg);
|
||||
self.pending_error_dispatch.push(msg.clone());
|
||||
self.errors.push(msg);
|
||||
.record(super::diagnostics::DiagnosticKind::Error, &log);
|
||||
self.pending_error_dispatch.push(handler);
|
||||
self.errors.push(log);
|
||||
}
|
||||
|
||||
/// [`Self::record_script_error`] for an error mlua caught: the log keeps mlua's `Display` (its
|
||||
/// word and traceback), the handler gets the bare Lua message `lua_pcall` leaves (`0x703b40`).
|
||||
pub(crate) fn record_lua_error(&mut self, e: &mlua::Error) {
|
||||
self.record_script_error(e.to_string(), super::stdlib::lua_message(e));
|
||||
}
|
||||
|
||||
/// Records a non-fatal warning for the host's terminal and the retained diagnostic log.
|
||||
@@ -1090,6 +1101,7 @@ impl Model {
|
||||
region_names: HashMap::new(),
|
||||
scripts: HashMap::new(),
|
||||
on_update_frames: Vec::new(),
|
||||
on_update_joins: 0,
|
||||
on_size_changed_frames: Vec::new(),
|
||||
on_update_model_frames: Vec::new(),
|
||||
model_facts: HashMap::new(),
|
||||
|
||||
@@ -600,12 +600,13 @@ pub(super) fn create_frame(
|
||||
// this call, so the handler can reach the frame by name.
|
||||
let wrapper = frame_wrapper(lua, id)?;
|
||||
if let Some(template) = template {
|
||||
let messages = crate::loader::apply_template(lua, &wrapper, &kind, &template);
|
||||
if !messages.is_empty() {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
for m in messages {
|
||||
model.record_warning(m);
|
||||
}
|
||||
let report = crate::loader::apply_template(lua, &wrapper, &kind, &template);
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
for (log, heard) in report.raised() {
|
||||
model.record_script_error(log.to_string(), heard.to_string());
|
||||
}
|
||||
for w in report.warnings {
|
||||
model.record_warning(w);
|
||||
}
|
||||
}
|
||||
Ok(wrapper)
|
||||
|
||||
@@ -240,7 +240,10 @@ fn set_script(lua: &Lua, this: &Table, name: &str, func: Option<Function>) -> ml
|
||||
// The first handler of a kind joins that kind's list; nothing else writes `scripts`.
|
||||
if model.scripts.entry(h).or_default().insert(kind) {
|
||||
match kind {
|
||||
"OnUpdate" => model.on_update_frames.push(h),
|
||||
"OnUpdate" => {
|
||||
model.on_update_frames.push(h);
|
||||
model.on_update_joins += 1;
|
||||
}
|
||||
"OnSizeChanged" => model.on_size_changed_frames.push(h),
|
||||
"OnUpdateModel" => model.on_update_model_frames.push(h),
|
||||
_ => {}
|
||||
|
||||
@@ -304,7 +304,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
@@ -327,7 +327,7 @@ fn fire_scroll(lua: &Lua, this: &Table, script: &str, value: f32) -> mlua::Resul
|
||||
{
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ impl super::UiScript {
|
||||
// The rest of the line after the alias and one space, spaces intact: the reference's
|
||||
// `strsub(text, strlen(cmdString) + 2)`.
|
||||
if let Err(e) = handler.call::<()>(args.to_string()) {
|
||||
self.model_mut().record_script_error(format!("/{cmd}: {e}"));
|
||||
self.model_mut()
|
||||
.record_script_error(format!("/{cmd}: {e}"), super::stdlib::lua_message(&e));
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ fn fire_value_changed(lua: &Lua, this: &Table, changed: Option<f32>) -> mlua::Re
|
||||
) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ fn fire_value_changed(lua: &Lua, this: &Table, changed: Option<f32>) -> mlua::Re
|
||||
) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -274,7 +274,7 @@ pub(super) fn fire_engine_value_changed(lua: &Lua, bar: crate::widget::FrameHand
|
||||
) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,20 @@
|
||||
|
||||
use mlua::{Lua, Value, Variadic};
|
||||
|
||||
/// An mlua error's bare Lua message, as `loadstring` returns it: without the `syntax error: ` or
|
||||
/// `runtime error: ` word mlua's `Display` adds and the reference never does.
|
||||
pub(super) fn lua_message(e: &mlua::Error) -> String {
|
||||
/// An mlua error's bare Lua message: what `lua_pcall` leaves for a 1.12 error function (`0x703b40`)
|
||||
/// and what `loadstring` returns. It drops the `syntax error: ` and `runtime error: ` words mlua's
|
||||
/// `Display` adds, the `stack traceback:` block mlua's message handler appends to a raise, and the
|
||||
/// wrappers a Rust callback's failure arrives in.
|
||||
pub fn lua_message(e: &mlua::Error) -> String {
|
||||
match e {
|
||||
mlua::Error::SyntaxError { message, .. } => message.clone(),
|
||||
mlua::Error::RuntimeError(m) => m.clone(),
|
||||
mlua::Error::RuntimeError(m) => m
|
||||
.rfind("\nstack traceback:\n")
|
||||
.map_or(m.as_str(), |at| &m[..at])
|
||||
.to_string(),
|
||||
mlua::Error::CallbackError { cause, .. } | mlua::Error::WithContext { cause, .. } => {
|
||||
lua_message(cause)
|
||||
}
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,3 +464,41 @@ fn a_runtime_template_fires_its_onshow_after_its_onload_when_visible() {
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A template's OnLoad runs under the error function as a file's does (`CreateFrame` builds through
|
||||
/// `0x6ee280`, OnLoad through `0x704d50`): a raise reaches the chosen error handler as the bare
|
||||
/// message, and `CreateFrame` still returns the frame.
|
||||
#[test]
|
||||
fn a_raising_template_onload_reaches_the_error_handler() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
register(
|
||||
&s,
|
||||
r#"<Ui>
|
||||
<Frame name="RaisingTemplate" virtual="true">
|
||||
<Scripts><OnLoad>this:NoSuchMethod()</OnLoad></Scripts>
|
||||
</Frame>
|
||||
</Ui>"#,
|
||||
);
|
||||
s.run(
|
||||
"caught = {} seterrorhandler(function(m) table.insert(caught, m) end) \
|
||||
made = CreateFrame('Frame', 'Raised', UIParent, 'RaisingTemplate')",
|
||||
)
|
||||
.unwrap();
|
||||
s.dispatch_script_errors_to_handler();
|
||||
let caught: Vec<String> = s.eval("return caught").unwrap();
|
||||
assert_eq!(caught.len(), 1, "{caught:?}");
|
||||
assert!(
|
||||
caught[0].ends_with("attempt to call method `NoSuchMethod' (a nil value)")
|
||||
&& !caught[0].contains('\n'),
|
||||
"{:?}",
|
||||
caught[0]
|
||||
);
|
||||
let made: bool = s
|
||||
.eval("return made ~= nil and made:GetName() == 'Raised'")
|
||||
.unwrap();
|
||||
assert!(
|
||||
made,
|
||||
"CreateFrame returns the frame after its OnLoad raised"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ mod tooltip_item;
|
||||
mod tooltip_spell;
|
||||
mod tooltip_unit;
|
||||
mod toplevel;
|
||||
mod update_walk;
|
||||
mod visibility;
|
||||
mod widget_surface;
|
||||
mod worldframe;
|
||||
@@ -221,6 +222,51 @@ fn a_widget_handler_error_reaches_the_chosen_error_handler() {
|
||||
assert!(caught.contains("slide boom"), "OnValueChanged: {caught}");
|
||||
}
|
||||
|
||||
/// 1.12 calls the error handler with the one value the failed call raised (`0x703b40`: the
|
||||
/// handler pushed, inserted below the message, `lua_call` 1 argument): the bare Lua message, no
|
||||
/// `runtime error: ` word and no traceback, which only benilla's own log carries.
|
||||
#[test]
|
||||
fn the_error_handler_receives_the_bare_lua_message() {
|
||||
let mut s = crate::script::UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.run(
|
||||
"caught = {} seterrorhandler(function(m) table.insert(caught, m) end) \
|
||||
local f = CreateFrame('Frame') \
|
||||
f:RegisterEvent('ERROR_PROBE') \
|
||||
f:SetScript('OnEvent', function() this:NoSuchMethod() end) \
|
||||
local b = CreateFrame('Button') \
|
||||
b:SetScript('OnClick', function() local t = nil; t.x = 1 end) \
|
||||
b:Click() \
|
||||
local c = CreateFrame('Button') \
|
||||
c:SetScript('OnClick', function() SetPortraitTexture() end) \
|
||||
c:Click()",
|
||||
)
|
||||
.unwrap();
|
||||
s.fire_event("ERROR_PROBE", vec![]);
|
||||
s.dispatch_script_errors_to_handler();
|
||||
let caught: Vec<String> = s.eval("return caught").unwrap();
|
||||
assert!(caught.len() >= 3, "{caught:?}");
|
||||
for m in &caught {
|
||||
assert!(
|
||||
!m.contains("runtime error") && !m.contains("stack traceback") && !m.contains('\n'),
|
||||
"bare message, one line: {m:?}"
|
||||
);
|
||||
}
|
||||
for want in [
|
||||
"attempt to call method `NoSuchMethod' (a nil value)",
|
||||
":1: attempt to index local `t' (a nil value)",
|
||||
] {
|
||||
assert!(
|
||||
caught
|
||||
.iter()
|
||||
.any(|m| m.starts_with("[string \"") && m.ends_with(want)),
|
||||
"{want}: {caught:?}"
|
||||
);
|
||||
}
|
||||
// The host log keeps the decorated text.
|
||||
assert!(s.take_errors().iter().any(|e| e.contains("runtime error")));
|
||||
}
|
||||
|
||||
/// `fire_global` carries `UPDATE_FACTION` from the faction-header verbs.
|
||||
#[test]
|
||||
fn a_self_unregistering_listener_does_not_rob_its_successor_on_the_internal_dispatch() {
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
//! The update walk (`0x765650`): strata from the bottom up (`0x7656c8`–`0x765793`), each
|
||||
//! stratum's levels from 0 up (`0x765774`), and each level's list of linked frames oldest link
|
||||
//! first, the list being head-inserted (`0x7648c9`) and walked from its tail (`0x7656e5`–`0x765707`).
|
||||
//! The walk saves the next frame before each call (`0x765707`), a hide repairs that cursor
|
||||
//! (`0x76498f`), and a frame linked into the level being walked is staged until the level ends
|
||||
//! (`0x76489b`, `0x76573e`).
|
||||
|
||||
use crate::script::UiScript;
|
||||
|
||||
use super::common::script;
|
||||
|
||||
/// `LOG` joined by spaces, then emptied.
|
||||
fn take_log(s: &UiScript) -> String {
|
||||
s.eval::<String>("local l = table.concat(LOG, ' ') LOG = {} return l")
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Lua that makes a frame `name`, logging `tag` from its `OnUpdate`.
|
||||
const LOGGED: &str = r#"
|
||||
LOG = {}
|
||||
function Logged(name, tag)
|
||||
local f = CreateFrame("Frame", name)
|
||||
f:SetScript("OnUpdate", function() table.insert(LOG, tag) end)
|
||||
return f
|
||||
end
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn the_walk_runs_strata_then_level_not_creation_order() {
|
||||
let mut s = script();
|
||||
s.run(LOGGED).unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
Logged("WalkTip", "tooltip"):SetFrameStrata("TOOLTIP")
|
||||
Logged("WalkBack", "background"):SetFrameStrata("BACKGROUND")
|
||||
Logged("WalkHigh", "level5"):SetFrameLevel(5)
|
||||
Logged("WalkLow", "level1"):SetFrameLevel(1)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
take_log(&s);
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "background level1 level5 tooltip");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Within a level the oldest link goes first, and a show links at the end: a frame hidden and
|
||||
/// shown again walks after one created later.
|
||||
#[test]
|
||||
fn a_reshown_frame_walks_after_its_levels_older_links() {
|
||||
let mut s = script();
|
||||
s.run(LOGGED).unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
Logged("LinkA", "a")
|
||||
Logged("LinkB", "b")
|
||||
LinkA:Hide()
|
||||
LinkA:Show()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "b a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_frame_hidden_by_an_earlier_on_update_is_not_updated() {
|
||||
let mut s = script();
|
||||
s.run(LOGGED).unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
Logged("HideA", "a"):SetScript("OnUpdate", function()
|
||||
table.insert(LOG, "a")
|
||||
HideB:Hide()
|
||||
end)
|
||||
Logged("HideB", "b")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "a");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A frame hidden and shown again before its turn relinks into the level being walked, which
|
||||
/// stages it: it waits for the next tick. (A blink every tick would stage it every tick.)
|
||||
#[test]
|
||||
fn a_frame_reshown_before_its_turn_in_the_walked_level_waits_a_tick() {
|
||||
let mut s = script();
|
||||
s.run(LOGGED).unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
Logged("BlinkA", "a"):SetScript("OnUpdate", function()
|
||||
table.insert(LOG, "a")
|
||||
if not BLINKED then
|
||||
BLINKED = 1
|
||||
BlinkB:Hide()
|
||||
BlinkB:Show()
|
||||
end
|
||||
end)
|
||||
Logged("BlinkB", "b")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "a");
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "a b");
|
||||
}
|
||||
|
||||
/// A frame shown into a level the walk has not reached yet is updated that tick; one shown into
|
||||
/// the level being walked or one already walked waits for the next.
|
||||
#[test]
|
||||
fn a_frame_shown_mid_walk_is_updated_only_if_its_level_is_still_ahead() {
|
||||
let mut s = script();
|
||||
s.run(LOGGED).unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
Logged("ShowA", "a"):SetScript("OnUpdate", function()
|
||||
table.insert(LOG, "a")
|
||||
ShowTip:Show()
|
||||
ShowSame:Show()
|
||||
ShowBack:Show()
|
||||
end)
|
||||
Logged("ShowTip", "tip"):SetFrameStrata("TOOLTIP")
|
||||
Logged("ShowSame", "same")
|
||||
Logged("ShowBack", "back"):SetFrameStrata("BACKGROUND")
|
||||
ShowTip:Hide()
|
||||
ShowSame:Hide()
|
||||
ShowBack:Hide()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "a tip");
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "back a same tip");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A level change relinks a shown frame at the head of its new level (`0x76a54c`), so a frame that
|
||||
/// moves itself to a level still ahead is walked again there.
|
||||
#[test]
|
||||
fn a_frame_that_moves_itself_to_a_level_ahead_is_walked_again() {
|
||||
let mut s = script();
|
||||
s.run(LOGGED).unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
Logged("Mover", "m"):SetScript("OnUpdate", function()
|
||||
table.insert(LOG, "m" .. this:GetFrameLevel())
|
||||
if this:GetFrameLevel() == 0 then this:SetFrameLevel(3) end
|
||||
end)
|
||||
Logged("Mid", "mid"):SetFrameLevel(2)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "m0 mid m3");
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "mid m3");
|
||||
}
|
||||
|
||||
/// The update reads the frame's handler when its turn comes (`0x76b2c3`), so an `OnUpdate` set by
|
||||
/// an earlier frame's runs that tick.
|
||||
#[test]
|
||||
fn an_on_update_set_before_the_frames_turn_runs_that_tick() {
|
||||
let mut s = script();
|
||||
s.run(LOGGED).unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
Logged("Setter", "setter"):SetScript("OnUpdate", function()
|
||||
table.insert(LOG, "setter")
|
||||
if not SET then
|
||||
SET = true
|
||||
Earlier:SetScript("OnUpdate", function() table.insert(LOG, "earlier") end)
|
||||
Later:SetScript("OnUpdate", function() table.insert(LOG, "later") end)
|
||||
end
|
||||
end)
|
||||
CreateFrame("Frame", "Earlier"):SetFrameStrata("BACKGROUND")
|
||||
CreateFrame("Frame", "Later"):SetFrameStrata("HIGH")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "setter later");
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "earlier setter later");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// "Ahead" is by stratum first: a frame shown into a later stratum is updated that tick even at a
|
||||
/// lower level than the one being walked, as the reference walks every stratum after this one.
|
||||
#[test]
|
||||
fn a_frame_shown_into_a_later_stratum_at_a_lower_level_runs_that_tick() {
|
||||
let mut s = script();
|
||||
s.run(LOGGED).unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
local late = Logged("LateHigh", "high")
|
||||
late:SetFrameStrata("HIGH")
|
||||
late:SetFrameLevel(1)
|
||||
late:Hide()
|
||||
local shower = Logged("ShowerMed", "medium")
|
||||
shower:SetFrameLevel(5)
|
||||
shower:SetScript("OnUpdate", function()
|
||||
table.insert(LOG, "medium")
|
||||
LateHigh:Show()
|
||||
end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
take_log(&s);
|
||||
s.tick(0.016);
|
||||
assert_eq!(take_log(&s), "medium high");
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use mlua::Lua;
|
||||
|
||||
use crate::widget::FrameHandle;
|
||||
|
||||
use super::{editbox, event, tooltip, Model, ScriptValue, UiScript};
|
||||
use super::{event, tooltip, Model, ScriptValue, UiScript};
|
||||
|
||||
impl UiScript {
|
||||
/// Queue an event for the start of the next tick, behind everything queued earlier and before
|
||||
@@ -61,7 +61,7 @@ pub(crate) fn fire_event_into(lua: &Lua, event: &str, args: Vec<ScriptValue>) {
|
||||
let id = model.frame_id(h);
|
||||
drop(model);
|
||||
if let Err(e) = event::fire_event_handler(lua, id, event, &args) {
|
||||
model_mut().record_script_error(e.to_string());
|
||||
model_mut().record_lua_error(&e);
|
||||
}
|
||||
at = next;
|
||||
}
|
||||
@@ -70,10 +70,10 @@ pub(crate) fn fire_event_into(lua: &Lua, event: &str, args: Vec<ScriptValue>) {
|
||||
}
|
||||
|
||||
impl super::UiScript {
|
||||
/// Advance a frame: the `GetTime()` clock and queued events, then the walk (`0x765650`),
|
||||
/// each shown frame's update in turn: its `OnUpdate(self, elapsed)` (`0x704f10`), and for an
|
||||
/// EditBox its flush and caret blink after it; then the engine's own fades, model panes and
|
||||
/// hover.
|
||||
/// Advance a frame: the `GetTime()` clock and queued events, then the update walk
|
||||
/// (`0x765650`), each linked frame's update in strata, level and link order: its
|
||||
/// `OnUpdate(self, elapsed)` (`0x704f10`), and for an EditBox its flush and caret blink after
|
||||
/// it; then the engine's own fades, model panes and hover.
|
||||
pub fn tick(&mut self, elapsed: f32) {
|
||||
self.model_mut().now += f64::from(elapsed);
|
||||
// Events queued since the last tick fire before this tick's OnUpdate.
|
||||
@@ -81,74 +81,7 @@ impl super::UiScript {
|
||||
for (event, args) in pending {
|
||||
self.fire_event(&event, args);
|
||||
}
|
||||
// Each entry: the frame's id, whether it has an `OnUpdate`, and the box if an EditBox.
|
||||
let walk: Vec<(u32, bool, Option<FrameHandle>)> = {
|
||||
let mut model = self.model_mut();
|
||||
// `SetScript` keeps the OnUpdate list; a destroyed frame's handle compacts out here.
|
||||
let shown = |h: FrameHandle| model.arena.frame(h).is_some_and(|f| f.effective_visible);
|
||||
let mut frames: Vec<(FrameHandle, bool)> = model
|
||||
.on_update_frames
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&h| shown(h))
|
||||
.map(|h| (h, false))
|
||||
.collect();
|
||||
// The walk reaches every shown frame, not only those with a script, and an EditBox's
|
||||
// update (`0x77a790`, vtable `+0x38`) flushes it whether or not it has an `OnUpdate`.
|
||||
frames.extend(
|
||||
model
|
||||
.arena
|
||||
.editbox_kinds()
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&h| shown(h))
|
||||
.map(|h| (h, true)),
|
||||
);
|
||||
if model
|
||||
.on_update_frames
|
||||
.iter()
|
||||
.any(|&h| model.arena.frame(h).is_none())
|
||||
{
|
||||
let arena = &model.arena;
|
||||
let live: Vec<FrameHandle> = model
|
||||
.on_update_frames
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&h| arena.frame(h).is_some())
|
||||
.collect();
|
||||
model.on_update_frames = live;
|
||||
}
|
||||
let mut walk: Vec<(u32, bool, Option<FrameHandle>)> = frames
|
||||
.into_iter()
|
||||
.map(|(h, editbox)| (model.frame_id(h), !editbox, editbox.then_some(h)))
|
||||
.collect();
|
||||
// Creation order, by frame id: getters settle on demand, so a handler sees what an
|
||||
// earlier one did this sweep, and the order must be stable. The reference walks each
|
||||
// strata level's list of shown frames (`0x765650`). A box with an `OnUpdate` is
|
||||
// listed twice, and the two entries merge.
|
||||
walk.sort_unstable_by_key(|&(id, ..)| id);
|
||||
walk.dedup_by(|later, earlier| {
|
||||
let same = later.0 == earlier.0;
|
||||
if same {
|
||||
earlier.1 |= later.1;
|
||||
earlier.2 = earlier.2.or(later.2);
|
||||
}
|
||||
same
|
||||
});
|
||||
walk
|
||||
};
|
||||
for (id, on_update, editbox) in walk {
|
||||
if on_update {
|
||||
if let Err(e) = event::fire_update_handler(&self.lua, id, elapsed) {
|
||||
self.push_error(e);
|
||||
}
|
||||
}
|
||||
// The box's own update runs its flush after its `OnUpdate` (`0x77a79a`, then
|
||||
// `0x77a7a1`), so the events an edit raised fire in the box's place in the walk.
|
||||
if let Some(h) = editbox {
|
||||
editbox::update(&self.lua, h, elapsed);
|
||||
}
|
||||
}
|
||||
self.update_walk(elapsed);
|
||||
self.tick_model_panes(elapsed);
|
||||
// The engine's line fades, apart from any OnUpdate script: ScrollingMessageFrame's
|
||||
// (`0x788460`) and MessageFrame's (`0x786200`), which also keeps only the lines that fit
|
||||
@@ -255,7 +188,7 @@ impl UiScript {
|
||||
})
|
||||
.collect();
|
||||
let mut ids: Vec<u32> = frames.into_iter().map(|h| model.frame_id(h)).collect();
|
||||
ids.sort_unstable(); // creation order, as in the OnUpdate sweep
|
||||
ids.sort_unstable(); // creation order
|
||||
ids
|
||||
};
|
||||
for id in update_ids {
|
||||
|
||||
@@ -313,7 +313,7 @@ pub(super) fn fire_cleared(lua: &Lua, h: FrameHandle) {
|
||||
if let Err(e) = event::fire_widget_handler(lua, id, "OnTooltipCleared", Vec::new()) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ fn fire_add_money(lua: &Lua, h: crate::widget::FrameHandle, copper: u64) {
|
||||
) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ fn update_bar(lua: &Lua, h: FrameHandle, unit: Option<&UnitState>) {
|
||||
) {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.record_script_error(e.to_string());
|
||||
.record_lua_error(&e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
//! The update walk (`0x765650`), each linked frame's per-frame update in the reference's order: the
|
||||
//! nine strata from the bottom up (`0x7656c8`–`0x765793`), each stratum's levels from 0 up
|
||||
//! (the bound re-read after each level, `0x765774`), and each level's list of linked frames from its
|
||||
//! tail (`0x7656e5`–`0x765707`). Every link is a head insert (`0x7648c9`), so a level walks oldest
|
||||
//! link first: the order of [`Frame::insertion_seq`](crate::widget::Frame::insertion_seq).
|
||||
//!
|
||||
//! The walk is live. It saves the next frame before each call (`0x765707`), and a hide that
|
||||
//! unlinks that frame moves the cursor past it (`0x76498f`), so a frame hidden before its turn is
|
||||
//! not updated. A frame linked while its own level is being walked is staged until that level
|
||||
//! ends (`0x76489b`, spliced at `0x76573e`) and so waits a tick, as does one linked into a level
|
||||
//! already walked; one linked into a level still ahead is updated there, after that level's
|
||||
//! older links.
|
||||
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
|
||||
use crate::widget::{FrameHandle, FrameKind};
|
||||
|
||||
use super::{editbox, event, UiScript};
|
||||
|
||||
/// A place in the walk: stratum, level, then within the level the frames linked before the walk
|
||||
/// began (0, by link stamp) ahead of those linked during it (1, by the order they linked).
|
||||
type Place = (u8, u16, u8, u64);
|
||||
|
||||
impl UiScript {
|
||||
/// Run the walk: each frame's `OnUpdate(self, elapsed)` (`0x76b2c0`, reading the handler when
|
||||
/// its turn comes), and for an EditBox its flush after it (`0x77a79a`, then `0x77a7a1`), so the
|
||||
/// events an edit raised fire in the box's place.
|
||||
pub(super) fn update_walk(&mut self, elapsed: f32) {
|
||||
let mut ahead: BTreeMap<Place, FrameHandle> = BTreeMap::new();
|
||||
let (renumbers, mut joins, mut seen) = {
|
||||
let mut model = self.model_mut();
|
||||
// `SetScript` keeps the OnUpdate list; a destroyed frame's handle compacts out here.
|
||||
if model
|
||||
.on_update_frames
|
||||
.iter()
|
||||
.any(|&h| model.arena.frame(h).is_none())
|
||||
{
|
||||
let arena = &model.arena;
|
||||
let live: Vec<FrameHandle> = model
|
||||
.on_update_frames
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&h| arena.frame(h).is_some())
|
||||
.collect();
|
||||
model.on_update_frames = live;
|
||||
}
|
||||
// Only frames with an `OnUpdate` or an EditBox's flush do anything when walked.
|
||||
let mut seen = HashSet::new();
|
||||
for &h in model
|
||||
.on_update_frames
|
||||
.iter()
|
||||
.chain(model.arena.editbox_kinds())
|
||||
{
|
||||
if let Some(f) = model.arena.frame(h).filter(|f| f.effective_visible) {
|
||||
let place = (f.strata as u8, f.level, 0, u64::from(f.insertion_seq));
|
||||
ahead.insert(place, h);
|
||||
seen.insert(h);
|
||||
}
|
||||
}
|
||||
model.arena.open_link_log();
|
||||
(model.arena.renumbers(), model.on_update_joins, seen)
|
||||
};
|
||||
// Each frame linked during the walk, by the number of its latest link.
|
||||
let mut relinked: HashMap<FrameHandle, u64> = HashMap::new();
|
||||
let mut links = 0u64;
|
||||
while let Some((place, h)) = ahead.pop_first() {
|
||||
let (on_update, editbox) = {
|
||||
let mut model = self.model_mut();
|
||||
let Some(f) = model.arena.frame(h) else {
|
||||
continue;
|
||||
};
|
||||
// Still at this place: linked, and not relinked since the place was taken.
|
||||
let here = f.effective_visible
|
||||
&& match place.2 {
|
||||
0 => !relinked.contains_key(&h),
|
||||
_ => relinked.get(&h) == Some(&place.3),
|
||||
};
|
||||
let editbox = f.kind == FrameKind::EditBox;
|
||||
let on_update = here
|
||||
&& model
|
||||
.scripts
|
||||
.get(&h)
|
||||
.is_some_and(|s| s.contains("OnUpdate"));
|
||||
(on_update.then(|| model.frame_id(h)), here && editbox)
|
||||
};
|
||||
if let Some(id) = on_update {
|
||||
if let Err(e) = event::fire_update_handler(&self.lua, id, elapsed) {
|
||||
self.push_error(e);
|
||||
}
|
||||
}
|
||||
if editbox {
|
||||
editbox::update(&self.lua, h, elapsed);
|
||||
}
|
||||
let mut model = self.model_mut();
|
||||
for linked in model.arena.take_linked() {
|
||||
links += 1;
|
||||
relinked.insert(linked, links);
|
||||
let Some(f) = model.arena.frame(linked) else {
|
||||
continue;
|
||||
};
|
||||
if (f.strata as u8, f.level) > (place.0, place.1) {
|
||||
ahead.insert((f.strata as u8, f.level, 1, links), linked);
|
||||
}
|
||||
}
|
||||
// A frame that gained an `OnUpdate` keeps its place if the walk has not passed it. Its
|
||||
// stamp compares with the walk's only while no renumber came between; else it waits.
|
||||
if model.on_update_joins != joins {
|
||||
joins = model.on_update_joins;
|
||||
if model.arena.renumbers() == renumbers {
|
||||
let joined: Vec<FrameHandle> = model
|
||||
.on_update_frames
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|h| !seen.contains(h) && !relinked.contains_key(h))
|
||||
.collect();
|
||||
for j in joined {
|
||||
seen.insert(j);
|
||||
let Some(f) = model.arena.frame(j).filter(|f| f.effective_visible) else {
|
||||
continue;
|
||||
};
|
||||
let at = (f.strata as u8, f.level, 0, u64::from(f.insertion_seq));
|
||||
if at > place {
|
||||
ahead.insert(at, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.model_mut().arena.close_link_log();
|
||||
}
|
||||
}
|
||||
@@ -152,6 +152,6 @@ fn model_mut(lua: &Lua) -> mlua::AppDataRefMut<'_, Model> {
|
||||
/// A handler error is recorded in [`Model::errors`], never propagated to the verb.
|
||||
fn record(lua: &Lua, fired: mlua::Result<()>) {
|
||||
if let Err(e) = fired {
|
||||
model_mut(lua).record_script_error(e.to_string());
|
||||
model_mut(lua).record_lua_error(&e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +250,12 @@ pub struct WidgetArena {
|
||||
regions: Arena<Region>,
|
||||
names: HashMap<String, FrameHandle>,
|
||||
next_insertion: u32,
|
||||
/// How many times the link stamps were renumbered at the cap, so a reader holding stamps can
|
||||
/// tell they are still comparable with the live ones.
|
||||
renumbers: u32,
|
||||
/// While the update walk runs, every frame linked into a bucket since it opened, in link order
|
||||
/// ([`Self::open_link_log`]).
|
||||
link_log: Option<Vec<FrameHandle>>,
|
||||
/// The draw list's fingerprint cache ([`crate::order::traversal`]).
|
||||
pub(crate) order_cache: crate::order::OrderCache,
|
||||
/// Minimap frames ever created, so the state feed notices a new one without a walk.
|
||||
@@ -301,6 +307,8 @@ impl WidgetArena {
|
||||
regions: Arena::new(),
|
||||
names: HashMap::new(),
|
||||
next_insertion: 0,
|
||||
renumbers: 0,
|
||||
link_log: None,
|
||||
minimap_created: 0,
|
||||
order_cache: Default::default(),
|
||||
ticked_kinds: Vec::new(),
|
||||
@@ -381,14 +389,42 @@ impl WidgetArena {
|
||||
}
|
||||
}
|
||||
self.next_insertion = order.len() as u32;
|
||||
self.renumbers += 1;
|
||||
}
|
||||
let seq = self.next_insertion;
|
||||
if let Some(f) = self.frame_mut(h) {
|
||||
f.insertion_seq = seq;
|
||||
self.next_insertion += 1;
|
||||
if let Some(log) = &mut self.link_log {
|
||||
log.push(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start recording every link into a bucket, for the update walk (`0x765650`), which must see
|
||||
/// the frames a handler links while it runs.
|
||||
pub(crate) fn open_link_log(&mut self) {
|
||||
self.link_log = Some(Vec::new());
|
||||
}
|
||||
|
||||
/// The frames linked since the last call, in link order, the log staying open.
|
||||
pub(crate) fn take_linked(&mut self) -> Vec<FrameHandle> {
|
||||
self.link_log
|
||||
.as_mut()
|
||||
.map(std::mem::take)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Stop recording links.
|
||||
pub(crate) fn close_link_log(&mut self) {
|
||||
self.link_log = None;
|
||||
}
|
||||
|
||||
/// How many times the link stamps have been renumbered; stamps read under one count compare.
|
||||
pub(crate) fn renumbers(&self) -> u32 {
|
||||
self.renumbers
|
||||
}
|
||||
|
||||
/// The first frame created with `name`; a later duplicate never takes it over (`0x701bd0`).
|
||||
pub fn lookup(&self, name: &str) -> Option<FrameHandle> {
|
||||
self.names.get(name).copied()
|
||||
@@ -502,8 +538,14 @@ impl WidgetArena {
|
||||
| FrameKind::DressUpModel
|
||||
| FrameKind::TabardModel
|
||||
);
|
||||
let visible = frame.effective_visible;
|
||||
let (index, generation) = self.frames.insert(frame);
|
||||
let handle = FrameHandle { index, generation };
|
||||
if visible {
|
||||
if let Some(log) = &mut self.link_log {
|
||||
log.push(handle);
|
||||
}
|
||||
}
|
||||
if ticked {
|
||||
self.ticked_kinds.push(handle);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ dev = ["benilla-app/dev"]
|
||||
# Profiling switches, forwarded so `cargo run -p benilla --features tracy` works.
|
||||
tracy = ["benilla-app/tracy"]
|
||||
trace_chrome = ["benilla-app/trace_chrome"]
|
||||
# Forwards to benilla-app's `bevy_dynamic_linking`, which enables bevy/dynamic_linking.
|
||||
bevy_dynamic_linking = ["benilla-app/bevy_dynamic_linking"]
|
||||
|
||||
[dependencies]
|
||||
# `default-features = false`, or `--no-default-features` here still builds benilla-app with `dev`.
|
||||
|
||||
+2
-1
@@ -110,7 +110,8 @@ run() {
|
||||
|
||||
run fmt cargo fmt --all -- --check
|
||||
run clippy cargo clippy $pflags --all-targets -- -D warnings
|
||||
run test env BENILLA_SKIP_LOG="$skips" cargo test $pflags
|
||||
# --no-fail-fast: one red test binary does not hide the failures in the ones after it.
|
||||
run test env BENILLA_SKIP_LOG="$skips" cargo test --no-fail-fast $pflags
|
||||
# Data-gated tests skip without the install or the addon corpus and libtest swallows their line,
|
||||
# so each skip is also logged to $BENILLA_SKIP_LOG and counted here.
|
||||
if [ -s "$skips" ]; then
|
||||
|
||||
+2
-2
@@ -120,12 +120,12 @@ else
|
||||
"corpus=$([ -d "$root/wow-addons-vanilla" ] && echo yes || echo no)):" \
|
||||
"data-gated tests skip silently on this machine"
|
||||
fi
|
||||
run test env ${require_data:+BENILLA_REQUIRE_DATA=1} BENILLA_SKIP_LOG="$skips" cargo test --workspace
|
||||
run test env ${require_data:+BENILLA_REQUIRE_DATA=1} BENILLA_SKIP_LOG="$skips" cargo test --no-fail-fast --workspace
|
||||
report_skips test
|
||||
|
||||
# test-no-install: refuses a test that reads the install without `wow_data_or_skip!`. `WOW_DATA=`
|
||||
# (set, empty) is the resolver's "no install"; no build reads it, so the test binaries are reused.
|
||||
run test-no-install env -u BENILLA_REQUIRE_DATA WOW_DATA= cargo test --workspace
|
||||
run test-no-install env -u BENILLA_REQUIRE_DATA WOW_DATA= cargo test --no-fail-fast --workspace
|
||||
|
||||
# doc-links: refuses a doc link to a `crate::`, `super::`, `self::` or `Self::` path whose leaf is
|
||||
# declared nowhere in the workspace. Narrow on purpose: every other unresolved link is a convention.
|
||||
|
||||
Reference in New Issue
Block a user