- the world map's arrow shows on every zone sheet, and alt-clicking a town jumps there: the stock file creates the player arrow as a `Model` child of `WorldMapFrame` and lays a zone sheet's explored-area overlays as ARTWORK textures on `WorldMapDetailFrame` — one `(strata, level)` bucket, where the draw layer outranks the frame — and benilla drew a model's scene at the frame's own layer-0 slot, so every overlay painted over it (the continent sheet has no overlays, which is why it was the only sheet with an arrow); wow-re's carve: a layer batch carries three sub-arrays — quads, the string batch, a render-callback list — that `0x76fb00` drains in that order, and `0x76d160` registers a model's callback only for layer 2 (`0x76d17f cmp ebx,2`), so a model's scene is ARTWORK content, neither a separate pass nor a layer-0 slot; a `ModelPane` quad now keys at the ARTWORK content rank, which re-orders the pet page's happiness icon (a sibling's BACKGROUND quad) under the pane's scene — the reference's own order, harmless because the bake is transparent where the model is not; a model against a LATER-linked ARTWORK font string in the same bucket is still ordered by link stamp, named not built; and the alt-click jump, which required the topmost frame under the cursor to be `WorldMapButton` itself while the stock file parents every POI icon and blip to that button, now walks up from whatever was hit to the button and takes its rect — the map's own chrome still answers nothing (1992) - `UIParent` is the reference's own: `UIParent.xml` loads off the chain at its toc's slot, right after `WorldFrame.xml`, sourcing `UIParent.lua` and `LocaleProperties.lua` itself, and our `UIParent.xml` + `UiPanels.xml` — 3086 lines of transcribed `UIParent.lua` — are deleted; the one unbound verb the stock file ticks, `CheckReadyCheckTime`, was already 1989's; back from the reference: all sixty-six registered events and their arms (`PLAYER_CONTROL_LOST`/`_GAINED`, `RAID_ROSTER_UPDATE`, the trade-request pair, `PLAYER_SKINNED`, `GOSSIP_ENTER_CODE`), the ESC ladder as written — StaticPopup, the options window's Cancel, the game menu, `CloseMenus`, `SpellStopCasting`, `SpellStopTargeting`, `CloseAllWindows`, `ClearTarget`, else open — so ESC no longer clears a held cursor item or cancels the colour picker, two arms ours had grown that the reference does not have; the fade/flash/pulse kits walked from `UIParent`'s own OnUpdate in place of three 1×1 driver frames of ours, so a cinematic's `UIParent:Hide()` stops the walks as it does there; the managed bottom-stack positions without our `UIParent_RegisterManagedPositionListener` extension, which no shipped file registered any more; `LowHealthFrame`/`OutOfControlFrame`; and the `firstTimeLoaded` close on re-show; what stayed ours moved out — the window tab template and its OnUpdate settle (1004's async-measure adapter) and `BENILLA_FALLBACK_ICON` to `ScrollTemplates.xml`, 0561's bag verbs to `ContainerFrameAdapters`, the script log's slot row to its own file, `OptionsFrameCancel = OptionsFrameCloseButton` beside the two options aliases — and the Auction/Inspect/Macro/Talent panel rows, verbatim copies of the LoadOnDemand addons' own registrations, go; the reference also decides that the area spirit healer's dialog accepts on show with one CANCEL button, that losing control closes every window bar the reference's exceptions, and that the bottom-bar clearance reads the saved globals `SHOW_MULTI_ACTIONBAR_1`/`_2` rather than the bars' shown state; three integration binaries that kept private copies of the manifest loader use the shared one now (1988) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
211 lines
9.1 KiB
Rust
211 lines
9.1 KiB
Rust
//! The timed-quest countdown window against the transcribed reference behaviour (decision 1150,
|
||
//! closing B234): the extracted 1.12 `QuestTimerFrame.lua` is the spec — one `SecondsToTime` line
|
||
//! per timed quest, the frame's height `45 + 16·n`, hidden entirely at zero timers, and every row
|
||
//! mapping back to its quest for the click and the hover.
|
||
//!
|
||
//! Three halves, and the last two are the load-bearing ones (`mirror_timer_tests`' lesson, and
|
||
//! 0675's): the state tests read the Lua back, which is blind to what actually *paints*; the
|
||
//! [`draw list`](UiScript::extract) test is what would catch a window that is shown, correctly
|
||
//! filled and invisible; and the geometry diff is what would catch every number being wrong while
|
||
//! all of the above passes.
|
||
|
||
use benilla_ui::script::{
|
||
QuestLogEntryView, QuestLogObjectiveView, QuestLogState, ScriptValue, UiScript,
|
||
};
|
||
|
||
use super::test_ui::load_ui as load_xml;
|
||
|
||
/// Fonts + UIParent (SecondsToTime lives there) + the window itself. Deliberately NOT the whole
|
||
/// manifest: the window must stand up on its own dependencies, and a harness that loads everything
|
||
/// hides a missing one.
|
||
fn harness() -> UiScript {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
// The window's title is `text="QUEST_TIMERS"`, resolved against the global at load — without
|
||
// the player's own strings it draws the KEY, which is exactly what a `text=` attribute does
|
||
// with an unknown name.
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
// `SecondsToTime`, which the reference's `QuestTimerFrame_Update` formats every row with, and
|
||
// `UIParent_ManageFramePositions`, which its OnShow/OnHide call.
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
// `MAX_QUESTS`, the loop bound the reference's repaint hides its spare rows with. 1.12
|
||
// declares it on QuestLogFrame.lua:2 and so do we (1751 window 16) — a nil there is
|
||
// `'for' limit must be a number` on the first repaint, not a missing row.
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.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, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestTimerFrame.xml");
|
||
s
|
||
}
|
||
|
||
/// A quest-log row. `timer` is the descriptor slot's absolute deadline (0 = untimed).
|
||
fn quest(id: u32, title: &str, timer: u32) -> QuestLogEntryView {
|
||
QuestLogEntryView {
|
||
quest_id: id,
|
||
title: title.into(),
|
||
level: 10,
|
||
timer,
|
||
objectives: vec![QuestLogObjectiveView {
|
||
text: "Bloodscalp Scout slain: 0/8".into(),
|
||
kind: "monster".into(),
|
||
finished: false,
|
||
cur: 0,
|
||
req: 8,
|
||
}],
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
fn log(entries: Vec<QuestLogEntryView>) -> QuestLogState {
|
||
QuestLogState {
|
||
num_quests: entries.len() as u32,
|
||
entries,
|
||
detail: None,
|
||
}
|
||
}
|
||
|
||
fn shown(s: &UiScript, frame: &str) -> bool {
|
||
s.eval::<bool>(&format!("return {frame}:IsShown()"))
|
||
.unwrap()
|
||
}
|
||
|
||
fn row_text(s: &UiScript, i: u32) -> String {
|
||
s.eval::<String>(&format!("return QuestTimer{i}Text:GetText()"))
|
||
.unwrap()
|
||
}
|
||
|
||
/// The reference's whole visible contract in one pass: nothing at all with no timed quest; the
|
||
/// window with one line per timed quest, in log order, captioned by `SecondsToTime`; the height
|
||
/// `45 + 16·n`; and back to hidden when the last timer goes.
|
||
#[test]
|
||
fn the_window_follows_the_timed_quests() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = harness();
|
||
|
||
// An untimed log: QUEST_LOG_UPDATE fires, the frame stays down.
|
||
s.set_quest_log(log(vec![quest(783, "A Threat Within", 0)]));
|
||
s.set_server_unix_time(1_000_000.0);
|
||
s.fire_event("QUEST_LOG_UPDATE", vec![]);
|
||
assert!(!shown(&s, "QuestTimerFrame"), "no timed quest, no window");
|
||
|
||
// Two timed quests either side of an untimed one. 14:20 and 45 s out.
|
||
s.set_quest_log(log(vec![
|
||
quest(1, "Deliver the Message", 1_000_860),
|
||
quest(2, "A Threat Within", 0),
|
||
quest(3, "Escort the Caravan", 1_000_045),
|
||
]));
|
||
s.fire_event("QUEST_LOG_UPDATE", vec![]);
|
||
|
||
assert!(shown(&s, "QuestTimerFrame"));
|
||
assert!(shown(&s, "QuestTimer1") && shown(&s, "QuestTimer2"));
|
||
assert!(!shown(&s, "QuestTimer3"), "only two timers are live");
|
||
// SecondsToTime's shipped spelling, plural rule and trailing space, all of it (ref
|
||
// UIParent.lua:1004-1031) — over the reference's own `−1` (decision 1154), so a 860-second
|
||
// gap reads 859 and a 45-second one 44.
|
||
assert_eq!(row_text(&s, 1), "14 Mins 19 Secs ");
|
||
assert_eq!(row_text(&s, 2), "44 Secs ");
|
||
assert_eq!(
|
||
s.eval::<f64>("return QuestTimerFrame:GetHeight()").unwrap(),
|
||
45.0 + 16.0 * 2.0
|
||
);
|
||
|
||
// Row 2 belongs to the THIRD log entry — the untimed row in the middle must not shift it.
|
||
assert_eq!(s.eval::<i64>("return GetQuestIndexForTimer(2)").unwrap(), 3);
|
||
|
||
// The clock alone moves and the OnUpdate repaints — no QUEST_LOG_UPDATE, which is the whole
|
||
// point of the engine owning the subtraction.
|
||
s.set_server_unix_time(1_000_030.0);
|
||
s.tick(0.2);
|
||
assert_eq!(row_text(&s, 2), "14 Secs ");
|
||
|
||
// The last timer expires out of the list: the window goes with it.
|
||
s.set_quest_log(log(vec![quest(2, "A Threat Within", 0)]));
|
||
s.fire_event("QUEST_LOG_UPDATE", vec![]);
|
||
assert!(!shown(&s, "QuestTimerFrame"));
|
||
}
|
||
|
||
/// The countdown must actually reach the screen. The state test above passes just as happily for a
|
||
/// window that is shown, correctly filled, and drawing nothing — the failure mode `cast_tests`
|
||
/// and `mirror_timer_tests` were both written for.
|
||
#[test]
|
||
fn the_countdown_reaches_the_draw_list() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = harness();
|
||
s.set_quest_log(log(vec![quest(1, "Deliver the Message", 1_000_860)]));
|
||
s.set_server_unix_time(1_000_000.0);
|
||
s.fire_event("QUEST_LOG_UPDATE", vec![]);
|
||
s.tick(0.0);
|
||
|
||
let quads = s.extract();
|
||
let text: Vec<String> = quads
|
||
.iter()
|
||
.filter_map(|q| match &q.content {
|
||
benilla_ui::script::QuadContent::Text { text, .. } => Some(text.clone()),
|
||
_ => None,
|
||
})
|
||
.flatten()
|
||
.collect();
|
||
assert!(
|
||
text.iter().any(|t| t.contains("14 Mins")),
|
||
"the countdown never reached the draw list: {text:?}"
|
||
);
|
||
assert!(
|
||
text.iter().any(|t| t == "Quest Timers"),
|
||
"the QUEST_TIMERS title never reached the draw list: {text:?}"
|
||
);
|
||
}
|
||
|
||
/// PLAYER_ENTERING_WORLD is the other event the reference registers (`QuestTimerFrame.lua:3`) —
|
||
/// a world entry with a timed quest already in the log must paint without waiting for the log to
|
||
/// change again.
|
||
#[test]
|
||
fn a_world_entry_paints_a_quest_already_running() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = harness();
|
||
s.set_quest_log(log(vec![quest(1, "Deliver the Message", 1_000_090)]));
|
||
s.set_server_unix_time(1_000_000.0);
|
||
s.fire_event("PLAYER_ENTERING_WORLD", vec![ScriptValue::Bool(true)]);
|
||
assert!(shown(&s, "QuestTimerFrame"));
|
||
// Singular at exactly one minute — SecondsToTime's `_P1` plural rule, the ref's own; 90 s of
|
||
// gap reads 89 through the `−1`.
|
||
assert_eq!(row_text(&s, 1), "1 Min 29 Secs ");
|
||
}
|
||
|
||
/// **The reference file is the test oracle** (decision 0675). Scrapes both this window and the
|
||
/// extracted reference `QuestTimerFrame.xml` for `<AbsDimension>` pairs per named element and
|
||
/// asserts every shared element's numbers match — the guard that catches a transcription whose
|
||
/// frames all load, click and populate while every number is wrong.
|
||
///
|
||
/// Verified to fail: perturbing `QuestTimer1`'s TOP offset from -30 to -29 reports
|
||
/// `QuestTimer1: ours [(140.0, 16.0), (0.0, -29.0)] != ref [(140.0, 16.0), (0.0, -30.0)]`.
|
||
#[test]
|
||
fn the_window_geometry_matches_the_reference_framexml() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let Some(reference) =
|
||
super::framexml_diff::reference("Interface\\FrameXML\\QuestTimerFrame.xml")
|
||
else {
|
||
eprintln!("skipping: no extracted FrameXML");
|
||
return;
|
||
};
|
||
// No exemptions: this window's frames keep the reference's bare names (decision 0591 §3 — the
|
||
// manage pass and the ref's own row lookup both resolve by literal name), and every number in
|
||
// it is the reference's. If an entry ever needs to go here, it names its reason.
|
||
super::framexml_diff::assert_geometry_matches(
|
||
"Interface\\FrameXML\\QuestTimerFrame.xml",
|
||
&reference,
|
||
&[],
|
||
22,
|
||
);
|
||
}
|