the minimap and world map go stock, and the chat loses its white plate
- the minimap cluster and the battleground list are the reference's own `Minimap.xml` and `BattlefieldFrame.xml`, ours deleted, and the sixteen engine names the static probe found unbound are built, not stubbed: the battleground list and queue family (the as-group join refused client-side with no packet when the map's `MaxPlayers` is below the group), the bodyless `CMSG_BATTLEFIELD_STATUS` on world enter — benilla never sent it, so a queue joined before a reconnect was invisible — the 5.5556 yd `BATTLEFIELDS_CLOSED` leash, and the meeting-stone pair over the `0x295` five-way table with both asymmetries; the ping's lifetime is the stock Lua's now (held 5 s, faded 0.5 s), `GetPingPosition` answers two numbers always — our nil was a Lua error on every frame of the stock `Minimap_OnUpdate` — and the map-change drop the reference never had is gone (1974)
- the world map is the reference's own `WorldMapFrame.xml`, our transcription deleted; built for it: the arrow-frame family (an anonymous `Model` child at its 33.6 px footprint, drawn app-side as the minimap's own arrow sprite through one shared constant), the battleground position family over `MSG_BATTLEGROUND_PLAYER_POSITIONS` both ways (the reference's 5000 ms request throttle), and `GetPlayerMapPosition` resolving `raid1..raid40` where it knew only the party (B320); `SetupFullscreenScale` — the engine binding `0x48c270` the map's and the cinematic frame's OnShow call, which our map had stood in for with Lua — is bound to the carved law `min(0.75 · aspect, 1)` off the configured aspect, written through the frame's own `SetScale` (1.0 at 4:3 and wider, 0.9375 at 5:4); a hovered POI writes the map's own area label, not the tooltip; named gap: `WorldMapPing` draws nothing yet (1980, 1981)
- the tutorial frame is the reference's own `TutorialFrame.xml`, over a tutorial system benilla never had: two bit banks filled from `SMSG_TUTORIAL_FLAGS` — the fire-once bank `TriggerTutorial` reads and sets (never a send) and the acknowledged bank `FlagTutorial` writes and sends; there is no tutorial CVar in the binary (210 `CVar::Register` sites censused), so "disable tutorials" is every bit acknowledged; the sites this app can produce are wired (the world-enter Welcome cascade, the level-up arm, item pushes, the loot sparkle, the meeting-stone and battleground arms), the rest named; the loader learned `<TitleRegion>` — the stock frame's drag handle, a region that emits no quad; and the `TabardModel` widget kind arrives ahead of its window, five ints bounded by the `.rdata` table `{170, 17, 6, 17, 51}` and the constant 100 000 tabard cost (1976, 1977)
- the chat window drew as an opaque white plate: the chat-cache loader, gated on `InWorld` alone, restored into the boot VM 130 ms before the deferred entry load filled it, and its per-VM memo then blocked the restore that would have reached the real frames — the plate the stock Lua tints black stayed the XML's white; between the entry edge and the load there is now no VM in the world (the edge parks the boot VM, the load takes it back), so no in-world feed pushes into a VM with no interface; the perf pill reads `59 fps 7.0 ms 17.3 cpu` — the MAIN thread's milliseconds in full text and the process-wide sum dim, because the sum of every thread's CPU time had been read as a frame time (a raid read 17 on it at a solid 60 with the main thread at 7); and `WOW_PROBE_UI_ONE_TEX=1` is a pricing lever, never a look, for the draw-count ceiling a UI atlas would reach before one is built (1978, 1954)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
//! The stock `BattlefieldFrame.xml` and `Minimap.xml`'s queue icon (decision 1974), driven
|
||||
//! engine-only over the queue family's pushes: the icon and its tooltip across
|
||||
//! `UPDATE_BATTLEFIELD_STATUS`, the confirm dialog, the list window across `BATTLEFIELDS_SHOW`
|
||||
//! / `_CLOSED`, and the join button's request.
|
||||
|
||||
use benilla_ui::script::{BattlefieldListView, BattlefieldMapInfo, BattlefieldQueueSlot, UiScript};
|
||||
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
/// The stock pair with everything they call at load and on the events below — the durability
|
||||
/// harness's shape (UIParent.xml for the panel kit and `UIPanelWindows`; StaticPopup for the
|
||||
/// confirm dialog; UIDropDownMenu for the icon's menu; UIPanelTemplates for the buttons).
|
||||
fn session() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// The list verbs read the local player object at call time (1974) — seat one, as every
|
||||
// in-world call has.
|
||||
s.set_unit(
|
||||
"player",
|
||||
Some(benilla_ui::script::UnitState {
|
||||
exists: true,
|
||||
name: Some("Probefour".into()),
|
||||
level: 60,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
s.run("function PlaySound() end").unwrap();
|
||||
// `FloatingChatFrame.lua`'s constant (l.9), which the queue icon's fade-in reads; the chat
|
||||
// files are not part of this harness.
|
||||
s.run("CHAT_FRAME_FADE_TIME = 0.15").unwrap();
|
||||
for f in [
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\Localization.xml",
|
||||
"Interface\\FrameXML\\LocaleProperties.lua",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
"UIParent.xml",
|
||||
r"Interface\FrameXML\MoneyFrame.lua",
|
||||
r"Interface\FrameXML\MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"Interface\\FrameXML\\GameTooltip.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"Interface\\FrameXML\\StaticPopup.xml",
|
||||
"Interface\\FrameXML\\BattlefieldFrame.xml",
|
||||
"Interface\\FrameXML\\Minimap.xml",
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
s.resolve();
|
||||
s
|
||||
}
|
||||
|
||||
fn slot(map_id: u32, name: &str, status: u32, instance: u32) -> BattlefieldQueueSlot {
|
||||
BattlefieldQueueSlot {
|
||||
map_id,
|
||||
map_name: Some(name.into()),
|
||||
status,
|
||||
instance_id: instance,
|
||||
min_level: 20,
|
||||
max_level: 29,
|
||||
port_expiration_ms: 75_000,
|
||||
estimated_wait_ms: 90_000,
|
||||
time_waited_ms: 12_000,
|
||||
}
|
||||
}
|
||||
|
||||
fn empty() -> BattlefieldQueueSlot {
|
||||
BattlefieldQueueSlot {
|
||||
map_name: Some("Eastern Kingdoms".into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn visible(s: &UiScript, frame: &str) -> bool {
|
||||
s.eval::<bool>(&format!("return {frame}:IsVisible()"))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The minimap's queue icon: hidden with no queue, shown by a queued slot with the reference's
|
||||
/// three-line tooltip, the confirm dialog on a ready slot, hidden again when the queue clears.
|
||||
#[test]
|
||||
fn the_queue_icon_follows_the_slots_across_update_battlefield_status() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = session();
|
||||
assert!(!visible(&s, "MiniMapBattlefieldFrame"), "no queue at load");
|
||||
|
||||
s.set_battlefield_queue(vec![slot(529, "Arathi Basin", 1, 0), empty(), empty()], 0);
|
||||
s.fire_event("UPDATE_BATTLEFIELD_STATUS", vec![]);
|
||||
assert!(
|
||||
visible(&s, "MiniMapBattlefieldFrame"),
|
||||
"a queued slot shows the icon"
|
||||
);
|
||||
let tooltip = s
|
||||
.eval::<String>("return MiniMapBattlefieldFrame.tooltip")
|
||||
.unwrap();
|
||||
assert!(
|
||||
tooltip.starts_with("You are in the queue for Arathi Basin\n"),
|
||||
"{tooltip}"
|
||||
);
|
||||
// `SecondsToTime` leaves a trailing space after every unit — the reference's own text.
|
||||
assert!(
|
||||
tooltip.contains("Average wait time: 1 Min (Last 10 players)"),
|
||||
"{tooltip}"
|
||||
);
|
||||
assert!(tooltip.contains("Time in queue: 12 Secs \n"), "{tooltip}");
|
||||
assert!(
|
||||
tooltip.ends_with("|cffffffff<Right Click> for PvP Options|r"),
|
||||
"{tooltip}"
|
||||
);
|
||||
assert!(!visible(&s, "StaticPopup1"), "queued is not yet a question");
|
||||
|
||||
// Ready: the confirm dialog with the map name and the port countdown.
|
||||
s.set_battlefield_queue(vec![slot(529, "Arathi Basin", 2, 3), empty(), empty()], 0);
|
||||
s.fire_event("UPDATE_BATTLEFIELD_STATUS", vec![]);
|
||||
assert!(
|
||||
visible(&s, "StaticPopup1"),
|
||||
"confirm raises CONFIRM_BATTLEFIELD_ENTRY"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return StaticPopup1Text:GetText()")
|
||||
.unwrap(),
|
||||
"You are now eligible to enter Arathi Basin 3, choose an action:"
|
||||
);
|
||||
let tooltip = s
|
||||
.eval::<String>("return MiniMapBattlefieldFrame.tooltip")
|
||||
.unwrap();
|
||||
assert!(
|
||||
tooltip.starts_with(
|
||||
"You are eligible to enter Arathi Basin 3\nYou will be removed from the queue in 1 Min 15 Secs "
|
||||
),
|
||||
"{tooltip}"
|
||||
);
|
||||
// The dialog's Enter Battle is AcceptBattlefieldPort(slot, 1).
|
||||
s.run("StaticPopup1Button1:Click()").unwrap();
|
||||
assert_eq!(s.take_battlefield_port_requests(), vec![(1, true)]);
|
||||
|
||||
// The queue clears: the icon goes.
|
||||
s.set_battlefield_queue(vec![empty(), empty(), empty()], 0);
|
||||
s.fire_event("UPDATE_BATTLEFIELD_STATUS", vec![]);
|
||||
assert!(
|
||||
!visible(&s, "MiniMapBattlefieldFrame"),
|
||||
"no queue hides the icon"
|
||||
);
|
||||
}
|
||||
|
||||
/// The list window: `BATTLEFIELDS_SHOW` opens it on the pushed list — the fake "First
|
||||
/// Available" row, one row per instance, the description — the join button asks for the
|
||||
/// selection, and `BATTLEFIELDS_CLOSED` hides it.
|
||||
#[test]
|
||||
fn the_list_window_opens_on_battlefields_show_and_joins_the_selection() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = session();
|
||||
s.set_battlefield_list(BattlefieldListView {
|
||||
instances: vec![4, 9],
|
||||
bracket_min: 20,
|
||||
bracket_max: 29,
|
||||
info: Some(BattlefieldMapInfo {
|
||||
name: "Arathi Basin".into(),
|
||||
description: Some("The Arathi Basin is one of the main staging points.".into()),
|
||||
min_level: 20,
|
||||
max_level: 60,
|
||||
field_16: -1,
|
||||
field_17: 0.0,
|
||||
field_18: 0.0,
|
||||
}),
|
||||
group_queue: true,
|
||||
});
|
||||
s.set_battlefield_queue(vec![empty(), empty(), empty()], 0);
|
||||
assert!(!visible(&s, "BattlefieldFrame"));
|
||||
s.fire_event("BATTLEFIELDS_SHOW", vec![]);
|
||||
assert!(visible(&s, "BattlefieldFrame"), "the event shows the panel");
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BattlefieldFrameFrameLabel:GetText()")
|
||||
.unwrap(),
|
||||
"Arathi Basin"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BattlefieldZone1:GetText()")
|
||||
.unwrap(),
|
||||
"First Available"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BattlefieldZone2:GetText()")
|
||||
.unwrap(),
|
||||
"Arathi Basin 4"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BattlefieldZone3:GetText()")
|
||||
.unwrap(),
|
||||
"Arathi Basin 9"
|
||||
);
|
||||
assert!(
|
||||
!visible(&s, "BattlefieldZone4"),
|
||||
"two instances, three rows"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BattlefieldFrameZoneDescription:GetText()")
|
||||
.unwrap(),
|
||||
"The Arathi Basin is one of the main staging points."
|
||||
);
|
||||
assert!(
|
||||
visible(&s, "BattlefieldFrameGroupJoinButton"),
|
||||
"the map allows group queues: the button shows"
|
||||
);
|
||||
assert!(
|
||||
!s.eval::<bool>("return BattlefieldFrameGroupJoinButton:IsEnabled() ~= 0")
|
||||
.unwrap(),
|
||||
"…disabled while not leading a group"
|
||||
);
|
||||
|
||||
// Join the default selection (row 1 = first available), then a specific instance.
|
||||
s.run("BattlefieldFrameJoinButton:Click()").unwrap();
|
||||
assert_eq!(s.take_battlefield_join_requests(), vec![(0, false)]);
|
||||
assert!(!visible(&s, "BattlefieldFrame"), "joining hides the panel");
|
||||
s.fire_event("BATTLEFIELDS_SHOW", vec![]);
|
||||
s.run("BattlefieldZone3:Click()").unwrap();
|
||||
assert_eq!(s.eval::<i64>("return GetSelectedBattlefield()").unwrap(), 2);
|
||||
s.run("BattlefieldFrameJoinButton:Click()").unwrap();
|
||||
assert_eq!(s.take_battlefield_join_requests(), vec![(9, false)]);
|
||||
|
||||
s.fire_event("BATTLEFIELDS_SHOW", vec![]);
|
||||
assert!(visible(&s, "BattlefieldFrame"));
|
||||
s.fire_event("BATTLEFIELDS_CLOSED", vec![]);
|
||||
assert!(
|
||||
!visible(&s, "BattlefieldFrame"),
|
||||
"the leash's event hides it"
|
||||
);
|
||||
}
|
||||
|
||||
/// The queue icon's status pass marks the listed rows: a queued instance reads "(In Queue)"
|
||||
/// beside its row, and the first-available row carries it when the queue names no instance.
|
||||
#[test]
|
||||
fn the_list_rows_carry_the_queue_status() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = session();
|
||||
s.set_battlefield_list(BattlefieldListView {
|
||||
instances: vec![4],
|
||||
info: Some(BattlefieldMapInfo {
|
||||
name: "Arathi Basin".into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
s.set_battlefield_queue(vec![slot(529, "Arathi Basin", 1, 4), empty(), empty()], 0);
|
||||
s.fire_event("BATTLEFIELDS_SHOW", vec![]);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BattlefieldZone2Status:GetText()")
|
||||
.unwrap(),
|
||||
"(In Queue)"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BattlefieldZone1Status:GetText()")
|
||||
.unwrap(),
|
||||
""
|
||||
);
|
||||
}
|
||||
@@ -79,7 +79,7 @@ fn shipped_xml() -> Vec<(String, String)> {
|
||||
out.sort();
|
||||
// A sanity floor for the walk, not a census: `assets/ui` retires file by file (1751), so
|
||||
// the floor sits well under the count rather than one step above it (1956).
|
||||
assert!(out.len() >= 15, "only {} xml files swept", out.len());
|
||||
assert!(out.len() >= 10, "only {} xml files swept", out.len());
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ fn corpse_map_position_binding() {
|
||||
.eval::<(f64, f64)>("return GetCorpseMapPosition()")
|
||||
.unwrap();
|
||||
assert_eq!((x, y), (0.0, 0.0), "no corpse ⇒ the (0,0) hide sentinel");
|
||||
s.set_world_map_feed(None, None, 0.0, Some((0.25, 0.75)), Vec::new());
|
||||
s.set_world_map_feed(None, None, 0.0, Some((0.25, 0.75)), Vec::new(), Vec::new());
|
||||
let (x, y) = s
|
||||
.eval::<(f64, f64)>("return GetCorpseMapPosition()")
|
||||
.unwrap();
|
||||
|
||||
@@ -27,7 +27,9 @@ fn harness() -> UiScript {
|
||||
"Interface\\FrameXML\\StaticPopup.xml",
|
||||
"UIParent.xml",
|
||||
"Interface\\FrameXML\\GameTooltip.xml",
|
||||
"MinimapCluster.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"Interface\\FrameXML\\BattlefieldFrame.xml",
|
||||
"Interface\\FrameXML\\Minimap.xml",
|
||||
// The reference's own file. This module carried a private disk-only `load_xml` until the
|
||||
// swap, which structurally could not name a chain entry — [`super::test_ui::load_ui`] is
|
||||
// the one reader that speaks both stores, and it is why that helper exists.
|
||||
@@ -404,7 +406,9 @@ fn the_quest_tracker_stacks_below_the_durability_guy() {
|
||||
"Interface\\FrameXML\\StaticPopup.xml",
|
||||
"UIParent.xml",
|
||||
"Interface\\FrameXML\\GameTooltip.xml",
|
||||
"MinimapCluster.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"Interface\\FrameXML\\BattlefieldFrame.xml",
|
||||
"Interface\\FrameXML\\Minimap.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"Interface\\FrameXML\\DurabilityFrame.xml",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
|
||||
@@ -1328,8 +1328,51 @@ fn convert_entry(
|
||||
//
|
||||
// A pane with no name, or one no window claims, draws nothing (`SetModel` panes included:
|
||||
// this engine holds their scene and renders no M2 into it). Nothing is stubbed white.
|
||||
QuadContent::ModelPane { name } => {
|
||||
QuadContent::ModelPane {
|
||||
name,
|
||||
model,
|
||||
facing,
|
||||
model_scale: _,
|
||||
} => {
|
||||
use crate::portrait::PortraitSource;
|
||||
// The world-map arrow (decision 1980): the stock `WorldMapFrame.lua` /
|
||||
// `Blizzard_BattlefieldMinimap.lua` create an anonymous `Model` child, `SetModel` it to
|
||||
// the minimap arrow, and steer it with `SetFacing`/`SetPosition` every update. benilla
|
||||
// draws that one file as the minimap's own arrow sprite — the same art the minimap
|
||||
// ring draws (`crate::minimap::blips`), turned the same way (`-facing`: the M2's yaw
|
||||
// is CCW-positive, the screen's is CW), filling the pane the engine sized to the
|
||||
// reference's 33.6 px footprint times the arrow's `SetModelScale`. No 3D pane needs
|
||||
// to exist for a two-triangle arrow.
|
||||
if model.as_deref() == Some(benilla_ui::script::ARROW_MODEL) {
|
||||
use crate::minimap::blips::{
|
||||
PLAYER_ARROW_OFFSET_PX, PLAYER_ARROW_QUAD_PX, PLAYER_ARROW_TEXTURE,
|
||||
};
|
||||
let Some(handle) = assets
|
||||
.as_mut()
|
||||
.and_then(|a| a.sprite_texture(PLAYER_ARROW_TEXTURE, images))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// The M2's single quad sits off its origin by an authored offset that turns with
|
||||
// the facing — the minimap's arrow applies the same one at its 33.6 px basis, and
|
||||
// the pane the engine sized (`worldmap_arrow.rs`: the footprint times the
|
||||
// `SetModelScale`) scales it here. `model_scale` is already in the rect.
|
||||
let rotation = -facing;
|
||||
let (sin, cos) = rotation.sin_cos();
|
||||
let off = PLAYER_ARROW_OFFSET_PX * (rect.width() / PLAYER_ARROW_QUAD_PX);
|
||||
let off = Vec2::new(off.x * cos - off.y * sin, off.x * sin + off.y * cos);
|
||||
let rect = Rect::from_center_size(rect.center() + off, rect.size());
|
||||
out.push(UiQuad {
|
||||
rect,
|
||||
z_key: eq.z,
|
||||
texture: Some(handle),
|
||||
color: [1.0, 1.0, 1.0, eq.alpha],
|
||||
rotation,
|
||||
clip,
|
||||
..default()
|
||||
});
|
||||
return;
|
||||
}
|
||||
let Some(slot) = name.as_deref().and_then(crate::portrait::model_pane_booth) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -192,12 +192,9 @@ const KNOWN: &[Known] = &[
|
||||
// entries here — twelve `MerchantItem<N>` plus `MerchantBuyBackItem` — saying ours took the
|
||||
// mouse on the row itself where the reference splits each row into an inert container plus a
|
||||
// `$parentItemButton`. That is exactly what this gate exists to notice going away.
|
||||
Known {
|
||||
frame: "WorldMapFrame",
|
||||
flag: Flag::Mouse,
|
||||
why: "our map body takes the mouse so a click on it cannot reach the world behind a \
|
||||
FULLSCREEN_DIALOG window; the reference relies on WorldMapButton alone",
|
||||
},
|
||||
// The `WorldMapFrame` mouse entry RETIRED (1980): it said our map body took the mouse where
|
||||
// the reference relies on `WorldMapButton` alone — true of our transcription, and the map is
|
||||
// the reference's own file now.
|
||||
// ── The faux scroll panes: a different WIDGET KIND, not a missing flag ─────────────────────
|
||||
//
|
||||
// The reference declares each of these `<ScrollFrame …inherits="FauxScrollFrameTemplate">` and
|
||||
@@ -576,10 +573,11 @@ fn the_shipped_frames_carry_the_references_flags() {
|
||||
// 1956 our SkillFrame.xml, 1958 our UnitPopup.xml and 1959 our FriendsFrame.xml (their
|
||||
// frames are the reference's own now), which took the paired count from the low 400s to
|
||||
// the high 150s; 1966 (TradeFrame), 1968 (GameTooltip), 1969 (DressUpFrame) and 1970
|
||||
// (MailFrame) to the high 80s. The floor guards the pairing, not the census — it comes
|
||||
// down with every window that migrates, and reaches zero with the last file of ours
|
||||
// that declares a reference-named frame.
|
||||
compared > 60,
|
||||
// (MailFrame) to the high 80s, and 1980 (WorldMapFrame, whose 50-odd blip frames were
|
||||
// most of what was left) to the high teens. The floor guards the pairing, not the
|
||||
// census — it comes down with every window that migrates, and reaches zero with the
|
||||
// last file of ours that declares a reference-named frame.
|
||||
compared > 10,
|
||||
"only {compared} frames compared — the pairing broke, and the sweep guards nothing"
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -568,7 +568,17 @@ fn the_world_map_cannot_open_behind_the_menu_and_gives_its_slot_back() {
|
||||
"ScrollTemplates.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"WorldMapFrame.xml",
|
||||
// The stock map's OnShow/OnHide call `UpdateMicroButtons` unguarded (1980), which the
|
||||
// micro menu defines over the action bar it sits in — the spellbook kit's cluster.
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
"MicroMenu.xml",
|
||||
r"Interface\FrameXML\WorldMapFrame.xml",
|
||||
]);
|
||||
|
||||
// Opens normally, and takes the full-screen slot.
|
||||
@@ -619,7 +629,17 @@ fn nothing_opens_behind_the_world_map_and_escape_closes_it_first() {
|
||||
"ScrollTemplates.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"WorldMapFrame.xml",
|
||||
// The action-bar cluster the micro menu sits in — the stock map's OnShow/OnHide call
|
||||
// `UpdateMicroButtons` unguarded (1980).
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
"MicroMenu.xml",
|
||||
r"Interface\FrameXML\WorldMapFrame.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
// The loot window is the reference's own since 1751 — see `test_ui::LOOT_UI` for what
|
||||
// each of these buys; `PartyFrame`'s MAX_PARTY_MEMBERS is needed at LOAD time.
|
||||
|
||||
@@ -151,10 +151,30 @@ fn ui_wanted(world: &World) -> bool {
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct PendingEntryUiLoad;
|
||||
|
||||
/// `OnEnter(InWorld)`: arm the deferred entry load. The load itself runs a few frames later —
|
||||
/// see [`PendingEntryUiLoad`].
|
||||
pub(crate) fn arm_entry_ui_load(mut commands: Commands) {
|
||||
commands.insert_resource(PendingEntryUiLoad);
|
||||
/// **The boot VM, parked for the deferral window** (decision 1978). Between the world-entry edge
|
||||
/// and the deferred entry load there is no VM in the world at all: the boot VM waits here, out of
|
||||
/// every feed's reach, and the load takes it back. Every feed takes the VM as an `Option` and
|
||||
/// already returns on `None` — the glue phase's shape — so a feed keyed on a per-VM memo cannot
|
||||
/// push into a VM that has no interface yet and then hold the real push back (the chat plate
|
||||
/// that stayed white, and 1348's whole class). `ingame_ui_pending` stays for the feeds that
|
||||
/// name it; with the VM parked it is belt and braces.
|
||||
pub(crate) struct ParkedBootVm(UiScript);
|
||||
|
||||
/// `OnEnter(InWorld)`: arm the deferred entry load and park the boot VM. The load itself runs a
|
||||
/// few frames later — see [`PendingEntryUiLoad`] and [`ParkedBootVm`].
|
||||
pub(crate) fn arm_entry_ui_load(world: &mut World) {
|
||||
world.insert_resource(PendingEntryUiLoad);
|
||||
if let Some(vm) = world.remove_non_send_resource::<UiScript>() {
|
||||
world.insert_non_send_resource(ParkedBootVm(vm));
|
||||
}
|
||||
}
|
||||
|
||||
/// The parked boot VM back into the world, if one is parked — the session end and the
|
||||
/// left-before-the-load arm both want the VM where the tail expects it.
|
||||
fn unpark_boot_vm(world: &mut World) {
|
||||
if let Some(ParkedBootVm(vm)) = world.remove_non_send_resource::<ParkedBootVm>() {
|
||||
world.insert_non_send_resource(vm);
|
||||
}
|
||||
}
|
||||
|
||||
/// **Is the in-game UI still owed for this world entry?** True from `OnEnter(InWorld)` until
|
||||
@@ -200,6 +220,7 @@ pub(crate) fn run_pending_entry_load(world: &mut World) {
|
||||
if !in_world {
|
||||
// Left the world before the load ran — nothing to build a UI for.
|
||||
world.remove_resource::<PendingEntryUiLoad>();
|
||||
unpark_boot_vm(world);
|
||||
return;
|
||||
}
|
||||
let covering = world
|
||||
@@ -245,6 +266,9 @@ pub(crate) fn run_pending_entry_load(world: &mut World) {
|
||||
/// capture boots straight into `InWorld`, so before that this would have run ahead of
|
||||
/// [`benilla_assets::AssetSet::Open`] and loaded against no patch chain.
|
||||
pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
|
||||
// The VM the entry edge parked (1978); the live slot is the fallback for a caller that did
|
||||
// not go through the arm.
|
||||
unpark_boot_vm(world);
|
||||
if !ui_wanted(world) {
|
||||
return;
|
||||
}
|
||||
@@ -449,6 +473,9 @@ pub(crate) fn shutdown_ui_state(script: &mut UiScript, identity: Option<&(String
|
||||
/// Exclusive rather than a `NonSendMut` system because it both drops and installs a `NonSend`, and
|
||||
/// because the shutdown writes must be ordered against each other — see [`shutdown_ui_state`].
|
||||
pub(crate) fn end_ui_session(world: &mut World) {
|
||||
// A VM still parked (the load never ran) goes back into the world first, so the tail below
|
||||
// runs against the same slot it always did (1978).
|
||||
unpark_boot_vm(world);
|
||||
// An armed-but-unrun entry load ([`PendingEntryUiLoad`]) means this session never built an
|
||||
// in-game UI: there are no globals to save and no addon state to write, and running the
|
||||
// shutdown tail against the boot VM would overwrite the real files with that emptiness.
|
||||
@@ -701,3 +728,32 @@ pub(crate) fn is_emote_token_line(line: &str) -> bool {
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_')
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The entry edge parks the boot VM (1978): between the arm and the load there is no VM in
|
||||
/// the world for any feed to push into, and the session end puts a never-loaded one back.
|
||||
#[test]
|
||||
fn the_entry_edge_parks_the_boot_vm_and_the_session_end_returns_it() {
|
||||
let mut world = World::new();
|
||||
let boot = UiScript::new().unwrap();
|
||||
let session = boot.session();
|
||||
world.insert_non_send_resource(boot);
|
||||
arm_entry_ui_load(&mut world);
|
||||
assert!(
|
||||
world.get_non_send_resource::<UiScript>().is_none(),
|
||||
"no VM in the deferral window"
|
||||
);
|
||||
assert!(world.get_resource::<PendingEntryUiLoad>().is_some());
|
||||
assert!(world.get_non_send_resource::<ParkedBootVm>().is_some());
|
||||
// Left the world before the load ran: the VM comes back, untouched.
|
||||
unpark_boot_vm(&mut world);
|
||||
let vm = world
|
||||
.get_non_send_resource::<UiScript>()
|
||||
.expect("the parked VM is back");
|
||||
assert_eq!(vm.session(), session, "the same VM, no session moved");
|
||||
assert!(world.get_non_send_resource::<ParkedBootVm>().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The shipped `MinimapCluster.xml` driven engine-only: the +/- zoom buttons must re-sync their
|
||||
//! The stock `Minimap.xml` (1974) driven engine-only: the +/- zoom buttons must re-sync their
|
||||
//! enabled state when the active zoom index switches (stepping inside/outside a WMO flips to the
|
||||
//! other, independent level). Regression guard for the director-caught stale-button bug (2026-07-09):
|
||||
//! `ZoomIn` greyed from an outdoor max-zoom stayed greyed indoors at the middle default level.
|
||||
@@ -27,7 +27,11 @@ fn minimap_zoom_buttons_resync_when_switching_inside_and_outside() {
|
||||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||||
load_xml(&s, "MinimapCluster.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BattlefieldFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Minimap.xml");
|
||||
|
||||
// OnLoad seeds the +/- state from the live zoom. The CVar default is 3 — a middle level, so
|
||||
// BOTH buttons start enabled (the old hardcoded `MinimapZoomOut:Disable()` assumed zoom 0).
|
||||
@@ -92,7 +96,11 @@ fn tracking_frame_follows_get_tracking_texture_across_player_auras_changed() {
|
||||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||||
load_xml(&s, "MinimapCluster.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BattlefieldFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Minimap.xml");
|
||||
|
||||
let vis = |s: &UiScript| {
|
||||
s.eval::<bool>("return MiniMapTrackingFrame:IsVisible()")
|
||||
@@ -142,7 +150,11 @@ fn game_time_session(hour: u32, minute: u32) -> UiScript {
|
||||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||||
load_xml(&s, "MinimapCluster.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BattlefieldFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Minimap.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GameTime.xml");
|
||||
s
|
||||
}
|
||||
@@ -255,7 +267,11 @@ fn a_click_on_the_minimap_parks_a_centre_relative_ping_request() {
|
||||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||||
load_xml(&s, "MinimapCluster.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BattlefieldFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Minimap.xml");
|
||||
s.resolve();
|
||||
|
||||
let cx = s
|
||||
@@ -269,14 +285,16 @@ fn a_click_on_the_minimap_parks_a_centre_relative_ping_request() {
|
||||
// Nothing is parked until somebody clicks.
|
||||
assert_eq!(s.take_minimap_ping_request(), None);
|
||||
|
||||
// A click 20 UI units right and 12 up of the centre — inside the 70-unit disc.
|
||||
// A click 20 UI units right and 12 up of the centre — inside the 70-unit disc. The stock
|
||||
// `Minimap_OnClick` adds its `CURSOR_OFFSET_X/Y` (−7, −9) to the cursor before subtracting
|
||||
// the centre, so what it parks is the click less that offset.
|
||||
s.mouse_button(cx + 20.0, cy + 12.0, "LeftButton", true);
|
||||
s.mouse_button(cx + 20.0, cy + 12.0, "LeftButton", false);
|
||||
let (dx, dy) = s
|
||||
.take_minimap_ping_request()
|
||||
.expect("OnMouseUp → Minimap_OnClick → PingLocation");
|
||||
assert!((dx - 20.0).abs() < 0.01, "x right of centre: {dx}");
|
||||
assert!((dy - 12.0).abs() < 0.01, "y UP from centre: {dy}");
|
||||
assert!((dx - 13.0).abs() < 0.01, "x right of centre, less 7: {dx}");
|
||||
assert!((dy - 3.0).abs() < 0.01, "y UP from centre, less 9: {dy}");
|
||||
// Draining is a drain: the app must not see the same click twice.
|
||||
assert_eq!(s.take_minimap_ping_request(), None);
|
||||
|
||||
@@ -286,11 +304,12 @@ fn a_click_on_the_minimap_parks_a_centre_relative_ping_request() {
|
||||
assert_eq!(s.take_minimap_ping_request(), None, "off-widget is no ping");
|
||||
}
|
||||
|
||||
/// `Minimap:GetPingPosition()` answers **nil** with no ping, and the live pair once the app
|
||||
/// publishes one — never `(0, 0)` for "there isn't one", which a caller cannot tell from a ping
|
||||
/// under its own feet.
|
||||
/// `Minimap:GetPingPosition()` answers **two numbers always** (the reference recomputes them
|
||||
/// from statics nothing clears — wow-re `minimap-ping-law.md`), and the stock
|
||||
/// `Minimap_OnUpdate` leans on exactly that: while its 5 s timer runs it multiplies the answer
|
||||
/// with no nil test. The pair is the app's last publish, `(0, 0)` before any.
|
||||
#[test]
|
||||
fn get_ping_position_is_nil_until_there_is_a_ping() {
|
||||
fn get_ping_position_answers_two_numbers_always() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
@@ -301,14 +320,23 @@ fn get_ping_position_is_nil_until_there_is_a_ping() {
|
||||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||||
load_xml(&s, "MinimapCluster.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BattlefieldFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Minimap.xml");
|
||||
|
||||
assert_eq!(
|
||||
s.eval::<Option<f32>>("return (Minimap:GetPingPosition())")
|
||||
s.eval::<i64>("return select('#', Minimap:GetPingPosition())")
|
||||
.unwrap(),
|
||||
None
|
||||
2
|
||||
);
|
||||
s.set_minimap_ping(Some((0.25, -0.125)));
|
||||
assert_eq!(
|
||||
s.eval::<f32>("return (Minimap:GetPingPosition())").unwrap(),
|
||||
0.0,
|
||||
"before any ping: a number, not nil"
|
||||
);
|
||||
s.set_minimap_ping((0.25, -0.125));
|
||||
let x = s
|
||||
.eval::<f32>("local x = Minimap:GetPingPosition(); return x")
|
||||
.unwrap();
|
||||
@@ -319,10 +347,106 @@ fn get_ping_position_is_nil_until_there_is_a_ping() {
|
||||
(x - 0.25).abs() < 1e-6 && (y + 0.125).abs() < 1e-6,
|
||||
"{x} {y}"
|
||||
);
|
||||
s.set_minimap_ping(None);
|
||||
assert_eq!(
|
||||
s.eval::<Option<f32>>("return (Minimap:GetPingPosition())")
|
||||
.unwrap(),
|
||||
None
|
||||
|
||||
// The stock lifetime, end to end: MINIMAP_PING shows the model frame and starts the 5 s
|
||||
// timer; OnUpdate re-seats it from GetPingPosition every frame; past 5 s it "fades" and
|
||||
// hides. The app's sprite follows this frame (`minimap/ping.rs`).
|
||||
assert!(
|
||||
!s.eval::<bool>("return MiniMapPing:IsVisible()").unwrap(),
|
||||
"hidden until a ping"
|
||||
);
|
||||
s.fire_event(
|
||||
"MINIMAP_PING",
|
||||
vec![
|
||||
benilla_ui::script::ScriptValue::Str("player".into()),
|
||||
benilla_ui::script::ScriptValue::Number(0.25),
|
||||
benilla_ui::script::ScriptValue::Number(-0.125),
|
||||
],
|
||||
);
|
||||
assert!(s.eval::<bool>("return MiniMapPing:IsVisible()").unwrap());
|
||||
s.tick(1.0);
|
||||
assert!(
|
||||
s.eval::<bool>("return MiniMapPing:IsVisible()").unwrap(),
|
||||
"held at 1 s"
|
||||
);
|
||||
let cx = s
|
||||
.eval::<f32>(
|
||||
"local x = MiniMapPing:GetCenter(); local mx = Minimap:GetCenter(); return x - mx",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
(cx - 0.25 * 140.0).abs() < 0.5,
|
||||
"re-seated at the normalized offset times the width: {cx}"
|
||||
);
|
||||
s.tick(4.2);
|
||||
s.tick(0.6);
|
||||
assert!(
|
||||
!s.eval::<bool>("return MiniMapPing:IsVisible()").unwrap(),
|
||||
"5 s hold + 0.5 s fade, then hidden"
|
||||
);
|
||||
}
|
||||
|
||||
/// The meeting-stone icon (stock `Minimap.xml`, 1974): hidden until `IsInMeetingStoneQueue()`
|
||||
/// answers across `MEETINGSTONE_CHANGED`, its hover the cached status text, hidden again when
|
||||
/// the queue empties — and a click raises the leave-queue question.
|
||||
#[test]
|
||||
fn the_meeting_stone_icon_follows_the_queue_across_meetingstone_changed() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.run("function GetMinimapZoneText() return '' end")
|
||||
.unwrap();
|
||||
s.run("function PlaySound() end").unwrap();
|
||||
for f in [
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\Localization.xml",
|
||||
"Interface\\FrameXML\\LocaleProperties.lua",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
"UIParent.xml",
|
||||
r"Interface\FrameXML\MoneyFrame.lua",
|
||||
r"Interface\FrameXML\MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"Interface\\FrameXML\\GameTooltip.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"Interface\\FrameXML\\StaticPopup.xml",
|
||||
"Interface\\FrameXML\\BattlefieldFrame.xml",
|
||||
"Interface\\FrameXML\\Minimap.xml",
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
s.resolve();
|
||||
let vis = |s: &UiScript| {
|
||||
s.eval::<bool>("return MiniMapMeetingStoneFrame:IsVisible()")
|
||||
.unwrap()
|
||||
};
|
||||
assert!(!vis(&s), "hidden at load");
|
||||
|
||||
s.set_meeting_stone(1519, Some("Looking for more for Stormwind City".into()));
|
||||
s.fire_event("MEETINGSTONE_CHANGED", vec![]);
|
||||
assert!(vis(&s), "a queued area shows the icon");
|
||||
s.run("this = MiniMapMeetingStoneFrame; MiniMapMeetingStoneFrame:GetScript('OnEnter')()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GameTooltipTextLeft1:GetText()")
|
||||
.unwrap(),
|
||||
"Looking for more for Stormwind City"
|
||||
);
|
||||
s.run("MiniMapMeetingStoneFrame:Click()").unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
||||
"a click asks CONFIRM_LEAVE_QUEUE"
|
||||
);
|
||||
s.run("StaticPopup1Button1:Click()").unwrap();
|
||||
assert_eq!(
|
||||
s.take_meeting_stone_cancels(),
|
||||
1,
|
||||
"Accept is CancelMeetingStoneRequest()"
|
||||
);
|
||||
|
||||
s.set_meeting_stone(0, Some("Looking for more for Unknown".into()));
|
||||
s.fire_event("MEETINGSTONE_CHANGED", vec![]);
|
||||
assert!(!vis(&s), "area 0 hides it again");
|
||||
}
|
||||
|
||||
@@ -1023,6 +1023,12 @@ mod quest_tests;
|
||||
#[cfg(test)]
|
||||
mod quest_timer_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod battlefield_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tutorial_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod durability_tests;
|
||||
// `pub(crate)` for its `harness`/`push`/`row` helpers: `perf::hud`'s own test drives the readout
|
||||
|
||||
@@ -468,7 +468,7 @@ fn hovering_the_diet_icon_lists_what_the_pet_eats() {
|
||||
let pane = quads
|
||||
.iter()
|
||||
.find(|q| {
|
||||
matches!(&q.content, QuadContent::ModelPane { name: Some(n) } if n == "PetModelFrame")
|
||||
matches!(&q.content, QuadContent::ModelPane { name: Some(n), .. } if n == "PetModelFrame")
|
||||
})
|
||||
.expect("the pet model pane is in the render list");
|
||||
let pane_rect = pane.rect.expect("…with a resolved rect");
|
||||
|
||||
@@ -1567,78 +1567,6 @@ mod tests {
|
||||
(1889; the producer gate carries the event). Gated since the addon became a reached \
|
||||
LoadOnDemand row (1967).",
|
||||
),
|
||||
(
|
||||
"Blizzard_BattlefieldMinimap.xml",
|
||||
"CreateMiniWorldMapArrowFrame",
|
||||
"the world map's arrow-frame family (Create/Position/Show/UpdateWorldMapArrowFrames), engine verbs the reference's WorldMapFrame.lua also calls — unbuilt; ours draws the map's arrows app-side, and they land with the world map's own migration. The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
|
||||
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
|
||||
battleground's world states); its pane is a <Minimap>-kind widget this engine \
|
||||
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
|
||||
its update would reach.",
|
||||
),
|
||||
(
|
||||
"Blizzard_BattlefieldMinimap.xml",
|
||||
"GetBattlefieldFlagPosition",
|
||||
"the battlefield minimap's flag-position getter (0x4ac230, structural row only; the position family is uncarved). The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
|
||||
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
|
||||
battleground's world states); its pane is a <Minimap>-kind widget this engine \
|
||||
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
|
||||
its update would reach.",
|
||||
),
|
||||
(
|
||||
"Blizzard_BattlefieldMinimap.xml",
|
||||
"GetBattlefieldMapIconScale",
|
||||
"the battlefield minimap's icon-scale getter (uncarved; reads the queue slot's Map.dbc row). The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
|
||||
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
|
||||
battleground's world states); its pane is a <Minimap>-kind widget this engine \
|
||||
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
|
||||
its update would reach.",
|
||||
),
|
||||
(
|
||||
"Blizzard_BattlefieldMinimap.xml",
|
||||
"GetBattlefieldPosition",
|
||||
"the battlefield minimap's player-position getter (wow-re `bindings.md` 0x4abf90, a structural row — the position family is outside 195cd099's carve). The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
|
||||
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
|
||||
battleground's world states); its pane is a <Minimap>-kind widget this engine \
|
||||
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
|
||||
its update would reach.",
|
||||
),
|
||||
(
|
||||
"Blizzard_BattlefieldMinimap.xml",
|
||||
"PositionMiniWorldMapArrowFrame",
|
||||
"as CreateMiniWorldMapArrowFrame — the arrow-frame family. The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
|
||||
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
|
||||
battleground's world states); its pane is a <Minimap>-kind widget this engine \
|
||||
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
|
||||
its update would reach.",
|
||||
),
|
||||
(
|
||||
"Blizzard_BattlefieldMinimap.xml",
|
||||
"ShowMiniWorldMapArrowFrame",
|
||||
"as CreateMiniWorldMapArrowFrame — the arrow-frame family. The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
|
||||
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
|
||||
battleground's world states); its pane is a <Minimap>-kind widget this engine \
|
||||
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
|
||||
its update would reach.",
|
||||
),
|
||||
(
|
||||
"Blizzard_BattlefieldMinimap.xml",
|
||||
"UpdateWorldMapArrowFrames",
|
||||
"as CreateMiniWorldMapArrowFrame — the arrow-frame family. The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
|
||||
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
|
||||
battleground's world states); its pane is a <Minimap>-kind widget this engine \
|
||||
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
|
||||
its update would reach.",
|
||||
),
|
||||
(
|
||||
"Blizzard_BattlefieldMinimap.xml",
|
||||
"WorldMap_GetPOITextureCoords",
|
||||
"a stock WorldMapFrame.lua function; our WorldMapFrame.xml is still ours and defines its own POI atlas walk, so the name arrives with the world map's migration. The addon is a reached LoadOnDemand row since 1972 (UIParent.xml's \
|
||||
BattlefieldMinimap_LoadUI, which the stock WorldStateFrame.lua calls on a \
|
||||
battleground's world states); its pane is a <Minimap>-kind widget this engine \
|
||||
draws nothing into yet, so the addon loads, paints nothing, and these are the verbs \
|
||||
its update would reach.",
|
||||
),
|
||||
(
|
||||
"StaticPopup.xml",
|
||||
"ReplaceTradeEnchant",
|
||||
@@ -2324,6 +2252,12 @@ mod tests {
|
||||
// built; none is 1819-shaped, because no PAIR is split (a half-fired pair is the tell).
|
||||
const UNPRODUCED: &[(&str, &str)] = &[
|
||||
("BAG_OPEN", "ContainerFrame.lua"),
|
||||
(
|
||||
"CLOSE_WORLD_MAP",
|
||||
"WorldMapFrame.lua — the engine-side close the reference fires when the map is \
|
||||
shut from outside its own frame; benilla closes the map through the frame's own \
|
||||
hide path only (1980)",
|
||||
),
|
||||
("DISPLAY_SIZE_CHANGED", "the four paperdoll files"),
|
||||
(
|
||||
"GMSURVEY_DISPLAY",
|
||||
|
||||
@@ -32,7 +32,7 @@ fn every_shipped_ui_xml_parses() {
|
||||
assert!(
|
||||
// A sanity floor for the walk, not a census: `assets/ui` retires file by file (1751),
|
||||
// so the floor sits well under the count rather than one step above it (1956).
|
||||
checked >= 15,
|
||||
checked >= 10,
|
||||
"only {checked} xml files swept — sweep broke"
|
||||
);
|
||||
}
|
||||
@@ -430,8 +430,9 @@ fn every_shipped_texture_path_resolves_in_the_client_archives() {
|
||||
}
|
||||
// Never let the sweep pass by matching nothing — the shipped UI still names over a hundred
|
||||
// textures (173 after 1971 retired the auction transcription, fewer again after 1973 the
|
||||
// crafting pair; the count falls with every window that migrates, and the floor follows it).
|
||||
assert!(refs.len() >= 80, "only {} texture paths swept", refs.len());
|
||||
// crafting pair, 64 after 1980 the world map; the count falls with every window that
|
||||
// migrates, and the floor follows it).
|
||||
assert!(refs.len() >= 50, "only {} texture paths swept", refs.len());
|
||||
|
||||
// The shape half: a doubled separator is the Lua escaping written into XML, and it resolves to
|
||||
// nothing. Checked without the client so a data-less machine still catches this exact class.
|
||||
@@ -624,10 +625,10 @@ fn every_archive_path_a_shipped_lua_chunk_names_survives_its_own_escaping() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Never let the sweep pass by matching nothing (19 after 1971; the floor follows the census
|
||||
// down as windows migrate).
|
||||
// Never let the sweep pass by matching nothing (19 after 1971, 8 after 1980; the floor
|
||||
// follows the census down as windows migrate).
|
||||
assert!(
|
||||
paths.len() >= 10,
|
||||
paths.len() >= 5,
|
||||
"only {} archive paths swept out of the shipped Lua",
|
||||
paths.len()
|
||||
);
|
||||
@@ -845,7 +846,7 @@ fn no_shipped_script_sets_a_global_string_key_as_display_text() {
|
||||
assert!(offenders.is_empty(), "{}", offenders.join("\n"));
|
||||
// The sweep must never pass by finding nothing to sweep.
|
||||
// The same walk floor as above (1956).
|
||||
assert!(swept >= 15, "only {swept} xml files swept — sweep broke");
|
||||
assert!(swept >= 10, "only {swept} xml files swept — sweep broke");
|
||||
}
|
||||
|
||||
/// **The `$parentTextureFrame` idiom's contract, over the whole shipped UI**: a frame whose art is
|
||||
@@ -1372,7 +1373,10 @@ fn a_cinematic_leaves_nothing_of_the_interface_on_screen() {
|
||||
// The sweep is only worth anything if there was something to hide in the first place.
|
||||
let before = names.iter().filter(|n| visible(&s, n)).count();
|
||||
assert!(
|
||||
before > 20, // 50 until 1938 took the three bar files (and their visible frames) stock
|
||||
// 50 until 1938 took the three bar files stock, 20 until 1974 took the minimap cluster
|
||||
// (its 22 named frames were most of what this sweep counted); the floor follows the
|
||||
// census of OUR files down, and 12 is what is left — the glue and dev frames.
|
||||
before > 10,
|
||||
"only {before} frames visible before the cinematic — the sweep found no interface to \
|
||||
hide, so it would pass no matter what the cascade did"
|
||||
);
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
//! The stock `TutorialFrame.xml` (decision 1976) driven engine-only: an alert button per
|
||||
//! `TUTORIAL_TRIGGER`, the window a click opens over the published id's strings, the
|
||||
//! `FlagTutorial` it makes, the unticked checkbox's `ClearTutorials` on hide, and
|
||||
//! `CINEMATIC_STOP` clicking the Welcome alert.
|
||||
|
||||
use benilla_ui::script::{ScriptValue, UiScript};
|
||||
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
fn session() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.run("function PlaySound() end").unwrap();
|
||||
for f in [
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\Localization.xml",
|
||||
"Interface\\FrameXML\\LocaleProperties.lua",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
"UIParent.xml",
|
||||
r"Interface\FrameXML\MoneyFrame.lua",
|
||||
r"Interface\FrameXML\MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"Interface\\FrameXML\\GameTooltip.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"Interface\\FrameXML\\TutorialFrame.xml",
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
s.resolve();
|
||||
s
|
||||
}
|
||||
|
||||
fn visible(s: &UiScript, frame: &str) -> bool {
|
||||
s.eval::<bool>(&format!("return {frame}:IsVisible()"))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn trigger(s: &mut UiScript, published: i64) {
|
||||
s.fire_event("TUTORIAL_TRIGGER", vec![ScriptValue::Int(published)]);
|
||||
}
|
||||
|
||||
/// A trigger queues an alert button; its click opens the window on the id's title and text and
|
||||
/// acknowledges the id (0-based on the drain); a second trigger queues a second button.
|
||||
#[test]
|
||||
fn a_trigger_queues_an_alert_whose_click_opens_the_window_and_flags_it() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = session();
|
||||
assert!(!visible(&s, "TutorialFrameAlertButton1"));
|
||||
assert!(!visible(&s, "TutorialFrame"));
|
||||
|
||||
trigger(&mut s, 41);
|
||||
assert!(
|
||||
visible(&s, "TutorialFrameAlertButton1"),
|
||||
"the alert button shows"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return TutorialFrameAlertButton1.tooltip")
|
||||
.unwrap(),
|
||||
"Elite Quests"
|
||||
);
|
||||
trigger(&mut s, 42);
|
||||
assert!(
|
||||
visible(&s, "TutorialFrameAlertButton2"),
|
||||
"a second id takes the next button"
|
||||
);
|
||||
|
||||
s.run("TutorialFrameAlertButton1:Click()").unwrap();
|
||||
assert!(visible(&s, "TutorialFrame"), "the click opens the window");
|
||||
assert_eq!(
|
||||
s.eval::<String>("return TutorialFrameTitle:GetText()")
|
||||
.unwrap(),
|
||||
"Elite Quests"
|
||||
);
|
||||
assert!(s
|
||||
.eval::<String>("return TutorialFrameText:GetText()")
|
||||
.unwrap()
|
||||
.starts_with("You have accepted an elite quest"));
|
||||
assert_eq!(
|
||||
s.take_tutorial_flag_requests(),
|
||||
vec![40],
|
||||
"FlagTutorial(41) → the 0-based id"
|
||||
);
|
||||
assert!(
|
||||
!visible(&s, "TutorialFrameAlertButton1"),
|
||||
"the clicked alert is spent; the other stays"
|
||||
);
|
||||
assert!(visible(&s, "TutorialFrameAlertButton2"));
|
||||
|
||||
// Okay hides it; the checkbox is ticked, so nothing is cleared.
|
||||
s.run("TutorialFrameOkayButton:Click()").unwrap();
|
||||
assert!(!visible(&s, "TutorialFrame"));
|
||||
assert_eq!(s.take_tutorial_clears(), 0);
|
||||
}
|
||||
|
||||
/// Unticking "Display Tips" and closing calls `ClearTutorials` and drops every alert.
|
||||
#[test]
|
||||
fn an_unticked_checkbox_clears_the_tutorials_on_hide() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = session();
|
||||
trigger(&mut s, 41);
|
||||
trigger(&mut s, 42);
|
||||
s.run("TutorialFrameAlertButton1:Click()").unwrap();
|
||||
let _ = s.take_tutorial_flag_requests();
|
||||
s.run("TutorialFrameCheckButton:SetChecked(nil) TutorialFrame:Hide()")
|
||||
.unwrap();
|
||||
assert_eq!(s.take_tutorial_clears(), 1, "ClearTutorials() on hide");
|
||||
assert!(
|
||||
!visible(&s, "TutorialFrameAlertButton2"),
|
||||
"every alert dropped"
|
||||
);
|
||||
}
|
||||
|
||||
/// `CINEMATIC_STOP` clicks the Welcome alert (id 42) if it is queued — the first thing a new
|
||||
/// character sees after the intro — and moves the window to the screen's centre.
|
||||
#[test]
|
||||
fn cinematic_stop_opens_the_welcome_window() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = session();
|
||||
trigger(&mut s, 42);
|
||||
trigger(&mut s, 1);
|
||||
s.fire_event("CINEMATIC_STOP", vec![]);
|
||||
assert!(visible(&s, "TutorialFrame"));
|
||||
assert_eq!(
|
||||
s.eval::<String>("return TutorialFrameTitle:GetText()")
|
||||
.unwrap(),
|
||||
"Welcome to World of Warcraft!"
|
||||
);
|
||||
assert_eq!(s.take_tutorial_flag_requests(), vec![41]);
|
||||
assert!(
|
||||
visible(&s, "TutorialFrameAlertButton2"),
|
||||
"the Questgivers alert stays queued"
|
||||
);
|
||||
}
|
||||
@@ -3,10 +3,13 @@
|
||||
//! (`SMSG_GOSSIP_POI` → [`crate::poi_marker`]); the `AreaPOI.dbc` rows are 0203's deferred slice
|
||||
//! and arrive through the same list, so these tests pin the *pool*, not the marker.
|
||||
//!
|
||||
//! Driven through the shipped `assets/ui/WorldMapFrame.xml` in a bare engine (no Bevy) — the
|
||||
//! panel-test idiom: push host state, run the repaint, read the frames back.
|
||||
//! Driven through the reference's own `WorldMapFrame.xml` off the player's chain (1980) in a bare
|
||||
//! engine (no Bevy) — the panel-test idiom: push host state, run the repaint, read the frames back.
|
||||
|
||||
use benilla_ui::script::{UiScript, WorldMapLandmarkView};
|
||||
use benilla_ui::script::{
|
||||
BattlefieldFlagView, BattlefieldPositionView, QuadContent, UiScript, WorldMapLandmarkView,
|
||||
ARROW_MODEL,
|
||||
};
|
||||
|
||||
/// The map plus everything its OnLoad touches, in the shipped list's own order.
|
||||
fn harness() -> UiScript {
|
||||
@@ -26,11 +29,12 @@ fn harness() -> UiScript {
|
||||
"Interface\\FrameXML\\GameTooltip.xml",
|
||||
"Interface\\FrameXML\\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",
|
||||
// The stock update walks MAX_PARTY_MEMBERS and MAX_RAID_MEMBERS, which the party and
|
||||
// raid files own.
|
||||
r"Interface\FrameXML\PartyMemberFrame.lua",
|
||||
r"Interface\FrameXML\RaidFrame.lua",
|
||||
// The reference's own map, which <Include>s its blip templates itself.
|
||||
r"Interface\FrameXML\WorldMapFrame.xml",
|
||||
] {
|
||||
// `test_ui::load_ui`, not a local read: a manifest entry carrying a path separator is the
|
||||
// REFERENCE's own file and must come off the player's chain, which
|
||||
@@ -44,6 +48,14 @@ fn harness() -> UiScript {
|
||||
s
|
||||
}
|
||||
|
||||
/// The stock per-frame update, driven the way its `OnUpdate` script runs it: `this` is the map
|
||||
/// button and the elapsed time its one argument. Everything on the sheet — the arrow, the party
|
||||
/// and raid blips, the battleground teammates and flags, the corpse — is seated here.
|
||||
fn update(s: &mut UiScript) {
|
||||
s.run("this = WorldMapButton WorldMapButton_OnUpdate(0.1) this = nil")
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn landmark(name: &str, icon: u32, uv: (f32, f32)) -> WorldMapLandmarkView {
|
||||
WorldMapLandmarkView {
|
||||
name: name.into(),
|
||||
@@ -140,8 +152,9 @@ fn the_poi_pool_grows_and_parks_its_tail() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Hovering a POI names it. The description line only appears on landmarks that carry one — the
|
||||
/// guard's directions never do, a battleground node's "In Conflict" would.
|
||||
/// Hovering a POI names it in the map's area label; the description line beneath carries the
|
||||
/// landmark's status only when it has one — the guard's directions never do, a battleground
|
||||
/// node's "In Conflict" would — and is blanked otherwise (stock `WorldMapPOI_OnEnter`).
|
||||
#[test]
|
||||
fn hovering_a_poi_names_it_and_adds_a_status_line_only_when_there_is_one() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
@@ -151,14 +164,15 @@ fn hovering_a_poi_names_it_and_adds_a_status_line_only_when_there_is_one() {
|
||||
s.run("this = WorldMapFramePOI1 this:GetScript(\"OnEnter\")() this = nil")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GameTooltipTextLeft1:GetText()")
|
||||
s.eval::<String>("return WorldMapFrameAreaLabel:GetText()")
|
||||
.unwrap(),
|
||||
"Lion's Pride Inn"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return GameTooltipTextLeft2:GetText() == nil or GameTooltipTextLeft2:GetText() == \"\"")
|
||||
assert_eq!(
|
||||
s.eval::<String>("return WorldMapFrameAreaDescription:GetText()")
|
||||
.unwrap(),
|
||||
"no description → no second line"
|
||||
"",
|
||||
"no description → an empty description line"
|
||||
);
|
||||
|
||||
let mut with_status = landmark("Stables", 6, (0.5, 0.5));
|
||||
@@ -168,7 +182,7 @@ fn hovering_a_poi_names_it_and_adds_a_status_line_only_when_there_is_one() {
|
||||
s.run("this = WorldMapFramePOI1 this:GetScript(\"OnEnter\")() this = nil")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GameTooltipTextLeft2:GetText()")
|
||||
s.eval::<String>("return WorldMapFrameAreaDescription:GetText()")
|
||||
.unwrap(),
|
||||
"In Conflict"
|
||||
);
|
||||
@@ -240,7 +254,7 @@ fn party_blips_sit_at_their_map_positions_and_hide_when_absent() {
|
||||
};
|
||||
|
||||
// Nobody in the party: every slot answers the hide sentinel.
|
||||
s.run("WorldMapParty_Update()").unwrap();
|
||||
update(&mut s);
|
||||
for i in 1..=4 {
|
||||
assert!(!shown(&s, i), "slot {i} hides while the party is empty");
|
||||
}
|
||||
@@ -253,8 +267,9 @@ fn party_blips_sit_at_their_map_positions_and_hide_when_absent() {
|
||||
0.0,
|
||||
None,
|
||||
vec![Some((0.25, 0.75)), None, Some((0.5, 0.125))],
|
||||
Vec::new(),
|
||||
);
|
||||
s.run("WorldMapParty_Update()").unwrap();
|
||||
update(&mut s);
|
||||
let diag = s
|
||||
.eval::<String>(
|
||||
r#"return "p1="..tostring(GetPlayerMapPosition("party1")).." p3="..tostring(GetPlayerMapPosition("party3"))"#,
|
||||
@@ -289,14 +304,162 @@ fn party_blips_sit_at_their_map_positions_and_hide_when_absent() {
|
||||
);
|
||||
|
||||
// The party breaks up: the blips go with it.
|
||||
s.set_world_map_feed(None, Some((0.5, 0.5)), 0.0, None, Vec::new());
|
||||
s.run("WorldMapParty_Update()").unwrap();
|
||||
s.set_world_map_feed(None, Some((0.5, 0.5)), 0.0, None, Vec::new(), Vec::new());
|
||||
update(&mut s);
|
||||
for i in 1..=4 {
|
||||
assert!(!shown(&s, i), "slot {i} hides when the party is gone");
|
||||
}
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// **The player arrow is the reference's own** (1980): `WorldMapFrame_OnLoad` creates it
|
||||
/// (`CreateWorldMapArrowFrame(WorldMapFrame)`, an anonymous `Model` pane holding the minimap
|
||||
/// arrow), and the update seats it at the player's UV on the detail sheet — the same point the
|
||||
/// `WorldMapPlayer` mouseover button is seated at — turns it to the facing and shows it; off-map
|
||||
/// hides it. The pane extracts as the minimap-arrow model the app draws as a sprite.
|
||||
#[test]
|
||||
fn the_player_arrow_is_the_stock_model_pane_seated_and_turned_by_the_update() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
// The whole manifest, not the kit: the arrow's quad is read off the render list, which
|
||||
// needs the map VISIBLE — and showing it is `ShowUIPanel`'s full-screen route through
|
||||
// `UIParent` (see the furniture test below for the same setup and why the player exists).
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1600.0, 900.0);
|
||||
s.set_unit(
|
||||
"player",
|
||||
Some(benilla_ui::script::UnitState {
|
||||
exists: true,
|
||||
name: Some("Probefour".into()),
|
||||
level: 60,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
let failures = super::load_default_ui(&s);
|
||||
assert!(failures.is_empty(), "manifest load errors: {failures:#?}");
|
||||
s.resolve();
|
||||
s.run("ShowUIPanel(WorldMapFrame)").unwrap();
|
||||
s.resolve();
|
||||
s.set_world_map_feed(None, Some((0.25, 0.75)), 1.25, None, Vec::new(), Vec::new());
|
||||
update(&mut s);
|
||||
s.resolve();
|
||||
let arrow = |s: &mut UiScript| {
|
||||
s.extract().into_iter().find(|q| {
|
||||
matches!(&q.content, QuadContent::ModelPane { model: Some(m), .. } if m == ARROW_MODEL)
|
||||
})
|
||||
};
|
||||
let pane = arrow(&mut s).expect("the arrow pane is in the render list");
|
||||
assert!(
|
||||
matches!(&pane.content, QuadContent::ModelPane { facing, .. } if (*facing - 1.25).abs() < 1e-6),
|
||||
"turned to the facing: {:?}",
|
||||
pane.content
|
||||
);
|
||||
let rect = pane.rect.expect("…with a resolved rect");
|
||||
assert!(
|
||||
rect.right > rect.left && rect.top > rect.bottom,
|
||||
"sized: {rect:?}"
|
||||
);
|
||||
// The seat, read back in Lua: the arrow is anonymous, so it is found among the map's
|
||||
// children by kind and compared against the mouseover button the update seats at the same
|
||||
// point.
|
||||
let (dx, dy) = s
|
||||
.eval::<(f64, f64)>(
|
||||
r#"local px, py = WorldMapPlayer:GetCenter()
|
||||
for _, child in ipairs({ WorldMapFrame:GetChildren() }) do
|
||||
if child:GetObjectType() == "Model" and child:IsShown() then
|
||||
local ax, ay = child:GetCenter()
|
||||
return ax - px, ay - py
|
||||
end
|
||||
end
|
||||
return 1e9, 1e9"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
dx.abs() < 0.5 && dy.abs() < 0.5,
|
||||
"seated with WorldMapPlayer ({dx}, {dy})"
|
||||
);
|
||||
|
||||
// Off the displayed map: the (0,0) sentinel hides it.
|
||||
s.set_world_map_feed(None, None, 0.0, None, Vec::new(), Vec::new());
|
||||
update(&mut s);
|
||||
s.resolve();
|
||||
assert!(arrow(&mut s).is_none(), "off-map hides the arrow");
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// **Battleground teammates and the flag carrier** (1980) come through the position family the
|
||||
/// stock update polls after the party arm: each teammate takes the next `WorldMapRaid` frame
|
||||
/// past the raid's own, carrying its name for the tooltip; the carrier takes `WorldMapFlag1`
|
||||
/// wearing the token's texture; the `(0, 0)` sentinel hides; and the empty push — leaving the
|
||||
/// battleground — hides them all.
|
||||
#[test]
|
||||
fn battleground_teammates_and_the_flag_draw_from_the_position_family() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
s.set_world_map_feed(None, Some((0.5, 0.5)), 0.0, None, Vec::new(), Vec::new());
|
||||
s.set_battlefield_positions(
|
||||
vec![
|
||||
BattlefieldPositionView {
|
||||
uv: (0.25, 0.75),
|
||||
name: Some("Probe".into()),
|
||||
},
|
||||
BattlefieldPositionView {
|
||||
uv: (0.0, 0.0),
|
||||
name: None,
|
||||
},
|
||||
],
|
||||
Some(BattlefieldFlagView {
|
||||
uv: (0.4, 0.6),
|
||||
token: Some("HordeFlag".into()),
|
||||
}),
|
||||
1.0,
|
||||
);
|
||||
update(&mut s);
|
||||
assert!(
|
||||
s.eval::<bool>(r#"return WorldMapRaid1:IsShown() and WorldMapRaid1.name == "Probe""#)
|
||||
.unwrap(),
|
||||
"the placed teammate shows on the first raid frame with its name"
|
||||
);
|
||||
assert!(
|
||||
!s.eval::<bool>("return WorldMapRaid2:IsShown()").unwrap(),
|
||||
"the (0,0) teammate hides"
|
||||
);
|
||||
assert!(s.eval::<bool>("return WorldMapFlag1:IsShown()").unwrap());
|
||||
assert_eq!(
|
||||
s.eval::<String>("return WorldMapFlag1Texture:GetTexture()")
|
||||
.unwrap(),
|
||||
r"Interface\WorldStateFrame\HordeFlag"
|
||||
);
|
||||
assert!(!s.eval::<bool>("return WorldMapFlag2:IsShown()").unwrap());
|
||||
let (w, h) = s
|
||||
.eval::<(f64, f64)>(
|
||||
"return WorldMapDetailFrame:GetWidth(), WorldMapDetailFrame:GetHeight()",
|
||||
)
|
||||
.unwrap();
|
||||
let (sheet_left, sheet_top) = s
|
||||
.eval::<(f64, f64)>("return WorldMapDetailFrame:GetLeft(), WorldMapDetailFrame:GetTop()")
|
||||
.unwrap();
|
||||
let (bx, by) = s
|
||||
.eval::<(f64, f64)>("return WorldMapRaid1:GetCenter()")
|
||||
.unwrap();
|
||||
assert!(
|
||||
(bx - (sheet_left + 0.25 * w)).abs() < 0.5,
|
||||
"u scales by the sheet's width"
|
||||
);
|
||||
assert!(
|
||||
(by - (sheet_top - 0.75 * h)).abs() < 0.5,
|
||||
"v runs DOWN from the sheet's top"
|
||||
);
|
||||
|
||||
s.set_battlefield_positions(Vec::new(), None, 1.0);
|
||||
update(&mut s);
|
||||
assert!(
|
||||
!s.eval::<bool>("return WorldMapRaid1:IsShown() or WorldMapFlag1:IsShown()")
|
||||
.unwrap(),
|
||||
"leaving the battleground hides them"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// **Opening the map must not hide the map's own furniture.** (Director report: the world showed
|
||||
/// through at the left and right of the sheet, where black bars had always been.)
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user