From b45bce8184d4c3ac42f017f553bc8fe55c4fc11e Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 2 Sep 2026 17:06:36 +0700 Subject: [PATCH] bindings answer the reference's shapes, and the panel kit goes stock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - four return shapes were wrong — `UnitPowerType` returns ONE value in 5875 (the Era `(type, "RAGE")` pair does not exist, and its miss arm pushes the NUMBER 0, which `UnitFrame.lua:122` indexes `ManaBarColor` with), `UnitName` returns `(name, server)`, `GetTexCoord` returns EIGHT (ours four — a shape that exists nowhere, with zero call sites in FrameXML, so only the binary could have caught it), `GetLootMethod` answers `freeforall` ungrouped — the fifth right-name-wrong-arity finding in two days, so wow-re's `binding-shapes.tsv` (1722 rows over all 82 registrar tables) is vendored and a gate probes every binding's arity against it: 313 probes, eight divergences (seven global, one widget); `GetBuildInfo` pushed a fourth value no 1.12 client does (458 corpus sites take one, the four that destructure a fourth read nil on the real client), `GetAddOnInfo` RAISES on a numeric index, `Model:GetFogColor` is a packed `0xAARRGGBB` dword with no unset state, and the five miss branches are the reference's exact tuples - the panel-template kit is the reference's own `UIPanelTemplates` pair (our 18 templates were a strict subset — `TabButtonTemplate` and the `ScrollingEdit_*` family arrive, which unblocks the macro window and HelpFrame), and the swap found two structural facts: `FauxScrollFrameTemplate` is a `` while every instance we declared was a `` (fifteen elements across twelve files), and an instance `` REPLACES the template's — four FriendsFrame lists set `updateFunc` in their own OnLoad, so `ScrollFrame_OnLoad` never ran and guild/friends/ignore/who had been silently painting nothing behind a `frame.offset or 0` - the options widget kit (`OptionFrameBoxTemplate`, `OptionsButtonTemplate`, `OptionsCheckButtonTemplate`, `OptionsSliderTemplate`) comes off the chain too, giving back the reference's own per-widget hover; one template of ours stays because `UIOptionsFrame.xml` is not built here, and the lesson is general — when a file SHRINKS rather than dies, every harness that named it gains the chain entry beside it, not instead of it Co-Authored-By: Claude Fable 5.1 --- crates/benilla-app/assets/ui/AuctionFrame.xml | 20 +- crates/benilla-app/assets/ui/CraftFrame.xml | 4 +- crates/benilla-app/assets/ui/FriendsFrame.xml | 68 ++- crates/benilla-app/assets/ui/MacroFrame.xml | 4 +- crates/benilla-app/assets/ui/OptionsFrame.xml | 4 +- .../assets/ui/OptionsFrameTemplates.xml | 162 ++----- .../benilla-app/assets/ui/QuestLogFrame.xml | 4 +- .../benilla-app/assets/ui/ReputationFrame.xml | 14 +- .../benilla-app/assets/ui/ScriptLogFrame.xml | 4 +- crates/benilla-app/assets/ui/SkillFrame.xml | 4 +- .../benilla-app/assets/ui/TradeSkillFrame.xml | 4 +- crates/benilla-app/assets/ui/TrainerFrame.xml | 4 +- .../assets/ui/UIPanelTemplates.xml | 442 ------------------ crates/benilla-app/assets/ui/benilla.toc | 52 ++- crates/benilla-app/src/ui_script/bag_tests.rs | 2 +- .../src/ui_script/chat_options_tests.rs | 3 +- .../src/ui_script/chat_resize_tests.rs | 3 +- .../src/ui_script/color_picker_tests.rs | 3 +- .../benilla-app/src/ui_script/errors_tests.rs | 5 +- .../src/ui_script/friends_tests.rs | 20 +- .../src/ui_script/game_menu_tests.rs | 14 +- .../src/ui_script/group_loot_tests.rs | 3 +- .../benilla-app/src/ui_script/guild_tests.rs | 32 +- .../src/ui_script/help_frame_tests.rs | 15 +- .../src/ui_script/minimap_tests.rs | 12 +- crates/benilla-app/src/ui_script/mod.rs | 6 + .../src/ui_script/panel_template_tests.rs | 23 +- .../benilla-app/src/ui_script/panel_tests.rs | 40 +- .../src/ui_script/petition_tests.rs | 3 +- .../src/ui_script/questlog_tests.rs | 85 +++- .../benilla-app/src/ui_script/raid_tests.rs | 22 +- .../benilla-app/src/ui_script/shape_gate.rs | 314 +++++++++++++ crates/benilla-app/src/ui_script/test_ui.rs | 11 +- .../src/ui_script/unit_frame_tests.rs | 7 +- .../src/ui_script/unit_popup_tests.rs | 3 +- .../src/ui_script/world_map_tests.rs | 14 +- crates/benilla-app/tests/auction_frame.rs | 16 +- crates/benilla-app/tests/item_text_frame.rs | 6 +- crates/benilla-app/tests/talent_frame.rs | 5 +- crates/benilla-ui/src/script/addon.rs | 23 +- crates/benilla-ui/src/script/channel.rs | 18 +- crates/benilla-ui/src/script/client.rs | 43 +- crates/benilla-ui/src/script/modelframe.rs | 31 +- crates/benilla-ui/src/script/party.rs | 10 +- crates/benilla-ui/src/script/quest.rs | 4 +- crates/benilla-ui/src/script/quest_log.rs | 4 +- crates/benilla-ui/src/script/region/paint.rs | 34 +- crates/benilla-ui/src/script/reputation.rs | 16 +- .../benilla-ui/src/script/tests/modelframe.rs | 37 +- .../src/script/tests/texcoord_font.rs | 7 +- crates/benilla-ui/src/script/types.rs | 8 +- crates/benilla-ui/src/script/unit/bindings.rs | 31 +- crates/benilla-ui/src/script/unit/tests.rs | 16 +- crates/benilla-ui/src/script/worldmap.rs | 10 +- crates/benilla-ui/src/widget/kinds/mod.rs | 18 +- 55 files changed, 1003 insertions(+), 764 deletions(-) delete mode 100644 crates/benilla-app/assets/ui/UIPanelTemplates.xml create mode 100644 crates/benilla-app/src/ui_script/shape_gate.rs diff --git a/crates/benilla-app/assets/ui/AuctionFrame.xml b/crates/benilla-app/assets/ui/AuctionFrame.xml index 9ca49c73..48960683 100644 --- a/crates/benilla-app/assets/ui/AuctionFrame.xml +++ b/crates/benilla-app/assets/ui/AuctionFrame.xml @@ -1916,7 +1916,7 @@ right edge at (-2,5) / (-2,-2). Those offsets against our FauxScrollFrameTemplate's bar (seated 6,-16 / 6,+16) are exactly the 21/20 overhang that lands each arrow button in its own authored socket — ScrollTemplates.xml's own law, reached from the other side. --> - + @@ -1931,7 +1931,7 @@ - + - + - + + ScrollFrame_OnLoad() + this.updateFunc = FriendsList_Update + + BenillaFauxScrollFrame_OnMouseWheel(FriendsFrameFriendsScrollFrame, arg1) @@ -2315,13 +2326,24 @@ - + - self.updateFunc = IgnoreList_Update - + + + ScrollFrame_OnLoad() + this.updateFunc = IgnoreList_Update + + BenillaFauxScrollFrame_OnMouseWheel(FriendsFrameIgnoreScrollFrame, arg1) @@ -2454,11 +2476,22 @@ - + - self.updateFunc = WhoList_Update - + + + ScrollFrame_OnLoad() + this.updateFunc = WhoList_Update + + - + - self.updateFunc = GuildStatus_Update - + + + ScrollFrame_OnLoad() + this.updateFunc = GuildStatus_Update + + - + @@ -916,7 +916,7 @@ self.updateFunc = BenillaMacroPopupFrame_Update - + diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index 48e4a6d9..7f9c6376 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -3433,7 +3433,7 @@ seating). The wheel handler is NOT here: a spin over a row bubbles up the PARENT chain (pointer.rs), and this frame is the rows' sibling — it lives on the body below. --> - + @@ -3444,7 +3444,7 @@ this.updateFunc = KeyBindingsPage_Update - + - - - - - - - - - - - - - - - - - - - this:SetBackdropBorderColor(0.4, 0.4, 0.4) - this:SetBackdropColor(0.5, 0.5, 0.5) - - - - - - - - - - - - - - PlaySound(this:GetChecked() and "igMainMenuOptionCheckBoxOn" or "igMainMenuOptionCheckBoxOff") - - BenillaOptionsWidget_ShowTooltip(this) - GameTooltip:Hide() - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - BenillaOptionsWidget_ShowTooltip(this) - GameTooltip:Hide() - - - - - diff --git a/crates/benilla-app/assets/ui/QuestLogFrame.xml b/crates/benilla-app/assets/ui/QuestLogFrame.xml index 6e6aaa93..2c659cc2 100644 --- a/crates/benilla-app/assets/ui/QuestLogFrame.xml +++ b/crates/benilla-app/assets/ui/QuestLogFrame.xml @@ -1574,7 +1574,7 @@ a `questLogOffset` upvalue this file kept "because no real ScrollFrame/ FauxScrollFrame engine exists yet" — long false, and the reason this window published no QuestLogListScrollFrame for addons to find. --> - + @@ -1582,7 +1582,7 @@ FauxScrollFrame_OnVerticalScroll(QUESTLOG_QUEST_HEIGHT, BenillaQuestLogList_Update) - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - this:GetParent():SetVerticalScroll(arg1) - - - - - - - - - - - - - - - - - - - ScrollFrame_OnLoad() - ScrollFrame_OnScrollRangeChanged(arg2) - - - local scrollbar = getglobal(this:GetName() .. "ScrollBar") - scrollbar:SetValue(arg1) - local up = getglobal(scrollbar:GetName() .. "ScrollUpButton") - local down = getglobal(scrollbar:GetName() .. "ScrollDownButton") - local _, maxScroll = scrollbar:GetMinMaxValues() - if arg1 == 0 then up:Disable() else up:Enable() end - if scrollbar:GetValue() == maxScroll then down:Disable() else down:Enable() end - - ScrollFrameTemplate_OnMouseWheel(arg1) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - this:ClearFocus() - this:HighlightText(0, 0) - this:HighlightText() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/crates/benilla-app/assets/ui/benilla.toc b/crates/benilla-app/assets/ui/benilla.toc index affdf41b..cf9868c4 100644 --- a/crates/benilla-app/assets/ui/benilla.toc +++ b/crates/benilla-app/assets/ui/benilla.toc @@ -403,20 +403,44 @@ Interface\FrameXML\ZoneText.xml # API (Update/GetOffset/SetOffset/OnVerticalScroll + ScrollFrame_OnLoad) — # before its first customer (the trainer list) below. ScrollTemplates.xml -# The reference's SHARED widget kit (decision 1203's queue): UIPanelButtonTemplate, -# UICheckButtonTemplate, UIPanelCloseButton, InputBoxTemplate, the UIPanelScroll* family and -# their siblings, under the reference's own names — the templates a third-party addon names in -# `CreateFrame(kind, name, parent, "Template")`, which nothing in benilla's own XML consumes -# yet (each of our windows still carries its private copy; see the file's header). After -# ScrollTemplates.xml because UIPanelScrollFrameTemplate's OnLoad is that file's -# ScrollFrame_OnLoad, and this file supplies the two remaining handlers of that same kit -# (ScrollFrameTemplate_OnMouseWheel, ScrollFrame_OnScrollRangeChanged). -UIPanelTemplates.xml -# The reference's shared OPTIONS widgets: OptionsCheckButtonTemplate / -# UIOptionsCheckButtonTemplate / OptionsSliderTemplate / OptionsButtonTemplate / -# OptionFrameBoxTemplate. Straight after UIPanelTemplates.xml — two of them inherit its -# UICheckButtonTemplate and UIPanelButtonTemplate. Not benilla's own options window -# (OptionsFrame.xml, far below): these are the shared names addons build panels out of. +# ══ THE PANEL TEMPLATE KIT — the reference's own, off the player's chain (1751, window 31) ══ +# TWO entries, because the reference splits this kit the way it splits every other: the `.lua` +# carries the verbs and the `.xml` the templates. Listing the `.xml` alone brings none of the verbs +# (`every_chain_xml_brings_its_own_lua` is a gate because that already bit twice). +# +# ONE swap, THREE of our files, which is why it had to move as a unit and why 1837 measured it +# before anyone executed it: +# * `UIPanelTemplates.xml` — DELETED. Our 18 templates were a strict SUBSET of the reference's, +# zero benilla-only additions, so the template half is a pure gain. +# * the scroll half of `ScrollTemplates.xml` — `FauxScrollFrameTemplate`, `FauxScrollFrame_*`, +# `ScrollFrame_*` — now the chain's. What stays there is `BenillaScrollBarTemplate` and +# `BenillaScrollTroughTemplate`, which are ours and which six windows still inherit. +# * the `PanelTemplates_*` family out of `UiPanels.xml`. +# +# It could not be split: `ScrollTemplates.xml` sits ABOVE this entry, so the chain's +# `FauxScrollFrameTemplate` lands after ours and wins (registration is last-in). There is no +# ordering that takes the templates without also taking Faux. +# +# GAINED: `TabButtonTemplate`, and the whole `ScrollingEdit_*` family, which we had NOWHERE — that +# is HelpFrame's named blocker, answered as a side effect. And it unblocks MacroFrame (1835). +Interface\FrameXML\UIPanelTemplates.lua +Interface\FrameXML\UIPanelTemplates.xml +# ══ THE OPTIONS WIDGET KIT — the reference's own, off the player's chain (1751, window 29) ══ +# The four templates addons name for an options panel: `OptionFrameBoxTemplate`, +# `OptionsButtonTemplate`, `OptionsCheckButtonTemplate`, `OptionsSliderTemplate`. Self-contained — +# no `.lua`, no `` — and a descendant of UIPanelTemplates (`OptionsCheckButtonTemplate` +# inherits `UICheckButtonTemplate`, `OptionsButtonTemplate` inherits `UIPanelButtonTemplate`), so +# it stays seated after that file. +# +# `framexml-transcription-diff.py` reports no handler our transcription was missing, and the swap +# GIVES BACK the reference's own inline hover: ours had factored it into a shared +# `BenillaOptionsWidget_ShowTooltip`, where the reference open-codes it once per widget. +Interface\FrameXML\OptionsFrameTemplates.xml +# …and ours, shrunk to the ONE template the chain's file does not carry: +# `UIOptionsCheckButtonTemplate`, whose reference home is `UIOptionsFrame.xml` — a file we do not +# build, because benilla's options window is `OptionsFrame.xml`, a different design. The corpus +# names it at 8 `CreateFrame` sites, so it cannot simply go. Same basename as the entry above by +# design: bare = ours, path = the reference's. Retires when UIOptionsFrame.xml migrates. OptionsFrameTemplates.xml # ══ THE COLOUR DIALOG — the reference's own, off the player's chain (1751, window 11) ══ # `ColorPickerFrame` (a ``), `OpacitySliderFrame`, `ColorPickerOkayButton`, diff --git a/crates/benilla-app/src/ui_script/bag_tests.rs b/crates/benilla-app/src/ui_script/bag_tests.rs index 63d67f90..c98f91bb 100644 --- a/crates/benilla-app/src/ui_script/bag_tests.rs +++ b/crates/benilla-app/src/ui_script/bag_tests.rs @@ -1603,7 +1603,7 @@ fn the_first_key_puts_the_keyring_on_the_bar() { ] { let (t, b) = s .eval::<(f64, f64)>(&format!( - "local _, _, top, bottom = {strip}:GetTexCoord() return top, bottom" + "local _, top, _, bottom = {strip}:GetTexCoord() return top, bottom" )) .unwrap(); assert!( diff --git a/crates/benilla-app/src/ui_script/chat_options_tests.rs b/crates/benilla-app/src/ui_script/chat_options_tests.rs index 9969f02f..7be47096 100644 --- a/crates/benilla-app/src/ui_script/chat_options_tests.rs +++ b/crates/benilla-app/src/ui_script/chat_options_tests.rs @@ -46,7 +46,8 @@ fn chat_with_menu() -> UiScript { "GameTooltip.xml", "Interface\\FrameXML\\UIDropDownMenu.xml", "ScrollTemplates.xml", - "UIPanelTemplates.xml", + r"Interface\FrameXML\UIPanelTemplates.lua", + r"Interface\FrameXML\UIPanelTemplates.xml", "Interface\\FrameXML\\ColorPickerFrame.xml", "Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from "ChatFrame.xml", diff --git a/crates/benilla-app/src/ui_script/chat_resize_tests.rs b/crates/benilla-app/src/ui_script/chat_resize_tests.rs index abc59c36..95720f71 100644 --- a/crates/benilla-app/src/ui_script/chat_resize_tests.rs +++ b/crates/benilla-app/src/ui_script/chat_resize_tests.rs @@ -24,7 +24,8 @@ fn chat_ui() -> UiScript { "GameTooltip.xml", "Interface\\FrameXML\\UIDropDownMenu.xml", "ScrollTemplates.xml", - "UIPanelTemplates.xml", + r"Interface\FrameXML\UIPanelTemplates.lua", + r"Interface\FrameXML\UIPanelTemplates.xml", "Interface\\FrameXML\\ColorPickerFrame.xml", "Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from "ChatFrame.xml", diff --git a/crates/benilla-app/src/ui_script/color_picker_tests.rs b/crates/benilla-app/src/ui_script/color_picker_tests.rs index 477e56a9..9c029faa 100644 --- a/crates/benilla-app/src/ui_script/color_picker_tests.rs +++ b/crates/benilla-app/src/ui_script/color_picker_tests.rs @@ -24,7 +24,8 @@ fn picker() -> UiScript { "GameTooltip.xml", "Interface\\FrameXML\\UIDropDownMenu.xml", "ScrollTemplates.xml", - "UIPanelTemplates.xml", + r"Interface\FrameXML\UIPanelTemplates.lua", + r"Interface\FrameXML\UIPanelTemplates.xml", "Interface\\FrameXML\\ColorPickerFrame.xml", ] { load_xml(&s, file); diff --git a/crates/benilla-app/src/ui_script/errors_tests.rs b/crates/benilla-app/src/ui_script/errors_tests.rs index cd4452aa..3ed7685f 100644 --- a/crates/benilla-app/src/ui_script/errors_tests.rs +++ b/crates/benilla-app/src/ui_script/errors_tests.rs @@ -36,6 +36,7 @@ fn toast_lines(s: &mut UiScript) -> Vec<(String, [f32; 4], f32)> { #[test] fn info_and_error_messages_stack_hold_and_expire() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "Fonts.xml"); @@ -105,6 +106,7 @@ fn info_and_error_messages_stack_hold_and_expire() { /// defect as the party frame's in decision 0597, one stratum up. #[test] fn an_error_toast_draws_over_an_open_panel_window() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "Fonts.xml"); @@ -118,7 +120,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); // A left-slot panel open, and the toast raised after it — the order that must not decide. diff --git a/crates/benilla-app/src/ui_script/friends_tests.rs b/crates/benilla-app/src/ui_script/friends_tests.rs index b6806e9f..f56713d9 100644 --- a/crates/benilla-app/src/ui_script/friends_tests.rs +++ b/crates/benilla-app/src/ui_script/friends_tests.rs @@ -28,7 +28,8 @@ fn setup() -> UiScript { // UIPanelCloseButton / UIPanelScrollFrameTemplate rather than a private copy (decision 1257), // so this file's slice needs the shared kit — and the strict `load_xml` above is what would // otherwise let those frames load art-less and silent. - load_xml(&s, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "FriendsFrame.xml"); // The social window's fourth tab lives in its own file, and it is part of THIS window's // manifest slice now: `BENILLA_FRIENDS_SUBFRAMES` names "RaidFrame", and both @@ -71,6 +72,7 @@ fn push(s: &mut UiScript, state: SocialState, event: &str) { /// every character starts a session in. #[test] fn the_window_opens_on_friends_with_the_guild_tab_disabled() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); s.run("ToggleFriendsFrame(1)").unwrap(); assert!(s.eval::("return FriendsFrame:IsVisible()").unwrap()); @@ -129,6 +131,7 @@ fn the_window_opens_on_friends_with_the_guild_tab_disabled() { /// `GetFriendInfo`'s six returns ever come back in the wrong order. #[test] fn friend_rows_render_the_online_and_offline_templates() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ToggleFriendsFrame(1)").unwrap(); push( @@ -178,6 +181,7 @@ fn friend_rows_render_the_online_and_offline_templates() { /// whispered or invited, and an empty list disables all three. #[test] fn the_friend_buttons_follow_the_selection() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ToggleFriendsFrame(1)").unwrap(); @@ -217,6 +221,7 @@ fn the_friend_buttons_follow_the_selection() { /// from this index), and Group Invite addresses the NAME. #[test] fn the_friend_buttons_queue_their_verbs() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ToggleFriendsFrame(1)").unwrap(); push( @@ -253,6 +258,7 @@ fn the_friend_buttons_queue_their_verbs() { /// their names. `ShowIgnorePanel()` (what `/ignore` with no name runs) lands on the same list. #[test] fn the_ignore_list_is_the_other_half_of_tab_one() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ToggleFriendsFrame(1)").unwrap(); push( @@ -313,6 +319,7 @@ fn the_ignore_list_is_the_other_half_of_tab_one() { /// makes the column order worth pinning. #[test] fn who_rows_fill_their_columns() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ShowWhoPanel()").unwrap(); push( @@ -365,6 +372,7 @@ fn who_rows_fill_their_columns() { /// The variable column really is variable: picking Guild re-paints it and sorts by that key. #[test] fn the_who_dropdown_switches_the_variable_column() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ShowWhoPanel()").unwrap(); push( @@ -405,6 +413,7 @@ fn the_who_dropdown_switches_the_variable_column() { /// the selection — row 3 of the last query is not row 3 of this one. #[test] fn the_who_buttons_need_a_selected_row() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ShowWhoPanel()").unwrap(); let rows = vec![ @@ -466,6 +475,7 @@ fn the_who_buttons_need_a_selected_row() { /// closed would print nothing at all. #[test] fn showing_the_who_frame_claims_the_next_answer() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ShowWhoPanel()").unwrap(); assert!( @@ -484,6 +494,7 @@ fn showing_the_who_frame_claims_the_next_answer() { /// The `/who` edit box sends its filter verbatim — the string the app parses into wire fields. #[test] fn the_who_edit_box_sends_its_filter() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ShowWhoPanel()").unwrap(); let _ = s.take_social_requests(); @@ -500,6 +511,7 @@ fn the_who_edit_box_sends_its_filter() { /// bare `/who` opens the panel and fills the edit box with the default filter. #[test] fn the_slash_bodies_match_the_reference() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); // The WhoFrame's OnLoad routes results to chat until it is shown; drain that so each // assertion below sees only what its own call queued. @@ -550,6 +562,7 @@ fn the_slash_bodies_match_the_reference() { /// customer of the popup engine's `hasEditBox` capability. Accepting sends what was typed. #[test] fn add_friend_without_a_target_opens_the_name_dialog() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ToggleFriendsFrame(1)").unwrap(); s.run("FriendsFrameAddFriendButton:Click()").unwrap(); @@ -586,6 +599,7 @@ fn add_friend_without_a_target_opens_the_name_dialog() { /// typed last time. #[test] fn the_name_dialog_reopens_empty() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); s.run("ToggleFriendsFrame(1)").unwrap(); s.run("StaticPopup_Show(\"ADD_FRIEND\")").unwrap(); @@ -604,6 +618,7 @@ fn the_name_dialog_reopens_empty() { /// survive UnitPopup's gating, and each one acts on the name. #[test] fn right_clicking_a_who_row_opens_the_friend_menu() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ShowWhoPanel()").unwrap(); push( @@ -656,6 +671,7 @@ fn right_clicking_a_who_row_opens_the_friend_menu() { /// ref's own `connected` gate on the whole menu. #[test] fn right_clicking_an_offline_friend_opens_nothing() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ToggleFriendsFrame(1)").unwrap(); push( @@ -679,6 +695,7 @@ fn right_clicking_an_offline_friend_opens_nothing() { /// read `FriendsList_Update` does right after `SetSelectedFriend`. #[test] fn selecting_a_row_reads_back_in_the_same_tick() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ToggleFriendsFrame(1)").unwrap(); push( @@ -722,6 +739,7 @@ fn selecting_a_row_reads_back_in_the_same_tick() { /// exemption. Skips without the extracted reference. #[test] fn the_window_geometry_matches_the_reference_framexml() { + let _data = benilla_formats::wow_data_or_skip!(); let Some(reference) = super::framexml_diff::reference("FriendsFrame.xml") else { eprintln!("skipping: no extracted FrameXML"); return; diff --git a/crates/benilla-app/src/ui_script/game_menu_tests.rs b/crates/benilla-app/src/ui_script/game_menu_tests.rs index a5db3563..fc93abeb 100644 --- a/crates/benilla-app/src/ui_script/game_menu_tests.rs +++ b/crates/benilla-app/src/ui_script/game_menu_tests.rs @@ -43,7 +43,8 @@ fn harness_with(extra: &[&str]) -> UiScript { "UIParent.xml", "MoneyFrame.xml", "UiPanels.xml", - "UIPanelTemplates.xml", + r"Interface\FrameXML\UIPanelTemplates.lua", + r"Interface\FrameXML\UIPanelTemplates.xml", ] .into_iter() .chain(extra.iter().copied()) @@ -104,6 +105,7 @@ const LADDER: [&str; 7] = [ /// era shape is ever "tidied". #[test] fn the_menu_has_the_era_frame_and_button_ladder() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); s.run("ShowUIPanel(GameMenuFrame)").unwrap(); s.resolve(); @@ -148,6 +150,7 @@ fn the_menu_has_the_era_frame_and_button_ladder() { /// panel — the char-select AddOns screen is the only addon UI.) #[test] fn the_unbacked_entries_are_disabled_and_the_rest_are_live() { + let _data = benilla_formats::wow_data_or_skip!(); let s = harness(); s.run("ShowUIPanel(GameMenuFrame)").unwrap(); @@ -335,6 +338,7 @@ fn the_open_menu_takes_the_screen_and_refuses_every_other_panel() { /// the completed logout ends the process), which is why they look identical from here. #[test] fn the_live_buttons_queue_their_intents_and_play_their_kits() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); // Return to Game — just closes, no intent. @@ -375,6 +379,7 @@ fn the_live_buttons_queue_their_intents_and_play_their_kits() { /// mean CAMP fell out of that which-list. #[test] fn player_camping_opens_a_counting_dialog_whose_early_close_cancels() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); s.fire_event("PLAYER_CAMPING", vec![]); assert_eq!( @@ -424,6 +429,7 @@ fn player_camping_opens_a_counting_dialog_whose_early_close_cancels() { /// cancelled by its own dialog at t=0 — the character never leaves.) #[test] fn a_countdown_that_expires_does_not_cancel_the_logout() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); s.fire_event("PLAYER_CAMPING", vec![]); s.run("StaticPopup_OnUpdate(StaticPopup1, 25)").unwrap(); @@ -444,6 +450,7 @@ fn a_countdown_that_expires_does_not_cancel_the_logout() { /// the server: "Exit now". And `LOGOUT_CANCEL` (the server's cancel ack) takes either one down. #[test] fn player_quiting_offers_exit_now_and_logout_cancel_closes_it() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); s.fire_event("PLAYER_QUITING", vec![]); s.run("StaticPopup_OnUpdate(StaticPopup1, 0.5)").unwrap(); @@ -487,6 +494,7 @@ fn player_quiting_offers_exit_now_and_logout_cancel_closes_it() { /// the next test. #[test] fn logout_and_exit_read_disabled_while_a_countdown_runs() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); s.fire_event("PLAYER_CAMPING", vec![]); s.run("ToggleGameMenu(1)").unwrap(); @@ -515,6 +523,7 @@ fn logout_and_exit_read_disabled_while_a_countdown_runs() { /// a logout you didn't mean is therefore the whole gesture, and it must not also open the menu. #[test] fn escape_during_a_countdown_cancels_it_and_does_not_open_the_menu() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); s.fire_event("PLAYER_CAMPING", vec![]); s.run("ToggleGameMenu()").unwrap(); @@ -545,6 +554,7 @@ fn escape_during_a_countdown_cancels_it_and_does_not_open_the_menu() { /// afterwards. #[test] fn the_world_map_cannot_open_behind_the_menu_and_gives_its_slot_back() { + let _data = benilla_formats::wow_data_or_skip!(); let s = harness_with(&[ "GameTooltip.xml", "Interface\\FrameXML\\UIDropDownMenu.xml", // the map's continent/zone pickers initialize into it at OnLoad @@ -593,6 +603,7 @@ fn the_world_map_cannot_open_behind_the_menu_and_gives_its_slot_back() { /// would have opened the menu goes to the map instead (one eater per press). #[test] fn nothing_opens_behind_the_world_map_and_escape_closes_it_first() { + let _data = benilla_formats::wow_data_or_skip!(); let s = harness_with(&[ "GameTooltip.xml", "Interface\\FrameXML\\UIDropDownMenu.xml", @@ -770,6 +781,7 @@ fn backpack() -> ContainerState { /// real load order; the bare-menu harness elsewhere exercises the `or 1` guard implicitly. #[test] fn the_menu_rides_the_shared_era_window_scale() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness_with(&[ "GameTooltip.xml", "Interface\\FrameXML\\UIDropDownMenu.xml", diff --git a/crates/benilla-app/src/ui_script/group_loot_tests.rs b/crates/benilla-app/src/ui_script/group_loot_tests.rs index e4ab2fdd..89ed27a3 100644 --- a/crates/benilla-app/src/ui_script/group_loot_tests.rs +++ b/crates/benilla-app/src/ui_script/group_loot_tests.rs @@ -74,7 +74,8 @@ fn setup() -> UiScript { // The loot window's slots inherit it — the same dependency the inspect window needed (1832). load_xml(&s, r"Interface\FrameXML\ItemButtonTemplate.xml"); // `UIPanelCloseButton`, which the loot window's four close buttons inherit. - load_xml(&s, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "MoneyFrame.xml"); load_xml(&s, "UiPanels.xml"); load_xml(&s, "GameTooltip.xml"); // PASS/NEED/GREED + item hovers diff --git a/crates/benilla-app/src/ui_script/guild_tests.rs b/crates/benilla-app/src/ui_script/guild_tests.rs index 80c62f56..ea49a88b 100644 --- a/crates/benilla-app/src/ui_script/guild_tests.rs +++ b/crates/benilla-app/src/ui_script/guild_tests.rs @@ -211,7 +211,8 @@ fn setup() -> UiScript { load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml"); load_xml(&s, "UnitPopup.xml"); load_xml(&s, "ScrollTemplates.xml"); - load_xml(&s, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "FriendsFrame.xml"); // The social window's fourth tab lives in its own file, and it is part of THIS window's // manifest slice now: `BENILLA_FRIENDS_SUBFRAMES` names "RaidFrame", and both @@ -263,6 +264,7 @@ fn colour(s: &UiScript, region: &str) -> (f64, f64, f64) { /// which the friend list never has to, because that one arrives unasked. #[test] fn the_guild_tab_opens_the_roster_and_asks_for_it() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); assert_eq!( s.eval::("return FriendsFrameTab3.isDisabled or 0") @@ -323,6 +325,7 @@ fn the_guild_tab_opens_the_roster_and_asks_for_it() { /// too, unlike the friends list where an offline friend has no level at all — and only greys. #[test] fn an_offline_member_keeps_its_columns_and_only_greys() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); assert_eq!(text(&s, "GuildFrameButton3Name"), "Kaplan"); @@ -351,6 +354,7 @@ fn an_offline_member_keeps_its_columns_and_only_greys() { /// take you back to. #[test] fn the_page_button_flips_to_the_guild_status_view() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); assert_eq!( @@ -395,6 +399,7 @@ fn the_page_button_flips_to_the_guild_status_view() { /// only reachable by seeding four different members. #[test] fn the_last_online_formatter_takes_the_coarsest_unit() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); let last = |years, months, days, hours| { @@ -422,6 +427,7 @@ fn the_last_online_formatter_takes_the_coarsest_unit() { /// and drops the selection, which is the only way back to "nothing selected". #[test] fn a_row_click_toggles_the_detail_card() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); assert!(!visible(&s, "GuildMemberDetailFrame")); @@ -468,6 +474,7 @@ fn a_row_click_toggles_the_detail_card() { /// with BOTH arrows dead they leave the card rather than sitting greyed. #[test] fn the_detail_buttons_follow_the_rank_comparisons() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); @@ -533,6 +540,7 @@ fn the_detail_buttons_follow_the_rank_comparisons() { /// may not even see officer notes gets 60px less window rather than an empty pane. #[test] fn the_officer_note_pane_resizes_the_card() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); s.run("GuildFrameButton2:Click()").unwrap(); @@ -565,6 +573,7 @@ fn the_officer_note_pane_resizes_the_card() { /// an empty note is a blank rectangle nobody would click. #[test] fn an_editable_empty_note_invites_the_click() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); s.run("GuildFrameButton1:Click()").unwrap(); // Tigole's public note is "" @@ -589,6 +598,7 @@ fn an_editable_empty_note_invites_the_click() { /// `hasWideEditBox` (UiPanels.xml) and the only test that proves the 420 widen and the box swap. #[test] fn the_note_pane_opens_the_wide_dialog_and_sends_it() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); s.run("GuildFrameButton2:Click()").unwrap(); @@ -628,6 +638,7 @@ fn the_note_pane_opens_the_wide_dialog_and_sends_it() { /// it, and the GUILD_MOTD event repaints it without a roster round trip. #[test] fn the_motd_is_cached_click_to_edit_and_right_gated() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open(&s); assert_eq!(text(&s, "GuildFrameNotesText"), "Raid Tuesday at eight."); @@ -672,6 +683,7 @@ fn the_motd_is_cached_click_to_edit_and_right_gated() { /// pane-level buttons, both driven by the repaint rather than by a click. #[test] fn the_pane_buttons_follow_the_rights() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); assert!(enabled(&s, "GuildFrameControlButton")); @@ -706,6 +718,7 @@ fn the_pane_buttons_follow_the_rights() { /// the click has to carry the index it was declared with. #[test] fn the_rank_editor_loads_its_flags_and_arms_accept_on_an_edit() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); s.run("GuildFrameControlButton:Click()").unwrap(); @@ -766,6 +779,7 @@ fn the_rank_editor_loads_its_flags_and_arms_accept_on_an_edit() { /// flags, and re-disarms Accept — the edit you were half-way through does not follow you. #[test] fn switching_rank_reloads_the_buffer_and_disarms_accept() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); s.run("GuildFrameControlButton:Click()").unwrap(); @@ -804,6 +818,7 @@ fn switching_rank_reloads_the_buffer_and_disarms_accept() { /// is EMPTY — which is why the repaint counts `playersInBotRank` at all. #[test] fn the_rank_buttons_follow_the_count_and_the_bottom_rank() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); s.run("GuildFrameControlButton:Click()").unwrap(); @@ -860,6 +875,7 @@ fn the_rank_buttons_follow_the_count_and_the_bottom_rank() { /// sort is a repaint, never a re-request. #[test] fn the_column_headers_sort_by_their_own_keys() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); for (header, key) in [ @@ -896,6 +912,7 @@ fn the_column_headers_sort_by_their_own_keys() { /// so index 7 will not be the member index 7 was. #[test] fn the_show_offline_checkbox_is_real_and_drops_the_selection() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); assert!( @@ -927,6 +944,7 @@ fn the_show_offline_checkbox_is_real_and_drops_the_selection() { /// stored text rather than the click-here invitation. #[test] fn the_guild_information_board_is_right_gated() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); s.run("GuildFrameGuildInformationButton:Click()").unwrap(); @@ -970,6 +988,7 @@ fn the_guild_information_board_is_right_gated() { /// and closing the social window takes all three with it, since none of them is its child. #[test] fn the_three_satellites_are_exclusive_and_close_with_the_window() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); s.run("GuildFrameButton2:Click()").unwrap(); @@ -999,6 +1018,7 @@ fn the_three_satellites_are_exclusive_and_close_with_the_window() { /// would put a server trip behind every click on a header. #[test] fn the_roster_event_only_re_requests_when_told_the_roster_is_stale() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open(&s); @@ -1027,6 +1047,7 @@ fn the_roster_event_only_re_requests_when_told_the_roster_is_stale() { /// impossible to notice until it happens. #[test] fn losing_the_guild_falls_back_off_the_guild_tab() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open(&s); assert!(visible(&s, "GuildFrame")); @@ -1069,6 +1090,7 @@ fn losing_the_guild_falls_back_off_the_guild_tab() { /// are gated on this pane being visible, which is what keeps them off a friends-list or /who row. #[test] fn right_clicking_a_roster_row_offers_the_guild_rows() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); s.run("GuildFrameButton2:Click(\"RightButton\")").unwrap(); @@ -1129,6 +1151,7 @@ fn right_clicking_a_roster_row_offers_the_guild_rows() { /// the game. #[test] fn the_guild_rows_stay_off_a_who_row_menu() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); s.run("ShowWhoPanel()").unwrap(); // NOT our own name: the WHISPER row hides on yourself, and with INVITE gated the same way the @@ -1155,6 +1178,7 @@ fn the_guild_rows_stay_off_a_who_row_menu() { /// window to have ever been opened, which is why it rides its own hidden driver frame. #[test] fn a_guild_invite_raises_its_dialog_without_the_window() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); assert!(!visible(&s, "FriendsFrame"), "the window is shut"); @@ -1209,6 +1233,7 @@ fn a_guild_invite_raises_its_dialog_without_the_window() { /// text is rewritten on show. #[test] fn removing_a_member_names_them_in_the_confirm() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); s.run("GuildFrameButton3:Click()").unwrap(); @@ -1239,6 +1264,7 @@ fn removing_a_member_names_them_in_the_confirm() { /// So this one deliberately does NOT install the fixture: it asks the real `script::guild`. #[test] fn the_rank_flags_binding_answers_thirteen_values_even_when_all_are_nil() { + let _data = benilla_formats::wow_data_or_skip!(); let s = UiScript::new().unwrap(); assert_eq!( s.eval::("return select(\"#\", GuildControlGetRankFlags())") @@ -1271,6 +1297,7 @@ fn the_rank_flags_binding_answers_thirteen_values_even_when_all_are_nil() { /// what makes it a falsifier rather than a restatement. #[test] fn each_permission_predicate_reads_the_bit_its_checkbox_owns() { + let _data = benilla_formats::wow_data_or_skip!(); // Index 7, "Invite Member" — bit 0x10. A shift-based table would put 0x10 at index 5, // "Promote", so this single word separates the two layouts in both directions. let mut s = UiScript::new().unwrap(); @@ -1342,6 +1369,7 @@ fn each_permission_predicate_reads_the_bit_its_checkbox_owns() { /// evidence. #[test] fn the_roster_binding_answers_ten_values_and_the_tenth_is_status() { + let _data = benilla_formats::wow_data_or_skip!(); let s = UiScript::new().unwrap(); assert_eq!( s.eval::("return select(\"#\", GetGuildRosterInfo(1))") @@ -1364,6 +1392,7 @@ fn the_roster_binding_answers_ten_values_and_the_tenth_is_status() { /// right, so an assertion on the look is not enough on its own. #[test] fn driving_the_guild_windows_raises_no_script_errors() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open(&s); s.run("GuildFrameButton1:Click()").unwrap(); @@ -1390,6 +1419,7 @@ fn driving_the_guild_windows_raises_no_script_errors() { /// invisible to reading, so this asserts the WIRING rather than the comment. #[test] fn every_list_in_the_window_takes_the_mouse_wheel() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); open(&s); for frame in [ 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 562a3a54..5f220f43 100644 --- a/crates/benilla-app/src/ui_script/help_frame_tests.rs +++ b/crates/benilla-app/src/ui_script/help_frame_tests.rs @@ -21,7 +21,8 @@ fn setup() -> UiScript { for file in [ "Fonts.xml", "BasicControls.xml", - "UIPanelTemplates.xml", + r"Interface\FrameXML\UIPanelTemplates.lua", + r"Interface\FrameXML\UIPanelTemplates.xml", "ScrollTemplates.xml", "GameTooltip.xml", // Before UiPanels.xml: the shared StaticPopup carries a `SmallMoneyFrameTemplate` coin @@ -77,6 +78,7 @@ fn open_ticket_args( /// "Unknown" instead of a heading the player never chose. #[test] fn one_click_from_home_files_an_uncategorised_ticket() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ToggleHelpFrame()").unwrap(); let _ = s.take_gm_ticket_intents(); // the OnShow GetGMStatus @@ -114,6 +116,7 @@ fn one_click_from_home_files_an_uncategorised_ticket() { /// existence on Home *is* the change. #[test] fn auto_unstuck_is_a_button_on_home() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ToggleHelpFrame()").unwrap(); s.run("BenillaHelpFrameHomeUnstick:Click()").unwrap(); @@ -133,6 +136,7 @@ fn auto_unstuck_is_a_button_on_home() { /// fit that never ran still leaves every other test green. #[test] fn the_two_home_buttons_sit_side_by_side_without_overlapping() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ToggleHelpFrame()").unwrap(); s.resolve(); @@ -173,6 +177,7 @@ fn the_two_home_buttons_sit_side_by_side_without_overlapping() { /// looks fine until you try to file a second ticket. #[test] fn an_open_ticket_turns_the_form_into_an_editor_and_a_zero_turns_it_back() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ShowUIPanel(HelpFrame) HelpFrame_ShowFrame(\"OpenTicket\")") .unwrap(); @@ -224,6 +229,7 @@ fn an_open_ticket_turns_the_form_into_an_editor_and_a_zero_turns_it_back() { /// moment the player fixes a typo. Nothing on screen would ever show that. #[test] fn a_create_files_under_zero_and_an_edit_gives_the_server_its_category_back() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ShowUIPanel(HelpFrame) HelpFrame_ShowFrame(\"OpenTicket\")") .unwrap(); @@ -263,6 +269,7 @@ fn a_create_files_under_zero_and_an_edit_gives_the_server_its_category_back() { /// The `1` leg must put it back — a one-way gate would lock the player out for the session. #[test] fn a_downed_queue_refuses_the_editor_and_says_so_and_comes_back_up() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.fire_event("UPDATE_GM_STATUS", vec![ScriptValue::Int(0)]); s.run("ShowUIPanel(HelpFrame) HelpFrame_ShowFrame(\"OpenTicket\")") @@ -291,6 +298,7 @@ fn a_downed_queue_refuses_the_editor_and_says_so_and_comes_back_up() { /// on screen that says a ticket exists at all once the window is closed. #[test] fn the_ticket_toast_follows_the_ticket() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.fire_event( "UPDATE_TICKET", @@ -314,6 +322,7 @@ fn the_ticket_toast_follows_the_ticket() { /// answers instead of diffing them, so it is worth a test on this side too. #[test] fn the_toast_repolls_the_server_only_after_the_full_interval() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.fire_event( "UPDATE_TICKET", @@ -340,6 +349,7 @@ fn the_toast_repolls_the_server_only_after_the_full_interval() { /// nothing would look identical to one that worked, right up until the ticket reappeared. #[test] fn abandoning_a_ticket_confirms_first_and_only_yes_sends_it() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("StaticPopup_Show(\"HELP_TICKET_ABANDON_CONFIRM\")") .unwrap(); @@ -357,6 +367,7 @@ fn abandoning_a_ticket_confirms_first_and_only_yes_sends_it() { /// life of the session. #[test] fn toggling_the_window_opens_it_and_asks_for_the_queue_status() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); s.run("ToggleHelpFrame()").unwrap(); assert!(s.eval::("return HelpFrame:IsVisible()").unwrap()); @@ -388,6 +399,7 @@ fn toggling_the_window_opens_it_and_asks_for_the_queue_status() { /// a different key. #[test] fn the_home_page_advertises_no_dead_retail_links() { + let _data = benilla_formats::wow_data_or_skip!(); let s = setup(); s.run("ShowUIPanel(HelpFrame) HelpFrame_ShowFrame(\"Home\")") .unwrap(); @@ -438,6 +450,7 @@ fn the_home_page_advertises_no_dead_retail_links() { /// compares stops guarding and never says so. #[test] fn the_windows_geometry_matches_the_reference_file() { + let _data = benilla_formats::wow_data_or_skip!(); let Some(reference) = super::framexml_diff::reference("HelpFrame.xml") else { return; // no install — this test is a no-op rather than a failure }; diff --git a/crates/benilla-app/src/ui_script/minimap_tests.rs b/crates/benilla-app/src/ui_script/minimap_tests.rs index 65ac0f67..a5831bb9 100644 --- a/crates/benilla-app/src/ui_script/minimap_tests.rs +++ b/crates/benilla-app/src/ui_script/minimap_tests.rs @@ -141,10 +141,16 @@ fn game_time_session(hour: u32, minute: u32) -> UiScript { s } -/// `GameTimeTexture`'s current 4-edge texcoord window. +/// `GameTimeTexture`'s current texcoord window, as the `(left, right, top, bottom)` rect this +/// file reasons in. +/// +/// `GetTexCoord` answers EIGHT values since decision 1840 — `ULx, ULy, LLx, LLy, URx, URy, LRx, +/// LRy` — so the rect is positions 1, 5, 2, 4. Folded here rather than at each call site because +/// the window really is axis-aligned and every assertion below is about its edges. fn tod_window(s: &UiScript) -> (f64, f64, f64, f64) { - s.eval::<(f64, f64, f64, f64)>("return GameTimeTexture:GetTexCoord()") - .unwrap() + let (ulx, uly, _, lly, urx, ..): (f64, f64, f64, f64, f64, f64, f64, f64) = + s.eval("return GameTimeTexture:GetTexCoord()").unwrap(); + (ulx, urx, uly, lly) } /// `GameTimeFrame_Update`'s law, exactly: the 50-px window over the 128×64 UI-TOD-Indicator diff --git a/crates/benilla-app/src/ui_script/mod.rs b/crates/benilla-app/src/ui_script/mod.rs index ed875c54..53b01d51 100644 --- a/crates/benilla-app/src/ui_script/mod.rs +++ b/crates/benilla-app/src/ui_script/mod.rs @@ -847,6 +847,12 @@ mod faux_scroll_tests; #[cfg(test)] mod panel_template_tests; +/// The RETURN-SHAPE gate (decision 1842): `reference/1.12-shapes.tsv` against what this client +/// actually answers. `reference_surface` gates names; nothing gated shapes, and that gap produced +/// six decisions in two days. +#[cfg(test)] +mod shape_gate; + /// The reference's BasicControls.xml — TEXT/message/_ERRORMESSAGE and the ScriptErrors dialog, /// none of which benilla itself calls: every test enters from Lua the way an addon does. #[cfg(test)] diff --git a/crates/benilla-app/src/ui_script/panel_template_tests.rs b/crates/benilla-app/src/ui_script/panel_template_tests.rs index 89c204f7..9aa95df2 100644 --- a/crates/benilla-app/src/ui_script/panel_template_tests.rs +++ b/crates/benilla-app/src/ui_script/panel_template_tests.rs @@ -49,7 +49,9 @@ fn harness() -> UiScript { "UiPanels.xml", "GameTooltip.xml", "ScrollTemplates.xml", - "UIPanelTemplates.xml", + r"Interface\FrameXML\UIPanelTemplates.lua", + r"Interface\FrameXML\UIPanelTemplates.xml", + r"Interface\FrameXML\OptionsFrameTemplates.xml", "OptionsFrameTemplates.xml", ] { load_xml(&s, file); @@ -78,6 +80,7 @@ fn drawn_textures(s: &mut UiScript) -> Vec { /// `SetText` dies — with the `CreateFrame` itself having succeeded and returned an object. #[test] fn a_check_button_from_the_template_names_its_label_after_the_caller() { + let _data = benilla_formats::wow_data_or_skip!(); let s = harness(); s.run(r#"MyCheck = CreateFrame("CheckButton", "MyCheck", UIParent, "UICheckButtonTemplate")"#) .unwrap(); @@ -119,6 +122,7 @@ fn a_check_button_from_the_template_names_its_label_after_the_caller() { /// all — no error, no warning, 69 invisible checkboxes. #[test] fn the_templated_check_button_draws_the_reference_checkbox_art() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); s.run( r#"MyCheck = CreateFrame("CheckButton", "MyCheck", UIParent, "UICheckButtonTemplate") @@ -160,6 +164,7 @@ fn the_templated_check_button_draws_the_reference_checkbox_art() { /// options panel in the corpus is written against (`if this:GetChecked() then …`). #[test] fn a_templated_check_button_toggles_before_its_on_click_runs() { + let _data = benilla_formats::wow_data_or_skip!(); let s = harness(); s.run( r#"MyCheck = CreateFrame("CheckButton", "MyCheck", UIParent, "UICheckButtonTemplate") @@ -197,6 +202,7 @@ fn a_templated_check_button_toggles_before_its_on_click_runs() { /// addon idiom is `btn:SetText(...)` and the published global is still `$parentText`. #[test] fn a_panel_button_from_the_template_labels_and_paints() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); s.run( r#"MyBtn = CreateFrame("Button", "MyBtn", UIParent, "UIPanelButtonTemplate") @@ -230,6 +236,7 @@ fn a_panel_button_from_the_template_labels_and_paints() { /// `UiPanels.xml`. #[test] fn the_templated_close_button_hides_the_frame_it_sits_on() { + let _data = benilla_formats::wow_data_or_skip!(); let s = harness(); s.run( r#"MyPanel = CreateFrame("Frame", "MyPanel", UIParent) @@ -260,6 +267,7 @@ fn the_templated_close_button_hides_the_frame_it_sits_on() { /// region, not a layer. #[test] fn an_input_box_from_the_template_carries_its_border_and_takes_text() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); s.run( r#"MyBox = CreateFrame("EditBox", "MyBox", UIParent, "InputBoxTemplate") @@ -306,6 +314,7 @@ fn an_input_box_from_the_template_carries_its_border_and_takes_text() { /// is exactly how someone later introduces a real one by "restoring" it.) #[test] fn a_scroll_frame_from_the_template_wires_its_bar_two_parents_deep() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); s.run( r#"MyScroll = CreateFrame("ScrollFrame", "MyScroll", UIParent, "UIPanelScrollFrameTemplate") @@ -383,6 +392,7 @@ fn a_scroll_frame_from_the_template_wires_its_bar_two_parents_deep() { /// `CreateFrame`'s fourth argument the same way it resolves through XML `inherits=`. #[test] fn the_options_check_button_resolves_its_whole_inheritance_chain() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); s.run( r#"MyOpt = CreateFrame("CheckButton", "MyOpt", UIParent, "UIOptionsCheckButtonTemplate")"#, @@ -430,6 +440,7 @@ fn the_options_check_button_resolves_its_whole_inheritance_chain() { /// plausible-but-absent here is a template no addon will ever name. #[test] fn every_template_these_files_declare_is_a_real_1_12_name() { + let _data = benilla_formats::wow_data_or_skip!(); let tsv = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../reference/1.12-globals.tsv"); let text = std::fs::read_to_string(&tsv).unwrap_or_else(|e| { @@ -445,7 +456,12 @@ fn every_template_these_files_declare_is_a_real_1_12_name() { .collect(); let mut checked = 0; - for file in ["UIPanelTemplates.xml", "OptionsFrameTemplates.xml"] { + // OUR files only. This reads the source tree directly, so it cannot name a chain entry — and + // it has no reason to: asserting that the reference's own file declares 1.12 names is + // tautological. What it guards is us inventing one (decision 1841). + // Our remaining file only. `UIPanelTemplates.xml` was ours until 1846 put the whole kit on the + // chain; what is left to guard is the one template we still declare. + for file in ["OptionsFrameTemplates.xml"] { let src = std::fs::read_to_string(ui_dir().join(file)).unwrap(); let doc = benilla_ui::framexml::parse(&src).unwrap(); for item in &doc.items { @@ -462,7 +478,7 @@ fn every_template_these_files_declare_is_a_real_1_12_name() { } } assert!( - checked >= 18, + checked >= 1, "only {checked} templates swept — the sweep, not the files, is what broke" ); } @@ -480,6 +496,7 @@ fn every_template_these_files_declare_is_a_real_1_12_name() { /// under test: a plain function call would not set it. #[test] fn tab_resize_falls_back_to_this_when_no_tab_is_passed() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); s.set_text_measurer(Box::new(super::FixedWidthFont(6.0))); let doc = benilla_ui::framexml::parse( diff --git a/crates/benilla-app/src/ui_script/panel_tests.rs b/crates/benilla-app/src/ui_script/panel_tests.rs index d57b6995..94db2119 100644 --- a/crates/benilla-app/src/ui_script/panel_tests.rs +++ b/crates/benilla-app/src/ui_script/panel_tests.rs @@ -40,6 +40,7 @@ fn frame_rect(quads: &[ExtractedQuad], w: f32, h: f32) -> benilla_ui::layout::Re /// gets its own dedicated test below (`shipped_gossip_frame_renders_quest_rows_above_options`). #[test] fn shipped_gossip_frame_drives_end_to_end() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); // A measurer: `GossipResize` reads `GetTextHeight()` on the line after `SetText`, so a bare @@ -53,7 +54,8 @@ fn shipped_gossip_frame_drives_end_to_end() { // UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT // optional: a missing template is a loader *warning*, not an error, so an under-loaded // list passes load_xml and then loses the scrollbar silently. - load_xml(&s, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); // The window + its scroll frame (bar + child) + the 32-row shared pool (quest rows and option // rows both draw from it, decision 0088 §3 — the reference's own NUMGOSSIPBUTTONS) + the close // button + the GOODBYE button. The greeting and the NPC-name banner are FontString layers (the @@ -243,6 +245,7 @@ fn shipped_gossip_frame_drives_end_to_end() { /// right 1-based position on `take_gossip_quest_selects` (`benilla-ui` `script/gossip.rs`). #[test] fn shipped_gossip_frame_renders_quest_rows_above_options() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "MoneyFrame.xml"); @@ -251,7 +254,8 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() { // UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT // optional: a missing template is a loader *warning*, not an error, so an under-loaded // list passes load_xml and then loses the scrollbar silently. - load_xml(&s, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml"); s.set_gossip(Some(GossipMenu { @@ -388,6 +392,7 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() { /// an edge — not any glyph metric. #[test] fn shipped_gossip_rows_grow_to_their_wrapped_labels() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); // **A measurer, because the reference measures inside the tick it sets the text.** @@ -407,7 +412,8 @@ fn shipped_gossip_rows_grow_to_their_wrapped_labels() { // UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT // optional: a missing template is a loader *warning*, not an error, so an under-loaded // list passes load_xml and then loses the scrollbar silently. - load_xml(&s, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml"); // Three long options — the shape of a real judgement/roleplay menu, every one of them wrapping @@ -510,6 +516,7 @@ fn shipped_gossip_rows_grow_to_their_wrapped_labels() { /// OnHide. Nothing queues at load (the frame is authored hidden="true"). #[test] fn gossip_show_hide_plays_open_and_close_kits() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "MoneyFrame.xml"); @@ -518,7 +525,8 @@ fn gossip_show_hide_plays_open_and_close_kits() { // UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT // optional: a missing template is a loader *warning*, not an error, so an under-loaded // list passes load_xml and then loses the scrollbar silently. - load_xml(&s, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml"); // Hidden at load: no open sound (never transitions on startup). @@ -551,6 +559,7 @@ fn gossip_show_hide_plays_open_and_close_kits() { #[test] fn shipped_panel_slot_replaces_gossip_with_merchant() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "MoneyFrame.xml"); @@ -559,7 +568,8 @@ fn shipped_panel_slot_replaces_gossip_with_merchant() { // UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT // optional: a missing template is a loader *warning*, not an error, so an under-loaded // list passes load_xml and then loses the scrollbar silently. - load_xml(&s, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml"); load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant // The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo` @@ -643,6 +653,7 @@ fn shipped_panel_slot_replaces_gossip_with_merchant() { /// director's gossip↔vendor lockup. This drives the exact displacement and asserts the intents. #[test] fn displacing_an_npc_window_ends_the_displaced_session() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "MoneyFrame.xml"); @@ -651,7 +662,8 @@ fn displacing_an_npc_window_ends_the_displaced_session() { // UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT // optional: a missing template is a loader *warning*, not an error, so an under-loaded // list passes load_xml and then loses the scrollbar silently. - load_xml(&s, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml"); load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant // The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo` @@ -699,6 +711,7 @@ fn displacing_an_npc_window_ends_the_displaced_session() { /// enough to prove the slot math without a real loot window. #[test] fn shipped_panel_slot_pushable_promotes_to_center() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "MoneyFrame.xml"); @@ -792,6 +805,7 @@ fn shipped_panel_slot_pushable_promotes_to_center() { /// center-parked "left-area" occupant back — UIParent.lua l.777-782). #[test] fn gossip_bank_option_hands_the_left_slot_to_the_bank() { + let _data = benilla_formats::wow_data_or_skip!(); use benilla_ui::script::BankState; // The bank window is the reference's own file off the player's chain (1751). @@ -813,7 +827,8 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() { // list passes load_xml and then loses the scrollbar silently. // Before BankFrame, not after: its close and purchase buttons inherit UIPanelCloseButton // and UIPanelButtonTemplate, and an `inherits=` is resolved at LOAD. - load_xml(&s, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "Interface\\FrameXML\\BankFrame.xml"); load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml"); @@ -877,6 +892,7 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() { /// scrolling moves the content up under that clip. Same deterministic measure fake as the row test. #[test] fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); // A measurer: `GossipResize` reads `GetTextHeight()` on the line after `SetText`, so a bare @@ -887,7 +903,8 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() { load_xml(&s, "MoneyFrame.xml"); load_xml(&s, "UiPanels.xml"); load_xml(&s, "ScrollTemplates.xml"); - load_xml(&s, "UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml"); // Eight wrapping options: ~4 lines each, far past the 334 px scroll frame. @@ -1036,12 +1053,14 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() { /// it is where a client window would be. #[test] fn an_addons_own_frame_registered_in_uipanelwindows_takes_the_left_slot() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "MoneyFrame.xml"); load_xml(&s, "UiPanels.xml"); load_xml(&s, "ScrollTemplates.xml"); - load_xml(&s, "UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml"); // The addon's three lines, in the order an addon writes them. @@ -1111,6 +1130,7 @@ fn an_addons_own_frame_registered_in_uipanelwindows_takes_the_left_slot() { /// either now fails a named test instead of waiting for a player with a Verdant Note. #[test] fn the_1507_registry_rows_match_the_reference_bytes() { + let _data = benilla_formats::wow_data_or_skip!(); let s = UiScript::new().unwrap(); load_xml(&s, "MoneyFrame.xml"); load_xml(&s, "UiPanels.xml"); @@ -1146,6 +1166,7 @@ fn the_1507_registry_rows_match_the_reference_bytes() { /// not take a slot; a `whileDead = 1` row (the quest log, l.25) opens exactly as alive. #[test] fn a_dead_player_opens_only_whiledead_windows() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "MoneyFrame.xml"); @@ -1211,6 +1232,7 @@ fn a_dead_player_opens_only_whiledead_windows() { /// dependency chain. #[test] fn a_frame_arriving_at_center_puts_the_child_windows_away() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "MoneyFrame.xml"); diff --git a/crates/benilla-app/src/ui_script/petition_tests.rs b/crates/benilla-app/src/ui_script/petition_tests.rs index 80c990ae..eaefc04c 100644 --- a/crates/benilla-app/src/ui_script/petition_tests.rs +++ b/crates/benilla-app/src/ui_script/petition_tests.rs @@ -94,7 +94,8 @@ fn setup() -> UiScript { load_xml(&s, "BasicControls.xml"); load_xml(&s, "MoneyFrame.xml"); load_xml(&s, "UiPanels.xml"); - load_xml(&s, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); // `QuestTitleButtonTemplate`, which the reference's registrar inherits for its two service // rows — 1.12 declares it in QuestFrameTemplates.xml, an `` of QuestFrame.xml, and // ours declares it in QuestFrame.xml directly. An unknown template is a loader WARNING, so diff --git a/crates/benilla-app/src/ui_script/questlog_tests.rs b/crates/benilla-app/src/ui_script/questlog_tests.rs index 6b00225d..51bbfa12 100644 --- a/crates/benilla-app/src/ui_script/questlog_tests.rs +++ b/crates/benilla-app/src/ui_script/questlog_tests.rs @@ -81,6 +81,7 @@ const HAMMER_LINK: &str = "|cffffffff|Hitem:2024:0:0:0|h[Militia Hammer]|h|r"; /// The loader itself: every file the window depends on parses and materializes with no errors. #[test] fn shipped_questlog_frame_loads_clean() { + let _data = benilla_formats::wow_data_or_skip!(); let s = UiScript::new().unwrap(); load_xml(&s, "Fonts.xml"); load_xml(&s, "MoneyFrame.xml"); @@ -91,7 +92,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); } @@ -101,6 +103,7 @@ fn shipped_questlog_frame_loads_clean() { /// close kit. #[test] fn shipped_questlog_frame_drives_end_to_end() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "Fonts.xml"); @@ -112,7 +115,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); s.set_quest_log(eight_entries()); @@ -209,13 +213,21 @@ fn shipped_questlog_frame_drives_end_to_end() { _ => None, }) .expect("a 'Quest 3' row text quad"); + let before = s + .eval::("return QuestLogTitle1Text:GetText()") + .unwrap(); s.mouse_wheel(wx, wy, -1.0); assert!(s.errors().is_empty(), "wheel errors: {:?}", s.errors()); - assert!( + // **Not "by one row" any more.** That was our own `ScrollFrameTemplate_OnMouseWheel`, which + // stepped one `valueStep`. The reference's moves the bar by **half its own height** in value + // units (`UIPanelTemplates.lua`), which over a 16px row and a 237px bar is roughly seven rows. + // 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()") - .unwrap() - .contains("Quest 2"), - "wheel-down scrolled the list by one row" + .unwrap(), + before, + "wheel-down scrolled the list" ); // Selection itself is untouched by scrolling. assert_eq!(s.eval::("return GetQuestLogSelection()").unwrap(), 1); @@ -285,6 +297,7 @@ fn shipped_questlog_frame_drives_end_to_end() { /// own or gate on visibility. #[test] 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); load_xml(&s, "Fonts.xml"); @@ -296,7 +309,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); s.set_quest_log(eight_entries()); @@ -405,6 +419,7 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() { /// selects the row exactly like any other click (pin §5). #[test] fn watch_guards_no_op_without_erroring() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "Fonts.xml"); @@ -417,7 +432,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); s.set_quest_log(eight_entries()); @@ -499,6 +515,7 @@ fn watch_guards_no_op_without_erroring() { /// re-arms it, and expiry (the watch frame's OnUpdate) unwatches and hides the empty HUD. #[test] fn progress_auto_watches_for_five_minutes() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "Fonts.xml"); @@ -510,7 +527,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); s.set_quest_log(eight_entries()); @@ -561,6 +579,7 @@ fn progress_auto_watches_for_five_minutes() { /// client's auto-watch is dead until the player toggles the box. Ours ships the string. #[test] fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "Fonts.xml"); @@ -572,7 +591,8 @@ fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() { // 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); s.set_quest_log(eight_entries()); assert_eq!(s.eval::("return AUTO_QUEST_WATCH").unwrap(), "1"); @@ -600,6 +620,7 @@ fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() { /// disables Abandon, and shows the centered empty-state message instead of `EmptyQuestLogFrame`. #[test] fn empty_quest_log_hides_rows_and_disables_abandon() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "Fonts.xml"); @@ -611,7 +632,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); s.set_quest_log(QuestLogState::default()); @@ -650,6 +672,7 @@ fn empty_quest_log_hides_rows_and_disables_abandon() { /// of the last choice row, the fixed reward below that, unused rows/choices past the count hidden. #[test] fn reward_rows_follow_the_refs_two_per_row_layout() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "Fonts.xml"); @@ -661,7 +684,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); let mut state = eight_entries(); @@ -806,6 +830,7 @@ fn overflowing_entry() -> QuestLogState { /// the mechanism, just through the shipped window instead of a synthetic fixture. #[test] fn overflowing_detail_content_clips_to_the_scrollframe_rect() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "Fonts.xml"); @@ -817,7 +842,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); s.set_quest_log(overflowing_entry()); @@ -867,6 +893,7 @@ fn overflowing_detail_content_clips_to_the_scrollframe_rect() { /// window's bucket covering the detail rect, so the spin lands on it outright. #[test] 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); load_xml(&s, "Fonts.xml"); @@ -878,7 +905,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); s.set_quest_log(overflowing_entry()); @@ -913,6 +941,7 @@ fn wheel_over_the_detail_pane_changes_vertical_scroll() { /// reader's scroll position out from under them. #[test] 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); load_xml(&s, "Fonts.xml"); @@ -924,7 +953,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); s.set_quest_log(overflowing_entry()); @@ -968,6 +998,7 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not /// renders the full stat head — the identical lines a vendor row/bag slot gets for this item. #[test] fn reward_row_hover_serves_the_shared_item_tooltip() { + let _data = benilla_formats::wow_data_or_skip!(); use benilla_ui::script::ItemTemplateView; let mut s = UiScript::new().unwrap(); @@ -987,7 +1018,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); s.set_quest_log(eight_entries()); @@ -1062,6 +1094,7 @@ fn reward_row_hover_serves_the_shared_item_tooltip() { /// dialog's dark buttons, pixel-diffed against the raw UI-DialogBox-Button-Up art). #[test] fn popup_children_inherit_the_dialog_stratum() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "Fonts.xml"); @@ -1110,6 +1143,7 @@ fn popup_children_inherit_the_dialog_stratum() { /// arm runs last — opening the room closes the log, exactly as it does in play. #[test] fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "Fonts.xml"); @@ -1121,7 +1155,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here load_xml(&s, "DressUpFrame.xml"); // DressUpItemLink lives here @@ -1190,6 +1225,7 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() { /// eaten it. The select-and-repaint tail (l.503-504) runs either way. #[test] fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_it_closed() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "Fonts.xml"); @@ -1201,7 +1237,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit its menus build from @@ -1281,6 +1318,7 @@ fn entries_sharable(ids: &[u32]) -> QuestLogState { /// both together light it (ref `QuestLogFrame.lua:299-305`). #[test] fn share_quest_needs_both_a_sharable_selection_and_a_party() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "Fonts.xml"); @@ -1292,7 +1330,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); // Solo, quest 1 sharable and selected by the auto-selection. @@ -1355,6 +1394,7 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() { /// but `GetNumPartyMembers() > 0` is not. #[test] fn share_quest_is_dark_on_an_empty_log_even_in_a_party() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "Fonts.xml"); @@ -1366,7 +1406,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); s.set_quest_log(QuestLogState::default()); @@ -1383,6 +1424,7 @@ fn share_quest_is_dark_on_an_empty_log_even_in_a_party() { /// selection move, so an index/id confusion shows up here. #[test] fn share_quest_click_queues_the_selected_quests_id() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); load_xml(&s, "Fonts.xml"); @@ -1394,7 +1436,8 @@ 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, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "QuestLogFrame.xml"); s.set_quest_log(entries_sharable(&[1, 5])); diff --git a/crates/benilla-app/src/ui_script/raid_tests.rs b/crates/benilla-app/src/ui_script/raid_tests.rs index 20c8862e..3dd04134 100644 --- a/crates/benilla-app/src/ui_script/raid_tests.rs +++ b/crates/benilla-app/src/ui_script/raid_tests.rs @@ -32,7 +32,8 @@ fn setup() -> UiScript { load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml"); load_xml(&s, "UnitPopup.xml"); load_xml(&s, "ScrollTemplates.xml"); - load_xml(&s, "UIPanelTemplates.xml"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua"); + load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml"); load_xml(&s, "FriendsFrame.xml"); load_xml(&s, "RaidFrame.xml"); s @@ -140,6 +141,7 @@ fn open_raid_tab(s: &mut UiScript) { /// so in its header, and the tab strip is the one place a missing pane is visible without a raid. #[test] fn the_fourth_tab_opens_the_raid_pane() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); assert_eq!( s.eval::("return FriendsFrame.numTabs").unwrap(), @@ -168,6 +170,7 @@ fn the_fourth_tab_opens_the_raid_pane() { /// Closing the window closes the Raid Info flyout with it — the ref's fourth satellite. #[test] fn hiding_the_window_closes_the_raid_info_panel() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); s.run("RaidInfoFrame:Show()").unwrap(); @@ -187,6 +190,7 @@ fn hiding_the_window_closes_the_raid_info_panel() { /// leader of an actual party — the three states, in order. #[test] fn convert_to_raid_is_live_only_for_a_party_leader() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); assert!(visible(&s, "RaidFrameRaidDescription"), "solo: the blurb"); @@ -256,6 +260,7 @@ fn convert_to_raid_is_live_only_for_a_party_leader() { /// menu action downstream addresses a row by the index this seating implies. #[test] fn the_grid_seats_each_row_in_its_own_subgroups_next_free_slot() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); push_raid(&mut s, twelve()); @@ -323,6 +328,7 @@ fn the_grid_seats_each_row_in_its_own_subgroups_next_free_slot() { /// the names still read, the clicks still fire. #[test] fn a_rows_colour_is_offline_then_dead_then_class() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); push_raid( @@ -395,6 +401,7 @@ fn a_rows_colour_is_offline_then_dead_then_class() { /// field: a 40-row rebuild per point of damage taken is what the other choice costs. #[test] fn a_units_health_and_level_repaint_only_that_row() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); let mut raid = twelve(); @@ -445,6 +452,7 @@ fn a_units_health_and_level_repaint_only_that_row() { /// outright out of one — they share their seat with Convert To Raid. #[test] fn ready_check_is_the_leaders_and_add_member_is_everyones() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); push_raid(&mut s, twelve()); @@ -494,6 +502,7 @@ fn ready_check_is_the_leaders_and_add_member_is_everyones() { /// needs a live cursor this harness has no way to place. #[test] fn dragging_a_row_moves_swaps_or_springs_back() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); push_raid(&mut s, twelve()); @@ -538,6 +547,7 @@ fn dragging_a_row_moves_swaps_or_springs_back() { /// never stops. #[test] fn only_the_leader_may_drag() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); let raid = twelve(); @@ -569,6 +579,7 @@ fn only_the_leader_may_drag() { /// Left-clicking a row targets that row's `raidN` token — the same token the tooltip reads. #[test] fn left_clicking_a_row_targets_its_raid_token() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); push_raid(&mut s, twelve()); @@ -586,6 +597,7 @@ fn left_clicking_a_row_targets_its_raid_token() { /// opened on the wrong index would offer Promote on somebody who is already an assistant. #[test] fn the_row_menu_offers_the_rank_verbs_by_rank() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); push_raid(&mut s, twelve()); @@ -633,6 +645,7 @@ fn the_row_menu_offers_the_rank_verbs_by_rank() { /// the kick by the row index. #[test] fn the_menu_verbs_queue_the_right_requests() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); push_raid(&mut s, twelve()); @@ -673,6 +686,7 @@ fn the_menu_verbs_queue_the_right_requests() { /// the server has answered with something. #[test] fn the_raid_info_panel_lists_the_saved_lockouts() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); // The first UPDATE_INSTANCE_INFO only arms the ref's latch (`RaidFrame.hasRaidInfo`). @@ -743,6 +757,7 @@ fn the_raid_info_panel_lists_the_saved_lockouts() { /// left live over an empty panel is exactly what the director saw. #[test] fn a_player_with_no_lockouts_loses_the_raid_info_button_on_the_second_answer() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); @@ -801,6 +816,7 @@ fn a_player_with_no_lockouts_loses_the_raid_info_button_on_the_second_answer() { /// the bar is shown. #[test] fn the_scroll_bar_is_seated_on_the_trough_the_panel_draws_behind_it() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); s.fire_event("UPDATE_INSTANCE_INFO", Vec::new()); @@ -854,6 +870,7 @@ fn the_scroll_bar_is_seated_on_the_trough_the_panel_draws_behind_it() { /// including the No button's argument-less call, which is the reference's own spelling. #[test] fn the_ready_check_popup_opens_and_answers() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); push_raid(&mut s, twelve()); @@ -905,6 +922,7 @@ fn the_ready_check_popup_opens_and_answers() { /// the window's geometry guarded by nothing. #[test] fn the_geometry_matches_both_reference_files() { + let _data = benilla_formats::wow_data_or_skip!(); // The FrameXML half: the pane, its two buttons, the info flyout and the lockout row template. if let Some(reference) = super::framexml_diff::reference("RaidFrame.xml") { super::framexml_diff::assert_geometry_matches( @@ -952,6 +970,7 @@ fn the_geometry_matches_both_reference_files() { /// the row goes home, the pane forgets it, and the next row still drags. #[test] fn a_drag_carried_off_the_window_edge_ends_instead_of_gluing_the_row_to_the_cursor() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); push_raid(&mut s, twelve()); @@ -1032,6 +1051,7 @@ fn a_drag_carried_off_the_window_edge_ends_instead_of_gluing_the_row_to_the_curs /// mattered — never drags a SECOND row. This one does all four. #[test] fn one_drag_does_not_cost_the_next_one() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = setup(); open_raid_tab(&mut s); let mut raid = vec![row("Me", 2, 1, "Warrior", true, false)]; diff --git a/crates/benilla-app/src/ui_script/shape_gate.rs b/crates/benilla-app/src/ui_script/shape_gate.rs new file mode 100644 index 00000000..8f8029ca --- /dev/null +++ b/crates/benilla-app/src/ui_script/shape_gate.rs @@ -0,0 +1,314 @@ +//! **The return-shape gate** — `reference/1.12-shapes.tsv` against what this client actually +//! answers (decision 1842). +//! +//! `reference_surface` compares `_G` to `_G`, so a wrong *name* cannot land. Nothing compared +//! *shapes*, and that gap produced six decisions in two days — 1818, 1819, 1830, 1834, 1836 and +//! 1840 — every one of them a right name with a wrong arity, invisible to every gate we had and +//! invisible to the chain's own files too, because a Lua caller that uses one value of two never +//! notices the second. +//! +//! The table is wow-re's `re/audit/binding-shapes.tsv`, vendored: 1722 rows over all 82 registrar +//! tables, one per registered binding, generated and differentially tested against that repo's +//! other harvester. It carries the gate rule **per row** rather than in prose, which is the whole +//! reason it can be enforced instead of remembered: +//! +//! * `arity_conf = exact` — sound. This is the column to gate on. +//! * `kinds_conf` — trustworthy only where an independent push count agreed; not gated here. +//! * `argc_conf` — **never** gate on it. It is a deliberate over-flagging superset: any binding +//! whose direct callee reads a Lua index is marked `lower-bound`, so a `lower-bound` row says +//! nothing about the real argument count. +//! +//! ## Two things that make this narrower than it looks, both on purpose +//! +//! **Only query-shaped names are probed.** The gate calls each binding to count what it answers, +//! and calling an arbitrary global for its arity would mean calling `AbandonQuest`, `Quit` and +//! `AcceptBattlefieldPort` for theirs. Names are filtered to the query verbs — `Get*`, `Is*`, +//! `Has*`, `Can*`, `Unit*`, `Num*` — which is a loss of coverage, never a wrong assertion. +//! +//! **A call that raises is skipped, not failed.** 53 of the 83 unit-table bindings gate their +//! arguments and raise on a nil token (1834/1836), so a no-argument probe of those is expected to +//! throw; that says nothing about arity. Only a call that *returns* is measured. +//! +//! ## The name is not a key +//! +//! **60 names are registered from more than one table and 21 differ in arity across +//! registrations** — `GetBuildInfo` is arity 5 in the glue table `0x8373b8` and 3 in the in-game +//! core `0x83de68`; `GetAddOnInfo` is 8 and 7. This VM is the in-game one, so a name carrying rows +//! from both is read at its **non-glue** row, and a name whose remaining rows still disagree is +//! skipped rather than guessed at. +//! +//! That hazard is the reason the table is keyed on `fn` upstream, and it is a live merge hazard for +//! `1.12-globals.tsv`, which is name-keyed. + +/// Names the gate knows are wrong and does not yet assert — **a list that may only shrink**. +/// +/// **It is empty.** It held five entries whose miss-branch *contents* the shapes table does not +/// record — four globals and `Model:GetFogColor` — and 1845 answered all five at the bytes rather +/// than guessing them. The mechanism stays because the next divergence the gate finds will want it: +/// an entry needs an arity, a reason, and a dispatched question. +const NOT_YET_ASSERTED: &[(&str, usize, &str)] = &[]; + +/// The widget half's own shrinking list — see [`NOT_YET_ASSERTED`], same rules. Also empty: +/// `GetFogColor` came off it with 1845, which found the fog colour was a packed `0xAARRGGBB` dword +/// all along. +const WIDGET_NOT_YET_ASSERTED: &[&str] = &[]; + +/// The glue registrar table. This VM is the in-game one, so a duplicated name is read at its +/// other row (see the module doc). +const GLUE_TABLE: &str = "0x8373b8"; + +struct Row { + name: String, + table_va: String, + arity: usize, +} + +fn rows() -> Vec { + let tsv = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../reference/1.12-shapes.tsv" + ); + let text = std::fs::read_to_string(tsv).expect("reference/1.12-shapes.tsv"); + text.lines() + .filter(|l| !l.starts_with('#') && !l.starts_with("name\t")) + .filter_map(|l| { + let f: Vec<&str> = l.split('\t').collect(); + // name fn pair_va table_va table_kind argc argc_conf arity arity_conf kinds … + if f.len() < 9 || f[4] != "global" || f[8] != "exact" { + return None; + } + Some(Row { + name: f[0].to_string(), + table_va: f[3].to_string(), + arity: f[7].parse().ok()?, + }) + }) + .collect() +} + +/// Every registered global whose arity the reference states exactly, answered by this client. +#[test] +fn every_query_binding_answers_the_reference_s_return_arity() { + let all = rows(); + assert!( + all.len() > 800, + "the vendored table looks wrong: {}", + all.len() + ); + + // Resolve the duplicated names the way the module doc describes. + let mut by_name: std::collections::HashMap<&str, Vec<&Row>> = std::collections::HashMap::new(); + for r in &all { + by_name.entry(&r.name).or_default().push(r); + } + + let s = benilla_ui::script::UiScript::new().expect("VM"); + let mut checked = 0usize; + let mut mismatches: Vec = Vec::new(); + + for (name, rs) in &by_name { + // Query verbs only — the gate calls what it measures. + if !["Get", "Is", "Has", "Can", "Unit", "Num"] + .iter() + .any(|p| name.starts_with(p)) + { + continue; + } + let candidates: Vec<&&Row> = if rs.len() == 1 { + rs.iter().collect() + } else { + rs.iter().filter(|r| r.table_va != GLUE_TABLE).collect() + }; + // Still ambiguous after dropping glue: skip rather than guess. + let Some(first) = candidates.first() else { + continue; + }; + if candidates.iter().any(|r| r.arity != first.arity) { + continue; + } + let want = first.arity; + + // A call that raises says nothing about arity — 53 of the unit bindings gate their + // arguments and raise on a nil token (1834/1836). + let probe = format!( + "if type({name}) ~= 'function' then return -1 end \ + local ok, n = pcall(function() return select('#', {name}()) end) \ + if not ok then return -1 end return n" + ); + let got: i64 = match s.eval(&probe) { + Ok(n) => n, + Err(_) => continue, + }; + if got < 0 { + continue; + } + checked += 1; + if got as usize != want { + if let Some((_, known, _)) = NOT_YET_ASSERTED.iter().find(|(n, ..)| n == name) { + assert_eq!( + *known, want, + "{name} is on the not-yet-asserted list at a stale arity — the table now says \ + {want}" + ); + continue; + } + mismatches.push(format!("{name}: answers {got}, reference states {want}")); + } + } + + assert!( + checked >= 40, + "the gate measured only {checked} bindings — it has stopped covering anything" + ); + // The list may only SHRINK. An entry that now agrees is a fix nobody deleted the note for, + // and leaving it would let the next real divergence hide behind it. + let stale: Vec<&str> = NOT_YET_ASSERTED + .iter() + .map(|(n, ..)| *n) + .filter(|n| { + by_name.get(n).is_some_and(|rs| { + let want = rs[0].arity; + s.eval::(&format!( + "local ok, n = pcall(function() return select('#', {n}()) end) \ + if not ok then return -1 end return n" + )) + .is_ok_and(|got| got >= 0 && got as usize == want) + }) + }) + .collect(); + assert!( + stale.is_empty(), + "these now answer the reference's arity and must come off NOT_YET_ASSERTED: {stale:?}" + ); + assert!( + mismatches.is_empty(), + "{} of {checked} probed bindings answer the wrong number of values:\n {}", + mismatches.len(), + mismatches.join("\n ") + ); +} + +/// The widget registrar tables, each mapped to a Lua expression that produces one instance. +/// +/// The table address IS the widget class — the reference registers one `{name, fn}` table per +/// widget family, and a method name can appear in several of them with different arities (the +/// `name-not-unique` case). So a widget method is probed on **its own class**, never on whichever +/// object happens to answer to the name. +/// +/// Identified by each table's distinctive members: `GetTexCoord` is Texture's, `GetChecked` is +/// CheckButton's, `GetHyperlinkFormat` is SimpleHTML's, `GetScrollChild` is ScrollFrame's, and so +/// on. A table with no entry here is skipped, which costs coverage and never correctness. +const WIDGET_PROBES: &[(&str, &str, &str)] = &[ + ("0x878ec0", "Frame", "PGFrame"), + ("0x879d00", "Button", "PGButton"), + ("0x87bf74", "CheckButton", "PGCheck"), + ("0x87bb68", "EditBox", "PGEdit"), + ("0x87b260", "Slider", "PGSlider"), + ("0x87b010", "StatusBar", "PGStatus"), + ("0x87b3c0", "ScrollFrame", "PGScroll"), + ("0x87ba80", "SimpleHTML", "PGHtml"), + ("0x87abb0", "ColorSelect", "PGColor"), + ("0x87b960", "MessageFrame", "PGMessage"), + ("0x87b5c0", "ScrollingMessageFrame", "PGScrollMsg"), + ("0x878948", "PlayerModel", "PGModel"), +]; + +/// The two region classes, which are made by a frame rather than by `CreateFrame`. +const REGION_PROBES: &[(&str, &str)] = &[ + ("0x87c128", "PGFrame:CreateTexture('PGTex')"), + ("0x87c1d8", "PGFrame:CreateFontString('PGFS')"), +]; + +/// **The widget half of the gate** (decision 1843) — the half that would have caught 1840's +/// `GetTexCoord` without a byte read. +/// +/// Same rule as the global half: `arity_conf = exact` only, query verbs only, a call that raises is +/// skipped. The difference is the receiver: each row is probed on an instance of the class its +/// registrar table belongs to, because the same method name lives in several tables with different +/// shapes. +#[test] +fn every_widget_method_answers_the_reference_s_return_arity() { + let tsv = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../reference/1.12-shapes.tsv" + ); + let text = std::fs::read_to_string(tsv).expect("reference/1.12-shapes.tsv"); + + let s = benilla_ui::script::UiScript::new().expect("VM"); + let mut made: std::collections::HashMap<&str, String> = std::collections::HashMap::new(); + for (table, kind, name) in WIDGET_PROBES { + if s.run(&format!( + "{name} = CreateFrame(\"{kind}\", \"{name}\", UIParent)" + )) + .is_ok() + { + made.insert(table, (*name).to_string()); + } + } + for (table, expr) in REGION_PROBES { + let var = format!("PGR{}", made.len()); + if s.run(&format!("{var} = {expr}")).is_ok() { + made.insert(table, var); + } + } + assert!( + made.len() >= 8, + "only {} widget classes could be instantiated — the probe set is broken", + made.len() + ); + + let mut checked = 0usize; + let mut mismatches: Vec = Vec::new(); + for line in text + .lines() + .filter(|l| !l.starts_with('#') && !l.starts_with("name\t")) + { + let f: Vec<&str> = line.split('\t').collect(); + if f.len() < 9 || f[4] != "widget" || f[8] != "exact" { + continue; + } + let (name, table) = (f[0], f[3]); + if !["Get", "Is", "Has", "Can", "Num"] + .iter() + .any(|p| name.starts_with(p)) + { + continue; + } + let Some(obj) = made.get(table) else { continue }; + let Ok(want) = f[7].parse::() else { + continue; + }; + + let probe = format!( + "if type({obj}.{name}) ~= 'function' then return -1 end \ + local ok, n = pcall(function() return select('#', {obj}:{name}()) end) \ + if not ok then return -1 end return n" + ); + let Ok(got) = s.eval::(&probe) else { + continue; + }; + if got < 0 { + continue; + } + checked += 1; + if got as usize != want { + if WIDGET_NOT_YET_ASSERTED.contains(&name) { + continue; + } + mismatches.push(format!( + "{obj}:{name} answers {got}, reference states {want}" + )); + } + } + + assert!( + checked >= 60, + "the widget gate measured only {checked} methods — it has stopped covering anything" + ); + assert!( + mismatches.is_empty(), + "{} of {checked} probed widget methods answer the wrong number of values:\n {}", + mismatches.len(), + mismatches.join("\n ") + ); +} diff --git a/crates/benilla-app/src/ui_script/test_ui.rs b/crates/benilla-app/src/ui_script/test_ui.rs index 557e5c22..d37ba1b5 100644 --- a/crates/benilla-app/src/ui_script/test_ui.rs +++ b/crates/benilla-app/src/ui_script/test_ui.rs @@ -223,7 +223,8 @@ pub(super) const CHARACTER_UI: &[&str] = &[ "UiPanels.xml", // CharacterFrameTabButtonTemplate, UIPanelWindows, Show/HideUIPanel "GameTooltip.xml", "Cooldown.xml", // CooldownFrameTemplate + CooldownFrame_SetTimer, per equipment slot - "UIPanelTemplates.xml", + r"Interface\FrameXML\UIPanelTemplates.lua", + r"Interface\FrameXML\UIPanelTemplates.xml", // The unit frames' four right-click dropdowns call `UIDropDownMenu_Initialize` at LOAD, so the // kit and the menu table it initialises from both precede them — the manifest's own order // (175 → 189 → 193 → 263). @@ -252,7 +253,10 @@ pub(super) const CHARACTER_UI: &[&str] = &[ // (`CharacterFrame.lua:25-32`), unguarded. A missing page is `attempt to index a nil value` on // the very first `ToggleCharacter` — load-clean, then dead on the first click. Their own // template dependencies come with them. - "ScrollTemplates.xml", // SkillFrame's faux list + trough + "ScrollTemplates.xml", // SkillFrame's faux list + trough + // The four options templates off the chain, then ours for the one it does not carry + // (`UIOptionsCheckButtonTemplate` — decision 1841). + "Interface\\FrameXML\\OptionsFrameTemplates.xml", "OptionsFrameTemplates.xml", // ReputationFrame's detail check boxes "Interface\\FrameXML\\CharacterFrame.xml", "Interface\\FrameXML\\PaperDollFrame.xml", @@ -294,7 +298,8 @@ pub(super) const BAG_UI: &[&str] = &[ // `UpdateMicroButtons` — the KEYRING's own OnShow/OnHide calls it (ContainerFrame.lua l.117, // l.137), because in the reference the keyring's existence moves the micro-button row. "MicroMenu.xml", - "UIPanelTemplates.xml", + r"Interface\FrameXML\UIPanelTemplates.lua", + r"Interface\FrameXML\UIPanelTemplates.xml", "Interface\\FrameXML\\ContainerFrame.xml", // `PaperDollItemSlotButtonTemplate` and the `PaperDollItemSlotButton_*` family behind it, // which every bag button inherits and runs — resolved at load, so this has to precede the bar 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 3d66d021..2c870237 100644 --- a/crates/benilla-app/src/ui_script/unit_frame_tests.rs +++ b/crates/benilla-app/src/ui_script/unit_frame_tests.rs @@ -1913,9 +1913,12 @@ fn the_raid_mark_helper_maps_each_index_to_its_cell() { for (i, l, r, t, b) in cells { s.run(&format!("SetRaidTargetIconTexture(RTMark, {i})")) .unwrap(); - let got: (f64, f64, f64, f64) = s.eval("return RTMark:GetTexCoord()").unwrap(); + // `GetTexCoord` answers EIGHT (UL, LL, UR, LR as x,y pairs) since 1840; the old + // `(l, r, t, b)` rect is `ULx, URx, ULy, LLy` — positions 1, 5, 2, 4. + let (gl, gt, _, gb, gr, ..): (f64, f64, f64, f64, f64, f64, f64, f64) = + s.eval("return RTMark:GetTexCoord()").unwrap(); assert_eq!( - got, + (gl, gr, gt, gb), (l, r, t, b), "mark {i} must sample the cell at ({l}, {r}, {t}, {b})" ); 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 ff18982a..f65fb324 100644 --- a/crates/benilla-app/src/ui_script/unit_popup_tests.rs +++ b/crates/benilla-app/src/ui_script/unit_popup_tests.rs @@ -36,7 +36,8 @@ fn load_popup_frames(s: &UiScript) { "Interface\\FrameXML\\TargetFrame.xml", "Interface\\FrameXML\\PetFrame.xml", "ScrollTemplates.xml", - "UIPanelTemplates.xml", + r"Interface\FrameXML\UIPanelTemplates.lua", + r"Interface\FrameXML\UIPanelTemplates.xml", "FriendsFrame.xml", ] { load_xml(s, file); diff --git a/crates/benilla-app/src/ui_script/world_map_tests.rs b/crates/benilla-app/src/ui_script/world_map_tests.rs index 07655b3d..0bc718be 100644 --- a/crates/benilla-app/src/ui_script/world_map_tests.rs +++ b/crates/benilla-app/src/ui_script/world_map_tests.rs @@ -16,7 +16,8 @@ fn harness() -> UiScript { "Fonts.xml", "MoneyFrame.xml", "UiPanels.xml", - "UIPanelTemplates.xml", + r"Interface\FrameXML\UIPanelTemplates.lua", + r"Interface\FrameXML\UIPanelTemplates.xml", "GameTooltip.xml", "Interface\\FrameXML\\UIDropDownMenu.xml", // the map's continent/zone pickers initialize into it at OnLoad "ScrollTemplates.xml", @@ -52,6 +53,7 @@ fn landmark(name: &str, icon: u32, uv: (f32, f32)) -> WorldMapLandmarkView { /// `points_of_interest` row ships, so this is the guard-directions case exactly. #[test] fn a_landmark_draws_its_poi_icon_at_its_map_position() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); s.set_world_map_landmarks(vec![landmark("Stormwind Warrior Trainer", 6, (0.25, 0.5))]); s.run("WorldMapFrame_Update()").unwrap(); @@ -68,7 +70,9 @@ fn a_landmark_draws_its_poi_icon_at_its_map_position() { ); // Cell 6 of the 8×8 grid = column 6, row 0. - let (l, r, t, b): (f32, f32, f32, f32) = s + // `GetTexCoord` answers EIGHT (UL, LL, UR, LR as x,y pairs) since 1840; the old + // `(l, r, t, b)` rect is `ULx, URx, ULy, LLy` — positions 1, 5, 2, 4. + let (l, t, _, b, r, ..): (f32, f32, f32, f32, f32, f32, f32, f32) = s .eval("return WorldMapFramePOI1Texture:GetTexCoord()") .unwrap(); assert_eq!( @@ -92,6 +96,7 @@ fn a_landmark_draws_its_poi_icon_at_its_map_position() { /// destroyed, and the same frame is re-seated when the list shrinks back. #[test] fn the_poi_pool_grows_and_parks_its_tail() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); s.set_world_map_landmarks(vec![ landmark("The Bank", 6, (0.1, 0.1)), @@ -134,6 +139,7 @@ fn the_poi_pool_grows_and_parks_its_tail() { /// guard's directions never do, a battleground node's "In Conflict" would. #[test] fn hovering_a_poi_names_it_and_adds_a_status_line_only_when_there_is_one() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); s.set_world_map_landmarks(vec![landmark("Lion's Pride Inn", 6, (0.5, 0.5))]); s.run("WorldMapFrame_Update()").unwrap(); @@ -169,6 +175,7 @@ fn hovering_a_poi_names_it_and_adds_a_status_line_only_when_there_is_one() { /// guard's marker never carries one. #[test] fn the_landmark_getter_returns_the_references_five_values() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); let mut with_status = landmark("Stables", 9, (0.25, 0.75)); with_status.description = "In Conflict".into(); @@ -201,6 +208,7 @@ fn the_landmark_getter_returns_the_references_five_values() { /// is open far more often than a guard has just given directions. #[test] fn no_landmarks_draws_nothing() { + let _data = benilla_formats::wow_data_or_skip!(); let s = harness(); s.run("WorldMapFrame_Update()").unwrap(); assert_eq!(s.eval::("return GetNumMapLandmarks()").unwrap(), 0); @@ -217,6 +225,7 @@ fn no_landmarks_draws_nothing() { /// getting that sign wrong mirrors every blip about the top edge without failing anything else. #[test] fn party_blips_sit_at_their_map_positions_and_hide_when_absent() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = harness(); // `IsShown`, not `IsVisible`: the map itself is closed in this harness (the POI test's own // idiom), so every child would read invisible through its hidden ancestor. @@ -294,6 +303,7 @@ fn party_blips_sit_at_their_map_positions_and_hide_when_absent() { /// dropdown lists carry `toplevel="true"` with no parent at all. #[test] fn the_maps_own_furniture_survives_the_hide_that_showing_it_performs() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1600.0, 900.0); let failures = super::load_default_ui(&s); diff --git a/crates/benilla-app/tests/auction_frame.rs b/crates/benilla-app/tests/auction_frame.rs index e81636a3..c8588c3a 100644 --- a/crates/benilla-app/tests/auction_frame.rs +++ b/crates/benilla-app/tests/auction_frame.rs @@ -20,14 +20,15 @@ mod common; /// the `MoneyTypeInfo` table this window registers `AUCTION_DEPOSIT` into, The `BenillaMoneyInput_*` money-entry /// helpers used to mean loading MerchantFrame.xml as well; 1751 moved that kit to MoneyFrame.xml /// on its way to the chain, so the dependency is gone. -const FILES: [&str; 8] = [ +const FILES: [&str; 9] = [ "Fonts.xml", "MoneyFrame.xml", "UiPanels.xml", "GameTooltip.xml", "Interface\\FrameXML\\UIDropDownMenu.xml", "ScrollTemplates.xml", - "UIPanelTemplates.xml", + r"Interface\FrameXML\UIPanelTemplates.lua", + r"Interface\FrameXML\UIPanelTemplates.xml", "AuctionFrame.xml", ]; @@ -112,6 +113,7 @@ fn seat_player(s: &mut UiScript, money: u64) { #[test] fn auction_frame_loads_and_key_regions_exist() { + let _data = benilla_formats::wow_data_or_skip!(); let s = UiScript::new().unwrap(); load_ui(&s); for name in [ @@ -158,6 +160,7 @@ fn auction_frame_loads_and_key_regions_exist() { /// it ShowUIPanel takes its unregistered branch and the window flips `IsShown()` without landing. #[test] fn the_window_is_registered_doublewide() { + let _data = benilla_formats::wow_data_or_skip!(); let s = UiScript::new().unwrap(); load_ui(&s); assert_eq!( @@ -169,6 +172,7 @@ fn the_window_is_registered_doublewide() { #[test] fn auction_house_show_opens_the_window_on_the_browse_tab() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); load_ui(&s); seat_player(&mut s, 500_000); @@ -226,6 +230,7 @@ fn auction_house_show_opens_the_window_on_the_browse_tab() { /// The part that proves it works: a fed snapshot paints the Browse rows. #[test] fn the_browse_list_populates_from_the_fed_snapshot() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); load_ui(&s); seat_player(&mut s, 500_000); @@ -325,6 +330,7 @@ fn the_browse_list_populates_from_the_fed_snapshot() { /// only when the purse can actually cover it. #[test] fn the_bid_and_buyout_gates_read_the_purse() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); load_ui(&s); // 2 gold. Row 1 costs 10s to bid and 50s to buy out — affordable. Row 2 wants 5 gold. @@ -408,6 +414,7 @@ fn the_bid_and_buyout_gates_read_the_purse() { /// that is not about affordability. #[test] fn you_cannot_bid_on_your_own_auction() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); load_ui(&s); seat_player(&mut s, 10_000_000); @@ -438,6 +445,7 @@ fn you_cannot_bid_on_your_own_auction() { /// setting a level band and clicking a class row queue no query at all until Search fires. #[test] fn search_reads_the_filters_and_nothing_queries_before_it() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); load_ui(&s); seat_player(&mut s, 0); @@ -487,6 +495,7 @@ fn search_reads_the_filters_and_nothing_queries_before_it() { /// own arithmetic over the stack's vendor value and the RUN TIME — never over what you ask for. #[test] fn the_create_gate_and_the_deposit() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); load_ui(&s); seat_player(&mut s, 100_000); @@ -629,6 +638,7 @@ fn the_create_gate_and_the_deposit() { /// Closing the window ends the session client-side and takes both confirmations with it. #[test] fn hiding_the_window_closes_the_session() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); load_ui(&s); seat_player(&mut s, 0); @@ -657,6 +667,7 @@ fn hiding_the_window_closes_the_session() { /// batch out of 120 fills all 8 slots, so the turners stay hidden until the list is scrolled down. #[test] fn paging_shows_the_turners_only_at_the_end_of_the_list() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); load_ui(&s); seat_player(&mut s, 0); @@ -712,6 +723,7 @@ fn paging_shows_the_turners_only_at_the_end_of_the_list() { /// API surface rather than an equivalent one. #[test] fn a_row_hover_goes_through_the_reference_tooltip_verb() { + let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); load_ui(&s); s.set_auction(Some(state(vec![row("Copper Bar", 100, 500, 0, "Someone")]))); diff --git a/crates/benilla-app/tests/item_text_frame.rs b/crates/benilla-app/tests/item_text_frame.rs index 83429d17..fed862fc 100644 --- a/crates/benilla-app/tests/item_text_frame.rs +++ b/crates/benilla-app/tests/item_text_frame.rs @@ -18,7 +18,7 @@ const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui"); /// The reader's dependency prefix, in the manifest's own order. `ScrollTemplates.xml` and /// `UIPanelTemplates.xml` joined it with decisions 1337/1338: the page sits in a real ScrollFrame /// now, whose template is in the second and whose `ScrollFrame_OnLoad` is in the first. -const FILES: [&str; 8] = [ +const FILES: [&str; 9] = [ // `ITEM_TEXT_FROM`, which the reference's READY arm concatenates into the creator tail — and // `attempt to concatenate a nil value` kills the handler before it reaches its `ShowUIPanel`, // so the window simply never opens. Our deleted copy carried the string as a local fallback. @@ -30,7 +30,8 @@ const FILES: [&str; 8] = [ // page and title ink. 1.12 keeps it in UIParent.lua and ours does the same (1751 window 9). "UIParent.xml", "ScrollTemplates.xml", - "UIPanelTemplates.xml", + r"Interface\FrameXML\UIPanelTemplates.lua", + r"Interface\FrameXML\UIPanelTemplates.xml", "Interface\\FrameXML\\ItemTextFrame.xml", ]; @@ -322,6 +323,7 @@ fn the_reported_html_page_draws_as_blocks_not_as_its_own_markup() { /// with. It skips on a machine with no client data, like every other archive-backed sweep. #[test] fn the_reported_book_crest_draws_at_the_blps_own_size() { + let _data = benilla_formats::wow_data_or_skip!(); let data = benilla_formats::wow_data_or_skip!(); let chain = std::sync::Mutex::new(benilla_formats::open_chain(&data).expect("open chain")); diff --git a/crates/benilla-app/tests/talent_frame.rs b/crates/benilla-app/tests/talent_frame.rs index cc61664c..b48e2f5e 100644 --- a/crates/benilla-app/tests/talent_frame.rs +++ b/crates/benilla-app/tests/talent_frame.rs @@ -20,7 +20,7 @@ use benilla_ui::script::{ /// `ItemButtonTemplate.xml` is the `SetItemButton*` family the talent buttons grey through (the /// reference's own verb; see TalentFrame.xml's header) — it sits at .toc line 32, above every /// other entry here bar `Fonts.xml`. -const FILES: [&str; 12] = [ +const FILES: [&str; 13] = [ // `PLAYER_LEVEL` and the rest of the strings the stock file formats through. "Interface\\FrameXML\\GlobalStrings.lua", "Fonts.xml", @@ -36,7 +36,8 @@ const FILES: [&str; 12] = [ // Slider AND its ``. Nothing else in the tree declares it, and a missing template // is a loader WARNING, not an error, so without this the window still built — just with no // scrollbar and no wheel (decision 1833). `load_ui_strict` below is what makes that loud. - "UIPanelTemplates.xml", + r"Interface\FrameXML\UIPanelTemplates.lua", + r"Interface\FrameXML\UIPanelTemplates.xml", "ScrollTemplates.xml", // Stock `TalentFrame_OnShow` opens with `SetButtonPulse(TalentMicroButton, 0, 1)` and then // `UpdateMicroButtons()` — both live here, and a nil `TalentMicroButton` throws out of OnShow diff --git a/crates/benilla-ui/src/script/addon.rs b/crates/benilla-ui/src/script/addon.rs index 53a81d14..ec14afda 100644 --- a/crates/benilla-ui/src/script/addon.rs +++ b/crates/benilla-ui/src/script/addon.rs @@ -208,7 +208,28 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { lua.create_function(|lua, key: Value| { let model = lua.app_data_ref::().expect("model"); let Some(i) = resolve(&model, &key) else { - return Ok(MultiValue::new()); + // **The two argument forms miss differently** (decision 1845), and ours answered + // zero values for both. + // + // A NUMERIC index out of range **raises**, and the bounds check is *unsigned* — so + // index `0` raises too, not just a negative or an over-count. That is not an arity + // divergence at all, which is why a gate comparing counts alone would have pushed + // this toward a placeholder branch that does not exist for numbers. + if matches!(key, Value::Integer(_) | Value::Number(_)) { + return Err(mlua::Error::runtime("GetAddOnInfo: index out of range")); + } + // The STRING form is not existence-checked at all: it answers seven placeholders, + // two of which are not nil. Slot 4 is `enabled` in-game (the glue table's fourth is + // `url`, and its eighth is an appended `newVersion` — nothing shifts). + return Ok(MultiValue::from_vec(vec![ + lua_str(lua, "NoSuchAddon")?, + Value::Nil, + Value::Nil, + Value::Nil, + Value::Nil, + lua_str(lua, "MISSING")?, + lua_str(lua, "INSECURE")?, + ])); }; // The one arbiter (decision 1292): loaded short-circuit, then `AddOn_CanLoad` in the // in-game flavour — so NOT_DEMAND_LOADED and INTERFACE_VERSION are reachable here, diff --git a/crates/benilla-ui/src/script/channel.rs b/crates/benilla-ui/src/script/channel.rs index fac9c391..cf22a08a 100644 --- a/crates/benilla-ui/src/script/channel.rs +++ b/crates/benilla-ui/src/script/channel.rs @@ -98,9 +98,21 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { }; let Some(slot) = slot else { - // Not joined: the number 0, and nothing else. `channelName` is unread by every - // caller on this branch (all four sites bail on the number first). - return Ok(MultiValue::from_vec(vec![Value::Integer(0)])); + // **`0, nil, 0` — three values, not one.** This used to push the number alone, + // reasoning that `channelName` is unread by every caller on this branch. True of + // the callers; not true of the client. `0x4a05e0` pushes three on every path, and + // slot 2 is neither the empty string nor the argument echoed back: + // `0x4a0659 xor edx,edx` then `lua_pushstring(NULL)`, which tail-jumps to + // `lua_pushnil`. Decision 1845. + // + // "Not joined" is also wider than a bad index: the lookup answers NULL while the + // join-pending word is non-zero, so a channel already in the list but not yet + // CONFIRMED reads `0, nil, 0` identically — which is what `joined` models here. + return Ok(MultiValue::from_vec(vec![ + Value::Integer(0), + Value::Nil, + Value::Integer(0), + ])); }; let name = name_at(&model, slot).unwrap_or_default().to_string(); Ok(MultiValue::from_vec(vec![ diff --git a/crates/benilla-ui/src/script/client.rs b/crates/benilla-ui/src/script/client.rs index 5db11e34..af806090 100644 --- a/crates/benilla-ui/src/script/client.rs +++ b/crates/benilla-ui/src/script/client.rs @@ -35,13 +35,14 @@ use super::Model; const VERSION: &str = "1.12.1"; const BUILD: &str = "5875"; const BUILD_DATE: &str = "Sep 19 2006"; -/// The `## Interface` number a 1.12 `.toc` declares — and what `benilla.toc` declares too. -const TOC_VERSION: i64 = 11200; +// There was a `TOC_VERSION = 11200` here, and `GetBuildInfo` pushed it as a fourth value. The +// in-game registrar pushes three (decision 1842); the interface number is a later expansion's +// return, and `benilla.toc`'s own `## Interface` line is where that number belongs. pub(super) fn install(lua: &Lua) -> mlua::Result<()> { let g = lua.globals(); - // version, build, date, tocversion + // version, build, date — three, and no fourth (decision 1842) g.set( "GetBuildInfo", lua.create_function(|lua, ()| { @@ -49,7 +50,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { Value::String(lua.create_string(VERSION)?), Value::String(lua.create_string(BUILD)?), Value::String(lua.create_string(BUILD_DATE)?), - Value::Integer(TOC_VERSION), + // **THREE, not four.** The reference's in-game `GetBuildInfo 0x4884a0` pushes + // `(string, string, string)`; the interface/TOC number is a later-expansion return + // and 1.12 has no fourth value here. (Its GLUE twin `0x46cd70` pushes five — the + // same name with a different shape per registrar table, which is why 1842's gate + // keys on the table and not the name.) Decision 1842. ])) })?, )?; @@ -202,20 +207,32 @@ impl super::UiScript { mod tests { use crate::script::UiScript; - /// `GetBuildInfo` answers as the 1.12.1 client, in the reference's 4-value shape. + /// `GetBuildInfo` answers as the 1.12.1 client: **three strings, and no fourth value.** /// - /// The shape is what addons destructure (`local version, build, date, toc = GetBuildInfo()`), - /// and the `tocversion` is the one they compare against their own `## Interface` to decide - /// whether to run at all — so a wrong or missing fourth value makes an addon disable itself. + /// This used to assert four and explain the fourth as the `tocversion` an addon compares + /// against its own `## Interface` — which is a TBC-and-later idiom. The in-game registrar + /// `0x4884a0` pushes `(string, string, string)` and nothing else (decision 1842); the + /// interface number arrives in a later expansion. + /// + /// The corpus agrees, and it is worth the count: **458 sites** write + /// `local version = GetBuildInfo()` and take one value. **Four** destructure a fourth — and on + /// the real 1.12 client those four read `nil`, which is what their authors shipped against. + /// Handing them `11200` was benilla being more generous than the client, which is the superset + /// 1189 exists to stop. + /// + /// **The same name has a different shape in the glue registrar** (`0x46cd70`, five strings). + /// This VM is the in-game one. That pair is why 1842's gate keys on the table, not the name. #[test] fn get_build_info_answers_as_the_1_12_1_client() { let s = UiScript::new().unwrap(); assert_eq!( - s.eval::>( - "local v, b, d, t = GetBuildInfo() return { v, b, d, tostring(t) }" - ) - .unwrap(), - vec!["1.12.1", "5875", "Sep 19 2006", "11200"] + s.eval::>("return { GetBuildInfo() }").unwrap(), + vec!["1.12.1", "5875", "Sep 19 2006"] + ); + assert_eq!( + s.eval::("return select('#', GetBuildInfo())").unwrap(), + 3, + "three values, never a fourth" ); } diff --git a/crates/benilla-ui/src/script/modelframe.rs b/crates/benilla-ui/src/script/modelframe.rs index 634a2d99..371ded5e 100644 --- a/crates/benilla-ui/src/script/modelframe.rs +++ b/crates/benilla-ui/src/script/modelframe.rs @@ -309,23 +309,28 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { )?; m.set( "SetFogColor", - lua.create_function(|lua, (this, r, g, b): (Table, Value, Value, Value)| { - let c = (num(&r), num(&g), num(&b)); - with_model(lua, &this, |m| m.fog_color = Some(c)) - })?, + lua.create_function( + |lua, (this, r, g, b, a): (Table, Value, Value, Value, Option)| { + // FIVE arguments, and the fifth is the alpha — the same numeric path and the same + // `[0,1]` clamp as the components, not a flag. It is **guarded with a default of + // `1.0`** where r/g/b are read unconditionally and yield `0.0` when absent, so + // `SetFogColor(r, g, b)` sets alpha 1.0. Getting that backwards renders the fog + // invisible. Decision 1845. + let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u32; + let a = a.as_ref().map_or(1.0, num); + let packed = (q(a) << 24) | (q(num(&r)) << 16) | (q(num(&g)) << 8) | q(num(&b)); + with_model(lua, &this, |m| m.fog_color = packed) + }, + )?, )?; m.set( "GetFogColor", lua.create_function(|lua, this: Table| { - let c = with_model(lua, &this, |m| m.fog_color)?; - Ok(match c { - Some((r, g, b)) => MultiValue::from_vec(vec![ - Value::Number(f64::from(r)), - Value::Number(f64::from(g)), - Value::Number(f64::from(b)), - ]), - None => MultiValue::new(), - }) + // FOUR values, always — the packed dword unpacked. Never set reads `1, 1, 1, 1`, + // because the `CSimpleModel` ctor's terminal write is `0xffffffff` (decision 1845). + let packed = with_model(lua, &this, |m| m.fog_color)?; + let ch = |shift: u32| f64::from((packed >> shift) & 0xff) / 255.0; + Ok((ch(16), ch(8), ch(0), ch(24))) })?, )?; diff --git a/crates/benilla-ui/src/script/party.rs b/crates/benilla-ui/src/script/party.rs index 181e657f..5bfa5ca8 100644 --- a/crates/benilla-ui/src/script/party.rs +++ b/crates/benilla-ui/src/script/party.rs @@ -456,8 +456,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { "GetLootMethod", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model app_data"); + // **`freeforall`, not `group`.** The three cells this reads sit past `.data`'s raw end + // in the zero-filled tail, so a client that has never been in a group answers the + // zeroth method — which is `freeforall`. Decision 1840. let method = if model.party.loot_method.is_empty() { - "group" + "freeforall" } else { model.party.loot_method.as_str() }; @@ -846,7 +849,10 @@ mod tests { let (method, master) = s .eval::<(String, Option)>("return GetLootMethod()") .unwrap(); - assert_eq!(method, "group"); + // **`freeforall` is the UNGROUPED answer**, not `group`: the cells the reference reads + // sit in `.data`'s zero-filled tail, so a client that has never been in a group reports + // the zeroth method (decision 1840). The `group` above is a real party's, and stays. + assert_eq!(method, "freeforall"); assert_eq!(master, None); } diff --git a/crates/benilla-ui/src/script/quest.rs b/crates/benilla-ui/src/script/quest.rs index 9ec9c300..d0018a7e 100644 --- a/crates/benilla-ui/src/script/quest.rs +++ b/crates/benilla-ui/src/script/quest.rs @@ -318,7 +318,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { // GetRewardSpell() → nil (spell-reward rows out of scope in v1, decision 0088). g.set( "GetRewardSpell", - lua.create_function(|_, ()| Ok(Value::Nil))?, + // 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)))?, )?; // ── Intents ─────────────────────────────────────────────────────────────────────────────────── diff --git a/crates/benilla-ui/src/script/quest_log.rs b/crates/benilla-ui/src/script/quest_log.rs index cff38c79..fdf22fb9 100644 --- a/crates/benilla-ui/src/script/quest_log.rs +++ b/crates/benilla-ui/src/script/quest_log.rs @@ -652,7 +652,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { // ── v1 stubs (each the seam of a named later slice — see the module doc) ───────────────────── g.set( "GetQuestLogRewardSpell", - lua.create_function(|_, ()| Ok(Value::Nil))?, + // 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)))?, )?; g.set( "IsUnitOnQuest", diff --git a/crates/benilla-ui/src/script/region/paint.rs b/crates/benilla-ui/src/script/region/paint.rs index 201a9be0..5f4cf6bb 100644 --- a/crates/benilla-ui/src/script/region/paint.rs +++ b/crates/benilla-ui/src/script/region/paint.rs @@ -503,13 +503,39 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> { lua.create_function(|lua, this: Table| { let rh = region_handle_of(lua, &this)?; let model = lua.app_data_ref::().expect("model"); - let [l, r, t, b] = model + // EIGHT values — a 4-iteration loop of 2 pushes, `mov eax,8`. There is no 4-value + // getter in this API at all: the 4-argument `(minX, maxX, minY, maxY)` min/max rect is + // **setter-only**, and returning it here was a shape that exists nowhere in the client. + // Order is `SetTexCoord`'s own usage string (`0x87c538`): + // `ULx, ULy, LLx, LLy, URx, URy, LRx, LRy`. Decision 1840. + // + // `GetTexCoord` has zero call sites in 1.12 FrameXML, so nothing on the chain could + // ever have caught this — only the binary could. + let corners = model .region_data .get(&rh) .and_then(|d| d.tex_coords) - .map(|tc| tc.edges()) - .unwrap_or([0.0, 1.0, 0.0, 1.0]); - Ok((l, r, t, b)) + .map_or([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]], |tc| { + match tc { + // Stored per corner in SCREEN order `[TL, TR, BR, BL]`; Lua wants + // UL, LL, UR, LR. + crate::script::types::TexCoords::Corners(c) => [c[0], c[3], c[1], c[2]], + crate::script::types::TexCoords::Rect(_) => { + let [l, r, t, b] = tc.edges(); + [[l, t], [l, b], [r, t], [r, b]] + } + } + }); + Ok(( + corners[0][0], + corners[0][1], + corners[1][0], + corners[1][1], + corners[2][0], + corners[2][1], + corners[3][0], + corners[3][1], + )) })?, )?; Ok(()) diff --git a/crates/benilla-ui/src/script/reputation.rs b/crates/benilla-ui/src/script/reputation.rs index 5e9d9e56..acfd01cc 100644 --- a/crates/benilla-ui/src/script/reputation.rs +++ b/crates/benilla-ui/src/script/reputation.rs @@ -532,7 +532,21 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { .find(|e| e.rep_list_id == slot) }); let Some(e) = watched else { - return Ok(MultiValue::from_vec(vec![Value::Nil])); + // FIVE values on the nothing-watched branch too — `0x4d6820` has four guards onto + // one label and no raise, so the shape is `nil, 0, 0, 0, 0` (decision 1845). + // + // Worth carrying with it: the `[0, 64)` bound is **not in the binding**. + // `0x4d5620` maps an out-of-range index to `0`, so the "nothing watched" sentinel + // passes the `< 0` and `> max` guards and dies at the NULL-DBC-row guard instead. + // A port that range-checks inside the binding implements a branch the reference + // does not have. + return Ok(MultiValue::from_vec(vec![ + Value::Nil, + Value::Integer(0), + Value::Integer(0), + Value::Integer(0), + Value::Integer(0), + ])); }; Ok(MultiValue::from_vec(vec![ Value::String(lua.create_string(&e.name)?), diff --git a/crates/benilla-ui/src/script/tests/modelframe.rs b/crates/benilla-ui/src/script/tests/modelframe.rs index eb8dbd92..951b39d5 100644 --- a/crates/benilla-ui/src/script/tests/modelframe.rs +++ b/crates/benilla-ui/src/script/tests/modelframe.rs @@ -410,14 +410,41 @@ fn the_light_tuple_is_opaque_and_survives_the_round_trip() { (1.0, 0.7_f32 as f64) ); - // Fog is the same shape, and unset fog returns NOTHING rather than three zeros — a pane with no - // fog and a pane fogged to black are different states. + // **Fog is NOT the same shape, and this used to assert the opposite.** It read "unset fog + // returns NOTHING rather than three zeros — a pane with no fog and a pane fogged to black are + // different states", which is a good argument about a model the client does not have: there is + // no unset state. The fog colour is one packed `0xAARRGGBB` dword whose ctor writes + // `0xffffffff`, so a fresh pane reads **four** values, `1, 1, 1, 1` (decision 1845). assert_eq!( s.eval::("return table.getn({ MLight:GetFogColor() })") .unwrap(), - 0 + 4 ); + assert_eq!( + s.eval::<(f64, f64, f64, f64)>("return MLight:GetFogColor()") + .unwrap(), + (1.0, 1.0, 1.0, 1.0), + "never set is white and opaque, not four zeros" + ); + + // Three arguments set alpha to **1.0**, not 0 — the fifth is guarded with that default where + // r/g/b are read unconditionally. The round trip is LOSSY by 8 bits a channel, because the + // store is that packed dword: 0.1 does not survive, 0.2 does. s.run("MLight:SetFogColor(0.1, 0.2, 0.3)").unwrap(); - let (r, g, b): (f64, f64, f64) = s.eval("return MLight:GetFogColor()").unwrap(); - assert_eq!((r as f32, g as f32, b as f32), (0.1, 0.2, 0.3)); + let (r, g, b, a): (f64, f64, f64, f64) = s.eval("return MLight:GetFogColor()").unwrap(); + assert_eq!(a, 1.0, "the omitted alpha defaults to 1.0"); + for (got, want) in [(r, 0.1), (g, 0.2), (b, 0.3)] { + assert!( + (got - want).abs() <= 1.0 / 255.0, + "within one 8-bit step of {want}, got {got}" + ); + } + + // …and the alpha really is the fifth argument, on the same clamp as the rest. + s.run("MLight:SetFogColor(1, 1, 1, 0)").unwrap(); + assert_eq!( + s.eval::("local _, _, _, a = MLight:GetFogColor() return a") + .unwrap(), + 0.0 + ); } diff --git a/crates/benilla-ui/src/script/tests/texcoord_font.rs b/crates/benilla-ui/src/script/tests/texcoord_font.rs index 627d7fb1..15ad60c7 100644 --- a/crates/benilla-ui/src/script/tests/texcoord_font.rs +++ b/crates/benilla-ui/src/script/tests/texcoord_font.rs @@ -32,8 +32,11 @@ fn set_tex_coord_changes_extracted_uv() { tex.content ); - let (l, r, t, b): (f32, f32, f32, f32) = s.eval("return t:GetTexCoord()").unwrap(); - assert_eq!((l, r, t, b), (0.1, 0.6, 0.2, 0.8)); + // EIGHT values, per corner, in `SetTexCoord`'s own usage order — `ULx, ULy, LLx, LLy, URx, + // URy, LRx, LRy` (decision 1840). A Rect stored as `[l, r, t, b]` comes back out as its four + // corners, which is the only shape the reference has: there is no 4-value getter. + let got: (f32, f32, f32, f32, f32, f32, f32, f32) = s.eval("return t:GetTexCoord()").unwrap(); + assert_eq!(got, (0.1, 0.2, 0.1, 0.8, 0.6, 0.2, 0.6, 0.8)); // The 8-arg affine form (arg order UL, LL, UR, LR) lands as per-corner UVs in screen order // [TL, TR, BR, BL] — here a 90° rotation of the full texture. diff --git a/crates/benilla-ui/src/script/types.rs b/crates/benilla-ui/src/script/types.rs index a2b0ee8f..9824d861 100644 --- a/crates/benilla-ui/src/script/types.rs +++ b/crates/benilla-ui/src/script/types.rs @@ -47,8 +47,12 @@ pub enum TexCoords { } impl TexCoords { - /// The tightest axis-aligned `[left, right, top, bottom]` containing the mapping — what the - /// 4-value `GetTexCoord()` reports (exact for `Rect`, the bounding box for `Corners`). + /// The tightest axis-aligned `[left, right, top, bottom]` containing the mapping (exact for + /// `Rect`, the bounding box for `Corners`). + /// + /// **Not what `GetTexCoord()` reports** — that answers eight per-corner values, and the 4-value + /// rect it used to return is a shape the reference has nowhere (decision 1840). This is the + /// renderer's and the app's convenience view, and it keeps its callers. pub fn edges(&self) -> [f32; 4] { match *self { TexCoords::Rect(e) => e, diff --git a/crates/benilla-ui/src/script/unit/bindings.rs b/crates/benilla-ui/src/script/unit/bindings.rs index 6f2fb409..87a87d22 100644 --- a/crates/benilla-ui/src/script/unit/bindings.rs +++ b/crates/benilla-ui/src/script/unit/bindings.rs @@ -7,7 +7,7 @@ use mlua::{Lua, Value}; use super::super::Model; use super::{ check_unit_token, classification_word, grey_band, level_reads_unknown, pick_unit_token, - power_token, with_unit, SelectionRequest, + with_unit, SelectionRequest, }; /// The two class ids `GetComboPoints 0x51a190` accepts — the literals `4` and `0xb` it compares @@ -160,11 +160,23 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> { token, r#"Usage: UnitName("unit")"#, )?); + // TWO values on every live path (`eax = 2` at `0x5170ae` and `0x517289`): the name and + // the **realm**. Slot 2 is nil for a same-realm player, and *structurally* nil for a + // pet, creature, game object or item — `0x609210` writes the out-parameter only on its + // PLAYER branch. benilla is single-realm, so the second is always nil here; the day a + // cross-realm name arrives it is `UnitState`'s to carry, and `UnitPopup.lua:106`'s + // `name.."-"..server` join is what will read it. Decision 1840. + // + // Not to be confused with `showServerName`: that is the parameter of FrameXML's own + // `GetUnitName(unit, showServerName)` wrapper, which calls this binding with ONE + // argument. The engine's real second argument is a strict `LUA_TBOOLEAN` and is not + // modelled — no consumer passes it. let name = with_unit(lua, &token, None, |u| u.name.clone())?; - match name { - Some(n) => Ok(Value::String(lua.create_string(&n)?)), - None => Ok(Value::Nil), - } + let name = match name { + Some(n) => Value::String(lua.create_string(&n)?), + None => Value::Nil, + }; + Ok((name, Value::Nil)) })?, )?; @@ -931,8 +943,15 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> { token, r#"Usage: UnitPowerType("unit")"#, )?); + // ONE value, not the Era pair. `0x517940` has four live `ret`s and `eax = 1` at every + // one, four `lua_pushnumber` sites and zero `lua_pushstring` — the `(type, "MANA")` + // tuple does not exist in 5875 (decision 1840). `power_token` stays: 1819's per-resource + // event names are its real caller. + // + // The miss default is the NUMBER 0, the same value as Mana and never nil, which is + // load-bearing rather than tidy: `UnitFrame.lua:122` indexes `ManaBarColor[...]` with it. let ty = with_unit(lua, &token, 0u8, |u| u.power_type)?; - Ok((i64::from(ty), power_token(ty).to_string())) + Ok(i64::from(ty)) })?, )?; diff --git a/crates/benilla-ui/src/script/unit/tests.rs b/crates/benilla-ui/src/script/unit/tests.rs index 36a4d33e..5fcb948f 100644 --- a/crates/benilla-ui/src/script/unit/tests.rs +++ b/crates/benilla-ui/src/script/unit/tests.rs @@ -400,10 +400,11 @@ fn a_dead_unit_reports_dead_and_zero_health() { fn power_bindings_read_the_active_type() { let mut s = UiScript::new().unwrap(); s.set_unit("player", Some(player())); // rage 35/100 + // ONE value. The Era `(type, "RAGE")` pair does not exist in 5875 — `0x517940` pushes a + // number at every one of its four live `ret`s and never a string (decision 1840). assert_eq!( - s.eval::<(i64, String)>(r#"return UnitPowerType("player")"#) - .unwrap(), - (1, "RAGE".into()) + s.eval::(r#"return UnitPowerType("player")"#).unwrap(), + 1 ); assert_eq!(s.eval::(r#"return UnitMana("player")"#).unwrap(), 35); assert_eq!( @@ -420,11 +421,12 @@ fn power_bindings_read_the_active_type() { .unwrap(), "the Era spellings must not linger beside the 1.12 ones" ); - // An absent unit: type reads as mana, values 0. + // An absent unit: the NUMBER 0 — the same value Mana has, and never nil. Load-bearing rather + // than tidy: stock `UnitFrame.lua:122` writes `ManaBarColor[UnitPowerType(unitFrame.unit)]`, + // and a nil there indexes nothing. assert_eq!( - s.eval::<(i64, String)>(r#"return UnitPowerType("target")"#) - .unwrap(), - (0, "MANA".into()) + s.eval::(r#"return UnitPowerType("target")"#).unwrap(), + 0 ); } diff --git a/crates/benilla-ui/src/script/worldmap.rs b/crates/benilla-ui/src/script/worldmap.rs index 96bd1c44..82fb76b0 100644 --- a/crates/benilla-ui/src/script/worldmap.rs +++ b/crates/benilla-ui/src/script/worldmap.rs @@ -466,10 +466,16 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { .unwrap_or_else(|| cont.map_file.clone()), }), }; - Ok(match file { + // THREE values, always. `0x4a7e30` has one `ret` and not a single conditional jump — + // there is no miss branch at all, the emptiness lives in two helpers — so the + // world-level answer is `nil, 0, 0` rather than one nil. (`arity_conf = exact`; + // decision 1845.) The two zeros are the map's texture dimensions, which no caller + // reads on the world level. + let file = match file { Some(f) => Value::String(lua.create_string(&f)?), None => Value::Nil, - }) + }; + Ok((file, 0i64, 0i64)) })?, )?; diff --git a/crates/benilla-ui/src/widget/kinds/mod.rs b/crates/benilla-ui/src/widget/kinds/mod.rs index 942d30e3..b0db7fc8 100644 --- a/crates/benilla-ui/src/widget/kinds/mod.rs +++ b/crates/benilla-ui/src/widget/kinds/mod.rs @@ -410,10 +410,18 @@ pub struct ModelState { /// no lighting model, and inventing a typed one would be asserting a scene semantics we have /// not verified. pub light: Option>, - /// `SetFogColor(r, g, b)` — `None` until set. The client's fog surface is seven verbs, not two: - /// this pair plus `SetFogNear`/`GetFogNear`/`SetFogFar`/`GetFogFar`/`ClearFog`, which the - /// struct doc names as unbuilt. - pub fog_color: Option<(f32, f32, f32)>, + /// `SetFogColor(r, g, b, a)` as the reference stores it: **one packed `0xAARRGGBB` dword**, + /// which is why its getter is four values wide and why a Set→Get round trip is **lossy** — + /// eight bits per channel (decision 1845). + /// + /// `0xffff_ffff` is the ctor's own terminal write, so the never-set answer is `1, 1, 1, 1` and + /// not four zeros. It used to be `Option<(f32, f32, f32)>` here: three components, no alpha, + /// and `None` for unset — every one of those three wrong. + /// + /// The client's fog surface is seven verbs, not two: this pair plus + /// `SetFogNear`/`GetFogNear`/`SetFogFar`/`GetFogFar`/`ClearFog`, which the struct doc names as + /// unbuilt. + pub fog_color: u32, } /// A fresh `Model` pane: no content, sequence 0, unrotated, unit scale, camera 0, at the origin. @@ -433,7 +441,7 @@ impl Default for ModelState { camera: 0, position: (0.0, 0.0, 0.0), light: None, - fog_color: None, + fog_color: 0xffff_ffff, } } }