This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.
Today's work, which is the reason a baseline exists at all:
- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
characters were entering the sun's shadow cascades, so every skinned-rig
shadow specialization compiled live mid-frame; the hitch detector logged
"main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
frame-cost reading no longer pays 2.7 ms a frame for it.
Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
434 lines
16 KiB
Rust
434 lines
16 KiB
Rust
//! The stock guild registrar and charter windows over a Lua stand-in for the engine's petition API,
|
|
//! so only the windows are under test. The stand-in keeps the API's shapes: booleans are `1`/`nil`,
|
|
//! and `GetPetitionInfo`'s fourth return is the wire's signature requirement.
|
|
|
|
use benilla_ui::script::UiScript;
|
|
|
|
use super::test_ui::load_ui_strict as load_xml;
|
|
|
|
/// The petition API in Lua, seeded as a signer's view with two signatures. Tests mutate
|
|
/// `BenillaPetitionFixture`'s fields, never the table, which the closures hold as an upvalue.
|
|
const PETITION_FIXTURE: &str = r#"
|
|
BenillaPetitionFixture = {
|
|
petitionType = "charter",
|
|
title = "Legacy of Steel",
|
|
bodyText = "",
|
|
-- The WIRE's requirement, deliberately not 9, so a test can tell it apart from
|
|
-- MAX_PETITION_SIGNATURES.
|
|
maxSignatures = 4,
|
|
originator = "Tigole",
|
|
isOriginator = nil,
|
|
canSign = 1,
|
|
signers = { "Furor", "Kaplan" },
|
|
charterCost = 1000,
|
|
calls = {},
|
|
}
|
|
|
|
local F = BenillaPetitionFixture
|
|
|
|
function BenillaPetitionCalls()
|
|
local out = table.concat(F.calls, "|")
|
|
F.calls = {}
|
|
return out
|
|
end
|
|
|
|
local function record(call)
|
|
table.insert(F.calls, call)
|
|
end
|
|
|
|
function GetPetitionInfo()
|
|
return F.petitionType, F.title, F.bodyText, F.maxSignatures, F.originator, F.isOriginator
|
|
end
|
|
|
|
function GetNumPetitionNames() return table.getn(F.signers) end
|
|
function GetPetitionNameInfo(i) return F.signers[i] end
|
|
function CanSignPetition() return F.canSign end
|
|
function GetGuildCharterCost() return F.charterCost end
|
|
|
|
function SignPetition() record("SignPetition") end
|
|
function OfferPetition() record("OfferPetition") end
|
|
function ClosePetition() record("ClosePetition") end
|
|
function CloseGuildRegistrar() record("CloseGuildRegistrar") end
|
|
function TurnInGuildCharter() record("TurnInGuildCharter") end
|
|
function BuyGuildCharter(name) record("BuyGuildCharter:" .. name) end
|
|
function RenamePetition(name) record("RenamePetition:" .. name) end
|
|
"#;
|
|
|
|
/// The windows' manifest slice in `benilla.toc` order, the fixture first; `MoneyFrame.xml` comes
|
|
/// before the registrar, whose price row inherits `MoneyFrameTemplate` at load.
|
|
fn setup() -> UiScript {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
s.run(PETITION_FIXTURE).unwrap();
|
|
// Stock `PetitionFrame_Update` formats `GlobalStrings` entries with no fallback.
|
|
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
|
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
|
|
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
|
|
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
|
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
|
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
|
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
|
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
|
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
|
// `QuestFrame.xml` includes `QuestTitleButtonTemplate`, which the registrar's rows inherit.
|
|
load_xml(&s, "ScrollTemplates.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
|
|
// `ChatFrameEditBox`, which the stock purchase button focuses unguarded
|
|
// (`GuildRegistrarFrame.xml:271`).
|
|
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml"); // TOOLTIP_DEFAULT_COLOR, for dropdowns
|
|
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml"); // ChatFrame's dropdowns inherit it
|
|
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
|
|
load_xml(&s, "Interface\\FrameXML\\ChatFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
|
|
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\GuildRegistrarFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\PetitionFrame.xml");
|
|
s
|
|
}
|
|
|
|
fn text(s: &UiScript, expr: &str) -> String {
|
|
s.eval::<String>(&format!("return {expr}:GetText() or \"\""))
|
|
.unwrap_or_else(|e| panic!("{expr}:GetText() — {e}"))
|
|
}
|
|
|
|
fn visible(s: &UiScript, frame: &str) -> bool {
|
|
s.eval::<bool>(&format!("return {frame}:IsVisible()"))
|
|
.unwrap_or_else(|e| panic!("{frame}:IsVisible() — {e}"))
|
|
}
|
|
|
|
fn calls(s: &UiScript) -> String {
|
|
s.eval::<String>("return BenillaPetitionCalls()").unwrap()
|
|
}
|
|
|
|
fn show_petition(s: &mut UiScript) {
|
|
s.fire_event("PETITION_SHOW", vec![]);
|
|
assert!(s.errors().is_empty(), "PETITION_SHOW: {:?}", s.errors());
|
|
}
|
|
|
|
fn show_registrar(s: &mut UiScript) {
|
|
s.fire_event("GUILD_REGISTRAR_SHOW", vec![]);
|
|
assert!(
|
|
s.errors().is_empty(),
|
|
"GUILD_REGISTRAR_SHOW: {:?}",
|
|
s.errors()
|
|
);
|
|
}
|
|
|
|
/// Rows 1-2 carry the signers and 3-9 read `<not yet signed>`: `GetPetitionNameInfo` is 1-based.
|
|
#[test]
|
|
fn a_signers_charter_shows_the_sign_face_and_nine_rows() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = setup();
|
|
show_petition(&mut s);
|
|
|
|
assert!(visible(&s, "PetitionFrame"));
|
|
assert_eq!(
|
|
text(&s, "PetitionFrameNpcNameText"),
|
|
"Legacy of Steel Guild Charter",
|
|
"GUILD_CHARTER_TEMPLATE filled with the title"
|
|
);
|
|
assert_eq!(text(&s, "PetitionFrameCharterName"), "Legacy of Steel");
|
|
assert_eq!(text(&s, "PetitionFrameMasterName"), "Tigole");
|
|
// The three static labels come from `text=` attributes; no script paints them.
|
|
assert_eq!(text(&s, "PetitionFrameCharterTitle"), "Guild Name");
|
|
assert_eq!(text(&s, "PetitionFrameMasterTitle"), "Guild Master");
|
|
assert_eq!(text(&s, "PetitionFrameMemberTitle"), "Members");
|
|
|
|
assert_eq!(text(&s, "PetitionFrameMemberName1"), "Furor");
|
|
assert_eq!(text(&s, "PetitionFrameMemberName2"), "Kaplan");
|
|
for i in 3..=9 {
|
|
assert_eq!(
|
|
text(&s, &format!("PetitionFrameMemberName{i}")),
|
|
"<not yet signed>",
|
|
"row {i} is an empty seat"
|
|
);
|
|
}
|
|
|
|
// Every part draws, not merely holds text.
|
|
for part in [
|
|
"PetitionFrameCharterTitle",
|
|
"PetitionFrameCharterName",
|
|
"PetitionFrameMasterTitle",
|
|
"PetitionFrameMasterName",
|
|
"PetitionFrameMemberTitle",
|
|
"PetitionFrameMemberName1",
|
|
"PetitionFrameMemberName9",
|
|
"PetitionFrameInstructions",
|
|
"PetitionFrameCancelButton",
|
|
"PetitionFrameCloseButton",
|
|
] {
|
|
assert!(visible(&s, part), "{part} is on screen");
|
|
}
|
|
assert!(visible(&s, "PetitionFrameSignButton"));
|
|
assert!(!visible(&s, "PetitionFrameRequestButton"));
|
|
assert!(
|
|
!visible(&s, "PetitionFrameRenameButton"),
|
|
"only the charter's owner may rename it"
|
|
);
|
|
assert_eq!(
|
|
text(&s, "PetitionFrameInstructions"),
|
|
"Click the <Sign Charter> button to become a charter member of this guild."
|
|
);
|
|
}
|
|
|
|
/// Request and Rename replace Sign; the buttons share one anchor, so both halves are asserted.
|
|
#[test]
|
|
fn the_owners_charter_swaps_sign_for_request_and_rename() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = setup();
|
|
s.run("BenillaPetitionFixture.isOriginator = 1").unwrap();
|
|
show_petition(&mut s);
|
|
|
|
assert!(visible(&s, "PetitionFrameRequestButton"));
|
|
assert!(visible(&s, "PetitionFrameRenameButton"));
|
|
assert!(!visible(&s, "PetitionFrameSignButton"));
|
|
assert_eq!(
|
|
text(&s, "PetitionFrameInstructions"),
|
|
"Select a player you wish to invite and click <request signature>. To create this \
|
|
guild, turn it in to the guild registrar when you have filled the charter."
|
|
);
|
|
}
|
|
|
|
/// Request disables at the wire's requirement, 4 here, not at the nine rows
|
|
/// (`PetitionFrame.lua:38`).
|
|
#[test]
|
|
fn request_signature_disables_at_the_wires_requirement_not_at_nine() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = setup();
|
|
s.run(
|
|
r#"
|
|
BenillaPetitionFixture.isOriginator = 1
|
|
BenillaPetitionFixture.signers = { "A", "B", "C" }
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
show_petition(&mut s);
|
|
assert!(
|
|
s.eval::<bool>("return PetitionFrameRequestButton:IsEnabled() ~= 0")
|
|
.unwrap(),
|
|
"three of four signatures — still asking"
|
|
);
|
|
|
|
s.run(r#"BenillaPetitionFixture.signers = { "A", "B", "C", "D" }"#)
|
|
.unwrap();
|
|
show_petition(&mut s);
|
|
assert!(
|
|
!s.eval::<bool>("return PetitionFrameRequestButton:IsEnabled() ~= 0")
|
|
.unwrap(),
|
|
"four of four — the charter is full, though only four of nine rows are used"
|
|
);
|
|
assert_eq!(
|
|
text(&s, "PetitionFrameMemberName5"),
|
|
"<not yet signed>",
|
|
"the nine rows are unaffected by the requirement"
|
|
);
|
|
}
|
|
|
|
/// A signer's view can still be unsignable: already signed, already guilded, or full.
|
|
#[test]
|
|
fn the_sign_button_follows_can_sign_petition() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = setup();
|
|
show_petition(&mut s);
|
|
assert!(s
|
|
.eval::<bool>("return PetitionFrameSignButton:IsEnabled() ~= 0")
|
|
.unwrap());
|
|
|
|
s.run("BenillaPetitionFixture.canSign = nil").unwrap();
|
|
show_petition(&mut s);
|
|
assert!(
|
|
!s.eval::<bool>("return PetitionFrameSignButton:IsEnabled() ~= 0")
|
|
.unwrap(),
|
|
"nil, the era false, disables it"
|
|
);
|
|
assert!(
|
|
visible(&s, "PetitionFrameSignButton"),
|
|
"disabled, not hidden — hiding would expose the Request button beneath it"
|
|
);
|
|
}
|
|
|
|
/// `ClosePetition` rides `OnHide`, so it fires however the window closes
|
|
/// (`PetitionFrame.xml:391-394`).
|
|
#[test]
|
|
fn the_charter_buttons_reach_their_verbs_and_the_close_clears_the_session() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = setup();
|
|
show_petition(&mut s);
|
|
let _ = calls(&s);
|
|
|
|
s.run("PetitionFrameSignButton:Click()").unwrap();
|
|
assert_eq!(calls(&s), "SignPetition");
|
|
|
|
s.run("BenillaPetitionFixture.isOriginator = 1").unwrap();
|
|
show_petition(&mut s);
|
|
let _ = calls(&s);
|
|
s.run("PetitionFrameRequestButton:Click()").unwrap();
|
|
assert_eq!(calls(&s), "OfferPetition");
|
|
|
|
s.run("HideUIPanel(PetitionFrame)").unwrap();
|
|
assert!(!visible(&s, "PetitionFrame"));
|
|
assert_eq!(calls(&s), "ClosePetition", "OnHide clears the session");
|
|
}
|
|
|
|
/// Stock `RENAME_GUILD` caps the name at 24 (`StaticPopup.lua:138`), the server's limit too.
|
|
#[test]
|
|
fn rename_guild_sends_the_box_text_and_caps_at_twenty_four() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = setup();
|
|
s.run("BenillaPetitionFixture.isOriginator = 1").unwrap();
|
|
show_petition(&mut s);
|
|
let _ = calls(&s);
|
|
|
|
s.run("PetitionFrameRenameButton:Click()").unwrap();
|
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
|
assert_eq!(
|
|
s.eval::<i64>("return StaticPopupDialogs[\"RENAME_GUILD\"].maxLetters")
|
|
.unwrap(),
|
|
24,
|
|
"the server's MAX_CHARTER_NAME"
|
|
);
|
|
assert!(
|
|
visible(&s, "StaticPopup1"),
|
|
"the popup engine raised the dialog"
|
|
);
|
|
s.run(
|
|
r#"
|
|
StaticPopup1EditBox:SetText("Second Legacy")
|
|
StaticPopup1Button1:Click()
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(calls(&s), "RenamePetition:Second Legacy");
|
|
}
|
|
|
|
/// Purchase only swaps panels; the buy waits for a typed name.
|
|
#[test]
|
|
fn the_registrar_opens_on_services_and_purchase_is_a_local_panel_swap() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = setup();
|
|
show_registrar(&mut s);
|
|
|
|
assert!(visible(&s, "GuildRegistrarFrame"));
|
|
assert!(visible(&s, "GuildRegistrarGreetingFrame"));
|
|
assert!(!visible(&s, "GuildRegistrarPurchaseFrame"));
|
|
// Visible, not merely labelled: a hidden button still answers `GetText()`.
|
|
for row in ["GuildRegistrarButton1", "GuildRegistrarButton2"] {
|
|
assert!(visible(&s, row), "{row} is on screen, not just loaded");
|
|
}
|
|
assert_eq!(
|
|
text(&s, "GuildRegistrarButton1"),
|
|
"Purchase a Guild Charter"
|
|
);
|
|
assert_eq!(
|
|
text(&s, "GuildRegistrarButton2"),
|
|
"Register a Guild Charter"
|
|
);
|
|
let _ = calls(&s);
|
|
|
|
s.run("GuildRegistrarButton1:Click()").unwrap();
|
|
assert!(visible(&s, "GuildRegistrarPurchaseFrame"));
|
|
assert!(!visible(&s, "GuildRegistrarGreetingFrame"));
|
|
for part in [
|
|
"GuildRegistrarPurchaseText",
|
|
"GuildRegistrarCostLabel",
|
|
"GuildRegistrarMoneyFrame",
|
|
"GuildRegistrarFrameEditBox",
|
|
"GuildRegistrarFramePurchaseButton",
|
|
"GuildRegistrarFrameCancelButton",
|
|
] {
|
|
assert!(visible(&s, part), "{part} is on screen");
|
|
}
|
|
assert_eq!(calls(&s), "", "the swap sends nothing");
|
|
}
|
|
|
|
/// The stock purchase button closes the window after the buy (`GuildRegistrarFrame.xml:269-270`).
|
|
#[test]
|
|
fn purchase_sends_the_typed_name_and_register_turns_the_charter_in() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = setup();
|
|
show_registrar(&mut s);
|
|
s.run("GuildRegistrarButton1:Click()").unwrap();
|
|
let _ = calls(&s);
|
|
|
|
s.run(
|
|
r#"
|
|
GuildRegistrarFrameEditBox:SetText("Legacy of Steel")
|
|
GuildRegistrarFramePurchaseButton:Click()
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
calls(&s),
|
|
"BuyGuildCharter:Legacy of Steel|CloseGuildRegistrar",
|
|
"the buy, then the window's own OnHide close"
|
|
);
|
|
assert!(!visible(&s, "GuildRegistrarFrame"));
|
|
|
|
show_registrar(&mut s);
|
|
let _ = calls(&s);
|
|
s.run("GuildRegistrarButton2:Click()").unwrap();
|
|
assert_eq!(
|
|
calls(&s),
|
|
"TurnInGuildCharter",
|
|
"no argument — the engine finds the charter in the bags"
|
|
);
|
|
}
|
|
|
|
/// Stock `GuildRegistrar_ShowPurchaseFrame` reads `GetGuildCharterCost()`, in copper, at the swap
|
|
/// (`GuildRegistrarFrame.lua:8-11`).
|
|
#[test]
|
|
fn the_charter_price_is_read_when_the_purchase_panel_opens() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = setup();
|
|
show_registrar(&mut s);
|
|
s.run("GuildRegistrarButton1:Click()").unwrap();
|
|
// 1000 copper = 10 silver: the gold slot is empty, the silver slot reads 10.
|
|
assert_eq!(text(&s, "GuildRegistrarMoneyFrameSilverButtonText"), "10");
|
|
assert_eq!(text(&s, "GuildRegistrarCostLabel"), "Cost:");
|
|
}
|
|
|
|
/// Stock `GuildRegistrar_OnShow` shows the greeting and hides the purchase panel
|
|
/// (`GuildRegistrarFrame.lua:1-3`).
|
|
#[test]
|
|
fn reopening_the_registrar_returns_to_the_services_list() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = setup();
|
|
show_registrar(&mut s);
|
|
s.run("GuildRegistrarButton1:Click()").unwrap();
|
|
assert!(visible(&s, "GuildRegistrarPurchaseFrame"));
|
|
|
|
s.fire_event("GUILD_REGISTRAR_CLOSED", vec![]);
|
|
assert!(!visible(&s, "GuildRegistrarFrame"));
|
|
show_registrar(&mut s);
|
|
assert!(visible(&s, "GuildRegistrarGreetingFrame"));
|
|
assert!(!visible(&s, "GuildRegistrarPurchaseFrame"));
|
|
}
|
|
|
|
/// Without a `UIPanelWindows` row, `ShowUIPanel` is a bare `Show()` and the windows overlap
|
|
/// (`UIParent.lua:33-34`).
|
|
#[test]
|
|
fn both_charter_windows_are_registered_left_slot_panels() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = setup();
|
|
for frame in ["GuildRegistrarFrame", "PetitionFrame"] {
|
|
assert_eq!(
|
|
s.eval::<String>(&format!("return UIPanelWindows[\"{frame}\"].area"))
|
|
.unwrap(),
|
|
"left",
|
|
"{frame} must hold a panel row"
|
|
);
|
|
}
|
|
// Both left with `pushable = 0`: one replaces the other.
|
|
show_registrar(&mut s);
|
|
show_petition(&mut s);
|
|
assert!(visible(&s, "PetitionFrame"));
|
|
assert!(
|
|
!visible(&s, "GuildRegistrarFrame"),
|
|
"one left slot, both pushable 0"
|
|
);
|
|
}
|