- the window tab is the reference's own: `CharacterFrameTemplates.xml` loads off the chain at the reference's toc l.21 — the reference never gives it a toc line, `UIOptionsFrame.xml` includes it, and ours is not the reference's options window, so the row is explicit — and our `CharacterFrameTabButtonTemplate` with its `BenillaTab_Room`/`BenillaTabButton_OnUpdate` settle is deleted, 192 lines; the stock template fits a tab ONCE, in `<OnShow>`, through `PanelTemplates_TabResize(0)`, which needs a `GetStringWidth` that answers inside the Lua call that asked — the engine's synchronous measurer is that, so the settle that re-fit from `OnUpdate` until a frame-late round-trip landed is dead weight; retired with it, the director's call: 1002's structural clamp (`benillaTabRightInset`/`benillaTabPadding`/`benillaTabMaxWidth`, a tab never past its window's drawn right edge) — the reference's only caps are the numbers each window passes `TabResize`, and nothing had set those fields since the macro window went stock; 1004's highlight law is unchanged and now the reference's own text (`LEFT +10`/`RIGHT −10`, two opposing anchors that never consult the width, so both `SetWidth` lines are dead code there and here); measured live on the frame the window opens, main's 115·115·115·115 becomes 89.8·99.3·70.8·75.6 on the character row, 77.9·66.1·70.8·66.1 on the social, 89.8·85.0 on the merchant — each its label's `GetStringWidth()` + 40 — and one frame later the two builds agree exactly, so what goes is a one-frame 115 px flash on every open of every one of those windows (1993) - the ferry seam: `tick_transports` leaves an off-map transport's transform unwritten — the reference's own freeze, `0x5f5248`'s leg map read against the current map and the return at `0x5f5279` past the pose stores — but 0455's riding worldport composes the rider's world pose as `boat.translation + boat.rotation * local_pos` from exactly that transform, so a client whose path clock had not crossed put the body at Booty Bay's coordinates on Kalimdor, which want ZERO tiles of Kalimdor's WDT: the reported `0/0 resident`, which cannot clear (`is_ready` needs `total > 0`) and cannot re-raise (`focus_resident` takes its vacuous off-grid arm); the fall is a second bug in the same window: `SelfMoveMessage` dropped the wire's `ON_TRANSPORT` tail and `apply_self_move` re-derived the deck-local pose as `world − boat.translation`, which against a frozen source-continent boat is the distance between two continents — consistent while the boat stays frozen, a ~13,000 yd fling off the deck the instant it writes an honest pose, `update_attachment` keeping the ride because the body is airborne and no settle hold to catch it; the rider's deck-local pose now comes off the wire, and `first_cycle_on_map` + `reseek_ridden_transport_at_worldport` seek our clock to the head of the destination map's leg and write the pose in the worldport frame itself, a clock that already agreed left untouched; cleared with numbers: the timetable matches vmangos's to 18 ms over path 241's 350 818 ms cycle, the period is bit-identical to the DB row, the live skew is 32 and 144 ms on the Orgrimmar zeppelin; two divergences kept knowingly — we spare the ridden transport where `SMSG_NEW_WORLD` (`0x401bc0`) destroys the whole object manager, and we hide an off-map boat the reference draws; an off-grid view focus names its map and tile once, a ridden transport whose map disagrees with `CurrentMap` for three frames warns, and the sea-crossing probe sends itself to a dock (`WOW_PROBE_DOCK=x,y,z`) because the fleet's seams are not interchangeable — one path crosses mid-cycle, another at the wrap; why the director saw 38.5 s and not a few frames is still open (N08, 2026) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
164 lines
6.2 KiB
Rust
164 lines
6.2 KiB
Rust
//! The guild tabard designer — the stock `TabardFrame.xml` off the player's chain (decision
|
|
//! 1977) in a bare engine: the wire's open event seeds and shows the window, the customization
|
|
//! rows cycle the design, the emblem cells wear the mask token, the Save button follows
|
|
//! `CanSaveTabardNow`, and the cancel path closes through `CloseTabardCreation`.
|
|
|
|
use benilla_ui::script::{
|
|
emblem_mask_path, TabardHost, TabardIntent, UiScript, UnitGuild, UnitState,
|
|
};
|
|
|
|
/// The kit, with the player in a guild at `rank` (0 = the master) — `GetGuildInfo("player")`
|
|
/// reads the unit model's own guild tag.
|
|
fn harness(rank: u32) -> UiScript {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
for (token, name, guild) in [
|
|
(
|
|
"player",
|
|
"Probefour",
|
|
Some(UnitGuild {
|
|
name: "Probe Guild".into(),
|
|
rank_name: if rank == 0 { "Guild Master" } else { "Member" }.into(),
|
|
rank_index: rank,
|
|
}),
|
|
),
|
|
("npc", "Tabard Vendor", None),
|
|
] {
|
|
s.set_unit(
|
|
token,
|
|
Some(UnitState {
|
|
exists: true,
|
|
name: Some(name.into()),
|
|
level: 60,
|
|
guild,
|
|
..Default::default()
|
|
}),
|
|
);
|
|
}
|
|
for file in [
|
|
"Interface\\FrameXML\\Fonts.xml",
|
|
r"Interface\FrameXML\UIParent.xml",
|
|
"ScrollTemplates.xml", // our scroll kit + the placeholder icon
|
|
r"Interface\FrameXML\MoneyFrame.lua",
|
|
r"Interface\FrameXML\MoneyFrame.xml",
|
|
r"Interface\FrameXML\UIPanelTemplates.lua",
|
|
r"Interface\FrameXML\UIPanelTemplates.xml",
|
|
"Interface\\FrameXML\\GlobalStrings.lua",
|
|
"Interface\\FrameXML\\BasicControls.xml",
|
|
"Interface\\FrameXML\\LocaleProperties.lua",
|
|
"Interface\\FrameXML\\StaticPopup.xml",
|
|
"Interface\\FrameXML\\GameTooltip.xml",
|
|
r"Interface\FrameXML\TabardFrame.xml",
|
|
] {
|
|
super::test_ui::load_ui(&s, file);
|
|
}
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
s
|
|
}
|
|
|
|
/// The reference's open path: `MSG_TABARDVENDOR_ACTIVATE` → `OPEN_TABARD_FRAME`. The stock
|
|
/// handler seeds the design off the guild record, paints the four emblem cells, sets the
|
|
/// greeting and the Save button by the guild rank, and shows the window through the panel
|
|
/// manager.
|
|
#[test]
|
|
fn the_open_event_seeds_the_design_paints_the_cells_and_shows_the_window() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = harness(0);
|
|
s.set_tabard_host(TabardHost {
|
|
guild_record: Some([12, 4, 1, 2, 9]),
|
|
save_pending: false,
|
|
});
|
|
s.fire_event("OPEN_TABARD_FRAME", vec![]);
|
|
s.resolve();
|
|
assert!(s.eval::<bool>("return TabardFrame:IsVisible()").unwrap());
|
|
assert_eq!(s.tabard_design(), Some([12, 4, 1, 2, 9]));
|
|
assert_eq!(
|
|
s.eval::<String>("return TabardFrameNameText:GetText()")
|
|
.unwrap(),
|
|
"Tabard Vendor"
|
|
);
|
|
let cell = s
|
|
.eval::<String>("return TabardFrameEmblemTopLeft:GetTexture()")
|
|
.unwrap();
|
|
assert_eq!(
|
|
emblem_mask_path(&cell),
|
|
Some(r"Textures\GuildEmblems\Emblem_12_04_TU_U")
|
|
);
|
|
let cell = s
|
|
.eval::<String>("return TabardFrameEmblemBottomRight:GetTexture()")
|
|
.unwrap();
|
|
assert!(cell.ends_with("Emblem_12_04_TL_U"), "{cell}");
|
|
assert!(
|
|
s.eval::<bool>("return TabardFrameAcceptButton:IsEnabled() == 1")
|
|
.unwrap(),
|
|
"a guild master with a cached record can save"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<String>("return TabardFrameGreetingText:GetText()")
|
|
.unwrap(),
|
|
s.eval::<String>("return TABARDVENDORGREETING").unwrap()
|
|
);
|
|
|
|
// The customization rows: the first row's right arrow cycles the emblem style and repaints.
|
|
s.run("TabardCustomization_Right(1)").unwrap();
|
|
assert_eq!(s.tabard_design(), Some([13, 4, 1, 2, 9]));
|
|
let cell = s
|
|
.eval::<String>("return TabardFrameEmblemTopRight:GetTexture()")
|
|
.unwrap();
|
|
assert!(cell.ends_with("Emblem_13_04_TU_U"), "{cell}");
|
|
s.run("TabardCustomization_Left(5) TabardCustomization_Left(5)")
|
|
.unwrap();
|
|
assert_eq!(s.tabard_design().unwrap()[4], 7);
|
|
|
|
// Save: the intent carries the five; the pending latch greys the button through the event.
|
|
s.run("TabardFrameAcceptButton:Click()").unwrap();
|
|
assert_eq!(
|
|
s.take_tabard_intents(),
|
|
vec![TabardIntent::Save([13, 4, 1, 2, 7])]
|
|
);
|
|
s.set_tabard_host(TabardHost {
|
|
guild_record: Some([12, 4, 1, 2, 9]),
|
|
save_pending: true,
|
|
});
|
|
s.fire_event("TABARD_SAVE_PENDING", vec![]);
|
|
assert!(
|
|
!s.eval::<bool>("return TabardFrameAcceptButton:IsEnabled() == 1")
|
|
.unwrap(),
|
|
"the latch greys the button"
|
|
);
|
|
|
|
// Cancel closes through the panel manager; the OnHide calls CloseTabardCreation.
|
|
s.run("TabardFrameCancelButton:Click()").unwrap();
|
|
s.resolve();
|
|
assert!(!s.eval::<bool>("return TabardFrame:IsVisible()").unwrap());
|
|
assert_eq!(s.take_tabard_intents(), vec![TabardIntent::Close]);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// A non-master, or a player with no guild, gets the no-guild greeting and a dead Save button —
|
|
/// the stock `TabardFrame_UpdateButtons` law over `GetGuildInfo("player")`.
|
|
#[test]
|
|
fn a_non_master_gets_the_no_guild_greeting_and_no_save() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = harness(3);
|
|
s.set_tabard_host(TabardHost {
|
|
guild_record: Some([-1; 5]),
|
|
save_pending: false,
|
|
});
|
|
s.fire_event("OPEN_TABARD_FRAME", vec![]);
|
|
assert!(!s
|
|
.eval::<bool>("return TabardFrameAcceptButton:IsEnabled() == 1")
|
|
.unwrap());
|
|
assert_eq!(
|
|
s.eval::<String>("return TabardFrameGreetingText:GetText()")
|
|
.unwrap(),
|
|
s.eval::<String>("return TABARDVENDORNOGUILDGREETING")
|
|
.unwrap()
|
|
);
|
|
// The close event from the engine side hides the window.
|
|
s.fire_event("CLOSE_TABARD_FRAME", vec![]);
|
|
s.resolve();
|
|
assert!(!s.eval::<bool>("return TabardFrame:IsVisible()").unwrap());
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|