the world map blips are the reference's, and particles go untinted

- the world map speaks the reference's names: WorldMapTooltip is declared (six corpus addons index it; pfUI dies on it at file scope), the blackout frame takes the reference's spelling BlackoutWorld, and the four hand-written party blips become the reference's own unit-blip templates — all 44 blips as instances, each with its published $parentIcon, the name pfUI recolours group blips by class through; the 40 raid ones are declared but not positioned yet, named in the OUT list rather than faked
- the templates addons build their own buttons from are published: ItemButtonTemplate, ContainerFrameItemButtonTemplate and the bag-slot chain — most of the handler scripts were live the whole time (this client executes the reference's own container and paper-doll code off the player's patch chain), so what was missing was mostly declarations, not behaviour — the bank arm stays out and is named as the omission it is; OptionsFrameSliders becomes real, with the three Graphics rows reading it and re-reading on show, so an addon's write to a slider's bounds actually moves the slider; and the template census that had ranked these at "zero demand" learns to see a local bound to a literal — its table went from 3 rows to 7
- the bindings answer in 1.12's own shapes: Button:IsEnabled() answers the NUMBER 1 or 0 (0 is truthy in Lua, so the reference's own == 0 tests got false forever, and three of our files had hand-worked around the divergence rather than fixed it); the constructors' string arguments are four shapes, not one rule — pfUI passes a Button where a template name goes and the reference constructs the FontString and ignores it; SetInventoryItem returns THREE values, the reference's own paper doll destructuring repairCost; UnitIsVisible is object presence and nothing else (57 bytes, one branch, no distance — an out-of-range party member is exists-yes, visible-nil); the tapped pair reads the two dynamic-flags bits, and its repaint wire fires UNIT_FACTION on the tap bit — the half a passing predicate test never sees; UnitIsPartyLeader is two ORed legs covering disjoint failures (a held stranger leading their own party, and an out-of-range member with no object to read a flag from); and the four nameplate verbs take no argument and return zero values
- the ghost's wisps compose onto its body instead of over it — the four emitters drew at full strength over a body at alpha 0.5, and now inherit the instance alpha down the attachment chain the way the reference recomposes it per frame; the residual we had parked ("the wisps should inherit the tint too") is REFUTED at the bytes: a particle batch commits hardcoded white RGB, and even that never reaches the fragment — particles are never tinted, only the alpha crosses, applied to the alpha byte alone — so the shipped shader split was already right and no code changes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-31 20:15:16 +07:00
co-authored by Claude Fable 5
parent 961d7e46a0
commit a0d2fc383b
68 changed files with 2186 additions and 272 deletions
@@ -133,7 +133,7 @@ fn resurrect_request_picks_variant_and_answers() {
"the offerer name formats into the no-sickness text"
);
assert!(s
.eval::<bool>("return StaticPopup1Button1:IsEnabled()")
.eval::<bool>("return StaticPopup1Button1:IsEnabled() ~= 0")
.unwrap());
s.run("StaticPopup_OnClick(StaticPopup1, 1)").unwrap();
assert_eq!(s.take_death_actions(), vec![DeathAction::AcceptResurrect]);
@@ -341,7 +341,7 @@ fn corpse_range_events_drive_recover_corpse() {
"RECOVER_CORPSE"
);
assert!(
!s.eval::<bool>("return StaticPopup1Button1:IsEnabled()")
!s.eval::<bool>("return StaticPopup1Button1:IsEnabled() ~= 0")
.unwrap(),
"Accept is delay-gated"
);
@@ -358,7 +358,7 @@ fn corpse_range_events_drive_recover_corpse() {
"Resurrect now?"
);
assert!(s
.eval::<bool>("return StaticPopup1Button1:IsEnabled()")
.eval::<bool>("return StaticPopup1Button1:IsEnabled() ~= 0")
.unwrap());
s.run("StaticPopup_OnClick(StaticPopup1, 1)").unwrap();
assert_eq!(s.take_death_actions(), vec![DeathAction::RetrieveCorpse]);
@@ -91,12 +91,12 @@ fn a_fresh_faux_frame_loads_at_the_top_with_no_range() {
.unwrap();
assert_eq!((lo, hi), (0.0, 0.0));
assert!(
!s.eval::<bool>("return TestScrollScrollBarScrollUpButton:IsEnabled()")
!s.eval::<bool>("return TestScrollScrollBarScrollUpButton:IsEnabled() ~= 0")
.unwrap(),
"up arrow greyed at load (ref UIPanelTemplates.lua l.245-246)"
);
assert!(!s
.eval::<bool>("return TestScrollScrollBarScrollDownButton:IsEnabled()")
.eval::<bool>("return TestScrollScrollBarScrollDownButton:IsEnabled() ~= 0")
.unwrap());
}
@@ -132,12 +132,12 @@ fn more_rows_than_fit_raise_the_bar_over_the_overflow_range() {
"the frame itself too — the ref's own frame:Show(), which is what an addon reads back"
);
assert!(
!s.eval::<bool>("return TestScrollScrollBarScrollUpButton:IsEnabled()")
!s.eval::<bool>("return TestScrollScrollBarScrollUpButton:IsEnabled() ~= 0")
.unwrap(),
"at the top: up greyed"
);
assert!(s
.eval::<bool>("return TestScrollScrollBarScrollDownButton:IsEnabled()")
.eval::<bool>("return TestScrollScrollBarScrollDownButton:IsEnabled() ~= 0")
.unwrap());
}
@@ -186,12 +186,6 @@ const KNOWN: &[Known] = &[
why: "the reference's /who sort dropdown carries its own handlers over the shared \
UIDropDownMenuTemplate; ours takes the template alone.",
},
Known {
frame: "WorldMapPlayer",
flag: Flag::Mouse,
why: "the reference's map dot has OnEnter -> WorldMapUnit_OnEnter, the name-on-hover \
tooltip. Wants that handler, not the flag.",
},
Known {
frame: "WorldStateAlwaysUpFrame",
flag: Flag::Mouse,
@@ -216,8 +216,10 @@ fn the_friend_buttons_follow_the_selection() {
push(&mut s, SocialState::default(), "FRIENDLIST_UPDATE");
for button in ["SendMessage", "GroupInvite", "RemoveFriend"] {
assert!(
!s.eval::<bool>(&format!("return FriendsFrame{button}Button:IsEnabled()"))
.unwrap(),
!s.eval::<bool>(&format!(
"return FriendsFrame{button}Button:IsEnabled() ~= 0"
))
.unwrap(),
"{button} is disabled with no friends"
);
}
@@ -232,12 +234,12 @@ fn the_friend_buttons_follow_the_selection() {
"FRIENDLIST_UPDATE",
);
assert!(
s.eval::<bool>("return FriendsFrameRemoveFriendButton:IsEnabled()")
s.eval::<bool>("return FriendsFrameRemoveFriendButton:IsEnabled() ~= 0")
.unwrap(),
"an offline friend can still be removed"
);
assert!(
!s.eval::<bool>("return FriendsFrameSendMessageButton:IsEnabled()")
!s.eval::<bool>("return FriendsFrameSendMessageButton:IsEnabled() ~= 0")
.unwrap(),
"…but not whispered"
);
@@ -459,14 +461,14 @@ fn the_who_buttons_need_a_selected_row() {
"WHO_LIST_UPDATE",
);
assert!(
!s.eval::<bool>("return WhoFrameAddFriendButton:IsEnabled()")
!s.eval::<bool>("return WhoFrameAddFriendButton:IsEnabled() ~= 0")
.unwrap(),
"nothing selected yet"
);
s.run("WhoFrameButton2:Click()").unwrap();
assert!(s
.eval::<bool>("return WhoFrameAddFriendButton:IsEnabled()")
.eval::<bool>("return WhoFrameAddFriendButton:IsEnabled() ~= 0")
.unwrap());
let _ = s.take_social_requests();
s.run("WhoFrameAddFriendButton:Click()").unwrap();
@@ -485,7 +487,7 @@ fn the_who_buttons_need_a_selected_row() {
"WHO_LIST_UPDATE",
);
assert!(
!s.eval::<bool>("return WhoFrameAddFriendButton:IsEnabled()")
!s.eval::<bool>("return WhoFrameAddFriendButton:IsEnabled() ~= 0")
.unwrap(),
"a fresh answer drops the old selection"
);
@@ -663,7 +665,7 @@ fn right_clicking_a_who_row_opens_the_friend_menu() {
);
// …and a right-click must NOT also select the row (the ref's two branches are exclusive).
assert!(
!s.eval::<bool>("return WhoFrameAddFriendButton:IsEnabled()")
!s.eval::<bool>("return WhoFrameAddFriendButton:IsEnabled() ~= 0")
.unwrap(),
"right-click does not select"
);
@@ -131,7 +131,7 @@ fn the_unbacked_entries_are_disabled_and_the_rest_are_live() {
for name in ["GameMenuButtonEditMode", "GameMenuButtonSupport"] {
assert!(
!s.eval::<bool>(&format!("return {name}:IsEnabled()"))
!s.eval::<bool>(&format!("return {name}:IsEnabled() ~= 0"))
.unwrap(),
"{name} has no panel behind it and must read that way"
);
@@ -144,7 +144,7 @@ fn the_unbacked_entries_are_disabled_and_the_rest_are_live() {
"GameMenuButtonContinue",
] {
assert!(
s.eval::<bool>(&format!("return {name}:IsEnabled()"))
s.eval::<bool>(&format!("return {name}:IsEnabled() ~= 0"))
.unwrap(),
"{name} is live"
);
@@ -468,12 +468,12 @@ fn logout_and_exit_read_disabled_while_a_countdown_runs() {
s.fire_event("PLAYER_CAMPING", vec![]);
s.run("ToggleGameMenu(1)").unwrap();
assert!(
!s.eval::<bool>("return GameMenuButtonLogout:IsEnabled()")
!s.eval::<bool>("return GameMenuButtonLogout:IsEnabled() ~= 0")
.unwrap(),
"Logout is dead while the camp timer runs"
);
assert!(!s
.eval::<bool>("return GameMenuButtonQuit:IsEnabled()")
.eval::<bool>("return GameMenuButtonQuit:IsEnabled() ~= 0")
.unwrap());
// With the countdown gone, a re-opened menu has them back.
@@ -481,7 +481,7 @@ fn logout_and_exit_read_disabled_while_a_countdown_runs() {
s.fire_event("LOGOUT_CANCEL", vec![]);
s.run("ToggleGameMenu(1)").unwrap();
assert!(s
.eval::<bool>("return GameMenuButtonLogout:IsEnabled()")
.eval::<bool>("return GameMenuButtonLogout:IsEnabled() ~= 0")
.unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -690,7 +690,7 @@ fn the_bag_row_greys_under_the_menu_without_any_of_it_disappearing() {
"closing the menu restores full colour: {toggle:?}"
);
assert!(
s.eval::<bool>("return MainMenuBarBackpackButton:IsEnabled()")
s.eval::<bool>("return MainMenuBarBackpackButton:IsEnabled() ~= 0")
.unwrap(),
"…and the button works again"
);
@@ -273,7 +273,7 @@ fn visible(s: &UiScript, frame: &str) -> bool {
}
fn enabled(s: &UiScript, button: &str) -> bool {
s.eval::<bool>(&format!("return {button}:IsEnabled()"))
s.eval::<bool>(&format!("return {button}:IsEnabled() ~= 0"))
.unwrap_or_else(|e| panic!("{button}:IsEnabled() — {e}"))
}
@@ -479,7 +479,7 @@ fn the_active_tab_is_inert_and_toggle_inspect_closes() {
// NB this engine's `IsEnabled` returns a plain bool, not the live API's 1/nil.
assert!(
!s.eval::<bool>("return BenillaInspectFrameTab1:IsEnabled()")
!s.eval::<bool>("return BenillaInspectFrameTab1:IsEnabled() ~= 0")
.unwrap(),
"the selected tab is disabled (PanelTemplates_SelectTab), so its click is inert"
);
@@ -101,12 +101,12 @@ fn the_page_is_an_options_category_with_the_collapsed_honest_tree() {
"Unbind Key exists on this page"
);
assert!(
!s.eval::<bool>("return OptionsFrameContainerUnbind:IsEnabled()")
!s.eval::<bool>("return OptionsFrameContainerUnbind:IsEnabled() ~= 0")
.unwrap(),
"…disabled until a capsule is selected"
);
assert!(s
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled()")
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled() ~= 0")
.unwrap());
// The section tree: the registry's category tokens, first-appearance order — exactly
// 1.12's file order — every section a COLLAPSED header row (the era default).
@@ -196,7 +196,7 @@ fn the_capture_flow_binds_steals_and_refuses_like_112() {
"a selected capsule arms the capture"
);
assert!(
s.eval::<bool>("return OptionsFrameContainerUnbind:IsEnabled()")
s.eval::<bool>("return OptionsFrameContainerUnbind:IsEnabled() ~= 0")
.unwrap(),
"Unbind arms with the selection"
);
@@ -94,7 +94,7 @@ fn new_then_pick_an_icon_then_okay_creates_the_macro() {
.eval::<bool>("return BenillaMacroPopupFrame:IsVisible()")
.unwrap());
assert!(
!s.eval::<bool>("return BenillaMacroPopupOkayButton:IsEnabled()")
!s.eval::<bool>("return BenillaMacroPopupOkayButton:IsEnabled() ~= 0")
.unwrap(),
"a nameless, iconless macro cannot be created"
);
@@ -104,7 +104,7 @@ fn new_then_pick_an_icon_then_okay_creates_the_macro() {
s.run("BenillaMacroPopupButton_OnClick(BenillaMacroPopupButton1)")
.unwrap();
assert!(
s.eval::<bool>("return BenillaMacroPopupOkayButton:IsEnabled()")
s.eval::<bool>("return BenillaMacroPopupOkayButton:IsEnabled() ~= 0")
.unwrap(),
"a name and an icon enable OKAY"
);
@@ -974,7 +974,8 @@ fn merchant_tabs_drive_buyback_page_and_repair_pair() {
.unwrap());
assert!(s
.eval::<bool>(
"return MerchantRepairAllButton:IsShown() and MerchantRepairAllButton:IsEnabled()"
"return MerchantRepairAllButton:IsShown() and \
MerchantRepairAllButton:IsEnabled() ~= 0"
)
.unwrap());
assert!(s
@@ -24,7 +24,7 @@ fn load_xml(s: &UiScript, file: &str) {
}
fn enabled(s: &UiScript, button: &str) -> bool {
s.eval::<bool>(&format!("return {button}:IsEnabled()"))
s.eval::<bool>(&format!("return {button}:IsEnabled() ~= 0"))
.unwrap()
}
@@ -808,7 +808,7 @@ fn the_audio_page_reads_the_cvar_table_on_select() {
.eval::<bool>("return OptionsFrameContainerBodyAudio:IsVisible()")
.unwrap());
assert!(s
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled()")
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled() ~= 0")
.unwrap());
// The music slider holds the stored 0.7 (f32 wobble tolerated), readout "70%".
assert!(s
@@ -894,10 +894,12 @@ fn the_checkbox_rows_write_flags_and_the_master_greys_ambience() {
vec![("MasterSoundEffects".to_string(), "0".to_string())]
);
assert!(!s
.eval::<bool>("return OptionsFrameContainerBodyAudioRowEnableAmbienceCheck:IsEnabled()")
.eval::<bool>(
"return OptionsFrameContainerBodyAudioRowEnableAmbienceCheck:IsEnabled() ~= 0"
)
.unwrap());
assert!(s
.eval::<bool>("return OptionsFrameContainerBodyAudioRowEnableMusicCheck:IsEnabled()")
.eval::<bool>("return OptionsFrameContainerBodyAudioRowEnableMusicCheck:IsEnabled() ~= 0")
.unwrap());
assert!(s
.take_sounds()
@@ -911,7 +913,9 @@ fn the_checkbox_rows_write_flags_and_the_master_greys_ambience() {
vec![("MasterSoundEffects".to_string(), "1".to_string())]
);
assert!(s
.eval::<bool>("return OptionsFrameContainerBodyAudioRowEnableAmbienceCheck:IsEnabled()")
.eval::<bool>(
"return OptionsFrameContainerBodyAudioRowEnableAmbienceCheck:IsEnabled() ~= 0"
)
.unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -969,7 +973,7 @@ fn the_graphics_page_reads_the_cvar_table_on_select() {
.eval::<bool>("return OptionsFrameContainerBodyGraphics:IsVisible()")
.unwrap());
assert!(s
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled()")
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled() ~= 0")
.unwrap());
assert!(s
.eval::<bool>(
@@ -1216,7 +1220,7 @@ fn the_nameplates_page_toggles_the_unit_name_cvars() {
.eval::<bool>("return OptionsFrameContainerBodyNameplates:IsVisible()")
.unwrap());
assert!(s
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled()")
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled() ~= 0")
.unwrap());
// The plate pair leads the page and rides `VPlateMode::default()`: enemy on (the 0167 boot
// divergence), friendly off (faithful, 0599).
@@ -1321,7 +1325,7 @@ fn the_ui_scale_slider_defers_to_the_apply_button() {
.eval::<bool>("return OptionsFrameApplyButton:IsVisible()")
.unwrap());
assert!(s
.eval::<bool>("return OptionsFrameApplyButton:IsEnabled()")
.eval::<bool>("return OptionsFrameApplyButton:IsEnabled() ~= 0")
.unwrap());
// A second move re-stages the pending value — still nothing queues (0989: the steppers
@@ -1600,7 +1604,7 @@ fn the_controls_page_reads_flags_with_the_sticky_inversion() {
.eval::<bool>("return OptionsFrameContainerBodyControls:IsVisible()")
.unwrap());
assert!(s
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled()")
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled() ~= 0")
.unwrap());
assert!(
s.eval::<bool>("return OptionsFrameContainerBodyControlsRowStickyTargetCheck:GetChecked()")
@@ -2274,7 +2278,7 @@ fn the_combat_page_writes_saved_variable_globals_and_applies_them() {
.eval::<bool>("return OptionsFrameContainerBodyCombat:IsVisible()")
.unwrap());
assert!(s
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled()")
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled() ~= 0")
.unwrap());
// Read: each box shows its global, at CombatText.xml's own file-scope value.
@@ -2351,7 +2355,7 @@ fn the_combat_master_greys_the_family_and_combo_points_is_class_gated() {
let enabled = |s: &mut UiScript, row: &str, control: &str| -> bool {
s.eval::<bool>(&format!(
"return OptionsFrameContainerBodyCombat{row}{control}:IsEnabled() and true or false"
"return OptionsFrameContainerBodyCombat{row}{control}:IsEnabled() ~= 0"
))
.unwrap()
};
@@ -2575,7 +2579,7 @@ fn the_interface_page_writes_the_three_stock_globals() {
.eval::<bool>("return OptionsFrameContainerBodyInterface:IsVisible()")
.unwrap());
assert!(s
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled()")
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled() ~= 0")
.unwrap());
// Read: all three ship on — QuestFrame.xml's "1" (the director's pin), QuestLogFrame.xml's "1"
@@ -2657,7 +2661,7 @@ fn the_target_of_target_rows_gate_each_other_and_write_their_globals() {
assert!(
!s.eval::<bool>(
"return OptionsFrameContainerBodyInterfaceRowTargetOfTargetModeDropdownButton \
:IsEnabled() and true or false"
:IsEnabled() ~= 0"
)
.unwrap(),
"the picker is dead while the switch is off"
@@ -2678,7 +2682,7 @@ fn the_target_of_target_rows_gate_each_other_and_write_their_globals() {
assert!(
s.eval::<bool>(
"return OptionsFrameContainerBodyInterfaceRowTargetOfTargetModeDropdownButton \
:IsEnabled() and true or false"
:IsEnabled() ~= 0"
)
.unwrap(),
"and the picker wakes with it"
@@ -3055,7 +3059,7 @@ fn the_defaults_button_is_armed_by_rows_not_by_a_category() {
s.run(&format!("OptionsFrameCategoryListRow{key}:Click()"))
.unwrap();
assert!(
s.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled()")
s.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled() ~= 0")
.unwrap(),
"{key}: Defaults is live on a page that has something to reset"
);
@@ -3065,7 +3069,7 @@ fn the_defaults_button_is_armed_by_rows_not_by_a_category() {
s.run("OptionsFrame_SelectCategory(\"NotACategory\")")
.unwrap();
assert!(
!s.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled()")
!s.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled() ~= 0")
.unwrap(),
"Defaults is dead when the selected page has no rows"
);
@@ -3093,7 +3097,7 @@ fn the_chat_page_toggles_the_chat_bubble_cvars() {
.eval::<bool>("return OptionsFrameContainerBodyChat:IsVisible()")
.unwrap());
assert!(s
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled()")
.eval::<bool>("return OptionsFrameContainerDefaults:IsEnabled() ~= 0")
.unwrap());
// Read from the table, not from a restated default: bubbles on, party bubbles off.
assert!(s
@@ -3579,8 +3583,11 @@ fn the_action_bars_page_toggles_the_real_bars() {
// Bar 4's row is DEAD while bar 3's is unticked — MultiBarLeft cannot stand without
// MultiBarRight (the reference's own rule for this pair, UIOptionsFrame.lua l.722-726).
assert!(
!s.eval::<bool>(&format!("return {}:IsEnabled()", box_of("RowMultiBar4")))
.unwrap(),
!s.eval::<bool>(&format!(
"return {}:IsEnabled() ~= 0",
box_of("RowMultiBar4")
))
.unwrap(),
"Show Right ActionBar 2 is disabled until Show Right ActionBar is on"
);
@@ -3607,8 +3614,11 @@ fn the_action_bars_page_toggles_the_real_bars() {
s.run(&format!("{}:Click()", box_of("RowMultiBar3")))
.unwrap();
assert!(
s.eval::<bool>(&format!("return {}:IsEnabled()", box_of("RowMultiBar4")))
.unwrap(),
s.eval::<bool>(&format!(
"return {}:IsEnabled() ~= 0",
box_of("RowMultiBar4")
))
.unwrap(),
"bar 3 on wakes bar 4's row"
);
assert!(shown(&s, "MultiBarRight"));
@@ -3630,8 +3640,11 @@ fn the_action_bars_page_toggles_the_real_bars() {
assert!(!shown(&s, "MultiBarLeft"), "MultiBarLeft rides on bar 3");
assert_eq!(s.eval::<i64>("return SHOW_MULTI_ACTIONBAR_4").unwrap(), 1);
assert!(
!s.eval::<bool>(&format!("return {}:IsEnabled()", box_of("RowMultiBar4")))
.unwrap(),
!s.eval::<bool>(&format!(
"return {}:IsEnabled() ~= 0",
box_of("RowMultiBar4")
))
.unwrap(),
"…and its row goes back to sleep"
);
@@ -343,7 +343,7 @@ fn a_scroll_frame_from_the_template_wires_its_bar_two_parents_deep() {
(0.0, 0.0)
);
assert!(!s
.eval::<bool>("return MyScrollScrollBarScrollUpButton:IsEnabled()")
.eval::<bool>("return MyScrollScrollBarScrollUpButton:IsEnabled() ~= 0")
.unwrap());
assert_eq!(s.eval::<i64>("return MyScroll.offset").unwrap(), 0);
@@ -369,7 +369,7 @@ fn a_scroll_frame_from_the_template_wires_its_bar_two_parents_deep() {
"192px of content in an 80px window — the bar's range is the overflow"
);
assert!(
s.eval::<bool>("return MyScrollScrollBarScrollDownButton:IsEnabled()")
s.eval::<bool>("return MyScrollScrollBarScrollDownButton:IsEnabled() ~= 0")
.unwrap(),
"there is somewhere to scroll to, so the down arrow woke"
);
@@ -382,7 +382,7 @@ fn a_scroll_frame_from_the_template_wires_its_bar_two_parents_deep() {
48.0
);
assert!(s
.eval::<bool>("return MyScrollScrollBarScrollUpButton:IsEnabled()")
.eval::<bool>("return MyScrollScrollBarScrollUpButton:IsEnabled() ~= 0")
.unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -111,8 +111,8 @@ fn shipped_gossip_frame_drives_end_to_end() {
// so the shared pool starts filling from option 1 at row 1 — decision 0088 §3).
let states: (bool, bool, bool, bool) = s
.eval(
"return GossipRow1:IsVisible(), GossipRow1:IsEnabled(),\n\
GossipRow2:IsEnabled(), GossipRow3:IsVisible()",
"return GossipRow1:IsVisible(), GossipRow1:IsEnabled() ~= 0,\n\
GossipRow2:IsEnabled() ~= 0, GossipRow3:IsVisible()",
)
.unwrap();
assert_eq!(
@@ -255,7 +255,7 @@ fn request_signature_disables_at_the_wires_requirement_not_at_nine() {
.unwrap();
show_petition(&mut s);
assert!(
s.eval::<bool>("return PetitionFrameRequestButton:IsEnabled() and true or false")
s.eval::<bool>("return PetitionFrameRequestButton:IsEnabled() ~= 0")
.unwrap(),
"three of four signatures — still asking"
);
@@ -264,7 +264,7 @@ fn request_signature_disables_at_the_wires_requirement_not_at_nine() {
.unwrap();
show_petition(&mut s);
assert!(
!s.eval::<bool>("return PetitionFrameRequestButton:IsEnabled() and true or false")
!s.eval::<bool>("return PetitionFrameRequestButton:IsEnabled() ~= 0")
.unwrap(),
"four of four — the charter is full, though only four of nine rows are used"
);
@@ -282,13 +282,13 @@ fn the_sign_button_follows_can_sign_petition() {
let mut s = setup();
show_petition(&mut s);
assert!(s
.eval::<bool>("return PetitionFrameSignButton:IsEnabled() and true or false")
.eval::<bool>("return PetitionFrameSignButton:IsEnabled() ~= 0")
.unwrap());
s.run("BenillaPetitionFixture.canSign = nil").unwrap();
show_petition(&mut s);
assert!(
!s.eval::<bool>("return PetitionFrameSignButton:IsEnabled() and true or false")
!s.eval::<bool>("return PetitionFrameSignButton:IsEnabled() ~= 0")
.unwrap(),
"nil, the era false, disables it"
);
@@ -434,7 +434,7 @@ fn detail_panel_action_buttons_resolve_to_real_onscreen_rects() {
"instant text keeps the writing sound: one scratch on the wake tick"
);
assert!(s
.eval::<bool>("return QuestAcceptButton:IsEnabled()")
.eval::<bool>("return QuestAcceptButton:IsEnabled() ~= 0")
.unwrap());
assert_eq!(
s.eval::<f32>("return QuestDetailTextAlphaFrame:GetAlpha()")
@@ -494,7 +494,7 @@ fn write_on_still_fades_when_instant_text_is_off() {
"the write-on scratches the WriteQuest quill each tick"
);
assert!(!s
.eval::<bool>("return QuestAcceptButton:IsEnabled()")
.eval::<bool>("return QuestAcceptButton:IsEnabled() ~= 0")
.unwrap());
// Half a second more runs the 13-char gradient off (40 chars/s): Accept wakes and the block
@@ -502,7 +502,7 @@ fn write_on_still_fades_when_instant_text_is_off() {
s.tick(0.5);
assert!(s.errors().is_empty(), "tick errors: {:?}", s.errors());
assert!(s
.eval::<bool>("return QuestAcceptButton:IsEnabled()")
.eval::<bool>("return QuestAcceptButton:IsEnabled() ~= 0")
.unwrap());
let alpha = s
.eval::<f32>("return QuestDetailTextAlphaFrame:GetAlpha()")
@@ -598,7 +598,7 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
);
assert!(!s.eval::<bool>("return QuestLogTitle1:IsVisible()").unwrap());
assert!(!s
.eval::<bool>("return QuestLogAbandonButton:IsEnabled()")
.eval::<bool>("return QuestLogAbandonButton:IsEnabled() ~= 0")
.unwrap());
// The Description header is now Lua-managed (BenillaQuestLogDetail_Clear blanks it) rather than
// a static `text=` — it used to float "Description" over the empty-log parchment with no
@@ -1226,7 +1226,7 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
"quest 1 carries the sharable bit"
);
assert!(
!s.eval::<bool>("return QuestLogPushQuestButton:IsEnabled()")
!s.eval::<bool>("return QuestLogPushQuestButton:IsEnabled() ~= 0")
.unwrap(),
"solo: sharable is not enough"
);
@@ -1235,7 +1235,7 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
s.set_party(party(2));
s.fire_event("PARTY_MEMBERS_CHANGED", vec![]);
assert!(
s.eval::<bool>("return QuestLogPushQuestButton:IsEnabled()")
s.eval::<bool>("return QuestLogPushQuestButton:IsEnabled() ~= 0")
.unwrap(),
"PARTY_MEMBERS_CHANGED alone re-tests the button"
);
@@ -1249,7 +1249,7 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
"quest 2 has no sharable bit"
);
assert!(
!s.eval::<bool>("return QuestLogPushQuestButton:IsEnabled()")
!s.eval::<bool>("return QuestLogPushQuestButton:IsEnabled() ~= 0")
.unwrap(),
"in a party: an unsharable selection is still dark"
);
@@ -1258,12 +1258,12 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
s.run("SelectQuestLogEntry(1)").unwrap();
s.run("BenillaQuestLogFrame_Update()").unwrap();
assert!(s
.eval::<bool>("return QuestLogPushQuestButton:IsEnabled()")
.eval::<bool>("return QuestLogPushQuestButton:IsEnabled() ~= 0")
.unwrap());
s.set_party(PartyState::default());
s.fire_event("PARTY_MEMBERS_CHANGED", vec![]);
assert!(!s
.eval::<bool>("return QuestLogPushQuestButton:IsEnabled()")
.eval::<bool>("return QuestLogPushQuestButton:IsEnabled() ~= 0")
.unwrap());
}
@@ -1284,7 +1284,7 @@ fn share_quest_is_dark_on_an_empty_log_even_in_a_party() {
s.set_party(party(4));
s.run("ToggleQuestLog()").unwrap();
assert!(!s
.eval::<bool>("return QuestLogPushQuestButton:IsEnabled()")
.eval::<bool>("return QuestLogPushQuestButton:IsEnabled() ~= 0")
.unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -118,6 +118,7 @@ fn push_raid(s: &mut UiScript, raid: Vec<RaidMemberInfo>) {
s.set_party(PartyState {
members,
leader_index: 0,
leader_guid: 0, // the player leads; their guid is unset in this fixture
raid,
loot_method: "group".into(),
master_looter: None,
@@ -218,7 +219,7 @@ fn convert_to_raid_is_live_only_for_a_party_leader() {
assert!(visible(&s, "RaidFrameRaidDescription"), "solo: the blurb");
assert!(visible(&s, "RaidFrameConvertToRaidButton"));
assert_eq!(
s.eval::<i64>("return RaidFrameConvertToRaidButton:IsEnabled() and 1 or 0")
s.eval::<i64>("return RaidFrameConvertToRaidButton:IsEnabled()")
.unwrap(),
0,
"solo there is no party to convert"
@@ -240,7 +241,7 @@ fn convert_to_raid_is_live_only_for_a_party_leader() {
});
s.fire_event("PARTY_MEMBERS_CHANGED", Vec::new());
assert_eq!(
s.eval::<i64>("return RaidFrameConvertToRaidButton:IsEnabled() and 1 or 0")
s.eval::<i64>("return RaidFrameConvertToRaidButton:IsEnabled()")
.unwrap(),
1,
"a party leader can convert"
@@ -263,7 +264,7 @@ fn convert_to_raid_is_live_only_for_a_party_leader() {
});
s.fire_event("PARTY_LEADER_CHANGED", Vec::new());
assert_eq!(
s.eval::<i64>("return RaidFrameConvertToRaidButton:IsEnabled() and 1 or 0")
s.eval::<i64>("return RaidFrameConvertToRaidButton:IsEnabled()")
.unwrap(),
0,
"a party member cannot"
@@ -714,7 +715,7 @@ fn the_raid_info_panel_lists_the_saved_lockouts() {
]);
s.fire_event("UPDATE_INSTANCE_INFO", Vec::new());
assert_eq!(
s.eval::<i64>("return RaidFrameRaidInfoButton:IsEnabled() and 1 or 0")
s.eval::<i64>("return RaidFrameRaidInfoButton:IsEnabled()")
.unwrap(),
1
);
@@ -743,7 +744,7 @@ fn the_raid_info_panel_lists_the_saved_lockouts() {
s.set_saved_instances(Vec::new());
s.fire_event("UPDATE_INSTANCE_INFO", Vec::new());
assert_eq!(
s.eval::<i64>("return RaidFrameRaidInfoButton:IsEnabled() and 1 or 0")
s.eval::<i64>("return RaidFrameRaidInfoButton:IsEnabled()")
.unwrap(),
0
);
@@ -781,7 +782,7 @@ fn a_player_with_no_lockouts_loses_the_raid_info_button_on_the_second_answer() {
// Answer two, saying the same nothing.
s.fire_event("UPDATE_INSTANCE_INFO", Vec::new());
assert_eq!(
s.eval::<i64>("return RaidFrameRaidInfoButton:IsEnabled() and 1 or 0")
s.eval::<i64>("return RaidFrameRaidInfoButton:IsEnabled()")
.unwrap(),
0,
"an empty lockout list is a dead button"
@@ -64,7 +64,28 @@ use bevy::prelude::*;
/// are a *contract addons call by name* AND benilla has no reason to compute them differently.
/// Anything we want to own — because our own windows are the consumer — belongs in `assets/ui` as
/// our own code, under the reference's names (`BagFrame.xml`'s `ToggleBackpack` is the pattern).
pub(crate) const SOURCED: &[&str] = &["Interface\\FrameXML\\ContainerFrame.lua"];
pub(crate) const SOURCED: &[&str] = &[
"Interface\\FrameXML\\ContainerFrame.lua",
// PaperDollFrame.lua earns its place for ONE family: `PaperDollItemSlotButton_OnLoad`/
// `_OnEvent`/`_OnClick`/`_Update`/`_UpdateLock`/`_OnEnter`/`_OnUpdate`. Every one of them is
// frame-agnostic in exactly the sense rule 2 above means — it acts on `this` and `this:GetID()`
// through live APIs, never on a named reference window — which is what makes it safe to hand
// an addon's own button. `PaperDollItemSlotButtonTemplate` and `BagSlotButtonTemplate` are
// declared over them (CharacterFrame.xml, BagFrame.xml), and pfUI builds its bag bar from the
// latter.
//
// Checked before adding, because this list is a decision:
// · TOP-LEVEL is four constants (NUM_RESISTANCE_TYPES, NUM_STATS, NUM_SHOPPING_TOOLTIPS,
// ATTACK_POWER_MAGIC_NUMBER). Nothing touches a frame at load, so sourcing cannot raise.
// · 18 of its 29 functions COLLIDE with ours (the PaperDollFrame_Set* stat family, the two
// damage-frame hovers, PaperDollFormatStat, PaperDollStatTooltip). Order settles all of
// them the documented way: this runs before assets/ui, so OUR bodies overwrite the
// reference's and keep driving our character sheet. Not one of the seven names we are here
// for is in that overlap.
// · Our own doll slots keep calling `BenillaPaperDollSlot_*` and are untouched — the sourced
// family is new surface for addons, not a replacement for ours.
"Interface\\FrameXML\\PaperDollFrame.lua",
];
/// Execute every [`SOURCED`] file into `script`, read off the install's patch chain.
///
@@ -467,7 +467,7 @@ fn clicking_a_bar_opens_the_detail_popup_on_that_faction() {
"at war, so the box is ticked"
);
assert!(
!s.eval::<bool>("return ReputationDetailAtWarCheckBox:IsEnabled() and true or false")
!s.eval::<bool>("return ReputationDetailAtWarCheckBox:IsEnabled() ~= 0")
.unwrap(),
"peace-forced, so the box is dead"
);
@@ -171,7 +171,8 @@ fn shipped_spellbook_drives_end_to_end() {
"Fire Blast"
);
assert!(
!s.eval::<bool>("return SpellButton2:IsEnabled()").unwrap(),
!s.eval::<bool>("return SpellButton2:IsEnabled() ~= 0")
.unwrap(),
"book id 7 is past the 2-spell Fire tab — disabled"
);
@@ -83,7 +83,7 @@ fn start_delay_gates_button1_then_swaps_the_text_in() {
.unwrap();
assert!(s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap());
assert!(
!s.eval::<bool>("return StaticPopup1Button1:IsEnabled()")
!s.eval::<bool>("return StaticPopup1Button1:IsEnabled() ~= 0")
.unwrap(),
"button1 starts disabled under StartDelay"
);
@@ -102,7 +102,7 @@ fn start_delay_gates_button1_then_swaps_the_text_in() {
"delay expiry swaps the real text in"
);
assert!(
s.eval::<bool>("return StaticPopup1Button1:IsEnabled()")
s.eval::<bool>("return StaticPopup1Button1:IsEnabled() ~= 0")
.unwrap(),
"delay expiry enables button1"
);
@@ -252,7 +252,7 @@ fn shipped_trainer_frame_drives_end_to_end() {
"Cost:"
);
assert!(s
.eval::<bool>("return BenillaTrainerTrainButton:IsEnabled()")
.eval::<bool>("return BenillaTrainerTrainButton:IsEnabled() ~= 0")
.unwrap());
// Train buys the selected service — the row's spell id reaches the app's drain.
@@ -266,7 +266,7 @@ fn shipped_trainer_frame_drives_end_to_end() {
s.run("SelectTrainerService(6); BenillaTrainerFrame_Update()")
.unwrap();
assert!(!s
.eval::<bool>("return BenillaTrainerTrainButton:IsEnabled()")
.eval::<bool>("return BenillaTrainerTrainButton:IsEnabled() ~= 0")
.unwrap());
s.resolve();
assert!(
@@ -293,7 +293,7 @@ fn shipped_trainer_frame_drives_end_to_end() {
"a known prerequisite shows white with its rank: {reqs}"
);
assert!(!s
.eval::<bool>("return BenillaTrainerTrainButton:IsEnabled()")
.eval::<bool>("return BenillaTrainerTrainButton:IsEnabled() ~= 0")
.unwrap());
// The app's client-side close: clear the snapshot + fire TRAINER_CLOSED → the window hides.
@@ -421,6 +421,9 @@ fn left_clicking_the_player_frame_targets_self() {
guid: 0xA11CE,
}],
leader_index: 0, // we lead
// The player's own guid, which this fixture leaves unset — spelled out because a bare 0
// is also the reference's "ungrouped" sentinel and this party has a member.
leader_guid: 0,
raid: Vec::new(),
loot_method: "group".into(),
master_looter: None,
@@ -510,6 +513,7 @@ fn raid_mark_clicks_through_the_nested_level() {
guid: 0xA11CE,
}],
leader_index: 0, // we lead — the mark rows are leader-gated
leader_guid: 0, // the player's own guid; this fixture leaves it unset
raid: Vec::new(),
loot_method: "group".into(),
master_looter: None,
@@ -1791,6 +1795,8 @@ fn the_player_frame_wears_the_leader_and_master_looter_icons() {
guid: 0x7A17,
}],
leader_index,
// Follows `leader_index`: 0 = the player (unset here), else the member who leads.
leader_guid: if leader_index == 0 { 0 } else { 0x7A17 },
raid: Vec::new(),
loot_method: method.into(),
master_looter,
@@ -828,7 +828,7 @@ fn an_idle_unit_popup_driver_parks_itself_off_the_tick() {
"an open menu ticks the enable pass"
);
assert!(
!s.eval::<bool>("return DropDownList1Button2:IsEnabled()")
!s.eval::<bool>("return DropDownList1Button2:IsEnabled() ~= 0")
.unwrap(),
"Whisper greys against a disconnected target"
);
@@ -851,7 +851,7 @@ fn an_idle_unit_popup_driver_parks_itself_off_the_tick() {
s.tick(0.016);
s.resolve();
assert!(
s.eval::<bool>("return DropDownList1Button2:IsEnabled()")
s.eval::<bool>("return DropDownList1Button2:IsEnabled() ~= 0")
.unwrap(),
"Whisper enables the frame the target reconnects"
);
@@ -20,6 +20,10 @@ fn harness() -> UiScript {
"GameTooltip.xml",
"UIDropDownMenu.xml", // the map's continent/zone pickers initialize into it at OnLoad
"ScrollTemplates.xml",
// The blip templates, which WorldMapFrame.xml instantiates with inherits=. Not
// optional: an unknown template is a loader WARNING, not an error, so leaving this
// out loads clean and then hands every blip a sizeless, anchorless frame.
"WorldMapFrameTemplates.xml",
"WorldMapFrame.xml",
] {
let text = std::fs::read_to_string(