- a comment that justifies code by an engine difference is a claim with an expiry date, and the tree had ~96 expired ones across `assets/ui`, `ui_script/` and `benilla-ui` — a hygiene pass, no behaviour change: dangling `Benilla*` names with zero definitions (`BenillaMoney_*` at thirteen sites after 1937/1962 converged the coin cluster onto the stock `SmallMoneyFrameTemplate`; `BenillaGossipRow_Resize`, `BenillaErrorsFrame_AddMessage`, the digit feed 1285 deleted, `BenillaMerchantItem_OnClick` and three more), "the shipped `assets/ui/<X>.xml`" for thirty-odd files the directory no longer holds — it holds nine — each site now naming the chain file that actually loads, `UiPanels.xml` in the present tense after 1988 took the panel manager and the dialog engine stock, and expired self-deprecation: SimpleHTML "a widget kind we do not have" (1337 built it), "benilla has no console" (1948 bound `ConsoleExec`), `Model`'s "seven verbs not published yet" and "`ClearFog` is a guess" (all 23 are published, the carve sits 500 lines below, `UNBUILT` has been `[&str; 0]` since 2027), `frame_flag_gate`'s doc describing four groups of accepted mouse divergences over a `KNOWN` list that is empty (the empty array and its enforcement stay), five comments saying key delivery is not gated on `keyboard_enabled` where `script::keyboard`'s walk filters on exactly that field (1319), `SetRaidTarget` called a verb this house lacks beside an assert that its intent IS queued (1820), two questlog tests deferring to a screenshot for `GetPoint`/`IsVisible` the regions already answer, `benilla.toc`'s "trade still needs four engine verbs" (all four bound, 1966) and its ping `Model`s "this engine draws no pixels for" (2013 renders `MinimapPing.mdx` itself), and `rig_anim`'s `set_paused` naming a booth caller it does not have; and the code the prose was propping up goes with it: `GameMenuFrame.xml`'s six `<OnLoad>` label setters, because `text=` on a `<Button>` is a global-string lookup at the loader's attribute step (0995, built 61 seconds before the file was written) — the buttons carry `text="LOGOUT"`/`"EXIT_GAME"`/`"MACROS"`/`"RETURN_TO_GAME"`/`"MAIN_MENU"` as the reference's own do; `ScriptLogFrame`'s five authored close-button lines, now `inherits="UIPanelCloseButton"` off the chain's `UIPanelTemplates.xml` seated 757 lines above it (1846); and the editbox's 22-line "gap" doc claiming only the self-acquire `autoFocus` path is built and its `false` default unverified — path 1 is 1686's `visibility_focus`, and the default is `true`, byte-read off `flags = 1` at `0x779a29`/`0x779a2e`, so the test named `autofocus_does_not_focus_on_show…` is renamed for what it actually pins (2044) - the `text=` divergence never existed: `resolve_text`'s doc had claimed since 0068 that falling back to the raw attribute on a GlobalStrings miss was "one deliberate divergence" from a reference that draws the empty string, and read at the byte it is the reference's own behaviour — `FrameScript_GetText 0x703bf0` does return a pre-seeded empty string (`0x882748`) when the global is not a string, but that return never reaches a label, because all three `text=` readers image-wide test it and substitute the raw attribute: `Button::LoadXML` at `0x778c07` (`0x778c31 mov eax,esi`), `CSimpleFontString::LoadXML` at `0x771006` byte-identically, and `0x7292a6`, which pushes `edi` on the empty arm; so the loader changes nothing, the two test comments that had called `PetLevelText`'s "Level level race class" placeholder and `CLOSE` our divergence now name the reference's arm (a real 1.12 client draws them too), and what IS ours is the extra warning on a key-shaped miss (0983 → 0991), a strict addition; beside it, `GetEffectiveScale`'s root factor is 1 not because "benilla has no `uiScale` CVar" — 1804 registered one, default 0.9 — but because the dial is applied at the raster seam, `768/uiScale` UI units of virtual-screen height rather than a scale on `UIParent`, so every coordinate the VM hands Lua is already in those units and the reference's `GetCursorPosition()/GetEffectiveScale()` transcriptions convert correctly with a root of 1 (2044) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
192 lines
9.2 KiB
Rust
192 lines
9.2 KiB
Rust
//! The shipped taxi-map window driven end-to-end, engine-only (no Bevy): the real
|
|
//! `Interface\FrameXML\TaxiFrame.xml` fed a synthetic two-node snapshot (the [`crate::ui_taxi`]
|
|
//! feed's exact output shape). Covers what only a runtime load exercises: the Lua parses and every
|
|
//! referenced global resolves (including the static node-button pool and the runtime-created
|
|
//! route-line textures — TaxiFrame.xml's own header note on both), `TAXIMAP_OPENED` shows the
|
|
//! window with the flight master's name and paints the node buttons at their pushed positions, a
|
|
//! click on a node drains through `TakeTaxiNode`, and `TAXIMAP_CLOSED` hides it.
|
|
|
|
use benilla_ui::script::{ScriptValue, TaxiNodeType, TaxiUiNode, TaxiUiState, UiScript};
|
|
|
|
use super::test_ui::load_ui as load_xml;
|
|
|
|
/// Load the taxi window + its deps into a fresh script, screen sized.
|
|
fn taxi_script() -> UiScript {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
|
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
|
|
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
|
load_xml(&s, r"Interface\FrameXML\UIParent.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, "Interface\\FrameXML\\GameTooltip.xml"); // TaxiNodeOnButtonEnter's tooltip + SetTooltipMoney
|
|
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml"); // `UIErrorsFrame:AddMessage` — DrawOneHopLines' refusal
|
|
// Three the reference's own TaxiFrame leans on that our transcription did not:
|
|
// · GlobalStrings — `ERR_TAXINOPATHS` is a GlobalString, and `AddMessage(nil)` draws an
|
|
// empty line rather than raising, so its absence is silent.
|
|
// · UIPanelTemplates (.lua then .xml) — `TaxiCloseButton` inherits `UIPanelCloseButton`,
|
|
// which the chain declares there. 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\\UIPanelTemplates.lua");
|
|
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\TaxiFrame.xml");
|
|
s
|
|
}
|
|
|
|
/// Seat the flight master as the `"npc"` unit. Stock `TaxiFrame_OnEvent` fills its label from
|
|
/// `UnitName("npc")` (TaxiFrame.lua:27), where our deleted transcription read the `TAXIMAP_OPENED`
|
|
/// argument — so a fixture that only fires the event leaves the reference's label blank.
|
|
fn seat_flight_master(s: &mut UiScript, name: &str) {
|
|
let npc = benilla_ui::script::UnitState {
|
|
name: Some(name.into()),
|
|
..Default::default()
|
|
};
|
|
s.set_unit("npc", Some(npc));
|
|
}
|
|
|
|
/// A two-node snapshot: Stormwind (Current) and the verified Sentinel Hill hop (Reachable, 110
|
|
/// copper, one route segment) — [`crate::ui_taxi::build_nodes`]'s exact real-data shape.
|
|
fn menu() -> TaxiUiState {
|
|
TaxiUiState {
|
|
art: "Interface\\TaxiFrame\\TAXIMAP0".into(),
|
|
nodes: vec![
|
|
TaxiUiNode {
|
|
name: "Stormwind, Elwynn".into(),
|
|
node_type: TaxiNodeType::Current,
|
|
pos: (0.43, 0.33),
|
|
cost: 0,
|
|
routes: vec![],
|
|
},
|
|
TaxiUiNode {
|
|
name: "Sentinel Hill, Westfall".into(),
|
|
node_type: TaxiNodeType::Reachable,
|
|
pos: (0.41, 0.25),
|
|
cost: 110,
|
|
routes: vec![[0.43, 0.33, 0.41, 0.25]],
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
/// The whole taxi window minus Bevy: hidden by default, `TAXIMAP_OPENED` shows it with the flight
|
|
/// master's name (the event-arg deviation TaxiFrame.xml's header note flags), paints exactly the
|
|
/// pushed node buttons (the rest of the 50-slot static pool stays hidden — the CreateFrame/template
|
|
/// deviation), a click on the reachable node drains through `TakeTaxiNode`, and `TAXIMAP_CLOSED`
|
|
/// hides it again.
|
|
#[test]
|
|
fn shipped_taxi_frame_drives_end_to_end() {
|
|
let mut s = taxi_script();
|
|
|
|
assert!(!s.eval::<bool>("return TaxiFrame:IsVisible()").unwrap());
|
|
|
|
s.set_taxi(Some(menu()));
|
|
seat_flight_master(&mut s, "Dungar Longdrink");
|
|
s.fire_event(
|
|
"TAXIMAP_OPENED",
|
|
vec![ScriptValue::Str("Dungar Longdrink".into())],
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
|
|
assert!(s.eval::<bool>("return TaxiFrame:IsVisible()").unwrap());
|
|
assert_eq!(
|
|
// The reference names the flight-master label `TaxiMerchant` and fills it from
|
|
// `UnitName("npc")` (TaxiFrame.lua:27) — NOT from the event's argument, which is what our
|
|
// transcription's `TaxiNameText` read. So the fixture seats the npc unit above.
|
|
s.eval::<String>("return TaxiMerchant:GetText()").unwrap(),
|
|
"Dungar Longdrink"
|
|
);
|
|
|
|
// The two pushed nodes show, and there is no third button AT ALL — the reference builds its
|
|
// node buttons on demand, one `CreateFrame("Button", "TaxiButton"..i, TaxiRouteMap,
|
|
// "TaxiButtonTemplate")` per node (TaxiFrame.lua:39), where our transcription pre-made a fixed
|
|
// pool and hid the tail. Same thing on screen, a different mechanism behind it — so the
|
|
// expectation moves from "hidden" to "absent".
|
|
assert!(s.eval::<bool>("return TaxiButton1:IsVisible()").unwrap());
|
|
assert!(s.eval::<bool>("return TaxiButton2:IsVisible()").unwrap());
|
|
assert!(s.eval::<bool>("return TaxiButton3 == nil").unwrap());
|
|
assert!(s.eval::<bool>("return TaxiButton50 == nil").unwrap());
|
|
|
|
// A click on node 2 (Sentinel Hill) drains through TakeTaxiNode.
|
|
s.resolve();
|
|
let (cx, cy) = s
|
|
.eval::<(f32, f32)>("return TaxiButton2:GetCenter()")
|
|
.unwrap();
|
|
s.mouse_button(cx, cy, "LeftButton", true);
|
|
s.mouse_button(cx, cy, "LeftButton", false);
|
|
assert_eq!(s.take_taxi_node(), vec![2]);
|
|
assert!(s.take_taxi_node().is_empty(), "drained");
|
|
|
|
// TAXIMAP_CLOSED hides the window.
|
|
s.set_taxi(None);
|
|
s.fire_event("TAXIMAP_CLOSED", vec![]);
|
|
assert!(!s.eval::<bool>("return TaxiFrame:IsVisible()").unwrap());
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The "no single-hop destination" refusal (ref `DrawOneHopLines`, fired from `OnShow`): a map
|
|
/// with only the `Current` node (no `Reachable` neighbor at all) hits `numSingleHops == 0` and
|
|
/// calls `UIErrorsFrame:AddMessage(ERR_TAXINOPATHS, …)` (stock `TaxiFrame.lua:161-162`) then
|
|
/// hides the window — driven end-to-end because the message frame's Lua seam is what the
|
|
/// refusal is visible through.
|
|
#[test]
|
|
fn no_single_hop_destination_posts_the_error_and_closes() {
|
|
let mut s = taxi_script();
|
|
s.set_taxi(Some(TaxiUiState {
|
|
art: "Interface\\TaxiFrame\\TAXIMAP0".into(),
|
|
nodes: vec![TaxiUiNode {
|
|
name: "Stormwind, Elwynn".into(),
|
|
node_type: TaxiNodeType::Current,
|
|
pos: (0.43, 0.33),
|
|
cost: 0,
|
|
routes: vec![],
|
|
}],
|
|
}));
|
|
seat_flight_master(&mut s, "Dungar Longdrink");
|
|
s.fire_event(
|
|
"TAXIMAP_OPENED",
|
|
vec![ScriptValue::Str("Dungar Longdrink".into())],
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
|
|
// OnShow ran DrawOneHopLines, which found zero single-hop nodes: the red error line posted
|
|
// and the window hid itself again — the reference's own reaction to a dead-end flight point.
|
|
assert!(!s.eval::<bool>("return TaxiFrame:IsVisible()").unwrap());
|
|
s.resolve();
|
|
let quads = s.extract();
|
|
let has_refusal = quads.iter().any(|q| match &q.content {
|
|
benilla_ui::script::QuadContent::Text { text: Some(t), .. } => {
|
|
t.contains("don\u{2019}t know any flight locations")
|
|
}
|
|
_ => false,
|
|
});
|
|
assert!(
|
|
has_refusal,
|
|
"ERR_TAXINOPATHS posted to UIErrorsFrame: {quads:?}"
|
|
);
|
|
}
|
|
|
|
/// The close button's own click path: `CloseTaxiMap()` queues the intent this engine drains, and
|
|
/// the window hides immediately (no one-frame lag) — the Gossip/Trainer close-button precedent.
|
|
#[test]
|
|
fn close_button_queues_the_intent_and_hides() {
|
|
let mut s = taxi_script();
|
|
s.set_taxi(Some(menu()));
|
|
seat_flight_master(&mut s, "Dungar Longdrink");
|
|
s.fire_event(
|
|
"TAXIMAP_OPENED",
|
|
vec![ScriptValue::Str("Dungar Longdrink".into())],
|
|
);
|
|
assert!(s.eval::<bool>("return TaxiFrame:IsVisible()").unwrap());
|
|
|
|
// The reference's close button is a plain `UIPanelCloseButton` (TaxiFrame.xml:133) — it has no
|
|
// handler of its own, so the click goes through the template's, which hides the parent panel.
|
|
// Ours carried a named `BenillaTaxiCloseButton_OnClick`; that went with the file.
|
|
s.run("TaxiCloseButton:Click()").unwrap();
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
assert!(s.take_taxi_close());
|
|
assert!(!s.eval::<bool>("return TaxiFrame:IsVisible()").unwrap());
|
|
}
|