Files
benilla-octo/crates/benilla-app/tests/trade_frame.rs
T
gromandClaude Opus 5 81d96af35d Everwood: the HD/OctoWoW line of benilla, imported onto upstream
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
2026-10-15 15:01:14 -06:00

348 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Drives the stock `TradeFrame.xml` through the engine: pushes a synthetic two-sided offer, opens
//! the window with `TRADE_SHOW`, and asserts the Lua paints both columns, the money and the accept
//! glows that follow `TRADE_ACCEPT_UPDATE(my, his)`.
mod common;
use benilla_ui::script::{ScriptValue, TradeSideState, TradeSlotItem, TradeState, UiScript};
/// The trade window's load prefix, in `assets/ui/benilla.toc` order.
const FILES: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
// `MoneyInputFrameTemplate` and the `MoneyInputFrame_*` functions the window calls.
r"Interface\FrameXML\MoneyInputFrame.lua",
r"Interface\FrameXML\MoneyInputFrame.xml",
r"Interface\FrameXML\UIParent.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", // the dialog engine
"Interface\\FrameXML\\GameTooltip.xml",
// The slot updates go through `ItemButtonTemplate.lua`'s `SetItemButton*` helpers.
r"Interface\FrameXML\ItemButtonTemplate.xml",
"Interface\\FrameXML\\TradeFrame.xml",
];
fn load_ui(script: &UiScript) {
for file in FILES {
common::load_ui(script, file);
}
}
fn item(item_id: u32, name: &str, count: u32, quality: u32) -> TradeSlotItem {
TradeSlotItem {
item_id,
name: Some(name.to_string()),
texture: Some("Interface\\Icons\\INV_Fabric_Linen_01".to_string()),
count,
quality: Some(quality),
enchantment: None,
link: Some(format!("|cffffffff|Hitem:{item_id}:0:0:0|h[{name}]|h|r")),
}
}
/// We offer Linen Cloth ×5 and 1g 23s 45c; the partner offers Silk Cloth and 5s.
fn state() -> TradeState {
let mut player = TradeSideState {
gold: 12_345,
..Default::default()
};
player.slots[0] = Some(item(2589, "Linen Cloth", 5, 1));
let mut target = TradeSideState {
gold: 500,
..Default::default()
};
target.slots[0] = Some(item(4306, "Silk Cloth", 1, 2));
TradeState {
player,
target,
partner_name: Some("Thrall".into()),
}
}
#[test]
fn trade_frame_loads_and_key_regions_exist() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
load_ui(&s);
for name in [
"TradeFrame",
"TradePlayerItem1",
"TradePlayerItem1ItemButton",
"TradePlayerItem7", // the enchant slot
"TradeRecipientItem1",
"TradeRecipientItem7",
"TradeFrameTradeButton",
"TradeFrameCancelButton",
"TradePlayerInputMoneyFrameGold", // our gold is the editable input
"TradeRecipientMoneyFrameCopperButton",
"TradeHighlightPlayer",
"TradeHighlightRecipientEnchant",
] {
assert!(
s.eval::<bool>(&format!("return getglobal('{name}') ~= nil"))
.unwrap(),
"region {name} should exist"
);
}
// The window is hidden until TRADE_SHOW.
assert!(!s.eval::<bool>("return TradeFrame:IsShown()").unwrap());
}
#[test]
fn trade_show_opens_and_both_columns_populate() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_trade(Some(state()));
// The stock header reads `UnitName("NPC")` (`TradeFrame.lua:43`); the app points the "npc"
// unit at the partner.
s.set_unit(
"npc",
Some(benilla_ui::script::UnitState {
exists: true,
name: Some("Thrall".into()),
..Default::default()
}),
);
s.fire_event("TRADE_SHOW", vec![]);
assert!(
s.eval::<bool>("return TradeFrame:IsShown()").unwrap(),
"the window opens on TRADE_SHOW"
);
// Slotted into the left panel by its `UIPanelWindows` row (`UIParent.lua:27`); unregistered,
// `ShowUIPanel` only calls `Show()` and the window never lands on screen.
assert_eq!(
s.eval::<String>("return GetLeftFrame() and GetLeftFrame():GetName() or ''")
.unwrap(),
"TradeFrame",
"TRADE_SHOW slots the window into the left panel"
);
assert_eq!(
s.eval::<String>("return TradePlayerItem1Name:GetText()")
.unwrap(),
"Linen Cloth"
);
assert_eq!(
s.eval::<String>("return TradeRecipientItem1Name:GetText()")
.unwrap(),
"Silk Cloth"
);
assert!(
s.eval::<bool>("return TradePlayerItem1ItemButtonIconTexture:IsShown()")
.unwrap(),
"a filled slot shows its icon"
);
// An empty slot clears its name (`SetText(nil)`) and hides its icon.
assert!(s
.eval::<Option<String>>("return TradePlayerItem2Name:GetText()")
.unwrap()
.unwrap_or_default()
.is_empty());
assert!(!s
.eval::<bool>("return TradePlayerItem2ItemButtonIconTexture:IsShown()")
.unwrap());
// The partner's name paints from `UnitName("NPC")`, set up above.
assert_eq!(
s.eval::<String>("return TradeFrameRecipientNameText:GetText()")
.unwrap(),
"Thrall"
);
// `TARGET_TRADE` collapses (`MoneyFrame.lua:69`), so 5s shows the silver coin alone.
assert!(s
.eval::<bool>(
"return TradeRecipientMoneyFrameSilverButton:IsShown() \
and not TradeRecipientMoneyFrameGoldButton:IsShown() \
and not TradeRecipientMoneyFrameCopperButton:IsShown()"
)
.unwrap());
assert!(s.take_errors().is_empty(), "clean repaint");
}
#[test]
fn enchant_slot_shows_the_not_traded_note() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
let mut st = state();
// Our enchant slot: index 6, slot 7.
st.player.slots[6] = Some(item(6217, "Copper Rod", 1, 1));
s.set_trade(Some(st));
s.fire_event("TRADE_SHOW", vec![]);
assert_eq!(
s.eval::<String>("return TradePlayerItem7Name:GetText()")
.unwrap(),
// `TRADEFRAME_NOT_MODIFIED_TEXT` (`TradeFrame.lua:64`).
"|cffffffffItem not yet modified|r"
);
assert!(s.take_errors().is_empty());
}
#[test]
fn accept_update_drives_the_column_glows() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_trade(Some(state()));
s.fire_event("TRADE_SHOW", vec![]);
// `TradeFrame_Update` hides all four highlights.
assert!(!s
.eval::<bool>("return TradeHighlightRecipient:IsShown()")
.unwrap());
// The partner accepts: their column and enchant glow show, ours stay hidden.
s.fire_event(
"TRADE_ACCEPT_UPDATE",
vec![ScriptValue::Int(0), ScriptValue::Int(1)],
);
assert!(s
.eval::<bool>("return TradeHighlightRecipient:IsShown()")
.unwrap());
assert!(s
.eval::<bool>("return TradeHighlightRecipientEnchant:IsShown()")
.unwrap());
assert!(!s
.eval::<bool>("return TradeHighlightPlayer:IsShown()")
.unwrap());
// We accept: our glow shows and the Trade button disables.
s.fire_event(
"TRADE_ACCEPT_UPDATE",
vec![ScriptValue::Int(1), ScriptValue::Int(0)],
);
assert!(s
.eval::<bool>("return TradeHighlightPlayer:IsShown()")
.unwrap());
assert!(!s
.eval::<bool>("return TradeFrameTradeButton:IsEnabled() ~= 0")
.unwrap());
assert!(s.take_errors().is_empty());
}
#[test]
fn closing_the_window_queues_the_cancel() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_trade(Some(state()));
s.fire_event("TRADE_SHOW", vec![]);
let _ = s.take_trade_close();
// The X hides the window, and its OnHide's `CloseTrade` queues the close.
s.run("TradeFrameCloseButton:Click()").unwrap();
assert!(!s.eval::<bool>("return TradeFrame:IsShown()").unwrap());
assert!(s.take_trade_close(), "closing queued the CloseTrade intent");
assert!(s.take_errors().is_empty());
// A server-driven `TRADE_CLOSED` also hides it.
s.fire_event("TRADE_SHOW", vec![]);
assert!(s.eval::<bool>("return TradeFrame:IsShown()").unwrap());
s.fire_event("TRADE_CLOSED", vec![]);
assert!(!s.eval::<bool>("return TradeFrame:IsShown()").unwrap());
}
#[test]
fn trade_button_click_queues_accept() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_trade(Some(state()));
s.fire_event("TRADE_SHOW", vec![]);
let _ = s.take_trade_accept();
s.run("TradeFrameTradeButton:Click()").unwrap();
assert!(s.take_trade_accept(), "the Trade button queues AcceptTrade");
assert!(s.take_errors().is_empty());
}
/// `PLAYER_TRADE_MONEY` fills the three boxes from `GetPlayerTradeMoney()` without re-offering,
/// and a keystroke offers the copper total through `SetTradeMoney`. vmangos never echoes our own
/// gold (it sends it to the partner only, `TradeData.cpp:118`); the app sets it client-side and
/// fires the event.
#[test]
fn player_money_input_reflects_then_offers() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_money(1_000_000); // SetTradeMoney is purse-gated
load_ui(&s);
s.set_trade(Some(state())); // state().player.gold == 12345 (1g 23s 45c)
s.fire_event("TRADE_SHOW", vec![]);
// The three numeric boxes exist and start empty.
for box_ in ["Gold", "Silver", "Copper"] {
assert!(
s.eval::<bool>(&format!(
"return getglobal('TradePlayerInputMoneyFrame{box_}') ~= nil"
))
.unwrap(),
"money box {box_} exists"
);
}
// The programmatic fill must not re-offer.
let _ = s.take_trade_money();
s.fire_event("PLAYER_TRADE_MONEY", vec![]);
assert_eq!(
s.eval::<(String, String, String)>(
"return TradePlayerInputMoneyFrameGold:GetText(), \
TradePlayerInputMoneyFrameSilver:GetText(), \
TradePlayerInputMoneyFrameCopper:GetText()"
)
.unwrap(),
("1".into(), "23".into(), "45".into()),
"the event fills 1g 23s 45c into the boxes"
);
assert_eq!(
s.take_trade_money(),
None,
"the guarded reflect does not re-offer"
);
// Lift the affordability clamp (`TradeFrame.lua:164`) for a typed offer.
s.run("GetMoney = function() return 100000000 end").unwrap();
s.run("TradePlayerInputMoneyFrameGold:SetText('2')")
.unwrap();
s.tick(0.0); // the deferred OnTextChanged drains here
assert_eq!(
s.take_trade_money(),
Some(2 * 10000 + 23 * 100 + 45),
"typing offers the copper total via SetTradeMoney"
);
assert!(s.take_errors().is_empty());
}
/// With an empty cursor, clicking our filled slot clears it (`ClickTradeButton`), while the
/// partner's column calls `ClickTargetTradeButton`, which queues nothing (`TradeFrame.xml:101`).
#[test]
fn slot_click_routes_player_to_clear_and_recipient_to_inert() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_trade(Some(state())); // both slot 1s are filled
s.fire_event("TRADE_SHOW", vec![]);
let _ = s.take_trade_clear_items();
s.run("TradePlayerItem1ItemButton:Click()").unwrap();
assert_eq!(
s.take_trade_clear_items(),
vec![1],
"clicking our filled slot clears it (ClickTradeButton)"
);
s.run("TradeRecipientItem1ItemButton:Click()").unwrap();
assert!(
s.take_trade_clear_items().is_empty() && s.take_trade_set_items().is_empty(),
"the partner slot is inert (ClickTargetTradeButton)"
);
assert!(s.take_errors().is_empty());
}