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()));