The realm ships its own ArenaFrame, so our stand-in only shadowed it
`CharacterFrameAdapters.xml` existed on one premise: "There is no `ArenaFrame.lua` or `.xml`
anywhere in the player's chain". That is no longer true — the realm's FrameXML carries
`Turtle_ArenaUI\Turtle_ArenaFrame.xml` (toc rows 106-110, which `benilla.toc` already loads at
270-274), and it defines `ArenaFrame` with `enableMouse="true"` and `id="5"`, sharing the PvP tab
rather than inventing a sixth. Our empty stub loaded 100 rows earlier and was overwritten, and
wherever it loaded last it replaced the realm's real arena window with a frame that has no
content. `frame_flag_gate` had both halves of that pinned as divergences (mouse false, id 6).
Twelve more red UI tests, each one a file the realm rewrote under an assertion written against
stock 1.12:
- `inspect_tests` (4): all four died in `InspectArenaFrame.xml`'s own OnLoad, which indexes
`_G[this:GetName().."Text"]` — a child of `ArenaTeamTemplate`, which lives in FrameXML and so is
always there live. The rotate buttons are `InspectModelFrame…`, not stock's `InspectModel…`.
- `character_tests` (4): `HonorLevelText` keeps its authored placeholder because the realm
commented that `SetText` out and hides the string instead (`PaperDollFrame.lua:126-127`);
`PaperDollFrame.xml` builds 77 frames, not 75, the two being the realm's player-titles dropdown
and its template's `$parentButton` (read off `FrameXML_Debug(1)`'s own trace, which also proved
each of the 77 is created exactly once — a grown file, not a double load);
`CHARACTERFRAME_SUBFRAMES` has six names; and one sound at load is the reference's own —
`UIDropDownMenu_Initialize` runs its init function on the spot and `UnitPopup_ShowMenu` ends in
`PlaySound("igMainMenuOpen")`.
- `loot_tests` (4): same load-time blip, drained and named; and the master-loot menu is rewritten
end to end — the party arm drops any candidate it cannot resolve to a party unit, the raid arm
groups by class with `hasArrow` submenus instead of by subgroup, and the event is now a toggle
whose memory `DropDownList1`'s own OnHide clears. It also needs `MAX_RAID_MEMBERS`, so the loot
window's prereq row names `RaidFrame.lua`.
- `dressup_tests` (2): `CharacterFrame_OnShow` gsubs `UnitPVPName("player")` unguarded, so the
fixture's player needs the name a real one always has.
The one accepted flag difference is now written down with its reason: our options window keeps
1.12's `toplevel` where the realm's rewrite of `OptionsFrame.xml` took `movable` instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
<!--
|
||||
Adapters over the stock CharacterFrame.xml; loaded after it, so these win.
|
||||
|
||||
Deviation: `ArenaFrame`, a sixth tab page 1.12 has no version of. TurtleWoW's `patch-4.mpq`
|
||||
copy of `CharacterFrame.lua` appends "ArenaFrame" to `CHARACTERFRAME_SUBFRAMES` — six names
|
||||
where the reference has five (line 1) — so `CharacterFrame_ShowSubFrame` indexes all six and
|
||||
raises `attempt to index field '?' a nil value` on the fifth tab's Hide. There is no
|
||||
`ArenaFrame.lua` or `.xml` anywhere in the player's chain, none in the reference `_G` capture,
|
||||
and benilla implements no arena frames, so the name has to exist or the character window's own
|
||||
tab loop dies. An empty hidden frame is the whole of it: no tab button, no content, no script.
|
||||
`id="6"` continues the reference's own numbering (PaperDoll 1, PetPaperDoll 2, Reputation 3,
|
||||
Skill 4, Honor 5), which is what `ToggleCharacter` hands `PanelTemplates_SetTab`.
|
||||
-->
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<Frame name="ArenaFrame" setAllPoints="true" parent="CharacterFrame" hidden="true" id="6" />
|
||||
</Ui>
|
||||
@@ -2672,7 +2672,9 @@
|
||||
</Frame>
|
||||
|
||||
<!-- The era SettingsPanel's size and strata, HIGH, where 1.12's OptionsFrame is DIALOG;
|
||||
toplevel as 1.12's OptionsFrame.xml:5. -->
|
||||
toplevel as 1.12's OptionsFrame.xml:5. The realm's rewrite of that file dropped the flag
|
||||
and made its window `movable` instead; ours is pinned at CENTER, so it keeps the flag.
|
||||
That difference is the one entry in `frame_flag_gate`'s KNOWN table. -->
|
||||
<Frame name="BenillaOptionsFrame" parent="UIParent" frameStrata="HIGH" toplevel="true" enableMouse="true" hidden="true">
|
||||
<Size><AbsDimension x="920" y="724"/></Size>
|
||||
<Anchors><Anchor point="CENTER"/></Anchors>
|
||||
|
||||
@@ -159,8 +159,6 @@ Interface\FrameXML\ReputationFrame.xml
|
||||
Interface\FrameXML\SkillFrame.xml
|
||||
# Brings HonorFrameTemplates.xml, whose row templates `Blizzard_InspectUI` inherits.
|
||||
Interface\FrameXML\HonorFrame.xml
|
||||
# Ours: the sixth tab page TurtleWoW's `CharacterFrame.lua` lists and 1.12 never shipped.
|
||||
CharacterFrameAdapters.xml
|
||||
Interface\FrameXML\DressUpFrame.xml
|
||||
# Deliberately absent, the chain's `StatsFrame.xml` would sit here (its `FrameXML.toc` 60): the
|
||||
# debug-build frame-stat overlay, `hidden="true"` with an OnUpdate that is the only reader of
|
||||
@@ -267,6 +265,11 @@ Interface\FrameXML\SpellChatLinks.lua
|
||||
# reading `$parentText`, `$parentDetails` and nine `$parentDetails*` children by name — without
|
||||
# that row the inspect window raised four times per inspect, three OnLoads and
|
||||
# `InspectArenaFrame_OnShow`.
|
||||
#
|
||||
# `Turtle_ArenaFrame.xml` is also the sixth page the realm's `CharacterFrame.lua` appends to
|
||||
# `CHARACTERFRAME_SUBFRAMES` (`ArenaFrame`, `id="5"`, sharing the PvP tab), which the window's own
|
||||
# sweep hides unguarded. We shipped an empty stand-in for that name until 2026-10-31; the realm's
|
||||
# file defines it, so the stand-in only shadowed the real window.
|
||||
Interface\FrameXML\Turtle_ArenaUI\Turtle_ArenaUI.lua
|
||||
Interface\FrameXML\Turtle_ArenaUI\Turtle_ArenaUITemplates.xml
|
||||
Interface\FrameXML\Turtle_ArenaUI\Turtle_ArenaFrame.xml
|
||||
|
||||
@@ -113,6 +113,12 @@ fn backpack_with_fitting_helm() -> benilla_ui::script::ContainerState {
|
||||
}
|
||||
|
||||
/// A frame count fingerprints the player's own file, not a target: it moves only when that does.
|
||||
///
|
||||
/// 77, not stock 1.12's 75: the realm added its player-titles control, which is two frames —
|
||||
/// `PaperDollFrameTitlesDropdown` (`PaperDollFrame.xml:761`, a `UIDropDownMenuTemplate` button) and
|
||||
/// the `$parentButton` that template brings. Read off the loader's own `-- Creating %s named %s`
|
||||
/// trace with `FrameXML_Debug(1)`, which also showed every one of the 77 created exactly once —
|
||||
/// the check that tells a grown file from a double load.
|
||||
#[test]
|
||||
fn shipped_character_frame_loads_clean() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
@@ -129,7 +135,7 @@ fn shipped_character_frame_loads_clean() {
|
||||
);
|
||||
assert_eq!(
|
||||
counts.get("Interface\\FrameXML\\PaperDollFrame.xml"),
|
||||
Some(&75),
|
||||
Some(&77),
|
||||
"the reference's own PaperDollFrame.xml"
|
||||
);
|
||||
}
|
||||
@@ -151,9 +157,15 @@ fn shipped_character_frame_drives_end_to_end() {
|
||||
s.set_player_combat_stats(Some(combat_stats()));
|
||||
s.set_inventory_slots(inventory_with_head_item());
|
||||
|
||||
assert!(
|
||||
s.take_sounds().is_empty(),
|
||||
"no sound at load (never transitions)"
|
||||
// One sound at load, and it is the reference's own: `PlayerFrameDropDown_OnLoad` calls
|
||||
// `UIDropDownMenu_Initialize(this, …, "MENU")`, which runs the init function on the spot
|
||||
// (`UIDropDownMenu.lua:51-54`, "Set the initialize function and call it"), and that function is
|
||||
// `UnitPopup_ShowMenu`, whose last line is `PlaySound("igMainMenuOpen")`
|
||||
// (`UnitPopup.lua:295`). The menu blip at UI load is 1.12's, not a transition of ours.
|
||||
assert_eq!(
|
||||
s.take_sounds(),
|
||||
vec![SoundRequest::KitName("igMainMenuOpen".into())],
|
||||
"the load-time menu blip, and nothing else (no panel transition)"
|
||||
);
|
||||
assert!(!s.eval::<bool>("return CharacterFrame:IsVisible()").unwrap());
|
||||
|
||||
@@ -393,10 +405,18 @@ fn level_line_reads_the_snapshot_and_repaints_on_unit_level() {
|
||||
.unwrap(),
|
||||
"Level 12 Night Elf Warrior"
|
||||
);
|
||||
// Written by the same call, which is why `HonorFrame.xml` is in the load list.
|
||||
// The honor copy of the line is NOT written: the realm commented that `SetText` out and
|
||||
// replaced it with `HonorLevelText:Hide()` (`PaperDollFrame.lua:126-127`), so the string keeps
|
||||
// `HonorFrame.xml:60`'s authored placeholder and stays down. `HonorFrame.xml` is still in the
|
||||
// load list because `PaperDollFrame_SetLevel` indexes the global unguarded.
|
||||
assert_eq!(
|
||||
s.eval::<String>("return HonorLevelText:GetText()").unwrap(),
|
||||
"Level 12 Night Elf Warrior"
|
||||
"Level level race class",
|
||||
"the authored placeholder, untouched — the realm no longer writes this line"
|
||||
);
|
||||
assert!(
|
||||
!s.eval::<bool>("return HonorLevelText:IsShown()").unwrap(),
|
||||
"and the realm's own `PaperDollFrame_SetLevel` hides it"
|
||||
);
|
||||
|
||||
// A level-up as the app sends it: a new snapshot, then `UNIT_LEVEL` for the player.
|
||||
@@ -931,11 +951,27 @@ fn a_keybind_page_switch_moves_the_tab_row_with_it() {
|
||||
"{page} must be in CHARACTERFRAME_SUBFRAMES"
|
||||
);
|
||||
}
|
||||
// The sixth name is the realm's own arena page, which has no tab of its own: it reuses
|
||||
// Honor's `id="5"` (`Turtle_ArenaFrame.xml:3`), so `ToggleCharacter("ArenaFrame")` leaves the
|
||||
// tab row on PvP, and it is in the list only so the sweep hides it with the rest.
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ArenaFrame:GetID()").unwrap(),
|
||||
5,
|
||||
"the arena page shares the PvP tab's id"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>(
|
||||
"for _, v in CHARACTERFRAME_SUBFRAMES do if v == \"ArenaFrame\" then return true end \
|
||||
end return false"
|
||||
)
|
||||
.unwrap(),
|
||||
"ArenaFrame must be in CHARACTERFRAME_SUBFRAMES"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return getn(CHARACTERFRAME_SUBFRAMES)")
|
||||
.unwrap(),
|
||||
5,
|
||||
"…and there is nothing past the end for a sixth page to arrive into unnoticed"
|
||||
6,
|
||||
"…and there is nothing past the end for a seventh page to arrive into unnoticed"
|
||||
);
|
||||
|
||||
let selected = |s: &mut UiScript| {
|
||||
|
||||
@@ -100,6 +100,10 @@ fn shown_paper_doll() -> UiScript {
|
||||
"player",
|
||||
Some(UnitState {
|
||||
exists: true,
|
||||
// The name is not decoration here: `CharacterFrame_OnShow` runs
|
||||
// `gsub(UnitPVPName("player"), "\n.+", "")` unguarded (`CharacterFrame.lua:85`), and
|
||||
// `gsub` raises on a nil first argument. A player who exists always has one.
|
||||
name: Some("Gromgosh".into()),
|
||||
level: 60,
|
||||
race: Some("Human".into()),
|
||||
race_file: Some("Human".into()),
|
||||
|
||||
@@ -84,10 +84,15 @@ impl std::fmt::Display for Flag {
|
||||
}
|
||||
}
|
||||
|
||||
/// The accepted differences, each with its reason; empty, so any new divergence fails the gate.
|
||||
/// The accepted differences, each with its reason; anything not named here fails the gate.
|
||||
const KNOWN: &[Known] = &[
|
||||
// A parent entry is acceptable only inside `UIParent`'s tree: a seat crossing between it and
|
||||
// the top level is a defect, since `SetFullScreenFrame` hides `UIParent` and all below it.
|
||||
Known {
|
||||
frame: "BenillaOptionsFrame",
|
||||
flag: Flag::Toplevel,
|
||||
why: "The `Benilla` prefix pairs this with the chain's `OptionsFrame`, but the realm rewrote that file and its window took a different trade: `movable=\"true\"` and no `toplevel` (stock 1.12's `OptionsFrame.xml:5` had `toplevel=\"true\"` and no `movable`). Ours is pinned at CENTER and never dragged, so it keeps the 1.12 flag it was built from: clicking it raises it inside HIGH, which is what a window the game menu opens over everything else needs.",
|
||||
},
|
||||
];
|
||||
|
||||
/// Every stock FrameXML document off the player's chain, in `FrameXML.toc` order with each
|
||||
|
||||
@@ -114,6 +114,15 @@ fn armed() -> UiScript {
|
||||
// `ItemButtonTemplate`, the honor page the honor row templates `HonorFrame.xml` brings.
|
||||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\HonorFrame.xml");
|
||||
// `ArenaTeamTemplate`, which the realm's own `InspectArenaFrame.xml` inherits for its three
|
||||
// team buttons. It lives in FrameXML (`Turtle_ArenaUI\Turtle_ArenaUITemplates.xml`, toc row
|
||||
// 107), so the live client always has it before a LoadOnDemand addon runs; without it the
|
||||
// `inherits=` resolves to nothing, `$parentText` is never built, and the button's own OnLoad
|
||||
// raises on `_G[this:GetName().."Text"]`.
|
||||
load_xml(
|
||||
&s,
|
||||
r"Interface\FrameXML\Turtle_ArenaUI\Turtle_ArenaUITemplates.xml",
|
||||
);
|
||||
// A LoadOnDemand addon, seated off the chain as a registry row and loaded by the stock
|
||||
// `InspectFrame_LoadUI` (`UIParent.lua:170`), which `InspectUnit` calls (`UIParent.lua:223`).
|
||||
super::test_ui::seat_chain_addon(&mut s, "Blizzard_InspectUI");
|
||||
@@ -146,6 +155,15 @@ fn shipped_inspect_frame_loads_clean() {
|
||||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\HonorFrame.xml");
|
||||
// `ArenaTeamTemplate`, which the realm's own `InspectArenaFrame.xml` inherits for its three
|
||||
// team buttons. It lives in FrameXML (`Turtle_ArenaUI\Turtle_ArenaUITemplates.xml`, toc row
|
||||
// 107), so the live client always has it before a LoadOnDemand addon runs; without it the
|
||||
// `inherits=` resolves to nothing, `$parentText` is never built, and the button's own OnLoad
|
||||
// raises on `_G[this:GetName().."Text"]`.
|
||||
load_xml(
|
||||
&s,
|
||||
r"Interface\FrameXML\Turtle_ArenaUI\Turtle_ArenaUITemplates.xml",
|
||||
);
|
||||
super::test_ui::seat_chain_addon(&mut s, "Blizzard_InspectUI");
|
||||
s.run("InspectFrame_LoadUI()").unwrap();
|
||||
// Each slot carries its `GetInventorySlotInfo` id (1..=19, no ammo slot) as its frame ID, set
|
||||
@@ -390,14 +408,16 @@ fn shipped_inspect_frame_drives_end_to_end() {
|
||||
);
|
||||
|
||||
// Rotate: left subtracts 0.03 from the booth yaw and right adds it, each click with its sound.
|
||||
s.run("InspectModelRotateLeftButton:Click()").unwrap();
|
||||
// The realm's own `InspectPaperDollFrame.xml:159` names these `InspectModelFrame…`, not stock
|
||||
// 1.12's `InspectModel…` — it rewrote the file, so the stock name is nil here and live.
|
||||
s.run("InspectModelFrameRotateLeftButton:Click()").unwrap();
|
||||
assert!(
|
||||
(s.model_pane_facing("InspectModelFrame") - 0.58).abs() < 0.0001,
|
||||
"rotate-left subtracts 0.03, got {}",
|
||||
s.model_pane_facing("InspectModelFrame")
|
||||
);
|
||||
s.run("InspectModelRotateRightButton:Click()").unwrap();
|
||||
s.run("InspectModelRotateRightButton:Click()").unwrap();
|
||||
s.run("InspectModelFrameRotateRightButton:Click()").unwrap();
|
||||
s.run("InspectModelFrameRotateRightButton:Click()").unwrap();
|
||||
assert!(
|
||||
(s.model_pane_facing("InspectModelFrame") - 0.64).abs() < 0.0001,
|
||||
"rotate-right adds 0.03, got {}",
|
||||
|
||||
@@ -254,6 +254,21 @@ fn shipped_loot_frame_drives_end_to_end() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The load-time menu blips this file set makes, drained and checked.
|
||||
///
|
||||
/// Not ours and not a defect: every `UIDropDownMenu_Initialize(frame, init, "MENU")` runs its init
|
||||
/// function on the spot (`UIDropDownMenu.lua:51-54`, "Set the initialize function and call it"),
|
||||
/// and each unit-popup init ends in `PlaySound("igMainMenuOpen")` (`UnitPopup.lua:295`). The
|
||||
/// reference plays one per such dropdown as the UI loads, which is why a `/reload` blips.
|
||||
fn drain_load_blips(s: &mut UiScript) {
|
||||
let blips = s.take_sounds();
|
||||
let menu_open = SoundRequest::KitName("igMainMenuOpen".into());
|
||||
assert!(
|
||||
blips.iter().all(|k| *k == menu_open),
|
||||
"only the load-time menu blips belong here, got {blips:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Only an empty loot plays a Lua sound, `LOOTWINDOWOPENEMPTY` (LootFrame.lua:135-136).
|
||||
#[test]
|
||||
fn loot_empty_roll_plays_the_empty_open_kit() {
|
||||
@@ -264,6 +279,7 @@ fn loot_empty_roll_plays_the_empty_open_kit() {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
drain_load_blips(&mut s);
|
||||
|
||||
s.set_loot(Some(coin_and_two_items()));
|
||||
s.fire_event("LOOT_OPENED", vec![]);
|
||||
@@ -301,6 +317,7 @@ fn fishing_loot_open_plays_the_reel_and_swaps_the_portrait() {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
drain_load_blips(&mut s);
|
||||
let has_icon = |quads: &[ExtractedQuad], needle: &str| {
|
||||
quads.iter().any(|q| {
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p.contains(needle))
|
||||
@@ -673,15 +690,54 @@ fn ctrl_and_shift_on_a_loot_row_preview_and_post_without_looting() {
|
||||
|
||||
/// Master loot: an item at or above `MASTER_LOOT_THREHOLD` (4, epic; the misspelling is the
|
||||
/// reference's, LootFrame.lua:3) asks `CONFIRM_LOOT_DISTRIBUTION` before it is given.
|
||||
///
|
||||
/// The realm rewrote this menu. Its party arm does not simply list `GetMasterLootCandidate(i)`
|
||||
/// the way stock 1.12 did: it resolves each candidate back to `"player"` or a `"party"..j` token
|
||||
/// and **drops any candidate it cannot** (`LootFrame.lua:317-349`), because it colours the row by
|
||||
/// `RAID_CLASS_COLORS[class]` off that unit's own `UnitClass`. So a party with the candidates in
|
||||
/// it is not decoration here — without one the menu has no rows at all and never opens.
|
||||
#[test]
|
||||
fn shipped_loot_frame_hands_a_master_row_to_a_candidate() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
use benilla_ui::script::{PartyMemberInfo, PartyState, UnitState};
|
||||
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for f in super::test_ui::LOOT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
drain_load_blips(&mut s);
|
||||
|
||||
// `GetNumPartyMembers() > 0` picks the party arm (`LootFrame.lua:317`).
|
||||
s.set_party(PartyState {
|
||||
members: vec![
|
||||
PartyMemberInfo {
|
||||
name: "Thrall".into(),
|
||||
guid: 0x11,
|
||||
},
|
||||
PartyMemberInfo {
|
||||
name: "Cairne".into(),
|
||||
guid: 0x12,
|
||||
},
|
||||
],
|
||||
loot_method: "master".into(),
|
||||
master_looter: Some(0),
|
||||
loot_threshold: 2,
|
||||
..PartyState::default()
|
||||
});
|
||||
let member = |name: &str, class: &str, token: &str| UnitState {
|
||||
exists: true,
|
||||
name: Some(name.into()),
|
||||
is_player: true,
|
||||
player_controlled: true,
|
||||
class: Some(class.into()),
|
||||
class_file: Some(token.into()),
|
||||
..UnitState::default()
|
||||
};
|
||||
s.set_unit("player", Some(member("Gromgosh", "Warrior", "WARRIOR")));
|
||||
s.set_unit("party1", Some(member("Thrall", "Shaman", "SHAMAN")));
|
||||
s.set_unit("party2", Some(member("Cairne", "Druid", "DRUID")));
|
||||
|
||||
let row = |name: &str, quality: u32| {
|
||||
Some(LootRow {
|
||||
@@ -793,24 +849,60 @@ fn shipped_loot_frame_hands_a_master_row_to_a_candidate() {
|
||||
"row 2 to candidate 1, only after the accept"
|
||||
);
|
||||
|
||||
// A row click hides a standing confirmation (LootFrame.lua:156).
|
||||
// A row click hides a standing confirmation (LootFrame.lua:156). The confirmation needs the
|
||||
// epic row, so the menu re-opens on row 2 first — and this runs while the menu is closed,
|
||||
// because an open `DropDownList1` sits over the loot rows and takes the click itself.
|
||||
click(&mut s, "LootButton2");
|
||||
let _ = s.take_loot_picks();
|
||||
s.fire_event("OPEN_MASTER_LOOT_LIST", vec![]);
|
||||
s.resolve();
|
||||
click(&mut s, "DropDownList1Button1");
|
||||
assert!(s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap());
|
||||
let _ = s.take_loot_master_gives();
|
||||
click(&mut s, "LootButton1");
|
||||
assert!(
|
||||
!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
||||
"clicking another row closes the standing confirmation"
|
||||
);
|
||||
|
||||
// The realm made the event a toggle (`LootFrame.lua:102-109`): it remembers which row the
|
||||
// open was for and, asked again for that same row, only hides the list. The memory is not
|
||||
// sticky — `LootFrame_OnLoad` hooks `DropDownList1`'s own OnHide to clear it
|
||||
// (`LootFrame.lua:38-42`) — so it closes only while the menu is still up, and the ask after
|
||||
// any close opens again. Stock 1.12 had neither the memory nor the hook.
|
||||
click(&mut s, "LootButton1");
|
||||
let _ = s.take_loot_picks();
|
||||
s.fire_event("OPEN_MASTER_LOOT_LIST", vec![]);
|
||||
s.resolve();
|
||||
assert!(
|
||||
s.eval::<bool>("return DropDownList1:IsVisible()").unwrap(),
|
||||
"the first ask for this row opens it"
|
||||
);
|
||||
s.fire_event("OPEN_MASTER_LOOT_LIST", vec![]);
|
||||
s.resolve();
|
||||
assert!(
|
||||
!s.eval::<bool>("return DropDownList1:IsVisible()").unwrap(),
|
||||
"the same row again, menu still up: the second ask only hides it"
|
||||
);
|
||||
s.fire_event("OPEN_MASTER_LOOT_LIST", vec![]);
|
||||
s.resolve();
|
||||
assert!(
|
||||
s.eval::<bool>("return DropDownList1:IsVisible()").unwrap(),
|
||||
"…and the hide cleared the memory, so the next ask opens again"
|
||||
);
|
||||
}
|
||||
|
||||
/// In a raid each candidate sits in its subgroup's five-slot block, and the menu keeps a "Group N"
|
||||
/// row only for an occupied block (LootFrame.lua:197-213).
|
||||
/// In a raid the realm's menu is grouped by **class**, not by subgroup: a `GIVE_LOOT` title, then
|
||||
/// one arrowed row per class with anyone in it, in `DropDownSorted`'s fixed order, each opening a
|
||||
/// level-2 list of that class's players (`LootFrame.lua:268-316`).
|
||||
///
|
||||
/// Stock 1.12 listed a "Group N" row per occupied five-slot block; the realm replaced that whole
|
||||
/// arm, and `LootFrame_UpdateRoster` (`:120-153`) is what fills the rows — off `GetRaidRosterInfo`
|
||||
/// for the name and `UnitClass("raid"..i)` for the class, so both have to be seated.
|
||||
#[test]
|
||||
fn the_master_loot_menu_groups_raid_candidates_by_subgroup() {
|
||||
fn the_master_loot_menu_groups_raid_candidates_by_class() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
use benilla_ui::script::{PartyState, RaidMemberInfo};
|
||||
use benilla_ui::script::{PartyState, RaidMemberInfo, UnitState};
|
||||
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
@@ -818,17 +910,52 @@ fn the_master_loot_menu_groups_raid_candidates_by_subgroup() {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
drain_load_blips(&mut s);
|
||||
|
||||
// A raid: `GetNumRaidMembers() > 0` is the nested arm's whole gate (LootFrame.lua:189).
|
||||
// A raid: `GetNumRaidMembers() > 0` is the raid arm's whole gate (`LootFrame.lua:293`), and
|
||||
// the roster walk reads one row per member, so the vector's length is the member count.
|
||||
let roster = [
|
||||
("Thrall", "Shaman", "SHAMAN"),
|
||||
("Cairne", "Druid", "DRUID"),
|
||||
("Sylvanas", "Rogue", "ROGUE"),
|
||||
];
|
||||
s.set_party(PartyState {
|
||||
raid: vec![RaidMemberInfo::default(); 7],
|
||||
raid: roster
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (name, class, token))| RaidMemberInfo {
|
||||
name: (*name).into(),
|
||||
guid: 0x10 + i as u64,
|
||||
subgroup: 0,
|
||||
level: 60,
|
||||
class: Some((*class).into()),
|
||||
class_file: Some((*token).into()),
|
||||
online: true,
|
||||
..RaidMemberInfo::default()
|
||||
})
|
||||
.collect(),
|
||||
loot_method: "master".into(),
|
||||
master_looter: Some(0),
|
||||
loot_threshold: 2,
|
||||
..PartyState::default()
|
||||
});
|
||||
// `UnitClass("raid"..i)`, which is where the roster takes the class from — not the roster row.
|
||||
for (i, (name, class, token)) in roster.iter().enumerate() {
|
||||
s.set_unit(
|
||||
&format!("raid{}", i + 1),
|
||||
Some(UnitState {
|
||||
exists: true,
|
||||
name: Some((*name).into()),
|
||||
is_player: true,
|
||||
player_controlled: true,
|
||||
class: Some((*class).into()),
|
||||
class_file: Some((*token).into()),
|
||||
..UnitState::default()
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Subgroup 1 holds slots 1-2 and subgroup 3 slot 11, the rest holes, as the client files them.
|
||||
// The candidate slots the server filled, with the holes the client files them into.
|
||||
let mut candidates = vec![None; 12];
|
||||
candidates[0] = Some("Thrall".to_string());
|
||||
candidates[1] = Some("Cairne".to_string());
|
||||
@@ -865,21 +992,67 @@ fn the_master_loot_menu_groups_raid_candidates_by_subgroup() {
|
||||
"Give Loot To:",
|
||||
"the raid arm opens with the title row the party arm has no such thing for"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return DropDownList1Button2:GetText()")
|
||||
// `DropDownSorted`'s order (`LootFrame.lua:267`), not the roster's: Druid, Rogue, Shaman.
|
||||
for (button, class) in [(2, "Druid"), (3, "Rogue"), (4, "Shaman")] {
|
||||
assert_eq!(
|
||||
s.eval::<String>(&format!("return DropDownList1Button{button}:GetText()"))
|
||||
.unwrap(),
|
||||
class,
|
||||
"button {button} is the localized class name, in DropDownSorted order"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>(&format!(
|
||||
"return DropDownList1Button{button}ExpandArrow:IsShown()"
|
||||
))
|
||||
.unwrap(),
|
||||
"Group 1"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return DropDownList1Button3:GetText()")
|
||||
.unwrap(),
|
||||
"Group 3",
|
||||
"the empty group 2 block contributes no row — and group 3 is NOT relabelled 2"
|
||||
);
|
||||
"a class row carries the submenu arrow (`info.hasArrow`, shown at \
|
||||
`UIDropDownMenu.lua:265-266`)"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return DropDownList1.numButtons").unwrap(),
|
||||
3,
|
||||
"title + two occupied groups, out of eight blocks"
|
||||
4,
|
||||
"title + one row per class with anyone in it, out of the nine DropDownSorted names"
|
||||
);
|
||||
|
||||
// Level 2: the class row's own players, each carrying its candidate index as the value the
|
||||
// give reads (`LootFrame.lua:271-291`).
|
||||
// The submenu is the **arrow's** own OnEnter, not the row's
|
||||
// (`UIDropDownMenuTemplates.xml:100-102`: `$parentExpandArrow` hovers into
|
||||
// `ToggleDropDownMenu(level + 1, this:GetParent().value)`).
|
||||
s.resolve();
|
||||
let (ax, ay) = s
|
||||
.eval::<(f64, f64)>("return DropDownList1Button4ExpandArrow:GetCenter()")
|
||||
.unwrap();
|
||||
s.mouse_move(ax as f32, ay as f32);
|
||||
s.resolve();
|
||||
assert!(
|
||||
s.eval::<bool>("return DropDownList2:IsVisible()").unwrap(),
|
||||
"hovering the class row's arrow opens level 2"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
assert_eq!(
|
||||
s.eval::<String>("return DropDownList2Button1:GetText()")
|
||||
.unwrap(),
|
||||
"Thrall"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return DropDownList2.numButtons").unwrap(),
|
||||
1,
|
||||
"one shaman in the raid, one row"
|
||||
);
|
||||
// Thrall is candidate 1, so the click gives slot 1 to candidate 1.
|
||||
let (bx, by) = s
|
||||
.eval::<(f64, f64)>("return DropDownList2Button1:GetCenter()")
|
||||
.unwrap();
|
||||
s.mouse_button(bx as f32, by as f32, "LeftButton", true);
|
||||
s.mouse_button(bx as f32, by as f32, "LeftButton", false);
|
||||
s.resolve();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
assert_eq!(
|
||||
s.take_loot_master_gives(),
|
||||
vec![(1, 1)],
|
||||
"row 1 to the candidate index the submenu row carried"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1957,10 +1957,6 @@ fn assets_ui_does_not_grow() {
|
||||
// falls through into `attempt to call local 'handledata'`. There is no stock file to
|
||||
// migrate — AIO is the realm's, not Blizzard's. The file's own header has the trace.
|
||||
"AIOHandlers.lua",
|
||||
// An `ArenaFrame` stub, the one name TurtleWoW's patched `CharacterFrame.lua` appends to
|
||||
// `CHARACTERFRAME_SUBFRAMES` with no file anywhere in the chain to define it; without it
|
||||
// the stock tab loop raises on every tab change. The file's own header has the trace.
|
||||
"CharacterFrameAdapters.xml",
|
||||
"ContainerFrameAdapters.xml",
|
||||
"GameMenuFrame.xml",
|
||||
"KeyBindingsPage.xml",
|
||||
|
||||
@@ -123,7 +123,16 @@ const PREREQS: &[(&str, &[&str])] = &[
|
||||
),
|
||||
(
|
||||
"lootframe.xml",
|
||||
&[r"Interface\FrameXML\ItemButtonTemplate.xml"],
|
||||
&[
|
||||
r"Interface\FrameXML\ItemButtonTemplate.xml",
|
||||
// The `.lua` alone, for `MAX_RAID_MEMBERS` (`RaidFrame.lua:1`): the limit the realm's
|
||||
// rewritten master-loot submenu counts candidates to (`LootFrame.lua:273`). A runtime
|
||||
// need, not a load one — the loot window is toc row 76 and `RaidFrame.xml` is 82, so
|
||||
// the live client has the constant long before a menu can open, while a harness that
|
||||
// seats the loot window alone raises `'for' limit must be a number` on the first
|
||||
// class-row hover.
|
||||
r"Interface\FrameXML\RaidFrame.lua",
|
||||
],
|
||||
),
|
||||
(
|
||||
"tradeframe.xml",
|
||||
@@ -666,9 +675,13 @@ pub(super) const CHARACTER_UI: &[&str] = &[
|
||||
r"Interface\FrameXML\ReputationFrame.xml",
|
||||
"Interface\\FrameXML\\SkillFrame.xml",
|
||||
"Interface\\FrameXML\\HonorFrame.xml",
|
||||
// Ours: the sixth page TurtleWoW's `CharacterFrame.lua` lists and 1.12 never shipped. It loads
|
||||
// here for the reason it does in `benilla.toc`: the subframe list is swept unguarded.
|
||||
"CharacterFrameAdapters.xml",
|
||||
// The sixth page TurtleWoW's `CharacterFrame.lua` lists and 1.12 never shipped. The realm
|
||||
// ships it itself, in FrameXML (toc rows 106-110), and the subframe list is swept unguarded,
|
||||
// so it must be seated before the window opens. The `.lua` leads: the window's `<OnLoad>`
|
||||
// calls `ArenaFrame_OnLoad`, and the templates precede the frame that inherits them.
|
||||
r"Interface\FrameXML\Turtle_ArenaUI\Turtle_ArenaUI.lua",
|
||||
r"Interface\FrameXML\Turtle_ArenaUI\Turtle_ArenaUITemplates.xml",
|
||||
r"Interface\FrameXML\Turtle_ArenaUI\Turtle_ArenaFrame.xml",
|
||||
];
|
||||
|
||||
/// The manifest slice stock `FriendsFrame.xml` and `RaidFrame.xml` reach at load or on show, in its
|
||||
|
||||
Reference in New Issue
Block a user