From b9a58c9806c877d89532c58c3c2df9b469a1ab76 Mon Sep 17 00:00:00 2001 From: grom Date: Sat, 31 Oct 2026 13:38:49 -0600 Subject: [PATCH] Seven more: the realm rewired what these windows actually do MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **Buff frame**: 32 helpful slots and 16 harmful (`BuffFrame.xml`), not 16 of each, so the first debuff is `BuffButton32`. `BuffButton16` is a helpful slot here and stays down with one buff up. - **Pet frame**: its `PetFrameDebuffN` row really is debuffs now — `RefreshBuffs(this, 0, "pet")` (`PetFrame.lua:38`, `:57`) picks `UnitDebuff`, where stock passed `1` and showed the pet's buffs under those names. The fixture seeds harmful auras. - **Pet paper doll**: the diet icon is the realm's own `PetStable-DietIcon` at 32x32, and it was moved out of the `` into a `frameLevel="5"` frame of its own (`PetPaperDollFrame.xml:245-258`), so it draws over the scene by level rather than by the model's ARTWORK callback draining last — the ordering this test used to pin. - **Merchant**: both mouse buttons share one shift arm now (`MerchantFrame.lua:304-338` and `:345-375` are the same body), and it opens the stack splitter for a bundle row too, stepping by the bundle size. The fork inside is only where `maxStack` comes from: `GetItemInfo` off the link for a bundle (so the row needs its template cached and a link to parse the id out of), `GetMerchantItemMaxStack` for a single. - **Trainer**: after a purchase the realm re-selects `ClassTrainerFrame.selectedService` (`Blizzard_TrainerUI.lua:75-91`) instead of leaving the pane blank, so the pane and the highlight follow the row that took the learned one's seat, and the engine's selection index comes back off the hidden tail. - **Game menu**: a `"full"` frame is exempt from both open gates (`UIParent.lua:927`, `:951` each spell out `frameArea ~= "full"`) and its arm runs `CloseAllWindows()` before taking the slot (`:962-966`), so asking for the map over the game menu closes the menu and opens the map. - A third prereq row, and a latent raise named: `WorldMapFrame.xml` needs `Overrides.lua`'s `CONTINENTS_LENGTH = 2` (toc row 13 against 85), because the continent and zone dropdown text helpers compare against it unguarded (`WorldMapFrame.lua:440`, `:457`). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- .../benilla-app/src/ui_script/buff_tests.rs | 17 +++--- .../src/ui_script/game_menu_tests.rs | 19 +++++-- .../src/ui_script/merchant_tests.rs | 57 +++++++++++++++---- .../src/ui_script/pet_frame_tests.rs | 21 +++++-- .../src/ui_script/pet_paperdoll_tests.rs | 23 ++++---- crates/benilla-app/src/ui_script/test_ui.rs | 5 ++ .../src/ui_script/trainer_tests.rs | 34 +++++++---- 7 files changed, 126 insertions(+), 50 deletions(-) diff --git a/crates/benilla-app/src/ui_script/buff_tests.rs b/crates/benilla-app/src/ui_script/buff_tests.rs index 3b99c6da..1b5f3a6b 100644 --- a/crates/benilla-app/src/ui_script/buff_tests.rs +++ b/crates/benilla-app/src/ui_script/buff_tests.rs @@ -135,8 +135,11 @@ fn buffs_and_debuffs_fill_their_own_rows_with_counts_and_the_dispel_tint() { assert!(shown(&s, "BuffButton0"), "first buff -> BuffButton0"); assert!(!shown(&s, "BuffButton1"), "no second buff"); - assert!(shown(&s, "BuffButton16"), "first debuff -> BuffButton16"); - assert!(!shown(&s, "BuffButton17"), "no second debuff"); + // The realm's `BuffFrame.xml` gives the player **32** helpful slots (0-31) and 16 harmful + // (32-47); stock 1.12 had 16 of each, so a debuff landed on `BuffButton16`. That button is a + // helpful slot here, and with one buff up it stays hidden. + assert!(shown(&s, "BuffButton32"), "first debuff -> BuffButton32"); + assert!(!shown(&s, "BuffButton33"), "no second debuff"); assert!( tex_quad(&quads, "Spell_Nature_Regeneration").is_some(), @@ -172,7 +175,7 @@ fn buffs_and_debuffs_fill_their_own_rows_with_counts_and_the_dispel_tint() { } // The count shows only above 1 (`BuffFrame.lua:97`). - assert_eq!(text(&s, "BuffButton16Count"), "3"); + assert_eq!(text(&s, "BuffButton32Count"), "3"); assert_eq!(text(&s, "BuffButton0Count"), ""); assert!(s.errors().is_empty(), "script errors: {:?}", s.errors()); } @@ -524,8 +527,8 @@ fn the_buttons_and_the_corpus_walk_agree_on_the_cache_positions() { ("BuffButton0", 0), // helpful ordinal 0 -> position 0 ("BuffButton1", 2), // helpful ordinal 1 -> position 2 (the debuff at 1 is skipped) ("BuffButton2", 4), // helpful ordinal 2 -> position 4 - ("BuffButton16", 1), // harmful ordinal 0 -> position 1 - ("BuffButton17", 3), // harmful ordinal 1 -> position 3 + ("BuffButton32", 1), // harmful ordinal 0 -> position 1 + ("BuffButton33", 3), // harmful ordinal 1 -> position 3 ] { assert!(shown(&s, button), "{button} draws its aura"); assert_eq!( @@ -543,8 +546,8 @@ fn the_buttons_and_the_corpus_walk_agree_on_the_cache_positions() { "a miss is -1, the sentinel the corpus terminates on — never nil" ); - assert_eq!(text(&s, "BuffButton16Count"), "3"); - assert_eq!(text(&s, "BuffButton17Count"), "5"); + assert_eq!(text(&s, "BuffButton32Count"), "3"); + assert_eq!(text(&s, "BuffButton33Count"), "5"); assert_eq!( text(&s, "BuffButton0Count"), "", 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 f70cabcb..c267e09b 100644 --- a/crates/benilla-app/src/ui_script/game_menu_tests.rs +++ b/crates/benilla-app/src/ui_script/game_menu_tests.rs @@ -587,19 +587,28 @@ fn the_world_map_cannot_open_behind_the_menu_and_gives_its_slot_back() { "the map vacated the full-screen slot" ); - // The M binding and the micro button both call `ToggleWorldMap`. + // The M binding and the micro button both call `ToggleWorldMap`. Asked for while the game + // menu is up, the map does not queue behind it and does not refuse: a `"full"` frame is + // exempt from the `CanOpenPanels` gate and from the centre-frame gate (`UIParent.lua:927`, + // `:951`, both of which spell out `frameArea ~= "full"`), and its own arm runs + // `CloseAllWindows()` before taking the slot (`:962-966`). So the menu goes and the map comes. s.run("ToggleGameMenu(1)").unwrap(); assert!(s.eval::("return GameMenuFrame:IsVisible()").unwrap()); s.run("ToggleWorldMap()").unwrap(); assert!( - !s.eval::("return WorldMapFrame:IsVisible()").unwrap(), - "the map must not open behind the game menu" + s.eval::("return WorldMapFrame:IsVisible()").unwrap(), + "a full-screen frame opens over the game menu" + ); + assert!( + !s.eval::("return GameMenuFrame:IsVisible()").unwrap(), + "…by closing it, not by stacking over it" ); assert!(s - .eval::("return GetFullScreenFrame() == nil") + .eval::("return GetFullScreenFrame():GetName() == \"WorldMapFrame\"") .unwrap()); - s.run("ToggleGameMenu(1)").unwrap(); + s.run("ToggleWorldMap()").unwrap(); + assert!(!s.eval::("return WorldMapFrame:IsVisible()").unwrap()); s.run("ToggleWorldMap()").unwrap(); assert!(s.eval::("return WorldMapFrame:IsVisible()").unwrap()); assert!(s.errors().is_empty(), "script errors: {:?}", s.errors()); diff --git a/crates/benilla-app/src/ui_script/merchant_tests.rs b/crates/benilla-app/src/ui_script/merchant_tests.rs index a6424183..7b7d0824 100644 --- a/crates/benilla-app/src/ui_script/merchant_tests.rs +++ b/crates/benilla-app/src/ui_script/merchant_tests.rs @@ -1,8 +1,8 @@ //! The stock merchant window (`MerchantFrame.xml`), engine-only, fed a synthetic stock and purse. use benilla_ui::script::{ - ContainerState, CursorPayload, DressUpIntent, ExtractedQuad, ItemStatsHead, MerchantItem, - MerchantState, QuadContent, ScriptValue, SoundRequest, UiScript, + ContainerState, DressUpIntent, ExtractedQuad, ItemStatsHead, MerchantItem, MerchantState, + QuadContent, ScriptValue, SoundRequest, UiScript, }; use super::test_ui::{bag_open, load_ui as load_xml, BAG_UI}; @@ -1093,8 +1093,13 @@ fn ctrl_and_shift_on_a_vendor_row_preview_and_post_without_buying() { assert!(s.errors().is_empty(), "script errors: {:?}", s.errors()); } +/// Shift on a vendor row opens the stack splitter either way, and on either mouse button: the +/// realm gave both buttons the same shift arm (`MerchantFrame.lua:304-338` and `:345-375` are the +/// same body), and the only fork inside it is where `maxStack` comes from — `GetItemInfo` off the +/// link for a bundle row, `GetMerchantItemMaxStack` for a single. Stock 1.12 picked one bundle up +/// on the left and bought one on the right. #[test] -fn shift_click_takes_one_bundle_and_splits_a_single() { +fn shift_click_splits_a_bundle_and_a_single_alike() { benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); s.set_screen_size(1024.0, 768.0); @@ -1121,6 +1126,10 @@ fn shift_click_takes_one_bundle_and_splits_a_single() { num_available: -1, item_id: 159, max_stack: Some(20), + // The realm's shift-click arm parses the id back out of the link + // (`MerchantFrame.lua:315`), so a bundle row without one reaches + // `GetItemInfo(0)` and compares a nil `maxStack`. + link: Some("|cffffffff|Hitem:159:0:0:0|h[Refreshing Spring Water]|h|r".into()), ..Default::default() }, MerchantItem { @@ -1147,23 +1156,47 @@ fn shift_click_takes_one_bundle_and_splits_a_single() { }; let split_shown = |s: &UiScript| s.eval::("return StackSplitFrame:IsShown()").unwrap(); + // The realm rewrote this arm (`MerchantFrame.lua:304-338`). A **bundle** row no longer picks + // one bundle up: it parses the item id out of `GetMerchantItemLink`, reads `GetItemInfo`'s + // `maxStack` — the item cache, not `GetMerchantItemMaxStack` — and opens the stack splitter + // stepping by the bundle size. So a bundle row needs its template cached, which the live + // client has by the time a merchant window is up. + s.set_item_template( + 159, + benilla_ui::script::ItemTemplateView { + name: "Refreshing Spring Water".into(), + stackable: 20, + ..Default::default() + }, + ); shift_click(&mut s, 1, "LeftButton"); - assert!(!split_shown(&s), "a bundle row opens no split"); assert!( - matches!(s.cursor_payload(), Some(CursorPayload::Merchant(m)) if m.item_id == 159), - "the left arm picks one bundle up: {:?}", + split_shown(&s), + "a bundle row opens the splitter: errors {:?}", + s.errors() + ); + assert!( + s.cursor_payload().is_none(), + "…and picks nothing up until the splitter is answered: {:?}", s.cursor_payload() ); - s.run("ClearCursor()").unwrap(); + s.run("StackSplitFrame:Hide()").unwrap(); + // Both mouse buttons take the same shift arm now (`:304` and `:345` are the same body), so + // the right button on a bundle row splits rather than buying one bundle. shift_click(&mut s, 1, "RightButton"); - assert!(!split_shown(&s), "a bundle row opens no split"); - assert_eq!( - s.take_merchant_buys(), - vec![(1, 1)], - "the right arm buys one bundle" + assert!( + split_shown(&s), + "the right button shares the left's shift arm" ); + assert!( + s.take_merchant_buys().is_empty(), + "…so it buys nothing on its own" + ); + s.run("StackSplitFrame:Hide()").unwrap(); + // A single row (`count == 1`) takes the other fork and reads `GetMerchantItemMaxStack`, so it + // needs no cached template. shift_click(&mut s, 2, "RightButton"); assert!(split_shown(&s), "a single row opens the split"); assert_eq!( diff --git a/crates/benilla-app/src/ui_script/pet_frame_tests.rs b/crates/benilla-app/src/ui_script/pet_frame_tests.rs index 7641df5c..675c424b 100644 --- a/crates/benilla-app/src/ui_script/pet_frame_tests.rs +++ b/crates/benilla-app/src/ui_script/pet_frame_tests.rs @@ -88,6 +88,15 @@ fn draws(s: &mut UiScript, path: &str) -> bool { } /// One helpful aura: the pet frame's row shows buffs (PetFrame.lua:37,56). +/// A harmful aura on the pet, which is what its row reads (see +/// [`the_debuff_row_fills_from_the_pets_own_auras`]). +fn pet_debuff(spell_id: u32, name: &str, count: u8) -> AuraState { + AuraState { + helpful: false, + ..pet_buff(spell_id, name, count) + } +} + fn pet_buff(spell_id: u32, name: &str, count: u8) -> AuraState { AuraState { spell_id, @@ -251,9 +260,10 @@ fn the_attack_overlay_follows_its_own_two_events() { .unwrap()); } -/// The row shows buffs despite its `PetFrameDebuffN` names: `RefreshBuffs(this, 1, "pet")` selects -/// `UnitBuff` (PetFrame.lua:54-57, BuffFrame.lua:277). Its `PartyBuffButtonTemplate` has an icon -/// and a border and no count (PartyFrameTemplates.xml:3-36). +/// The row shows **debuffs**, as its `PetFrameDebuffN` names say: the realm passes `0` for +/// `showBuffs` (`PetFrame.lua:38`, `:57`), which picks `UnitDebuff` (`BuffFrame.lua:282`), where +/// stock 1.12 passed `1` and showed the pet's buffs under those names. Its +/// `PartyBuffButtonTemplate` has an icon and a border and no count. #[test] fn the_debuff_row_fills_from_the_pets_own_auras() { benilla_formats::wow_data_or_skip!(); @@ -263,7 +273,10 @@ fn the_debuff_row_fills_from_the_pets_own_auras() { s.set_auras( "pet", - Some(vec![pet_buff(1000, "Rend", 1), pet_buff(1001, "Sunder", 3)]), + Some(vec![ + pet_debuff(1000, "Rend", 1), + pet_debuff(1001, "Sunder", 3), + ]), ); s.fire_event("UNIT_AURA", vec![ScriptValue::Str("pet".into())]); diff --git a/crates/benilla-app/src/ui_script/pet_paperdoll_tests.rs b/crates/benilla-app/src/ui_script/pet_paperdoll_tests.rs index bbf24521..e8258c3c 100644 --- a/crates/benilla-app/src/ui_script/pet_paperdoll_tests.rs +++ b/crates/benilla-app/src/ui_script/pet_paperdoll_tests.rs @@ -363,14 +363,16 @@ fn hovering_the_diet_icon_lists_what_the_pet_eats() { let icon = quads .iter() .find(|q| { + // The realm's own art: `PetStable-DietIcon`, where stock 1.12 cropped the diet + // corner out of the shared `UI-PetHappiness` sheet (`PetPaperDollFrame.xml:258`). matches!(&q.content, QuadContent::Texture { path: Some(p), .. } - if p.contains("UI-PetHappiness")) + if p.contains("PetStable-DietIcon")) }) - .expect("the diet icon's UI-PetHappiness quad is in the render list"); + .expect("the diet icon's PetStable-DietIcon quad is in the render list"); let rect = icon.rect.expect("…with a resolved rect"); assert!( - (rect.right - rect.left - 24.0).abs() < 0.5 && (rect.top - rect.bottom - 23.0).abs() < 0.5, - "24x23 (stock `PetPaperDollFrame.xml:245-248`), got {}x{}", + (rect.right - rect.left - 32.0).abs() < 0.5 && (rect.top - rect.bottom - 32.0).abs() < 0.5, + "32x32, `setAllPoints` over a 32x32 `PetPaperDollPetInfo` (`:245-248`), got {}x{}", rect.right - rect.left, rect.top - rect.bottom ); @@ -389,13 +391,14 @@ fn hovering_the_diet_icon_lists_what_the_pet_eats() { && pane_rect.top >= rect.top, "the icon sits wholly inside the pane, which is what makes the z-order matter" ); - // The pane's scene draws after the icon, as in the reference: the two share a (strata, level) - // bucket, the icon is BACKGROUND art, and a model drains from the ARTWORK batch last - // (`0x76d160` registers its callback for layer 2 only; `0x76fb00` drains quads, then text, - // then callbacks). The scene is transparent wherever the pet is not, so the icon shows. + // The icon draws **over** the scene, and by frame level rather than by batch order: the realm + // moved `PetPaperDollPetInfo` out of the `` and made it its own frame with + // `frameLevel="5"` (`PetPaperDollFrame.xml:245`), declared after ``. Stock 1.12 + // had it as BACKGROUND art inside the pane, where it came out of the same bucket and the + // model's ARTWORK callback drained last — which is the ordering this used to pin. assert!( - pane.z > icon.z, - "the pane's scene draws out of the ARTWORK batch, after a sibling's BACKGROUND art \ + icon.z > pane.z, + "the icon's own frame level puts it above the scene \ (icon z={:#x}, pane z={:#x})", icon.z, pane.z diff --git a/crates/benilla-app/src/ui_script/test_ui.rs b/crates/benilla-app/src/ui_script/test_ui.rs index 72d61bc8..a20e1741 100644 --- a/crates/benilla-app/src/ui_script/test_ui.rs +++ b/crates/benilla-app/src/ui_script/test_ui.rs @@ -121,6 +121,11 @@ const PREREQS: &[(&str, &[&str])] = &[ "merchantframe.xml", &[r"Interface\FrameXML\ItemButtonTemplate.xml"], ), + // `CONTINENTS_LENGTH = 2`, the realm's two-continent map (`Overrides.lua:1`, toc row 13 — 72 + // rows above this file). `WorldMap_UpdateContinentDropDownText` and its zone twin compare the + // current continent against it unguarded (`WorldMapFrame.lua:440`, `:457`), so a harness that + // seats the map alone raises `attempt to compare nil with number` on the second open. + ("worldmapframe.xml", &[r"Interface\FrameXML\Overrides.lua"]), ( "lootframe.xml", &[ diff --git a/crates/benilla-app/src/ui_script/trainer_tests.rs b/crates/benilla-app/src/ui_script/trainer_tests.rs index 56704ff3..ddfdadfa 100644 --- a/crates/benilla-app/src/ui_script/trainer_tests.rs +++ b/crates/benilla-app/src/ui_script/trainer_tests.rs @@ -1021,10 +1021,14 @@ fn learning_a_spell_keeps_the_filter_and_the_collapse_a_re_open_still_resets() { assert!(s.errors().is_empty(), "script errors: {:?}", s.errors()); } -/// After a purchase the reference shows an empty detail pane and no highlight: the Train click -/// clears `showSkillDetails`, which `TRAINER_UPDATE` restores only for a selection index of 0 or -/// 1, and the engine answers the learned service's index in the hidden tail, past -/// `GetNumTrainerServices()` (`0x4d7520`), so the stock arm hides the pane. +/// After a purchase the pane moves onto whatever took the learned row's seat, rather than being +/// stranded on the spell just bought or emptied. +/// +/// The realm rewrote the `TRAINER_UPDATE` arm (`Blizzard_TrainerUI.lua:75-91`): the engine still +/// answers the learned service's index in the hidden tail, past `GetNumTrainerServices()` +/// (`0x4d7520`), but where stock 1.12 then left the pane blank, the realm re-selects +/// `ClassTrainerFrame.selectedService` (defaulting to 2, `:62`) and runs +/// `ClassTrainer_SetSelection` on it — so the pane and the highlight follow the row. #[test] fn learning_a_spell_takes_the_detail_pane_with_it_instead_of_stranding_the_last_one() { benilla_formats::wow_data_or_skip!(); @@ -1091,22 +1095,28 @@ fn learning_a_spell_takes_the_detail_pane_with_it_instead_of_stranding_the_last_ "Cleave", "the trained row is gone from the list and the next one took its place" ); - assert!( - !pane(&mut s).0, - "and the pane below is empty rather than still describing the spell he just learned: {:?}", - pane(&mut s).1 + assert_eq!( + pane(&mut s), + (true, "Cleave".into()), + "the pane describes the row that took the learned one's place, not the spell just bought" ); assert!( - !s.eval::("return ClassTrainerSkillHighlightFrame:IsVisible()") + s.eval::("return ClassTrainerSkillHighlightFrame:IsVisible()") .unwrap(), - "nothing is highlighted either — the selection is off screen, not on Cleave" + "and that row is highlighted, because the realm re-selected it" + ); + // And the arm put the engine's own selection back on that visible row: `ClassTrainer_SetSelection` + // runs `SelectTrainerService`, so the index is no longer the hidden tail one the purchase left. + assert_eq!( + s.eval::("return GetTrainerSelectionIndex()").unwrap(), + 2 ); assert!( s.eval::("return GetTrainerSelectionIndex()").unwrap() - > s.eval::("return GetNumTrainerServices()").unwrap(), - "because the engine answers with the hidden row, which is what steers the window there" + <= s.eval::("return GetNumTrainerServices()").unwrap() ); + // Clicking that row changes nothing, which is the point of the realm's re-selection. s.run("this = ClassTrainerSkill2 ClassTrainerSkillButton_OnClick('LeftButton')") .unwrap(); assert_eq!(pane(&mut s), (true, "Cleave".into()));