diff --git a/Cargo.lock b/Cargo.lock
index ae593da8..1b03558c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4117,8 +4117,6 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "kira"
version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "065632c16b39f4c00bc5514de0698474d1b6c36120743a154c78ca27ffc1ffe1"
dependencies = [
"atomic-arena",
"glam 0.33.2",
diff --git a/Cargo.toml b/Cargo.toml
index cd531bfb..6269cd90 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -58,6 +58,9 @@ sha1 = "0.10.6"
# authority. `third_party/lua-src/BENILLA.md` has the current state and the verifying command.
[patch.crates-io]
lua-src = { path = "third_party/lua-src" }
+# kira 0.12.1 with one patch: spatial gains and the volume tween evaluated per CHUNK and
+# interpolated, not per frame (decision 1945; `third_party/kira/BENILLA.md`).
+kira = { path = "third_party/kira" }
# Optimize *dependencies* even in dev builds. avian/parry collision math (and wgpu/bevy render) is
# unusably slow at opt-level 0 — a plain `cargo run` crawls to <1 fps once physics is doing real work.
diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml
index 9ee7e028..2de6e879 100644
--- a/crates/benilla-app/assets/ui/OptionsFrame.xml
+++ b/crates/benilla-app/assets/ui/OptionsFrame.xml
@@ -209,6 +209,17 @@
RegisterForSave("LOCK_ACTIONBAR")
ALWAYS_SHOW_MULTIBARS = "0"
RegisterForSave("ALWAYS_SHOW_MULTIBARS")
+ -- The quest pair's two flags, likewise UIOptionsFrame_Init's (stock l.110-111, l.122-123),
+ -- here since the quest windows went stock (1944). QUEST_FADING_DISABLE is the reference's
+ -- "0". AUTO_QUEST_WATCH is the string "1" ON PURPOSE (decision 1136): the reference assigns
+ -- the NUMBER 1 while every consumer — stock QuestLogFrame.lua:64/68 and the checkbox's own
+ -- load — compares against the string, so on a fresh 1.12 install the row paints unchecked
+ -- and the auto-watch is dead until the box is clicked; the string ships the advertised
+ -- default as the real one. A recorded deviation (1804's rule), not a quiet one.
+ QUEST_FADING_DISABLE = "0"
+ RegisterForSave("QUEST_FADING_DISABLE")
+ AUTO_QUEST_WATCH = "1"
+ RegisterForSave("AUTO_QUEST_WATCH")
UIOptionsFrameCheckButtons = { };
------------------- Basic Options -------------------
diff --git a/crates/benilla-app/assets/ui/QuestFrame.xml b/crates/benilla-app/assets/ui/QuestFrame.xml
deleted file mode 100644
index 26e5d700..00000000
--- a/crates/benilla-app/assets/ui/QuestFrame.xml
+++ /dev/null
@@ -1,1366 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- BenillaQuestGreetingPanel_OnShow()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- QuestMoneyFrame_OnLoad()
-
-
-
-
-
-
-
-
- BenillaQuestDetailPanel_OnShow()
- BenillaQuestDetailPanel_OnUpdate(this, arg1)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- SmallMoneyFrame_OnLoad(); MoneyFrame_SetType("STATIC");
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- BenillaQuestProgressPanel_OnShow()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- QuestMoneyFrame_OnLoad()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- BenillaQuestRewardPanel_OnShow()
-
-
-
- QuestFrame_OnLoad()
- QuestFrame_OnEvent(event)
- BenillaQuestFrame_OnUpdate()
-
- PlaySound("igQuestListOpen")
-
- getglobal("QuestFrameGreetingPanel"):Hide()
- getglobal("QuestFrameDetailPanel"):Hide()
- getglobal("QuestFrameProgressPanel"):Hide()
- getglobal("QuestFrameRewardPanel"):Hide()
- CloseQuest()
- PlaySound("igQuestListClose")
-
-
-
-
diff --git a/crates/benilla-app/assets/ui/QuestLogFrame.xml b/crates/benilla-app/assets/ui/QuestLogFrame.xml
deleted file mode 100644
index 2082fcc8..00000000
--- a/crates/benilla-app/assets/ui/QuestLogFrame.xml
+++ /dev/null
@@ -1,1999 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- getglobal("QuestLogTrackTracking"):SetVertexColor(1.0, 0, 0)
-
-
- GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
- GameTooltip:SetText("Shift-click a quest to add or remove a quest from your quest watch list.", nil, nil, nil, nil, 1)
-
-
- GameTooltip:Hide()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- FauxScrollFrame_OnVerticalScroll(QUESTLOG_QUEST_HEIGHT, BenillaQuestLogList_Update)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- QuestMoneyFrame_OnLoad()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- BenillaQuestLogFrame_OnLoad(self)
- BenillaQuestLogFrame_OnEvent(self)
-
- QuestLog_OnUpdate(arg1)
-
- BenillaQuestLogFrame_OnMouseWheel(arg1)
-
- if UpdateMicroButtons then UpdateMicroButtons() end PlaySound("igQuestLogOpen") BenillaQuestLogFrame_Update()
- if UpdateMicroButtons then UpdateMicroButtons() end PlaySound("igQuestLogClose")
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- BenillaQuestWatchFrame_OnLoad(self)
- BenillaQuestWatchFrame_OnEvent(self)
- BenillaQuestWatchFrame_OnUpdate(self, arg1)
-
-
-
diff --git a/crates/benilla-app/assets/ui/QuestShareFrame.xml b/crates/benilla-app/assets/ui/QuestShareFrame.xml
index 2000a050..cc510130 100644
--- a/crates/benilla-app/assets/ui/QuestShareFrame.xml
+++ b/crates/benilla-app/assets/ui/QuestShareFrame.xml
@@ -6,7 +6,7 @@
registration and the one-line arm) — and it fixes the dialog key, the field names and the
one verb by name, so those are reproduced exactly.
- WHY THIS IS ITS OWN FILE, next to QuestFrame.xml and QuestLogFrame.xml rather than inside
+ WHY THIS IS ITS OWN FILE, next to the (stock, since 1944) QuestFrame.xml and QuestLogFrame.xml rather than inside
either. It belongs to neither window. The confirm has no window at all — it is raised by
`crate::ui_quest_share` off `SMSG_QUEST_CONFIRM_ACCEPT`, exactly as DuelFrame.xml's two
dialogs are raised off the duel packets, and answering it sends a packet neither quest
diff --git a/crates/benilla-app/assets/ui/UiPanels.xml b/crates/benilla-app/assets/ui/UiPanels.xml
index 32e4f5d0..bbdb08ee 100644
--- a/crates/benilla-app/assets/ui/UiPanels.xml
+++ b/crates/benilla-app/assets/ui/UiPanels.xml
@@ -2000,6 +2000,40 @@
hideOnEscape = 1,
}
+ -- The quest log's abandon confirm and its with-items twin — ref StaticPopup.lua l.749-775,
+ -- in the reference's own home, which is this file (the same move RENAME_GUILD made). They
+ -- lived in our QuestLogFrame.xml until 1944 took that window stock; stock's Abandon button
+ -- calls `StaticPopup_Show("ABANDON_QUEST", GetAbandonQuestName())` and registers nothing.
+ -- The wording is the GlobalStrings key by name, as this file spells its dialogs (the
+ -- reference wraps the same globals in TEXT()); the with-items twin is declared as the
+ -- reference declares it, whatever `GetAbandonQuestItems()` answers.
+ StaticPopupDialogs["ABANDON_QUEST"] = {
+ text = ABANDON_QUEST_CONFIRM,
+ button1 = YES,
+ button2 = NO,
+ OnAccept = function()
+ AbandonQuest()
+ PlaySound("igQuestLogAbandonQuest")
+ end,
+ timeout = 0,
+ whileDead = 1,
+ exclusive = 1,
+ hideOnEscape = 1,
+ }
+ StaticPopupDialogs["ABANDON_QUEST_WITH_ITEMS"] = {
+ text = ABANDON_QUEST_CONFIRM_WITH_ITEMS,
+ button1 = YES,
+ button2 = NO,
+ OnAccept = function()
+ AbandonQuest()
+ PlaySound("igQuestLogAbandonQuest")
+ end,
+ timeout = 0,
+ whileDead = 1,
+ exclusive = 1,
+ hideOnEscape = 1,
+ }
+
-- ref UIParent.lua l.324-343 + l.356-360, the four arms on one driver. Each equip arm
-- hides the OTHER dialog first — the two questions are mutually exclusive by construction
-- (one item, one destination), and the hide is what cancels the record the new question
diff --git a/crates/benilla-app/assets/ui/benilla.toc b/crates/benilla-app/assets/ui/benilla.toc
index 57244593..6f087c4c 100644
--- a/crates/benilla-app/assets/ui/benilla.toc
+++ b/crates/benilla-app/assets/ui/benilla.toc
@@ -976,10 +976,16 @@ CraftFrame.xml
# every verb the stock file calls at runtime — not just at load — was checked by hand against
# the bindings before this line changed.
Interface\FrameXML\LootFrame.xml
-# The questgiver window (decision 0088): four sub-panels over the Era quest API. Loaded after
-# MerchantFrame (grouping; the BenillaMoney coin helpers now live in MoneyFrame.xml) and GossipFrame (shared parchment
-# art conventions); its title/panel-button labels use the auto-centred ButtonText slot.
-QuestFrame.xml
+# ══ THE QUESTGIVER WINDOW — the reference's own, off the player's chain (1751, 1944) ══
+# Stock QuestFrame.xml sources QuestFrame.lua and INCLUDES QuestFrameTemplates.xml (the panel,
+# item and title-button templates the quest log below inherits too), so one row brings the
+# three-file unit 1913 measured. Its `QuestFrameItems_Update` paints all three windows' reward
+# grids — the shared function that made the quest log un-migratable alone until the money went
+# stock (1937). Needs, all above: Fonts (QuestFont/QuestTitleFont), MoneyFrame (the two
+# MoneyFrameTemplate frames, resolved at LOAD), UiPanels, UIPanelTemplates. Its two saved-variable
+# flags (QUEST_FADING_DISABLE, AUTO_QUEST_WATCH) are declared where the reference declares them —
+# the options window's init (OptionsFrame.xml, far above).
+Interface\FrameXML\QuestFrame.xml
# The two guild-charter windows (decision 1672): the guild registrar — the window the "How do I
# form a guild?" gossip row opens — and the charter itself, the sheet nine people sign. **Both are
# the reference's own files off the player's chain now** (1751, windows 12 and 13). Loaded here,
@@ -1002,11 +1008,13 @@ Interface\FrameXML\GuildRegistrarFrame.xml
# close/action buttons `inherits=` at LOAD), and the QuestGreeting parchment family it shares with
# GuildRegistrarFrame directly above.
Interface\FrameXML\PetitionFrame.xml
-# The quest log window (decision 0088 arc, the quest-log slice): durable player state, not an
-# NPC session — loaded after MerchantFrame (grouping; the coin helpers are in MoneyFrame.xml now) and
-# alongside QuestFrame (same arc); the 'L' binding (ui_script/input.rs) calls its
-# ToggleQuestLog().
-QuestLogFrame.xml
+# ══ THE QUEST LOG — the reference's own, off the player's chain (1751, 1944) ══
+# Stock QuestLogFrame.xml sources QuestLogFrame.lua; the 'L' binding calls its ToggleQuestLog(),
+# and its reward hovers are GameTooltip:SetQuestLogItem / SetQuestLogRewardSpell (engine, 1944).
+# After QuestFrame.xml (QuestItemTemplate, QuestMoneyFrame_OnLoad, QuestFrameItems_Update),
+# MicroMenu.xml (QuestLog_OnShow calls UpdateMicroButtons unguarded) and the same kit as the
+# giver; the reference's toc has the pair at l.53-54.
+Interface\FrameXML\QuestLogFrame.xml
# The timed-quest countdown window under the minimap (decision 1150): the reference's own
# file split. After MinimapCluster (its anchor), UIParent (the manage pass that re-seats it,
# and SecondsToTime), GameTooltip (its row hover), and QuestLogFrame — whose window a row
diff --git a/crates/benilla-app/src/capture/probes/live_fps.rs b/crates/benilla-app/src/capture/probes/live_fps.rs
index 3020335f..a066fd3b 100644
--- a/crates/benilla-app/src/capture/probes/live_fps.rs
+++ b/crates/benilla-app/src/capture/probes/live_fps.rs
@@ -52,6 +52,11 @@ impl Plugin for LiveFpsPlugin {
samples: Vec::new(),
cpu_samples: Vec::new(),
cpu_prev: None,
+ churn_samples: Vec::new(),
+ entities_prev: 0,
+ decodes_prev: 0,
+ load_samples: Vec::new(),
+ paced_samples: Vec::new(),
cpu_at_start: None,
sys_at_start: None,
occluded_now: false,
@@ -87,6 +92,22 @@ struct LiveFps {
/// frame was CPU work or a wait is the first question about a stutter.
cpu_samples: Vec,
cpu_prev: Option,
+ /// Per frame, beside `cpu_samples`: entities added net of removed, mesh assets added,
+ /// model materials added — the three spike signatures a crowd produces (a spell kit
+ /// spawning, a body dressing, a first-seen material realizing). Printed for the worst
+ /// frames only.
+ churn_samples: Vec<(i32, u32, u32, u32)>,
+ entities_prev: usize,
+ decodes_prev: u32,
+ /// Live particles and the GPU meter's freshest reading per frame — the two "same work,
+ /// more of it" signatures the churn triple cannot carry.
+ load_samples: Vec<(u32, f32)>,
+ /// The PACED clock's delta per frame (`Time`, `frame_pace`): under vsync with
+ /// pipelined rendering the raw delta is mis-split into long/short pairs that sum to exact
+ /// refresh multiples, so `samples`' tail counts presentation jitter as frames. The pacer
+ /// snaps a cadenced delta to whole refreshes; a paced delta of two periods is a frame the
+ /// display actually skipped.
+ paced_samples: Vec,
/// Process CPU seconds at the first sampled frame ([`crate::perf::process_cpu_secs`]) — the
/// baseline for the window's `cpu_ms`/`cpu_pct`.
cpu_at_start: Option,
@@ -144,6 +165,10 @@ type ScreenParams<'w, 's> = (
),
With,
>,
+ // Asset adds per frame, for the worst-frame annotation (`LiveFps::churn_samples`).
+ MessageReader<'w, 's, AssetEvent>,
+ MessageReader<'w, 's, AssetEvent>,
+ Res<'w, Time>,
);
#[derive(SystemParam)]
@@ -196,6 +221,18 @@ fn drive_live_fps(
&screen.5,
&screen.6,
);
+ let (mesh_events, mat_events) = (&mut screen.7, &mut screen.8);
+ let paced_ms = screen.9.delta_secs() * 1000.0;
+ // Read every frame (a reader that only reads inside the window would report the whole
+ // backlog on its first sampled frame).
+ let mesh_added = mesh_events
+ .read()
+ .filter(|e| matches!(e, AssetEvent::Added { .. }))
+ .count() as u32;
+ let mat_added = mat_events
+ .read()
+ .filter(|e| matches!(e, AssetEvent::Added { .. }))
+ .count() as u32;
// Drain every frame so the state is current whichever phase we're in — the window can be
// occluded before sampling ever starts (a detached launch spawns behind whatever is open).
for o in occlusions.read() {
@@ -276,12 +313,31 @@ fn drive_live_fps(
};
probe.cpu_prev = cpu_now;
probe.cpu_samples.push(cpu_ms);
+ let n_entities = entities.iter().count();
+ let d_entities = if probe.churn_samples.is_empty() {
+ 0
+ } else {
+ n_entities as i32 - probe.entities_prev as i32
+ };
+ probe.entities_prev = n_entities;
+ let decodes = crate::sound::kit_decodes();
+ let d_decodes = decodes.saturating_sub(probe.decodes_prev);
+ probe.decodes_prev = decodes;
+ probe
+ .churn_samples
+ .push((d_entities, mesh_added, mat_added, d_decodes));
+ let mut gpu_now = 0.0;
if let Some(gpu) = gpu {
let ns = gpu.0.load(std::sync::atomic::Ordering::Relaxed);
if ns > 0 {
- gpu_samples.push(ns as f32 / 1.0e6);
+ gpu_now = ns as f32 / 1.0e6;
+ gpu_samples.push(gpu_now);
}
}
+ probe
+ .load_samples
+ .push((census.live_particles() as u32, gpu_now));
+ probe.paced_samples.push(paced_ms);
if probe.samples.len() < probe.frames {
return;
}
@@ -382,14 +438,25 @@ fn drive_live_fps(
c.sort_by(f32::total_cmp);
let cat = |q: f32| c[(((c.len() - 1) as f32) * q).round() as usize];
let over = probe.samples.iter().filter(|&&f| f > 16.9).count();
+ // Real skipped frames: paced deltas of two refreshes or more.
+ let period = 1000.0 / 60.0;
+ let dropped = probe
+ .paced_samples
+ .iter()
+ .filter(|&&f| f > 1.5 * period)
+ .count();
+ let paced_max = probe.paced_samples.iter().copied().fold(0.0f32, f32::max);
let mut worst: Vec = (0..probe.samples.len()).collect();
worst.sort_by(|&a, &b| probe.samples[b].total_cmp(&probe.samples[a]));
let worst = worst
.iter()
.take(6)
.map(|&i| {
+ let (de, dm, dmat, dsnd) =
+ probe.churn_samples.get(i).copied().unwrap_or((0, 0, 0, 0));
+ let (np, g) = probe.load_samples.get(i).copied().unwrap_or((0, 0.0));
format!(
- "{i}:{:.1}/{:.1}",
+ "{i}:{:.1}/{:.1}(e{de:+},m+{dm},mat+{dmat},snd+{dsnd},p{np},g{g:.1})",
probe.samples[i],
probe.cpu_samples.get(i).copied().unwrap_or(0.0)
)
@@ -398,7 +465,7 @@ fn drive_live_fps(
.join(",");
format!(
" cpu_ms={per_frame_ms:.2} cpu_pct={:.0} cpu_p95={:.2} cpu_p99={:.2} \
- frames_over={over} worst=[{worst}]",
+ frames_over={over} dropped={dropped} paced_max={paced_max:.1} worst=[{worst}]",
per_frame_ms / mean as f64 * 100.0,
cat(0.95),
cat(0.99),
@@ -590,8 +657,21 @@ fn print_vis_census(seen: &benilla_world::world_census::CensusReport) {
.map(|(name, vis, gated)| format!("{name}={vis}/gated{gated}"))
.collect::>()
.join(" ");
+ let resident = seen
+ .kinds
+ .iter()
+ .zip(seen.resident.iter())
+ .map(|((name, _, _), n)| format!("{name}={n}"))
+ .collect::>()
+ .join(" ");
+ let why = seen
+ .why
+ .iter()
+ .map(|(w, n)| format!("{w}={n}"))
+ .collect::>()
+ .join(" ");
println!(
- "VIS_CENSUS visible-submeshes {line} | tagged={} hidden={} exempt={} no_aabb={}",
+ "VIS_CENSUS visible-submeshes {line} | resident {resident} | why {why} | tagged={} hidden={} exempt={} no_aabb={}",
seen.tagged, seen.hidden, seen.exempt, seen.no_aabb
);
// The escapees always print: a tagged, bounded object the cull left un-hidden is a defect by
diff --git a/crates/benilla-app/src/go_templates.rs b/crates/benilla-app/src/go_templates.rs
index d44abbc1..16656e9d 100644
--- a/crates/benilla-app/src/go_templates.rs
+++ b/crates/benilla-app/src/go_templates.rs
@@ -56,6 +56,11 @@ pub(crate) struct GoTemplate {
/// `Some` with a nonzero `page_id` is what makes a right-click *read* it; a type-9 template
/// with no page (vanilla ships a handful) opens nothing at all, exactly like the reference.
pub(crate) text_page: Option,
+ /// The `PageTextMaterial.dbc` id `GetQuestBackgroundMaterial` answers for a quest sourced from
+ /// this object — by `GAMEOBJECT_TYPE_ID`: QUESTGIVER (2) and GOOBER (9) read `data[2]`, CHEST
+ /// (10) reads `data[9]`, every other type answers none (`0x5f5950`, wow-re
+ /// quest-material-reward-spell-bindings.md §1).
+ pub(crate) quest_material: Option,
}
/// A MO_TRANSPORT template's path tuple (`gameobject_template.data0..2`, decision 0438).
@@ -134,6 +139,11 @@ impl GameObjectTemplates {
page_id: data[0].max(0) as u32,
material: data[2].max(0) as u32,
});
+ let quest_material = match type_id {
+ 2 | 9 => Some(data[2].max(0) as u32),
+ 10 => Some(data[9].max(0) as u32),
+ _ => None,
+ };
self.templates.insert(
entry,
GoTemplate {
@@ -143,6 +153,7 @@ impl GameObjectTemplates {
meeting_stone_area,
mo_transport,
text_page,
+ quest_material,
},
);
}
@@ -183,6 +194,29 @@ mod tests {
assert_eq!(go_lock_slot(19), None); // MAILBOX — no lock (opens by USE)
}
+ /// The quest-giver material arm — by type: QUESTGIVER (2) and GOOBER (9) read `data[2]`,
+ /// CHEST (10) reads `data[9]`, anything else none (`0x5f5950`, wow-re
+ /// quest-material-reward-spell-bindings.md §1).
+ #[test]
+ fn insert_captures_the_quest_material_by_type() {
+ let mut t = GameObjectTemplates::default();
+ let mut data = [0i32; 24];
+ data[2] = 2;
+ data[9] = 3;
+ t.insert(100, 2, "Wanted Poster".into(), &data);
+ t.insert(101, 9, "Book".into(), &data);
+ t.insert(102, 10, "Chest".into(), &data);
+ t.insert(103, 15, "Boat".into(), &data);
+ assert_eq!(t.templates[&100].quest_material, Some(2));
+ assert_eq!(t.templates[&101].quest_material, Some(2));
+ assert_eq!(
+ t.templates[&102].quest_material,
+ Some(3),
+ "a chest reads data[9]"
+ );
+ assert_eq!(t.templates[&103].quest_material, None);
+ }
+
#[test]
fn insert_captures_mo_transport_tuple() {
let mut t = GameObjectTemplates::default();
diff --git a/crates/benilla-app/src/lib.rs b/crates/benilla-app/src/lib.rs
index d33a95dc..07659941 100644
--- a/crates/benilla-app/src/lib.rs
+++ b/crates/benilla-app/src/lib.rs
@@ -791,11 +791,11 @@ pub fn run(build: BuildId) -> AppExit {
.add_plugins(UiLootPlugin)
.add_plugins(UiLootRollPlugin)
// The questgiver window (decision 0088): fills from the net drain's QuestGiver and drives
- // QuestFrame.xml's four sub-panels over the Era quest API.
+ // the stock questgiver window's four sub-panels over the Era quest API (1944).
.add_plugins(UiQuestPlugin)
// The quest-log window (decision 0088's deferred second slice): fills from the self player's
// PLAYER_QUEST_LOG descriptor slots + the SMSG_QUEST_QUERY_RESPONSE template cache, and drives
- // QuestLogFrame.xml over the Era quest-log API.
+ // the stock quest log over the Era quest-log API (1944).
.add_plugins(UiQuestLogPlugin)
// The party quest-share (decision 1733): the verdict lines on a quest we pushed, and the
// escort-quest confirm. Neither is bound to a window, so it is its own plugin rather than a
diff --git a/crates/benilla-app/src/sound/kit.rs b/crates/benilla-app/src/sound/kit.rs
index 987acbeb..31d8b19e 100644
--- a/crates/benilla-app/src/sound/kit.rs
+++ b/crates/benilla-app/src/sound/kit.rs
@@ -1052,10 +1052,16 @@ impl SoundKits {
.with_context(|| format!("reading {path}"))?;
let data = mixer::sfx_from_bytes(bytes)?;
self.cache.insert(key, data.clone());
+ DECODES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Ok(data)
}
}
+/// First-play decodes so far — a chain read plus a decode on the main thread, once per
+/// distinct kit file per session. `FPS_PROBE` reads it per frame to annotate a tail frame
+/// (a raid's buff wave meets dozens of distinct spell sounds in its first minute).
+pub(crate) static DECODES: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
+
/// Startup: load the kit catalog off the chain (absent → no resource; every play site tolerates
/// that, the same optional-catalog rule as `Creatures`).
pub(super) fn load_sound_kits(mut commands: Commands, assets: Option>) {
diff --git a/crates/benilla-app/src/sound/mod.rs b/crates/benilla-app/src/sound/mod.rs
index e8a38b07..9a2a18ea 100644
--- a/crates/benilla-app/src/sound/mod.rs
+++ b/crates/benilla-app/src/sound/mod.rs
@@ -50,6 +50,10 @@ mod water;
mod weather;
mod zone;
pub(crate) use emote::EmoteSounds;
+/// First-play kit decodes so far (`kit::DECODES`) — the probe's tail annotation.
+pub(crate) fn kit_decodes() -> u32 {
+ kit::DECODES.load(std::sync::atomic::Ordering::Relaxed)
+}
pub(crate) use glue::GlueSound;
pub(crate) use greeting::NpcGreetingRequest;
pub(crate) use message::MessageSounds;
diff --git a/crates/benilla-app/src/ui_item_text.rs b/crates/benilla-app/src/ui_item_text.rs
index 28db88d0..bf54a382 100644
--- a/crates/benilla-app/src/ui_item_text.rs
+++ b/crates/benilla-app/src/ui_item_text.rs
@@ -207,6 +207,33 @@ struct Readable {
page_head: u32,
}
+/// The material a quest sourced from `guid` paints on — `GetQuestBackgroundMaterial 0x502230`,
+/// whose body is `ItemTextGetMaterial`'s: the source object by guid, an item's template
+/// `PageMaterial` or a GameObject's template data by type, the id named through
+/// `PageTextMaterial.dbc`; a creature, an uncached template or an id with no row is `None`, which
+/// is FrameXML's "Parchment". A GameObject whose template is not cached yet is asked for.
+pub(crate) fn object_material(
+ guid: u64,
+ items: &mut Items,
+ go_templates: &mut GameObjectTemplates,
+ materials: Option<&PageMaterials>,
+ commands: &NetCommands,
+) -> Option {
+ let name = |id: u32| materials.and_then(|m| m.0.name(id)).map(str::to_string);
+ if benilla_protocol::guid::is_gameobject(guid) {
+ return match go_templates.get(guid) {
+ Some(go) => go.quest_material.and_then(name),
+ None => {
+ go_templates.request(guid, commands);
+ None
+ }
+ };
+ }
+ let entry = items.object(guid)?.object_entry()?;
+ let t = items.template(entry, guid, commands)?;
+ name(t.page_material)
+}
+
/// Resolve the object's title + material + page head, whether it is an item or a GameObject. The
/// reference does exactly this — one guid lookup with typemask 1, then virtual getters — so a
/// single resolve serves both sources.
@@ -498,3 +525,78 @@ mod tests {
assert!(matches!(&sess.source, ReadSource::Pages { visited } if visited.is_empty()));
}
}
+
+#[cfg(test)]
+mod quest_material_tests {
+ use super::*;
+
+ fn go_guid(entry: u64) -> u64 {
+ (u64::from(benilla_protocol::guid::HIGH_GAMEOBJECT) << 48) | (entry << 24) | 1
+ }
+
+ /// `GetQuestBackgroundMaterial`'s resolve, the reader's twin: a GameObject source answers its
+ /// template data by type through `PageTextMaterial.dbc`; a creature source answers nothing
+ /// (FrameXML's Parchment); an uncached GameObject is asked for and answers nothing meanwhile.
+ #[test]
+ fn a_quest_sourced_from_an_object_paints_the_objects_material() {
+ let mut items = Items::default();
+ let mut gos = GameObjectTemplates::default();
+ let (tx, rx) = crossbeam_channel::unbounded();
+ let commands = NetCommands(tx);
+ let catalog = PageMaterials(benilla_formats::PageTextMaterialCatalog::from_rows(&[
+ (1, "Parchment"),
+ (2, "Stone"),
+ (3, "Marble"),
+ ]));
+ let mut data = [0i32; 24];
+ data[2] = 2;
+ data[9] = 3;
+ gos.insert(100, 2, "Wanted Poster".into(), &data);
+ gos.insert(102, 10, "Chest".into(), &data);
+ assert_eq!(
+ object_material(
+ go_guid(100),
+ &mut items,
+ &mut gos,
+ Some(&catalog),
+ &commands
+ )
+ .as_deref(),
+ Some("Stone")
+ );
+ assert_eq!(
+ object_material(
+ go_guid(102),
+ &mut items,
+ &mut gos,
+ Some(&catalog),
+ &commands
+ )
+ .as_deref(),
+ Some("Marble"),
+ "a chest reads data[9]"
+ );
+ // A creature quest-giver: neither template, nothing asked.
+ let creature = (u64::from(benilla_protocol::guid::HIGH_UNIT) << 48) | (6 << 24) | 1;
+ assert_eq!(
+ object_material(creature, &mut items, &mut gos, Some(&catalog), &commands),
+ None
+ );
+ assert!(rx.try_recv().is_err(), "a creature source asks for nothing");
+ // An uncached object: nothing yet, and the template is asked for.
+ assert_eq!(
+ object_material(
+ go_guid(777),
+ &mut items,
+ &mut gos,
+ Some(&catalog),
+ &commands
+ ),
+ None
+ );
+ assert!(
+ rx.try_recv().is_ok(),
+ "an uncached object's template is asked for"
+ );
+ }
+}
diff --git a/crates/benilla-app/src/ui_quest.rs b/crates/benilla-app/src/ui_quest.rs
index 79dffcfb..aeb711ee 100644
--- a/crates/benilla-app/src/ui_quest.rs
+++ b/crates/benilla-app/src/ui_quest.rs
@@ -22,14 +22,14 @@ use benilla_protocol::messages::{
use bevy::prelude::*;
use benilla_ui::script::{
- QuestAction, QuestItemView, QuestPanel, QuestState, ScriptValue, UiScript,
+ QuestAction, QuestItemView, QuestPanel, QuestRewardSpell, QuestState, UiScript,
};
use crate::entities::ItemDisplays;
use crate::items::Items;
use crate::names::NameCache;
use crate::net::{ClientCommand, Guid, GuidIndex, NetCommands, ObjectStore, SelfPlayer};
-use crate::ui_action::{show_messages, ui_error_text, MessageSink, Shown, UiError};
+use crate::ui_action::{show_messages, ui_error_text, MessageSink, Shown, Spells, UiError};
use crate::ui_script::UiInput;
use crate::ui_session::{close_npc_session_out_of_range, npc_switched, NpcSession};
@@ -397,12 +397,35 @@ fn resolve_items(
/// Build the Lua-facing snapshot from the open view — `None` when no window is open. Every
/// server-authored text runs the shared chat-macro substitution (`$N`/`$B`/`$G` —
/// [`crate::npc_text`]): the wire delivers quest text un-expanded, the client substitutes.
+/// The quest's reward spell as the getters answer it: `rewSpell` off the packet, resolved to the
+/// spell's name and icon through the spell catalog (`Spell.dbc`). Zero — no spell — is `None`; an
+/// id the catalog cannot place still counts (the id is real), with nothing to paint.
+pub(crate) fn reward_spell_view(
+ spell_id: u32,
+ spells: Option<&Spells>,
+) -> Option {
+ if spell_id == 0 {
+ return None;
+ }
+ let d = spells.and_then(|s| s.catalog.get(spell_id));
+ Some(QuestRewardSpell {
+ spell_id,
+ name: d.map(|d| d.name.clone()),
+ texture: d.and_then(|d| d.icon.clone()),
+ // `isTradeskillSpell` is `Spell.dbc` col 6 `Attributes` bit 5 (`0x20`) — `0x501e59`,
+ // wow-re quest-material-reward-spell-bindings.md §3 (1161 of 22357 rows set; craft-cast
+ // spells set, `Pattern:` teaching spells clear).
+ tradeskill: d.is_some_and(|d| d.attributes & 0x20 != 0),
+ })
+}
+
fn snapshot(
giver: &QuestGiver,
items: &mut Items,
icons: Option<&ItemDisplays>,
commands: &NetCommands,
macros: &crate::npc_text::MacroContext,
+ spells: Option<&Spells>,
) -> Option {
let sub = |t: &str| crate::npc_text::substitute(t, macros);
Some(match giver.view.as_ref()? {
@@ -432,6 +455,7 @@ fn snapshot(
choices: resolve_items(&d.choices, items, icons, commands),
rewards: resolve_items(&d.rewards, items, icons, commands),
reward_money: d.money.max(0) as u32,
+ reward_spell: reward_spell_view(d.reward_spell, spells),
..Default::default()
},
QuestView::Progress(p) => QuestState {
@@ -450,6 +474,7 @@ fn snapshot(
choices: resolve_items(&o.choices, items, icons, commands),
rewards: resolve_items(&o.rewards, items, icons, commands),
reward_money: o.money.max(0) as u32,
+ reward_spell: reward_spell_view(o.reward_spell, spells),
..Default::default()
},
})
@@ -479,6 +504,9 @@ fn feed_quest(
mut names: ResMut,
states: Res,
self_q: Query<(&ObjectStore, &Guid), With>,
+ spells: Option>,
+ mut go_templates: ResMut,
+ materials: Option>,
mut sink: MessageSink,
mut last: Local>>,
mut last_name: Local>>,
@@ -518,7 +546,24 @@ fn feed_quest(
subject: player.as_ref(),
states: &states,
},
+ spells.as_deref(),
);
+ // The panel's background material is the SOURCE object's — an item's page material, a
+ // GameObject's template data — never a creature's, and never on the wire (1946; wow-re
+ // quest-material-reward-spell-bindings.md §1). Resolved the way the reader window resolves its
+ // own, since the two bindings share a body.
+ let fresh = fresh.map(|mut st| {
+ st.background_material = giver.npc.and_then(|source| {
+ crate::ui_item_text::object_material(
+ source,
+ &mut items,
+ &mut go_templates,
+ materials.as_deref(),
+ &commands,
+ )
+ });
+ st
+ });
let npc_name = giver
.npc
.and_then(|g| names.resolve(g, &commands).map(str::to_string));
@@ -531,7 +576,9 @@ fn feed_quest(
return;
}
script.set_quest(fresh.clone());
- let name_arg = || vec![ScriptValue::Str(npc_name.clone().unwrap_or_default())];
+ // The reference's QUEST_GREETING/DETAIL/PROGRESS/COMPLETE carry no argument: the window
+ // reads `UnitName("npc")` for its title (stock QuestFrame.lua:63), and the "npc" unit is
+ // the session's (ui_unit.rs). Ours used to pass the name as arg1 (1944 dropped it).
match (&*last, &fresh) {
(_, Some(f)) if switched => {
// A different giver → close the old panel, open the new (both kits play). QUEST_FINISHED
@@ -539,7 +586,7 @@ fn feed_quest(
// drain the pending actions so it does NOT clear the giver we just re-opened. Safe: a
// switch is net-driven, so no user action is queued this frame to lose.
script.fire_event("QUEST_FINISHED", vec![]);
- script.fire_event(panel_event(f.panel), name_arg());
+ script.fire_event(panel_event(f.panel), vec![]);
let _ = script.take_quest_actions();
}
(_, Some(f)) => {
@@ -551,7 +598,7 @@ fn feed_quest(
} else {
panel_event(f.panel)
};
- script.fire_event(event, name_arg());
+ script.fire_event(event, vec![]);
}
(Some(_), None) => script.fire_event("QUEST_FINISHED", vec![]),
(None, None) => {}
@@ -838,6 +885,7 @@ mod tests {
subject: Some(&player),
states: &crate::world_state::WorldStates::default(),
},
+ None,
)
.expect("open");
assert_eq!(snap.panel, QuestPanel::Detail);
@@ -882,6 +930,7 @@ mod tests {
subject: None,
states: &crate::world_state::WorldStates::default(),
},
+ None,
)
.unwrap();
assert_eq!(snap.panel, QuestPanel::Progress);
diff --git a/crates/benilla-app/src/ui_quest_log.rs b/crates/benilla-app/src/ui_quest_log.rs
index 527aa20b..563cff05 100644
--- a/crates/benilla-app/src/ui_quest_log.rs
+++ b/crates/benilla-app/src/ui_quest_log.rs
@@ -53,6 +53,7 @@ use crate::entities::ItemDisplays;
use crate::items::Items;
use crate::names::NameCache;
use crate::net::{ClientCommand, Guid, NetCommands, ObjectStore, SelfPlayer};
+use crate::ui_action::Spells;
use crate::ui_script::UiInput;
use crate::ui_unit::UnitFeed;
@@ -330,6 +331,7 @@ fn build_detail(
icons: Option<&ItemDisplays>,
commands: &NetCommands,
macros: &crate::npc_text::MacroContext,
+ spells: Option<&Spells>,
) -> QuestLogDetail {
let (required_money, reward_money) = money_split(template.money);
let rewards = template
@@ -354,6 +356,7 @@ fn build_detail(
reward_money,
choices,
rewards,
+ reward_spell: crate::ui_quest::reward_spell_view(template.reward_spell, spells),
}
}
@@ -486,6 +489,7 @@ fn feed_quest_log(
header_names: Option>,
tag_names: Option>,
states: Res,
+ spells: Option>,
mut last: Local>,
mut prior_quest_ids: Local>>>,
) {
@@ -702,9 +706,16 @@ fn feed_quest_log(
.filter(|e| !e.is_header)
.map(|e| e.quest_id)
.and_then(|quest_id| {
- quest_log
- .template(quest_id, &commands)
- .map(|t| build_detail(t, &mut items, icons.as_deref(), &commands, ¯os))
+ quest_log.template(quest_id, &commands).map(|t| {
+ build_detail(
+ t,
+ &mut items,
+ icons.as_deref(),
+ &commands,
+ ¯os,
+ spells.as_deref(),
+ )
+ })
});
let fresh = QuestLogState {
@@ -745,8 +756,6 @@ fn feed_quest_log(
script.set_quest_log(fresh.clone());
script.fire_event("QUEST_LOG_UPDATE", vec![]);
if !progressed.is_empty() {
- // Post-push = post-prune: the current watch list, the byte law's index space.
- let watched = script.quest_log_watched();
for quest in progressed {
for line in quest.changed_lines {
script.fire_event("UI_INFO_MESSAGE", vec![ScriptValue::Str(line)]);
@@ -759,13 +768,13 @@ fn feed_quest_log(
};
script.fire_event("UI_INFO_MESSAGE", vec![ScriptValue::Str(msg)]);
}
- let watch_index = watched
- .iter()
- .position(|&id| id == quest.quest_id)
- .map_or(0, |p| p as i64 + 1);
- script.fire_event("QUEST_WATCH_UPDATE", vec![ScriptValue::Int(watch_index)]);
+ // QUEST_WATCH_UPDATE's arg1 is the quest's LOG index: stock QuestLog_OnEvent hands it
+ // to AutoQuestWatch_Update(questIndex), whose end is AddQuestWatch(questIndex)
+ // (QuestLogFrame.lua:68, 752-769). Ours used to pass the watch-list position and
+ // fire a BENILLA_QUEST_PROGRESS twin carrying the log index; the twin went with the
+ // window (1944).
script.fire_event(
- "BENILLA_QUEST_PROGRESS",
+ "QUEST_WATCH_UPDATE",
vec![ScriptValue::Int(i64::from(quest.index))],
);
}
@@ -805,8 +814,6 @@ fn advanced(now: &QuestLogObjectiveView, was: &QuestLogObjectiveView) -> bool {
/// One quest whose objectives advanced between log states — the announce unit of
/// [`feed_quest_log`].
struct QuestProgress {
- /// The quest id (stable identity — the watch list is id-keyed).
- quest_id: u32,
/// The quest's 1-based index in the NEW log (the Era API's quest-log index space).
index: u32,
/// The quest title (the COMPLETE toast's `%s`; empty falls to the UNKNOWN form).
@@ -846,7 +853,6 @@ fn quests_with_progressed_objectives(
.collect();
let completed = e.complete == 1 && prev.complete != 1;
(!changed_lines.is_empty() || completed).then_some(QuestProgress {
- quest_id: e.quest_id,
index: i as u32 + 1,
title: e.title.clone(),
changed_lines,
diff --git a/crates/benilla-app/src/ui_script/bottom_hud_tests.rs b/crates/benilla-app/src/ui_script/bottom_hud_tests.rs
index 96f484a6..3d9da8a7 100644
--- a/crates/benilla-app/src/ui_script/bottom_hud_tests.rs
+++ b/crates/benilla-app/src/ui_script/bottom_hud_tests.rs
@@ -77,7 +77,7 @@ fn shipped_xml() -> Vec<(String, String)> {
})
.collect();
out.sort();
- assert!(out.len() >= 40, "only {} xml files swept", out.len());
+ assert!(out.len() >= 38, "only {} xml files swept", out.len());
out
}
diff --git a/crates/benilla-app/src/ui_script/durability_tests.rs b/crates/benilla-app/src/ui_script/durability_tests.rs
index 383ae05f..29216d43 100644
--- a/crates/benilla-app/src/ui_script/durability_tests.rs
+++ b/crates/benilla-app/src/ui_script/durability_tests.rs
@@ -389,6 +389,7 @@ fn the_quest_tracker_stacks_below_the_durability_guy() {
for f in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
+ "Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -397,9 +398,11 @@ fn the_quest_tracker_stacks_below_the_durability_guy() {
"MinimapCluster.xml",
"ScrollTemplates.xml",
"Interface\\FrameXML\\DurabilityFrame.xml",
- "Interface\\FrameXML\\GlobalStrings.lua",
- "QuestFrame.xml",
- "QuestLogFrame.xml",
+ "Interface\\FrameXML\\BasicControls.xml",
+ "Interface\\FrameXML\\ItemButtonTemplate.xml",
+ "Interface\\FrameXML\\QuestFrame.xml",
+ "MicroMenu.xml",
+ "Interface\\FrameXML\\QuestLogFrame.xml",
] {
load_xml(&s, f);
}
diff --git a/crates/benilla-app/src/ui_script/errors_tests.rs b/crates/benilla-app/src/ui_script/errors_tests.rs
index eba2c091..09ff622f 100644
--- a/crates/benilla-app/src/ui_script/errors_tests.rs
+++ b/crates/benilla-app/src/ui_script/errors_tests.rs
@@ -111,6 +111,7 @@ fn an_error_toast_draws_over_an_open_panel_window() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -122,9 +123,12 @@ fn an_error_toast_draws_over_an_open_panel_window() {
// pane's UIPanelScrollFrameTemplate). A MISSING template is a loader *warning*, so an
// under-loaded list passes and then dies on the first FauxScrollFrame_Update.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
// A left-slot panel open, and the toast raised after it — the order that must not decide.
s.eval::<()>("ShowUIPanel(QuestLogFrame)").unwrap();
diff --git a/crates/benilla-app/src/ui_script/friends_tests.rs b/crates/benilla-app/src/ui_script/friends_tests.rs
index 7cb9001c..1056c9ca 100644
--- a/crates/benilla-app/src/ui_script/friends_tests.rs
+++ b/crates/benilla-app/src/ui_script/friends_tests.rs
@@ -17,6 +17,7 @@ fn setup() -> UiScript {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
@@ -44,7 +45,6 @@ fn setup() -> UiScript {
// drags in the whole unit-frame cluster — and it must be the REAL frames: loading only
// `PartyFrame.lua` for the two names gets "attempt to index a nil value" the first time one
// iterates. The manifest already seats these far above RaidFrame (1874).
- load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
// The reference's own `BasicControls.xml`, which is manifest entry 3 — far above the pane
// (1874). It is here for the error/message pair and `TEXT`. NOTE: unlike `raid_tests`, this
// harness does not load `UIParent.xml`, so `UIParentLoadAddOn` — which 1881 moved there out
diff --git a/crates/benilla-app/src/ui_script/guild_tests.rs b/crates/benilla-app/src/ui_script/guild_tests.rs
index 298e31ab..e9f4e52d 100644
--- a/crates/benilla-app/src/ui_script/guild_tests.rs
+++ b/crates/benilla-app/src/ui_script/guild_tests.rs
@@ -206,6 +206,7 @@ fn setup() -> UiScript {
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
@@ -227,7 +228,6 @@ fn setup() -> UiScript {
// drags in the whole unit-frame cluster — and it must be the REAL frames: loading only
// `PartyFrame.lua` for the two names gets "attempt to index a nil value" the first time one
// iterates. The manifest already seats these far above RaidFrame (1874).
- load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\TextStatusBar.lua");
load_xml(&s, r"Interface\FrameXML\TextStatusBar.xml");
load_xml(&s, r"Interface\FrameXML\BuffFrame.xml");
diff --git a/crates/benilla-app/src/ui_script/help_frame_tests.rs b/crates/benilla-app/src/ui_script/help_frame_tests.rs
index e6ce90cd..9c591cc2 100644
--- a/crates/benilla-app/src/ui_script/help_frame_tests.rs
+++ b/crates/benilla-app/src/ui_script/help_frame_tests.rs
@@ -30,6 +30,7 @@ fn setup() -> UiScript {
// row, whose OnLoad calls `SmallMoneyFrame_OnLoad` — the TOC's own order (1580's
// talent-wipe fixture hit this first).
"MoneyFrame.xml",
+ "Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
// The stock file's `HelpFrame_OnShow` calls `UpdateMicroButtons()` before
// `GetGMStatus()`, so without the micro row the OnShow raises and the status ask never
@@ -39,7 +40,6 @@ fn setup() -> UiScript {
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"UIParent.xml",
- "Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
diff --git a/crates/benilla-app/src/ui_script/options_tests.rs b/crates/benilla-app/src/ui_script/options_tests.rs
index 5ad18478..51cda487 100644
--- a/crates/benilla-app/src/ui_script/options_tests.rs
+++ b/crates/benilla-app/src/ui_script/options_tests.rs
@@ -718,6 +718,7 @@ fn interface_harness() -> UiScript {
&[
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
+ "Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -750,7 +751,6 @@ fn interface_harness() -> UiScript {
"Interface\\FrameXML\\ColorPickerFrame.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
- "Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
@@ -765,8 +765,11 @@ fn interface_harness() -> UiScript {
"ScrollTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
- "QuestFrame.xml",
- "QuestLogFrame.xml",
+ "Interface\\FrameXML\\BasicControls.xml",
+ "Interface\\FrameXML\\ItemButtonTemplate.xml",
+ "Interface\\FrameXML\\QuestFrame.xml",
+ "MicroMenu.xml",
+ "Interface\\FrameXML\\QuestLogFrame.xml",
],
);
harness_on(s)
diff --git a/crates/benilla-app/src/ui_script/pet_bar_tests.rs b/crates/benilla-app/src/ui_script/pet_bar_tests.rs
index ac57e4dd..5c9ae6f6 100644
--- a/crates/benilla-app/src/ui_script/pet_bar_tests.rs
+++ b/crates/benilla-app/src/ui_script/pet_bar_tests.rs
@@ -12,6 +12,7 @@ use super::test_ui::load_ui as load_xml;
fn load_pet_bar(s: &UiScript) {
for file in [
"MoneyFrame.xml",
+ "Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
@@ -21,7 +22,6 @@ fn load_pet_bar(s: &UiScript) {
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
- "Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
@@ -369,6 +369,7 @@ fn pet_bar_row(with_multibar: bool) -> (usize, f32) {
s.set_screen_size(1024.0, 768.0);
for file in [
"MoneyFrame.xml",
+ "Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
@@ -378,7 +379,6 @@ fn pet_bar_row(with_multibar: bool) -> (usize, f32) {
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
- "Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
@@ -677,6 +677,7 @@ fn load_pet_bar_with_tooltip(s: &UiScript) {
for file in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
+ "Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -686,7 +687,6 @@ fn load_pet_bar_with_tooltip(s: &UiScript) {
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
- "Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
diff --git a/crates/benilla-app/src/ui_script/petition_tests.rs b/crates/benilla-app/src/ui_script/petition_tests.rs
index 0580d442..fa905ee6 100644
--- a/crates/benilla-app/src/ui_script/petition_tests.rs
+++ b/crates/benilla-app/src/ui_script/petition_tests.rs
@@ -102,7 +102,10 @@ fn setup() -> UiScript {
// without this the rows build with no art at all and nothing goes red — which is exactly the
// failure `load_ui_strict` exists to turn into a red test.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "QuestFrame.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
// `ChatFrameEditBox`, which the reference's own purchase button indexes on every click to
// decide where focus goes after the name box closes — a nil there raises before the charter is
// bought. Ours guarded it; the reference does not.
diff --git a/crates/benilla-app/src/ui_script/quest_tests.rs b/crates/benilla-app/src/ui_script/quest_tests.rs
index a5af95e1..319a1269 100644
--- a/crates/benilla-app/src/ui_script/quest_tests.rs
+++ b/crates/benilla-app/src/ui_script/quest_tests.rs
@@ -19,6 +19,7 @@ fn questgiver_show_hide_plays_open_and_close_kits() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -27,8 +28,11 @@ fn questgiver_show_hide_plays_open_and_close_kits() {
// MerchantFrame.xml (the same documented cross-window dep the bag tests load).
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
// Hidden at load: no open sound (never transitions on startup).
assert!(
@@ -82,14 +86,18 @@ fn panel_events_show_exactly_one_child_panel_and_hide_the_others() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
let panels = [
"QuestFrameGreetingPanel",
@@ -106,10 +114,15 @@ fn panel_events_show_exactly_one_child_panel_and_hide_the_others() {
active_titles: vec!["Report to Goldshire".into()],
..QuestState::default()
}));
- s.fire_event(
- "QUEST_GREETING",
- vec![ScriptValue::Str("Deputy Willem".into())],
+ s.set_unit(
+ "npc",
+ Some(benilla_ui::script::UnitState {
+ exists: true,
+ name: Some("Deputy Willem".into()),
+ ..Default::default()
+ }),
);
+ s.fire_event("QUEST_GREETING", vec![]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
for name in panels {
assert_eq!(
@@ -132,10 +145,15 @@ fn panel_events_show_exactly_one_child_panel_and_hide_the_others() {
title: "A Threat Within".into(),
..QuestState::default()
}));
- s.fire_event(
- "QUEST_DETAIL",
- vec![ScriptValue::Str("Deputy Willem".into())],
+ s.set_unit(
+ "npc",
+ Some(benilla_ui::script::UnitState {
+ exists: true,
+ name: Some("Deputy Willem".into()),
+ ..Default::default()
+ }),
);
+ s.fire_event("QUEST_DETAIL", vec![]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
for name in panels {
assert_eq!(
@@ -163,14 +181,18 @@ fn detail_panel_reward_grid_follows_the_refs_two_per_row_layout() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
let choice = |name: &str, quality: u32| benilla_ui::script::QuestItemView {
item_id: 0,
@@ -191,10 +213,15 @@ fn detail_panel_reward_grid_follows_the_refs_two_per_row_layout() {
reward_money: 150,
..QuestState::default()
}));
- s.fire_event(
- "QUEST_DETAIL",
- vec![ScriptValue::Str("Marshal McBride".into())],
+ s.set_unit(
+ "npc",
+ Some(benilla_ui::script::UnitState {
+ exists: true,
+ name: Some("Marshal McBride".into()),
+ ..Default::default()
+ }),
);
+ s.fire_event("QUEST_DETAIL", vec![]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(s.eval::("return QuestDetailItem1:IsShown()").unwrap());
@@ -208,7 +235,7 @@ fn detail_panel_reward_grid_follows_the_refs_two_per_row_layout() {
assert_eq!(
s.eval::("return QuestDetailItemChooseText:GetText()")
.unwrap(),
- "You may choose one of these rewards:"
+ "You will be able to choose one of these rewards:"
);
assert_eq!(
s.eval::("return QuestDetailItemReceiveText:GetText()")
@@ -235,8 +262,11 @@ fn detail_panel_reward_grid_follows_the_refs_two_per_row_layout() {
// arm at all: an unmodified click must not raise and must not set an itemChoice.
s.run("QuestDetailItem1:Click()").ok();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
+ // The reward panel's `itemChoice` is set only by its own OnShow (stock QuestFrame.lua:104,
+ // `= 0`) and a reward-row click; on the detail panel it is simply unset.
assert_eq!(
- s.eval::("return QuestFrame.itemChoice").unwrap(),
+ s.eval::("return QuestFrameRewardPanel.itemChoice or 0")
+ .unwrap(),
0,
"a detail-panel row never selects a reward"
);
@@ -250,14 +280,18 @@ fn reward_panel_choice_click_selects_and_completes_with_zero_based_index() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
let choice = |name: &str| benilla_ui::script::QuestItemView {
item_id: 0,
@@ -275,10 +309,15 @@ fn reward_panel_choice_click_selects_and_completes_with_zero_based_index() {
choices: vec![choice("Worn Sword"), choice("Worn Mace")],
..QuestState::default()
}));
- s.fire_event(
- "QUEST_COMPLETE",
- vec![ScriptValue::Str("Marshal McBride".into())],
+ s.set_unit(
+ "npc",
+ Some(benilla_ui::script::UnitState {
+ exists: true,
+ name: Some("Marshal McBride".into()),
+ ..Default::default()
+ }),
);
+ s.fire_event("QUEST_COMPLETE", vec![]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(
@@ -317,24 +356,33 @@ fn greeting_goodbye_button_closes_the_window() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest(Some(QuestState {
panel: QuestPanel::Greeting,
greeting: "Hello".into(),
..QuestState::default()
}));
- s.fire_event(
- "QUEST_GREETING",
- vec![ScriptValue::Str("Deputy Willem".into())],
+ s.set_unit(
+ "npc",
+ Some(benilla_ui::script::UnitState {
+ exists: true,
+ name: Some("Deputy Willem".into()),
+ ..Default::default()
+ }),
);
+ s.fire_event("QUEST_GREETING", vec![]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(s.eval::("return QuestFrame:IsVisible()").unwrap());
@@ -362,14 +410,18 @@ fn detail_panel_action_buttons_resolve_to_real_onscreen_rects() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
// Planted, not shipped: instant text is OFF out of the box (see the doc comment).
s.eval::<()>(r#"QUEST_FADING_DISABLE = "1""#).unwrap();
s.set_quest(Some(QuestState {
@@ -378,10 +430,15 @@ fn detail_panel_action_buttons_resolve_to_real_onscreen_rects() {
body: "Kill kobolds.".into(),
..QuestState::default()
}));
- s.fire_event(
- "QUEST_DETAIL",
- vec![ScriptValue::Str("Deputy Willem".into())],
+ s.set_unit(
+ "npc",
+ Some(benilla_ui::script::UnitState {
+ exists: true,
+ name: Some("Deputy Willem".into()),
+ ..Default::default()
+ }),
);
+ s.fire_event("QUEST_DETAIL", vec![]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(s
.eval::("return QuestFrameAcceptButton:IsShown()")
@@ -478,14 +535,18 @@ fn write_on_still_fades_when_instant_text_is_off() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.eval::<()>(r#"QUEST_FADING_DISABLE = "0""#).unwrap();
s.set_quest(Some(QuestState {
panel: QuestPanel::Detail,
@@ -493,10 +554,15 @@ fn write_on_still_fades_when_instant_text_is_off() {
body: "Kill kobolds.".into(),
..QuestState::default()
}));
- s.fire_event(
- "QUEST_DETAIL",
- vec![ScriptValue::Str("Deputy Willem".into())],
+ s.set_unit(
+ "npc",
+ Some(benilla_ui::script::UnitState {
+ exists: true,
+ name: Some("Deputy Willem".into()),
+ ..Default::default()
+ }),
);
+ s.fire_event("QUEST_DETAIL", vec![]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
// Mid-write (reveal edge at char 4 of 13): the quill scratches, Accept stays dead.
@@ -543,14 +609,18 @@ fn npc_name_reaches_the_title_bar_on_open_and_on_refresh() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest(Some(QuestState {
panel: QuestPanel::Detail,
@@ -558,10 +628,15 @@ fn npc_name_reaches_the_title_bar_on_open_and_on_refresh() {
body: "Kill things.".into(),
..QuestState::default()
}));
- s.fire_event(
- "QUEST_DETAIL",
- vec![ScriptValue::Str("Marshal McBride".into())],
+ s.set_unit(
+ "npc",
+ Some(benilla_ui::script::UnitState {
+ exists: true,
+ name: Some("Marshal McBride".into()),
+ ..Default::default()
+ }),
);
+ s.fire_event("QUEST_DETAIL", vec![]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert_eq!(
s.eval::("return getglobal('QuestFrameNpcNameText'):GetText() or ''")
@@ -608,16 +683,24 @@ fn npc_name_reaches_the_title_bar_on_open_and_on_refresh() {
fn greeting_panel_title_rows_grow_to_their_wrapped_titles() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
+ // Stock sizes each row at show time — `SetHeight(GetTextHeight() + 2)` right after SetText
+ // (QuestFrame.lua:245) — which the reference can do because its text measure is synchronous.
+ // So is the app's (measure.rs); the harness installs the same seam's stand-in.
+ s.set_text_measurer(Box::new(super::FixedWidthFont(7.0)));
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest(Some(QuestState {
panel: QuestPanel::Greeting,
@@ -629,10 +712,15 @@ fn greeting_panel_title_rows_grow_to_their_wrapped_titles() {
],
..QuestState::default()
}));
- s.fire_event(
- "QUEST_GREETING",
- vec![ScriptValue::Str("Deputy Willem".into())],
+ s.set_unit(
+ "npc",
+ Some(benilla_ui::script::UnitState {
+ exists: true,
+ name: Some("Deputy Willem".into()),
+ ..Default::default()
+ }),
);
+ s.fire_event("QUEST_GREETING", vec![]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
let answer_measures = |s: &mut UiScript| {
@@ -665,7 +753,7 @@ fn greeting_panel_title_rows_grow_to_their_wrapped_titles() {
};
let (t1, b1, h1) = row(1);
let (t2, b2, h2) = row(2);
- assert!(h1 >= 28.0 && h2 >= 28.0, "both titles wrap: {h1}, {h2}");
+ assert!(h1 >= 24.0 && h2 >= 24.0, "both titles wrap: {h1}, {h2}");
assert!(
(t1 - b1 - (h1 + 2.0)).abs() < 0.5,
"row 1 is its wrapped title + 2: got {}, title {h1}",
@@ -698,14 +786,18 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
load_xml(&s, "DressUpFrame.xml"); // DressUpItemLink lives here
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit its menus build from
@@ -737,10 +829,15 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
)],
..QuestState::default()
}));
- s.fire_event(
- "QUEST_COMPLETE",
- vec![ScriptValue::Str("Marshal McBride".into())],
+ s.set_unit(
+ "npc",
+ Some(benilla_ui::script::UnitState {
+ exists: true,
+ name: Some("Marshal McBride".into()),
+ ..Default::default()
+ }),
);
+ s.fire_event("QUEST_COMPLETE", vec![]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
// The regression this fork could break: a PLAIN click still picks the choice (the ref's third
@@ -748,7 +845,8 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
// move it if they leaked into the select arm.
s.run("QuestRewardItem2:Click()").unwrap();
assert_eq!(
- s.eval::("return QuestFrame.itemChoice").unwrap(),
+ s.eval::("return QuestFrameRewardPanel.itemChoice")
+ .unwrap(),
2,
"a plain click still selects the reward choice"
);
@@ -772,7 +870,8 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
"shift-click posted choice 1's link"
);
assert_eq!(
- s.eval::("return QuestFrame.itemChoice").unwrap(),
+ s.eval::("return QuestFrameRewardPanel.itemChoice")
+ .unwrap(),
2,
"the shift arm returns — it must NOT also select the clicked choice"
);
@@ -803,7 +902,8 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
"ctrl-click opened the room wearing choice 1"
);
assert_eq!(
- s.eval::("return QuestFrame.itemChoice").unwrap(),
+ s.eval::("return QuestFrameRewardPanel.itemChoice")
+ .unwrap(),
2,
"the ctrl arm returns — it must NOT also select the clicked choice"
);
diff --git a/crates/benilla-app/src/ui_script/quest_timer_tests.rs b/crates/benilla-app/src/ui_script/quest_timer_tests.rs
index 67b27fff..6f18c5d7 100644
--- a/crates/benilla-app/src/ui_script/quest_timer_tests.rs
+++ b/crates/benilla-app/src/ui_script/quest_timer_tests.rs
@@ -37,8 +37,11 @@ fn harness() -> UiScript {
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
load_xml(&s, "Interface\\FrameXML\\QuestTimerFrame.xml");
s
}
diff --git a/crates/benilla-app/src/ui_script/questlog_tests.rs b/crates/benilla-app/src/ui_script/questlog_tests.rs
index 221b20e9..15bf7fd8 100644
--- a/crates/benilla-app/src/ui_script/questlog_tests.rs
+++ b/crates/benilla-app/src/ui_script/questlog_tests.rs
@@ -71,6 +71,7 @@ fn eight_entries() -> QuestLogState {
// lands, so the fixture carries that exact shape.
link: Some(HAMMER_LINK.into()),
}],
+ reward_spell: None,
}),
}
}
@@ -85,6 +86,7 @@ fn shipped_questlog_frame_loads_clean() {
let s = UiScript::new().unwrap();
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -95,9 +97,12 @@ fn shipped_questlog_frame_loads_clean() {
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
}
/// The whole contract in one end-to-end drive: open plays the kit and renders row 1 + the count +
@@ -111,6 +116,7 @@ fn shipped_questlog_frame_drives_end_to_end() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -121,9 +127,12 @@ fn shipped_questlog_frame_drives_end_to_end() {
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(eight_entries());
@@ -147,7 +156,7 @@ fn shipped_questlog_frame_drives_end_to_end() {
// Row 1 carries entry 1's title (indented), the count line reads "Quests: |cffffffff8/20|r", and nothing having been
// selected, the first non-header entry auto-selects (pin §2's SetFirstValidSelection).
assert!(s
- .eval::("return QuestLogTitle1Text:GetText()")
+ .eval::("return QuestLogTitle1NormalText:GetText()")
.unwrap()
.contains("Quest 1"));
assert_eq!(
@@ -194,10 +203,12 @@ fn shipped_questlog_frame_drives_end_to_end() {
.unwrap(),
"You will receive:"
);
- assert_eq!(
- s.eval::("return QuestLogItemChooseText:GetText()")
+ // …hidden, not blanked: stock QuestFrameItems_Update Show()s/Hide()s the choose text
+ // (QuestFrame.lua:454-473) and leaves its string alone.
+ assert!(
+ !s.eval::("return QuestLogItemChooseText:IsShown()")
.unwrap(),
- ""
+ "no choices: the choose text is hidden"
);
// No choices, so the pool's first slot IS the fixed reward and the SECOND is the first
// unused one. (One pool now — the ref's shape; there is no separate "choice row 1".)
@@ -220,7 +231,7 @@ fn shipped_questlog_frame_drives_end_to_end() {
})
.expect("a 'Quest 3' row text quad");
let before = s
- .eval::("return QuestLogTitle1Text:GetText()")
+ .eval::("return QuestLogTitle1NormalText:GetText()")
.unwrap();
s.mouse_wheel(wx, wy, -1.0);
assert!(s.errors().is_empty(), "wheel errors: {:?}", s.errors());
@@ -230,7 +241,7 @@ fn shipped_questlog_frame_drives_end_to_end() {
// The migration reverts our step; how far a notch should travel is a look call, recorded in
// 1846 rather than re-authored back in.
assert_ne!(
- s.eval::("return QuestLogTitle1Text:GetText()")
+ s.eval::("return QuestLogTitle1NormalText:GetText()")
.unwrap(),
before,
"wheel-down scrolled the list"
@@ -240,7 +251,7 @@ fn shipped_questlog_frame_drives_end_to_end() {
// Abandon, No path: marks the selection, shows the registry's ABANDON_QUEST entry on the
// shared StaticPopup engine (decision 0308 §3), but No drains nothing and hides it.
- s.run("BenillaQuestLogAbandonButton_OnClick()").unwrap();
+ s.run("QuestLogFrameAbandonButton:Click()").unwrap();
assert!(s.eval::("return StaticPopup1:IsShown()").unwrap());
assert_eq!(
s.eval::("return GetAbandonQuestName()").unwrap(),
@@ -269,7 +280,7 @@ fn shipped_questlog_frame_drives_end_to_end() {
);
// Abandon, Yes path: the pinned index (1, the selection at click time) drains, plus the kit.
- s.run("BenillaQuestLogAbandonButton_OnClick()").unwrap();
+ s.run("QuestLogFrameAbandonButton:Click()").unwrap();
s.run("StaticPopup_OnClick(StaticPopup1, 1)").unwrap();
assert_eq!(s.take_quest_log_abandons(), vec![1]);
// Show → igMainMenuOpen; the ref OnClick runs OnAccept FIRST (the abandon kit), THEN hides
@@ -306,8 +317,12 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
+ // Stock seats the watch check at update time off `questNormalText:GetWidth()` (QuestLogFrame.lua:205-215),
+ // synchronously — the app's measure seam is synchronous too (measure.rs); its stand-in here.
+ s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -318,11 +333,23 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ // A shift-click reaches stock QuestLogTitleButton_OnClick's unguarded `ChatFrameEditBox:IsVisible()`
+ // (QuestLogFrame.lua:478) — the chat window is on the manifest long before the log (1944).
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\UIMenu.xml");
+ load_xml(&s, "ChatFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(eight_entries());
+ // The app fires QUEST_LOG_UPDATE when the log lands; stock QuestLog_OnEvent's arm runs the
+ // first QuestWatch_Update, which is what hides an empty tracker.
+ s.fire_event("QUEST_LOG_UPDATE", vec![]);
s.run("ToggleQuestLog()").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
@@ -403,9 +430,12 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
.unwrap(),
" - Kobold Vermin slain: 3/10"
);
- // A manual watch is permanent: no auto-watch timer entry rides it.
+ // A manual watch is permanent: stock's shift-click inserts it with QUEST_WATCH_NO_EXPIRE
+ // (QuestLogFrame.lua:496), the timer that AutoQuestWatch_OnUpdate never counts down.
assert!(s
- .eval::("return BENILLA_QUEST_WATCH_TIMERS[\"Quest 1\"] == nil")
+ .eval::(
+ "return QUEST_WATCH_LIST[1] ~= nil and QUEST_WATCH_LIST[1].timer == QUEST_WATCH_NO_EXPIRE"
+ )
.unwrap());
// Shift-click again unwatches: the checkbox clears and — nothing left watched — the whole HUD
@@ -433,6 +463,7 @@ fn watch_guards_no_op_without_erroring() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -444,9 +475,18 @@ fn watch_guards_no_op_without_erroring() {
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ // A shift-click reaches stock QuestLogTitleButton_OnClick's unguarded `ChatFrameEditBox:IsVisible()`
+ // (QuestLogFrame.lua:478) — the chat window is on the manifest long before the log (1944).
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\UIMenu.xml");
+ load_xml(&s, "ChatFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(eight_entries());
s.run("ToggleQuestLog()").unwrap();
@@ -482,8 +522,9 @@ fn watch_guards_no_op_without_erroring() {
);
assert_eq!(
s.eval::("return GetQuestLogSelection()").unwrap(),
- 2,
- "the click still selects"
+ 1,
+ "a shift-click never selects — stock QuestLogTitleButton_OnClick's shift branch returns \
+ before the plain-click arm's QuestLog_SetSelection (QuestLogFrame.lua:472-500)"
);
// Fill the watch list to the cap (5) with quests 2-6 via the Lua API directly — the engine-level
@@ -532,6 +573,7 @@ fn progress_auto_watches_for_five_minutes() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -542,14 +584,22 @@ fn progress_auto_watches_for_five_minutes() {
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ // AUTO_QUEST_WATCH is the options window's (UIOptionsFrame_Init in the reference; 1944), so the
+ // auto-watch needs that window loaded, as the manifest has it long before the log.
+ load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
+ load_xml(&s, "KeyBindingsPage.xml");
+ load_xml(&s, "OptionsFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(eight_entries());
// A kill credit lands: the engine fires the event with the quest's 1-based log index.
s.fire_event(
- "BENILLA_QUEST_PROGRESS",
+ "QUEST_WATCH_UPDATE",
vec![benilla_ui::script::ScriptValue::Int(1)],
);
assert!(s.errors().is_empty(), "auto-watch errors: {:?}", s.errors());
@@ -557,15 +607,20 @@ fn progress_auto_watches_for_five_minutes() {
assert!(s
.eval::("return QuestWatchFrame:IsVisible()")
.unwrap());
- assert!(s
- .eval::("return BENILLA_QUEST_WATCH_TIMERS[\"Quest 1\"] ~= nil")
- .unwrap());
+ assert!(
+ s.eval::(
+ "return QUEST_WATCH_LIST[1] ~= nil and QUEST_WATCH_LIST[1].index == 1 \
+ and QUEST_WATCH_LIST[1].timer == MAX_QUEST_WATCH_TIMER"
+ )
+ .unwrap(),
+ "the reference's QUEST_WATCH_LIST carries the armed timer (QuestLogFrame.lua:752-769)"
+ );
// 299 s in it still holds; fresh progress re-arms the timer; expiry unwatches and hides.
s.tick(299.0);
assert!(s.eval::("return IsQuestWatched(1)").unwrap());
s.fire_event(
- "BENILLA_QUEST_PROGRESS",
+ "QUEST_WATCH_UPDATE",
vec![benilla_ui::script::ScriptValue::Int(1)],
);
s.tick(299.0);
@@ -599,25 +654,35 @@ fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
+ // The flag is declared by the options window's init since 1944, as the reference declares it
+ // in UIOptionsFrame_Init — so the window that owns it loads first, as in the manifest.
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
+ load_xml(&s, "ScrollTemplates.xml");
+ load_xml(&s, "KeyBindingsPage.xml");
+ load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(eight_entries());
assert_eq!(s.eval::("return AUTO_QUEST_WATCH").unwrap(), "1");
s.run(r#"AUTO_QUEST_WATCH = "0""#).unwrap();
s.fire_event(
- "BENILLA_QUEST_PROGRESS",
+ "QUEST_WATCH_UPDATE",
vec![benilla_ui::script::ScriptValue::Int(1)],
);
assert!(
@@ -627,7 +692,7 @@ fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() {
s.run(r#"AUTO_QUEST_WATCH = "1""#).unwrap();
s.fire_event(
- "BENILLA_QUEST_PROGRESS",
+ "QUEST_WATCH_UPDATE",
vec![benilla_ui::script::ScriptValue::Int(1)],
);
assert!(s.eval::("return IsQuestWatched(1)").unwrap());
@@ -658,8 +723,12 @@ fn the_row_tag_is_its_own_right_flush_string_and_the_state_word_wins() {
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
let mut state = eight_entries();
// Row 1: a plain elite quest. Row 2: an elite quest that is COMPLETE — the state word wins.
@@ -695,11 +764,11 @@ fn the_row_tag_is_its_own_right_flush_string_and_the_state_word_wins() {
// The title is the bare (indented) name — the state word lives on the tag string now, so
// appending it here too would double it.
let title1 = s
- .eval::("return QuestLogTitle1Text:GetText()")
+ .eval::("return QuestLogTitle1NormalText:GetText()")
.unwrap();
assert_eq!(title1, " Quest 1");
let title2 = s
- .eval::("return QuestLogTitle2Text:GetText()")
+ .eval::("return QuestLogTitle2NormalText:GetText()")
.unwrap();
assert_eq!(title2, " Quest 2", "no \"(Complete)\" on the title");
@@ -722,6 +791,7 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -732,9 +802,12 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(QuestLogState::default());
s.run("ToggleQuestLog()").unwrap();
@@ -742,22 +815,24 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
// A bare FontString region has no Show/Hide/IsVisible in this engine (only Frames/Buttons do —
// `region.rs`'s method table) — the empty-state message is a toggled SetText, read back here.
+ // The empty log says the reference's QUESTLOG_NO_QUESTS_TEXT (GlobalStrings.lua:3225) through
+ // stock QuestLogNoQuestsText; "Your quest log is empty." was ours (1944).
assert_eq!(
s.eval::("return QuestLogNoQuestsText:GetText()")
.unwrap(),
- "Your quest log is empty."
+ "No Active Quests"
);
assert!(!s.eval::("return QuestLogTitle1:IsVisible()").unwrap());
assert!(!s
.eval::("return QuestLogFrameAbandonButton:IsEnabled() ~= 0")
.unwrap());
- // The Description header is now Lua-managed (BenillaQuestLogDetail_Clear blanks it) rather than
- // a static `text=` — it used to float "Description" over the empty-log parchment with no
- // selection (this task's fix; QuestLogFrame.xml's header comment on the FontString).
- assert_eq!(
- s.eval::("return QuestLogDescriptionTitle:GetText()")
+ // With no selection the reference hides the whole detail pane (stock
+ // `QuestLog_UpdateQuestDetails`: `QuestLogDetailScrollFrame:Hide()`), header included — it
+ // keeps its static `text="QUEST_DESCRIPTION"`; ours used to blank the header instead (1944).
+ assert!(
+ !s.eval::("return QuestLogDetailScrollFrame:IsVisible()")
.unwrap(),
- ""
+ "no selection: the detail pane is hidden, header and all"
);
assert_eq!(
s.eval::("return QuestLogQuestCount:GetText()")
@@ -777,6 +852,7 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -787,9 +863,12 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
let mut state = eight_entries();
state.detail = Some(QuestLogDetail {
@@ -826,6 +905,7 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
usable: true,
..Default::default()
}],
+ reward_spell: None,
});
s.set_quest_log(state);
s.run("ToggleQuestLog()").unwrap();
@@ -850,7 +930,7 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
assert_eq!(
s.eval::("return QuestLogItemChooseText:GetText()")
.unwrap(),
- "You may choose one of these rewards:"
+ "You will be able to choose one of these rewards:"
);
// With choices present, the receive text reads "...also..." (QuestFrame.lua:461-462) — it
@@ -886,6 +966,24 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
/// ResizeChild`) well past the pane — load-bearing for the scroll/clip tests below even
/// engine-only (no font atlas, so every auto-height FontString measures 0 here — this fixture
/// overflows on the FIXED-height portions alone: 10×12px objective rows + the reward row).
+/// The measure round-trip the app's resolve pass drives with its font engine: answer every
+/// FontString the layout asked about with the 6px-per-glyph, 12px-per-line stand-in the
+/// synchronous `FixedWidthFont(6.0)` uses, so the two agree.
+fn answer_measures(s: &mut UiScript) {
+ let answers: Vec<(u32, f32, f32, u64)> = s
+ .fontstrings_needing_measure()
+ .into_iter()
+ .map(|r| {
+ let ink = r.text.chars().count() as f32 * 6.0;
+ match r.wrap_width {
+ Some(w) if w > 0.0 && ink > w => (r.id, w, (ink / w).ceil() * 12.0, r.key),
+ _ => (r.id, ink, 12.0, r.key),
+ }
+ })
+ .collect();
+ s.set_measured_text_unwrapped(&answers);
+}
+
fn overflowing_entry() -> QuestLogState {
let objectives = (1..=10)
.map(|i| QuestLogObjectiveView {
@@ -921,6 +1019,7 @@ fn overflowing_entry() -> QuestLogState {
usable: true,
..Default::default()
}],
+ reward_spell: None,
}),
}
}
@@ -938,6 +1037,7 @@ fn overflowing_detail_content_clips_to_the_scrollframe_rect() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -948,9 +1048,12 @@ fn overflowing_detail_content_clips_to_the_scrollframe_rect() {
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(overflowing_entry());
s.run("ToggleQuestLog()").unwrap();
@@ -1002,8 +1105,10 @@ fn wheel_over_the_detail_pane_changes_vertical_scroll() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
+ s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -1014,13 +1119,21 @@ fn wheel_over_the_detail_pane_changes_vertical_scroll() {
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(overflowing_entry());
s.run("ToggleQuestLog()").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
+ // The detail pane's scroll range is the union of its measured content (0128): the layout's
+ // measure round-trip has to be answered, as the app's resolve pass answers it every frame.
+ s.resolve();
+ answer_measures(&mut s);
+ s.resolve(); // the measured span reaches the scroll range, and the bar, on this pass
s.resolve();
let quads = s.extract();
@@ -1053,8 +1166,10 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
+ s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -1065,16 +1180,27 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(overflowing_entry());
s.run("ToggleQuestLog()").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
+ // The detail pane's scroll range is the union of its measured content (0128): the layout's
+ // measure round-trip has to be answered, as the app's resolve pass answers it every frame.
+ s.resolve();
+ answer_measures(&mut s);
+ s.resolve(); // the measured span reaches the scroll range, and the bar, on this pass
s.resolve(); // GetVerticalScrollRange (SetVerticalScroll's clamp) reads resolved rects.
- s.run("QuestLogDetailScrollFrame:SetVerticalScroll(10)")
+ // Scroll THROUGH the bar, as the wheel and the thumb do (ScrollFrameTemplate_OnMouseWheel sets
+ // the bar; its OnValueChanged moves the frame): a reselect's `ScrollBar:SetValue(0)` is what
+ // snaps the pane back, and a bar already at 0 would fire no change over a frame-only scroll.
+ s.run("QuestLogDetailScrollFrameScrollBar:SetValue(10)")
.unwrap();
assert_eq!(
s.eval::("return QuestLogDetailScrollFrame:GetVerticalScroll()")
@@ -1093,8 +1219,7 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
);
// A manual reselect (the row-click path — no doNotScroll) DOES reset it.
- s.run(r#"BenillaQuestLogTitle_OnClick(QuestLogTitle1, "LeftButton")"#)
- .unwrap();
+ s.run("QuestLogTitle1:Click()").unwrap();
assert!(s.errors().is_empty(), "click errors: {:?}", s.errors());
assert_eq!(
s.eval::("return QuestLogDetailScrollFrame:GetVerticalScroll()")
@@ -1117,6 +1242,7 @@ fn reward_row_hover_serves_the_shared_item_tooltip() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -1132,9 +1258,12 @@ fn reward_row_hover_serves_the_shared_item_tooltip() {
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(eight_entries());
s.run("ToggleQuestLog()").unwrap();
@@ -1264,6 +1393,7 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -1274,9 +1404,15 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ // A shift-click reaches stock QuestLogTitleButton_OnClick's unguarded `ChatFrameEditBox:IsVisible()`
+ // (QuestLogFrame.lua:478) — the chat window is on the manifest long before the log (1944).
+ load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
load_xml(&s, "DressUpFrame.xml"); // DressUpItemLink lives here
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit its menus build from
@@ -1349,6 +1485,7 @@ fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_i
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -1359,9 +1496,15 @@ fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_i
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ // A shift-click reaches stock QuestLogTitleButton_OnClick's unguarded `ChatFrameEditBox:IsVisible()`
+ // (QuestLogFrame.lua:478) — the chat window is on the manifest long before the log (1944).
+ load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit its menus build from
load_xml(&s, "ChatFrame.xml"); // ChatFrameEditBox lives here
@@ -1445,6 +1588,7 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -1455,9 +1599,12 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
// Solo, quest 1 sharable and selected by the auto-selection.
s.set_quest_log(entries_sharable(&[1]));
@@ -1489,7 +1636,7 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
// Select an unsharable quest: party unchanged, button goes dark.
s.run("SelectQuestLogEntry(2)").unwrap();
- s.run("BenillaQuestLogFrame_Update()").unwrap();
+ s.run("QuestLog_Update()").unwrap();
assert!(
s.eval::("return GetQuestLogPushable() == nil")
.unwrap(),
@@ -1503,7 +1650,7 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
// Leaving the party darkens it again from the other side.
s.run("SelectQuestLogEntry(1)").unwrap();
- s.run("BenillaQuestLogFrame_Update()").unwrap();
+ s.run("QuestLog_Update()").unwrap();
assert!(s
.eval::("return QuestFramePushQuestButton:IsEnabled() ~= 0")
.unwrap());
@@ -1524,6 +1671,7 @@ fn share_quest_is_dark_on_an_empty_log_even_in_a_party() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -1534,9 +1682,12 @@ fn share_quest_is_dark_on_an_empty_log_even_in_a_party() {
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(QuestLogState::default());
s.set_party(party(4));
@@ -1557,6 +1708,7 @@ fn share_quest_click_queues_the_selected_quests_id() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -1567,16 +1719,19 @@ fn share_quest_click_queues_the_selected_quests_id() {
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(entries_sharable(&[1, 5]));
s.set_party(party(1));
s.run("ToggleQuestLog()").unwrap();
s.run("SelectQuestLogEntry(5)").unwrap();
- s.run("BenillaQuestLogFrame_Update()").unwrap();
+ s.run("QuestLog_Update()").unwrap();
s.run("QuestFramePushQuestButton:Click()").unwrap();
assert_eq!(s.take_quest_log_pushes(), vec![5]);
assert!(s.take_quest_log_pushes().is_empty(), "drained");
diff --git a/crates/benilla-app/src/ui_script/raid_tests.rs b/crates/benilla-app/src/ui_script/raid_tests.rs
index f176faa4..2e2e3882 100644
--- a/crates/benilla-app/src/ui_script/raid_tests.rs
+++ b/crates/benilla-app/src/ui_script/raid_tests.rs
@@ -26,6 +26,7 @@ fn setup() -> UiScript {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "GameTooltip.xml");
@@ -40,7 +41,6 @@ fn setup() -> UiScript {
// drags in the whole unit-frame cluster — and it must be the REAL frames: loading only
// `PartyFrame.lua` for the two names gets "attempt to index a nil value" the first time one
// iterates. The manifest already seats these far above RaidFrame (1874).
- load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
// The reference's own `BasicControls.xml`, which is manifest entry 3 — far above the pane
// (1874). `UIParentLoadAddOn` used to be what this line was here for; 1881 moved that
// function to `UIParent.xml`, its home in the reference, and this harness already loads
diff --git a/crates/benilla-app/src/ui_script/reference_ui.rs b/crates/benilla-app/src/ui_script/reference_ui.rs
index 195f4941..61d88e2e 100644
--- a/crates/benilla-app/src/ui_script/reference_ui.rs
+++ b/crates/benilla-app/src/ui_script/reference_ui.rs
@@ -2047,6 +2047,12 @@ mod tests {
("SHOW_COMPARE_TOOLTIP", "PaperDollFrame.lua"),
("SYSMSG", "UIErrorsFrame.lua"),
("UNIT_DEFENSE", "PetPaperDollFrame.lua"),
+ (
+ "UNIT_QUEST_LOG_CHANGED",
+ "QuestLogFrame.lua — a party member's quest-log fields changing (the reference \
+ fires it off the unit's PLAYER_QUEST_LOG_* descriptor updates); benilla's unit \
+ feed does not derive it yet (1944)",
+ ),
(
"UNIT_MODEL_CHANGED",
"four files — the paperdoll model refresh",
@@ -2115,10 +2121,20 @@ mod tests {
}
let lines: Vec<&str> = text.lines().map(str::trim).collect();
for (i, t) in lines.iter().enumerate() {
+ // …or the value of a `match` arm (`QuestPanel::Progress =>
+ // "QUEST_PROGRESS",` — `ui_quest.rs`'s `panel_event`).
+ let (t, arm_value) = match t.find("=> \"") {
+ Some(k) => (t[k + 3..].trim_end_matches(','), true),
+ None => (*t, false),
+ };
let Some(lit) = t.strip_prefix('"').and_then(|x| x.strip_suffix('"'))
else {
continue;
};
+ if arm_value && caps(lit) {
+ fired.insert(lit.to_string());
+ continue;
+ }
let arm = i > 0
&& lines[i - 1].ends_with('{')
&& lines.get(i + 1).is_some_and(|n| n.starts_with('}'));
diff --git a/crates/benilla-app/src/ui_script/shipped_xml_tests.rs b/crates/benilla-app/src/ui_script/shipped_xml_tests.rs
index 12e32535..7a8274be 100644
--- a/crates/benilla-app/src/ui_script/shipped_xml_tests.rs
+++ b/crates/benilla-app/src/ui_script/shipped_xml_tests.rs
@@ -30,7 +30,7 @@ fn every_shipped_ui_xml_parses() {
// Never let the sweep pass by finding nothing — a moved assets dir would otherwise turn this
// into a test that guards zero files while staying green.
assert!(
- checked >= 40,
+ checked >= 38, // 41 until 1944 took the two quest files stock
"only {checked} xml files swept — sweep broke"
);
}
@@ -753,7 +753,7 @@ fn no_shipped_script_sets_a_global_string_key_as_display_text() {
}
assert!(offenders.is_empty(), "{}", offenders.join("\n"));
// The sweep must never pass by finding nothing to sweep.
- assert!(swept >= 40, "only {swept} xml files swept — sweep broke");
+ assert!(swept >= 38, "only {swept} xml files swept — sweep broke");
}
/// **The `$parentTextureFrame` idiom's contract, over the whole shipped UI**: a frame whose art is
diff --git a/crates/benilla-app/src/ui_script/spellbook_tests.rs b/crates/benilla-app/src/ui_script/spellbook_tests.rs
index 9a5181ef..ae651c47 100644
--- a/crates/benilla-app/src/ui_script/spellbook_tests.rs
+++ b/crates/benilla-app/src/ui_script/spellbook_tests.rs
@@ -121,6 +121,7 @@ fn shipped_spellbook_drives_end_to_end() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -130,7 +131,6 @@ fn shipped_spellbook_drives_end_to_end() {
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
load_xml(&s, "UIParent.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
diff --git a/crates/benilla-app/src/ui_script/taxi_tests.rs b/crates/benilla-app/src/ui_script/taxi_tests.rs
index 9e7b1c11..eb5debf1 100644
--- a/crates/benilla-app/src/ui_script/taxi_tests.rs
+++ b/crates/benilla-app/src/ui_script/taxi_tests.rs
@@ -16,6 +16,7 @@ fn taxi_script() -> UiScript {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml"); // TaxiNodeOnButtonEnter's tooltip + SetTooltipMoney
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml"); // BenillaErrorsFrame_AddMessage — DrawOneHopLines' refusal
@@ -25,7 +26,6 @@ fn taxi_script() -> UiScript {
// · UIPanelTemplates (.lua then .xml) — `TaxiCloseButton` inherits `UIPanelCloseButton`,
// which lives there and NOT in our UiPanels.xml. Without it the close button loads as a
// bare Button with no handler and a click does nothing at all.
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\TaxiFrame.xml");
diff --git a/crates/benilla-app/src/ui_script/trainer_tests.rs b/crates/benilla-app/src/ui_script/trainer_tests.rs
index 2674fc28..9c9558e2 100644
--- a/crates/benilla-app/src/ui_script/trainer_tests.rs
+++ b/crates/benilla-app/src/ui_script/trainer_tests.rs
@@ -26,6 +26,7 @@ fn trainer_script() -> UiScript {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -33,7 +34,6 @@ fn trainer_script() -> UiScript {
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml"); // the filter dropdown's kit
load_xml(&s, "ScrollTemplates.xml"); // the faux-scroll bar kit
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // the merchant kit; the money frames are MoneyFrame.xml's (1937)
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "TrainerFrame.xml");
s.run(
"TRAINER_FILTER_AVAILABLE = 1 TRAINER_FILTER_UNAVAILABLE = 1 TRAINER_FILTER_USED = 1 \
diff --git a/crates/benilla-app/src/ui_script/unit_frame_tests.rs b/crates/benilla-app/src/ui_script/unit_frame_tests.rs
index a32e12eb..08e086ae 100644
--- a/crates/benilla-app/src/ui_script/unit_frame_tests.rs
+++ b/crates/benilla-app/src/ui_script/unit_frame_tests.rs
@@ -646,13 +646,19 @@ fn shipped_target_frame_runs_the_level_law() {
load_unit_frames(&s);
// GetDifficultyColor's own load chain (the quest log window, its ref home).
load_xml(&s, "MoneyFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
- load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
- load_xml(&s, "QuestFrame.xml");
- load_xml(&s, "QuestLogFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
+ load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
+ load_xml(&s, "UIParent.xml");
+ load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
+ load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
+ load_xml(&s, "MicroMenu.xml");
+ load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
// The player at level 3, both feeds (the snapshot UnitLevel("player") reads; the req state
// the −1 gate and GetQuestGreenRange read) — the app keeps the two in step.
s.set_player_req_state(PlayerReqState {
diff --git a/crates/benilla-app/src/ui_script/unit_popup_tests.rs b/crates/benilla-app/src/ui_script/unit_popup_tests.rs
index 8a03bf8e..0b45350d 100644
--- a/crates/benilla-app/src/ui_script/unit_popup_tests.rs
+++ b/crates/benilla-app/src/ui_script/unit_popup_tests.rs
@@ -466,6 +466,7 @@ fn load_pet_menu_frames(s: &UiScript) {
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
"MoneyFrame.xml",
+ "Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -483,7 +484,6 @@ fn load_pet_menu_frames(s: &UiScript) {
"Interface\\FrameXML\\PetFrame.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
- "Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
diff --git a/crates/benilla-app/tests/mail_frame.rs b/crates/benilla-app/tests/mail_frame.rs
index cf329162..a07b2a3b 100644
--- a/crates/benilla-app/tests/mail_frame.rs
+++ b/crates/benilla-app/tests/mail_frame.rs
@@ -19,11 +19,11 @@ const FILES: [&str; 10] = [
// the `MoneyInputFrame_*` verbs. Seated straight after MoneyFrame.xml, benilla.toc's order.
r"Interface\FrameXML\MoneyInputFrame.lua",
r"Interface\FrameXML\MoneyInputFrame.xml",
+ "Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"GameTooltip.xml",
- "Interface\\FrameXML\\GlobalStrings.lua",
"MailFrame.xml",
];
diff --git a/crates/benilla-formats/src/page_text_material.rs b/crates/benilla-formats/src/page_text_material.rs
index f70f2415..91a67f26 100644
--- a/crates/benilla-formats/src/page_text_material.rs
+++ b/crates/benilla-formats/src/page_text_material.rs
@@ -33,6 +33,14 @@ pub struct PageTextMaterialCatalog {
}
impl PageTextMaterialCatalog {
+ /// A catalog from `(id, basename)` rows — the shipped six are 1 Parchment · 2 Stone · 3 Marble
+ /// · 4 Silver · 5 Bronze · 6 Valentine (wow-re quest-material-reward-spell-bindings.md §1).
+ pub fn from_rows(rows: &[(u32, &str)]) -> Self {
+ Self {
+ by_id: rows.iter().map(|&(id, n)| (id, n.to_string())).collect(),
+ }
+ }
+
/// The basename for a material id, or `None` for `0`/an id the table doesn't carry — the
/// reference's own `nil`, which `ItemTextFrame.lua` substitutes with "Parchment" (and then
/// hides the corner art, since Parchment has none).
diff --git a/crates/benilla-protocol/src/guid.rs b/crates/benilla-protocol/src/guid.rs
index 0f838c57..9c16e366 100644
--- a/crates/benilla-protocol/src/guid.rs
+++ b/crates/benilla-protocol/src/guid.rs
@@ -72,6 +72,10 @@ pub fn is_transport(guid: u64) -> bool {
matches!(high(guid), HIGH_MO_TRANSPORT | HIGH_TRANSPORT)
}
+pub fn is_gameobject(guid: u64) -> bool {
+ high(guid) == HIGH_GAMEOBJECT
+}
+
/// The embedded template entry, for the guid families that carry one. Two different layouts, both
/// VERIFIED against vmangos:
///
diff --git a/crates/benilla-ui/src/script/mod.rs b/crates/benilla-ui/src/script/mod.rs
index 30d1e326..c10a7d72 100644
--- a/crates/benilla-ui/src/script/mod.rs
+++ b/crates/benilla-ui/src/script/mod.rs
@@ -207,7 +207,9 @@ pub use model::{TextureProbe, TextureSizeProbe};
pub use party::{PartyMemberInfo, PartyRequest, PartyState, RaidMemberInfo, SavedInstanceInfo};
pub use pet::{PetActionView, PetStats};
pub use pvp::{HonorState, InspectHonorData};
-pub use quest::{QuestAction, QuestItemView, QuestPanel, QuestSelect, QuestState};
+pub use quest::{
+ QuestAction, QuestItemView, QuestPanel, QuestRewardSpell, QuestSelect, QuestState,
+};
pub use quest_log::{QuestLogDetail, QuestLogEntryView, QuestLogObjectiveView, QuestLogState};
pub(crate) use region::{apply_font_parts, implicit_creation_anchor_lua};
pub use reputation::{FactionEntry, ReputationSend, ReputationState};
diff --git a/crates/benilla-ui/src/script/quest.rs b/crates/benilla-ui/src/script/quest.rs
index d0018a7e..b83d7c7e 100644
--- a/crates/benilla-ui/src/script/quest.rs
+++ b/crates/benilla-ui/src/script/quest.rs
@@ -68,6 +68,20 @@ pub struct QuestItemView {
/// One open questgiver panel: the active sub-panel plus every field the four panels might read.
/// Pushed whole by the app; `None` means no quest window is open.
+/// The quest's reward spell — `rewSpell` on `SMSG_QUESTGIVER_QUEST_DETAILS` /
+/// `SMSG_QUESTGIVER_OFFER_REWARD` and on `SMSG_QUEST_QUERY_RESPONSE` — resolved by the app to
+/// the name and icon `GetRewardSpell` / `GetQuestLogRewardSpell` answer (stock
+/// `QuestFrameItems_Update` counts it as one more reward slot, `rewardType = "spell"`, and the
+/// slot's hover is `GameTooltip:SetQuestRewardSpell()`). `tradeskill` is the third return the
+/// reference derives from the spell itself (1944 — the derivation is wow-re's to pin).
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct QuestRewardSpell {
+ pub spell_id: u32,
+ pub name: Option,
+ pub texture: Option,
+ pub tradeskill: bool,
+}
+
#[derive(Clone, Debug, PartialEq)]
pub struct QuestState {
pub panel: QuestPanel,
@@ -95,6 +109,12 @@ pub struct QuestState {
pub required_money: u32,
/// Whether the turn-in is completable (`IsQuestCompletable`, progress panel).
pub completable: bool,
+ /// The reward spell, if the quest teaches one (`GetRewardSpell`; detail/reward panels).
+ pub reward_spell: Option,
+ /// The panel's background material (`GetQuestBackgroundMaterial` — nil is the reference's own
+ /// "Parchment" fallback in `QuestFrame_GetMaterial`). What fills it on the wire is 1944's
+ /// wow-re question; until the binary says, nothing does, and the verb answers nil honestly.
+ pub background_material: Option,
}
impl Default for QuestState {
@@ -113,6 +133,8 @@ impl Default for QuestState {
reward_money: 0,
required_money: 0,
completable: false,
+ reward_spell: None,
+ background_material: None,
}
}
}
@@ -170,7 +192,36 @@ impl super::UiScript {
}
/// `GetQuestItemInfo(type, index)` reads from this vector by `type`; unknown types → `None`.
-fn item_vec<'a>(state: &'a QuestState, kind: &str) -> Option<&'a Vec> {
+/// The three returns of `GetRewardSpell` / `GetQuestLogRewardSpell`: `texture, name,
+/// isTradeskillSpell` — the third a 1/nil boolean in the reference's convention.
+pub(super) fn reward_spell_returns(
+ lua: &Lua,
+ spell: Option,
+) -> mlua::Result {
+ let Some(sp) = spell else {
+ return Ok(MultiValue::from_vec(vec![
+ Value::Nil,
+ Value::Nil,
+ Value::Nil,
+ ]));
+ };
+ let texture = match &sp.texture {
+ Some(t) => Value::String(lua.create_string(t)?),
+ None => Value::Nil,
+ };
+ let name = match &sp.name {
+ Some(n) => Value::String(lua.create_string(n)?),
+ None => Value::Nil,
+ };
+ let tradeskill = if sp.tradeskill {
+ Value::Integer(1)
+ } else {
+ Value::Nil
+ };
+ Ok(MultiValue::from_vec(vec![texture, name, tradeskill]))
+}
+
+pub(super) fn item_vec<'a>(state: &'a QuestState, kind: &str) -> Option<&'a Vec> {
match kind {
"choice" => Some(&state.choices),
"reward" => Some(&state.rewards),
@@ -315,12 +366,19 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
- // GetRewardSpell() → nil (spell-reward rows out of scope in v1, decision 0088).
+ // GetRewardSpell() → texture, name, isTradeskillSpell (three returns, 0x501df0; three nils on
+ // every empty path, the reference's own `(nil,nil,nil)` kind — 1842). Real since 1944: the
+ // app resolves `rewSpell` off the giver packets; stock's `if ( GetRewardSpell() ) then` counts
+ // the slot.
g.set(
"GetRewardSpell",
- // THREE values on every reachable path, and the reference's own kinds include
- // `(nil,nil,nil)` — so the empty answer is three nils, not one (decision 1842).
- lua.create_function(|_, ()| Ok((Value::Nil, Value::Nil, Value::Nil)))?,
+ lua.create_function(|lua, ()| {
+ let spell = {
+ let model = lua.app_data_ref::().expect("model app_data");
+ model.quest.as_ref().and_then(|q| q.reward_spell.clone())
+ };
+ reward_spell_returns(lua, spell)
+ })?,
)?;
// ── Intents ───────────────────────────────────────────────────────────────────────────────────
@@ -357,6 +415,57 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
install_action(lua, "CloseQuest", super::QuestAction::Close)?;
install_action(lua, "CompleteQuest", super::QuestAction::Continue)?;
+ // GetQuestBackgroundMaterial() → nil | string (0x502230; 0 args, 1 return). The reference's
+ // `QuestFrame_GetMaterial` (QuestFrame.lua:597-603) treats nil as "Parchment" and any string
+ // as the name of a material whose four `$parentMaterial*` quadrants and text colours
+ // (`GetMaterialTextColors`) the panel switches to. Answered from the pushed panel state.
+ g.set(
+ "GetQuestBackgroundMaterial",
+ lua.create_function(|lua, ()| {
+ let material = {
+ let model = lua.app_data_ref::().expect("model app_data");
+ model
+ .quest
+ .as_ref()
+ .and_then(|q| q.background_material.clone())
+ };
+ match material {
+ Some(m) => Ok(Value::String(lua.create_string(&m)?)),
+ None => Ok(Value::Nil),
+ }
+ })?,
+ )?;
+
+ // QuestChooseRewardError() — 0x5021a0, 0 args, 0 returns: the Complete button pressed with
+ // choices on offer and none picked (stock `QuestRewardCompleteButton_OnClick`,
+ // QuestFrame.lua:96-100). Sixteen bytes — `push 0x98; call 0x496720` — the game-error row
+ // `ERR_QUEST_MUST_CHOOSE` (wow-re quest-material-reward-spell-bindings.md §2): the row's sound
+ // cue `igQuestFailed` plays first, then kind 2 routes the GlobalStrings text through
+ // `UI_ERROR_MESSAGE` — synchronously, as SignalEvent is — and UIErrorsFrame prints it. The
+ // text is read from the same global the FrameXML shows, so a localised chain shows its own
+ // string; the shipped English is the fallback when no GlobalStrings is loaded.
+ g.set(
+ "QuestChooseRewardError",
+ lua.create_function(|lua, ()| {
+ {
+ let mut model = lua.app_data_mut::().expect("model app_data");
+ model
+ .sound_queue
+ .push(super::SoundRequest::KitName("igQuestFailed".to_string()));
+ }
+ let text: String = lua
+ .globals()
+ .get::