the loading screen lets nothing through, and the micro buttons go stock

- units could be hovered through the loading screen — the sword cursor gave it away: the world pick chain has no state gate, one of fifty-one input sites, so a gate per consumer is a rule that regresses; the cut is at the source instead — one `PreUpdate` system empties the button planes, drains the raw queues, zeroes the accumulators and blanks the window's cursor position while the cover is up (the alt-tabbed-away state every consumer already handles); a press under the cover never happened, a held button is released once, a gesture in flight is cancelled, and a wall test counts the channels; the reference does the same — its raise registers a veto stub (`0x406a50`) on five event-bus categories — and one gap closed with it: the reference parks the plain arrow and drops any item overlay before the screen goes up, where benilla kept a held item's icon over the loading art (1986, 1990)
- the micro-button row is the reference's own `MainMenuBarMicroButtons.xml`, our transcription deleted — no engine verb was missing, and the whole engine-side cost is one alias, `SoundOptionsFrame = OptionsFrame`, because the stock file reads the reference's three options windows and ours is the one window for all three; what the stock file brings back: `UpdateTalentButton`'s own −2 re-seat (the five buttons after the talent button sit 1 px further right — the reference's own pixel), the raw binding token in the hover label (`(C)`, `(SHIFT-P)`), the sixty-second talent-button pulse on a ding while the sheet is closed (0304's scope-out retired), the keyring arm, and "Abilities" on the spellbook hover for a character with no spells; `BenillaActionBarArt_SeatAbove` goes with the file — a child is born one level above its parent (`SetParent` `0x76ab65`), the seat it set by hand (1987)
- hovering the player arrow on the stock world map showed an empty tooltip: narrower than 4:3 the map is scaled under 1, and our `MouseIsOver` had dropped the reference's division by `GetEffectiveScale` as "always 1" — the frame getters answer in the frame's own units, `GetCursorPosition` in the screen's; the division is back, and the region getters divide by their owner's scale too; and the ready check times out: the stock `UIParent.xml` ticks `CheckReadyCheckTime` every frame, the one unbound name in that file's way, so the mechanism behind it is built — a 30 s deadline and a "has not answered" flag per roster entry, the leader getting no popup while members print "%s has initiated a ready check" and get it, and on expiry `RAID_MEMBERS_AFK` with the unanswered names or `READY_CHECK_NO_AFK`, the last answer closing the check at once (1985, 1989)
- a review of the floor grind closed eight stale-input and bypassed-side-effect siblings the gates had left open: a never-placed card places on a still frame, the dormant-emitter skip sits below the emitter's lifetime blocks, PVS changes (not only spawns) un-still the gates, the appear ramp's retirement and a far-side unmark come due, a host born under a parked camera is judged, the projection counts as a still input, a fresh palette range is zeroed, the stable feed runs once more after a close, the unit publisher's viewer query is filtered, solo UI materials are reused by key, and the draw-list cache compares exactly; and three doc links the chat window's stock move (1948) left dangling point at what replaced them — the colour table and the tell ring are the stock Lua's now — which is what had the doc-links gate (1925) red on main

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-04 21:01:38 +07:00
co-authored by Claude Fable 5.1
parent e800974806
commit bc1a2428dd
59 changed files with 2135 additions and 772 deletions
+3
View File
@@ -490,6 +490,8 @@ pub(crate) struct Model {
/// Party/loot intents (`AcceptGroup`/`InviteToParty`/`SetLootMethod`/…) queued since the
/// app's last [`super::UiScript::take_party_requests`] drain — the outbound seam ([`party`]).
pub(crate) party_requests: Vec<party::PartyRequest>,
/// The ready-check deadline and the unanswered flags the timeout tick reads (decision 1989).
pub(crate) ready_check: party::ReadyCheckState,
/// The saved raid lockouts the Raid tab's info panel reads (decision 1549), pushed by the app
/// from `SMSG_RAID_INSTANCE_INFO`. Its own field rather than a [`party::PartyState`] member
/// because it arrives on its own packet and survives every roster change.
@@ -1776,6 +1778,7 @@ impl Model {
joined_channels: Vec::new(),
party: party::PartyState::default(),
party_requests: Vec::new(),
ready_check: party::ReadyCheckState::default(),
saved_instances: Vec::new(),
raid_selection: 0,
instance_type: None,
+6 -3
View File
@@ -29,6 +29,7 @@ use crate::widget::{FrameHandle, FrameKind};
mod events_regions;
mod frame_state;
mod layout_methods;
pub(crate) use layout_methods::eff_scale;
pub(crate) mod movable;
pub(crate) mod toplevel;
pub(crate) use layout_methods::{anchor_bits_eq, anchor_retarget_is_structural};
@@ -358,9 +359,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
)?;
// GetCursorPosition() → x, y — the last cursor position the host fed (`mouse_move`/
// `mouse_button`), UI units y-up like every other coordinate read. The real client scales by
// the UI scale; ours is the constant 1 (`GetEffectiveScale`), so the ref's `/scale` dance is
// an identity. The world map polls this every OnUpdate for hover/click math.
// `mouse_button`), UI units y-up like every other coordinate read — the SCREEN's units, which
// is what the reference hands Lua too; a caller inside a scaled frame divides by its
// `GetEffectiveScale()` (the reference's own `MouseIsOver` does, and the stock world map at
// a scale under 1 is the case that made the division load-bearing here — decision 1985). The
// world map polls this every OnUpdate for hover/click math.
lua.globals().set(
"GetCursorPosition",
lua.create_function(|lua, ()| {
@@ -312,7 +312,7 @@ fn settle(lua: &Lua) {
crate::script::UiScript::resolve_layout(&mut model);
}
pub(super) fn eff_scale(model: &Model, h: FrameHandle) -> f32 {
pub(crate) fn eff_scale(model: &Model, h: FrameHandle) -> f32 {
let s = model
.arena
.frame(h)
+314
View File
@@ -155,6 +155,11 @@ pub struct PartyState {
/// the reference answers **`1` while solo**. A client that answers `nil` there is not
/// reproducing 1.12 (wow-re `ui/scratch/party-leader-and-nameplate-verbs.md`).
pub leader_guid: u64,
/// **The active player's own GUID** — the reference's `0x468550` read (`[0xb41414]+0xc0`),
/// `0` out of world. Fed by the app beside the leader; the pair is the leader gate the
/// ready-check timeout runs on (`CheckReadyCheckTime`, decision 1989): the reference compares
/// the two guids and nothing else, so a solo player (leader `0`, own non-zero) never matches.
pub own_guid: u64,
/// The **whole raid roster**, `GetRaidRosterInfo`'s 1-based array — empty outside a raid,
/// and **including the player** (the reference's array does; it is why `UnitInRaid("player")`
/// answers `1` in a raid and why this list is not `members`'s recipient-excluded shape).
@@ -245,6 +250,62 @@ impl super::UiScript {
/// job, never auto-fired here.
pub fn set_party(&mut self, state: PartyState) {
self.model_mut().party = state;
// `SMSG_GROUP_LIST`'s ready-check leg (`0x4ba5f0`, reached only from the `0x7d` handler):
// the roster scan over the NEW roster — a member who left took their pending flag with
// them — and when nobody is left pending, a forced close (decision 1989).
let lua = self.lua();
let mut model = self.model_mut();
if model.ready_check.deadline.is_some() {
let roster: Vec<u64> = model.party.raid.iter().map(|m| m.guid).collect();
model.ready_check.unanswered.retain(|g| roster.contains(g));
if model.ready_check.unanswered.is_empty() {
ready_check_force_close(lua, &mut model);
}
}
}
/// The `MSG_RAID_READY_CHECK` open form arrived (decision 1989). The handler `0x4ba360` splits
/// on the leader guid: the **leader** takes the response-collection arm — reads the per-member
/// records the body carries (none, on vmangos) and force-closes if nobody is left pending;
/// everyone **else** arms the 30 s deadline (`0x4ba535`) that the leader-gated tick never
/// acts on for them, and gets the `READY_CHECK` popup — which the app fires on its own
/// ticket edge, so this method carries only the state half.
pub fn ready_check_request(&mut self, we_lead: bool) {
let lua = self.lua();
let now = clock(lua);
let mut model = self.model_mut();
if we_lead {
if model.ready_check.deadline.is_some() && model.ready_check.unanswered.is_empty() {
ready_check_force_close(lua, &mut model);
}
} else {
model.ready_check.deadline = Some(now + READY_CHECK_SECONDS);
}
}
/// One member's answer, forwarded to the leader (`{guid, status}`, decision 1989): the
/// member's "has not answered" flag clears (`0x4ba40e`), and once nobody is left pending the
/// check closes at once (`0x4ba4ce` → the worker) rather than at the deadline.
///
/// **INFERRED, pending wow-re:** the flag clears on ANY answer, ready or not — the flag's
/// meaning is "has not answered yet" (its writer census), and the summary the reference
/// prints is headed "AFK", not "not ready". The polarity of the store at `0x4ba40e` against
/// the wire byte is dispatched for a byte read; `ready` is carried here so that one line
/// changes if the answer says otherwise.
pub fn ready_check_answered(&mut self, guid: u64, _ready: bool) {
let lua = self.lua();
let mut model = self.model_mut();
model.ready_check.unanswered.retain(|g| *g != guid);
if model.ready_check.deadline.is_some() && model.ready_check.unanswered.is_empty() {
ready_check_force_close(lua, &mut model);
}
}
/// The summary lines the timeout worker composed since the last drain — the reference's
/// `0x49a870(text, 10)` = a `CHAT_MSG_SYSTEM` line the client prints itself; the app pushes
/// them into the chat log the way every other client-composed system line goes.
pub fn take_ready_check_lines(&mut self) -> Vec<String> {
std::mem::take(&mut self.model_mut().ready_check.lines)
}
/// Drain the party/loot intents queued since the last call.
@@ -676,11 +737,45 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
g.set(
"DoReadyCheck",
lua.create_function(|lua, ()| {
let now = clock(lua);
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
// The worker `0x4bb1d0` (decision 1989): every roster member except the caller is
// flagged "has not answered" (`0x4bb24c`), the caller cleared (`0x4bb258`), the 30 s
// deadline armed (`0x4bb2b6`) — then the packet. A party has no raid roster, so the
// flags are empty there and the leader's own echo closes the check at once.
let own = model.party.own_guid;
model.ready_check.unanswered = model
.party
.raid
.iter()
.map(|m| m.guid)
.filter(|g| *g != own)
.collect();
model.ready_check.deadline = Some(now + READY_CHECK_SECONDS);
model.party_requests.push(PartyRequest::ReadyCheckStart);
Ok(())
})?,
)?;
// `CheckReadyCheckTime` (`0x4bc120` → `0x4bb310`) — the ready-check timeout tick, called from
// the stock `UIParent.xml`'s `<OnUpdate>` every frame and driving the 30-second expiry alone
// (nothing in the engine polls it). Four conjuncts, any failure writing nothing: a deadline is
// armed, it has been reached, and the active player's guid equals the group leader's — a
// LEADER gate, so it is observably inert for anyone else. On expiry it disarms the deadline
// (the only clear-to-zero outside module init), lists every roster member still flagged as
// unanswered, and prints `RAID_MEMBERS_AFK` with the `", "`-joined names — or
// `READY_CHECK_NO_AFK` — as a `CHAT_MSG_SYSTEM` line. Both keys are read raw off `_G`
// (`0x704350`: `lua_gettable` on GLOBALSINDEX, empty-string default), never through
// `GetText`. Sends no packet, raises nothing, returns nothing (decision 1989; wow-re
// `ui/scratch/uiparent-onupdate-engine-verbs.md` §3).
g.set(
"CheckReadyCheckTime",
lua.create_function(|lua, ()| {
let now = clock(lua);
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
ready_check_tick(lua, &mut model, now);
Ok(())
})?,
)?;
// ConfirmReadyCheck(ready) — the popup's two buttons, and the argument is read for TRUTH, not
// for a number: the reference's own No button calls it with no argument at all
// (`ConfirmReadyCheck()`) while Yes passes `1`, so "absent" has to mean "not ready".
@@ -772,6 +867,68 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Ok(())
}
/// The ready-check deadline, `0x7530` ms after the arm (`0x4ba535`, `0x4bb2b6`).
const READY_CHECK_SECONDS: f64 = 30.0;
/// The client's ready-check state (`RaidInfo.cpp`, decision 1989): the armed deadline
/// (`[0xb713f4]`, `None` = disarmed) and the roster members whose "has not answered yet" flag
/// (`[entry+0x158]`) is set, plus the summary lines the timeout worker composed and the app has
/// not yet pushed into chat. Outside [`PartyState`] because a roster push replaces that whole
/// snapshot and this survives one — the reference keeps the two in different tables too.
#[derive(Debug, Default)]
pub(crate) struct ReadyCheckState {
pub(crate) deadline: Option<f64>,
pub(crate) unanswered: Vec<u64>,
pub(crate) lines: Vec<String>,
}
/// `GetTime()`'s clock — the session seconds `UiScript::tick` advances.
fn clock(lua: &Lua) -> f64 {
lua.globals().get("__benilla_now").unwrap_or(0.0)
}
/// The timeout worker `0x4bb310` — see `CheckReadyCheckTime`'s note for the contract.
fn ready_check_tick(lua: &Lua, model: &mut Model, now: f64) {
let Some(deadline) = model.ready_check.deadline else {
return;
};
if now < deadline {
return;
}
if model.party.own_guid != model.party.leader_guid {
return;
}
model.ready_check.deadline = None;
// The roster walk: an unresolved name is silently skipped (`0x55f080` with its query arm
// off), so a flagged member the roster cannot name contributes nothing to the list.
let names: Vec<&str> = model
.ready_check
.unanswered
.iter()
.filter_map(|g| model.party.raid.iter().find(|m| m.guid == *g))
.map(|m| m.name.as_str())
.filter(|n| !n.is_empty())
.collect();
let raw = |key: &str| lua.globals().get::<String>(key).unwrap_or_default();
let text = if names.is_empty() {
raw("READY_CHECK_NO_AFK")
} else {
raw("RAID_MEMBERS_AFK").replacen("%s", &names.join(", "), 1)
};
model.ready_check.lines.push(text);
}
/// The two C-side force-close sites (`0x4ba4d4`, `0x4bacb4`): the deadline is set to `now − 1`
/// and the worker runs at once. The arm test is theirs too — both force only an armed check.
fn ready_check_force_close(lua: &Lua, model: &mut Model) {
if model.ready_check.deadline.is_none() {
return;
}
let now = clock(lua);
model.ready_check.deadline = Some(now - 1.0);
ready_check_tick(lua, model, now);
}
#[cfg(test)]
mod tests {
use crate::script::{PartyMemberInfo, PartyRequest, PartyState, UiScript};
@@ -790,6 +947,7 @@ mod tests {
],
leader_index: 1, // Alice (party1) leads
leader_guid: 0xA11CE,
own_guid: 0x5E1F,
raid: Vec::new(),
loot_method: "group".into(),
master_looter: None,
@@ -1489,4 +1647,160 @@ mod tests {
.eval::<bool>(r#"return GetRaidTargetIndex("target") == nil"#)
.unwrap());
}
// ── The ready-check timeout (decision 1989) ─────────────────────────────────────────────
/// A raid we lead: us, Alice and Bob on the roster; the two strings the summary reads raw.
fn raid_we_lead(s: &UiScript) -> PartyState {
s.run(
r#"RAID_MEMBERS_AFK = "The following players are AFK: %s"
READY_CHECK_NO_AFK = "No players are AFK""#,
)
.unwrap();
let row = |name: &str, guid: u64| crate::script::RaidMemberInfo {
name: name.into(),
guid,
..Default::default()
};
PartyState {
members: vec![
PartyMemberInfo {
name: "Alice".into(),
guid: 0xA11CE,
},
PartyMemberInfo {
name: "Bob".into(),
guid: 0xB0B,
},
],
leader_index: 0,
leader_guid: 0x5E1F,
own_guid: 0x5E1F,
raid: vec![
row("Probefour", 0x5E1F),
row("Alice", 0xA11CE),
row("Bob", 0xB0B),
],
loot_method: "group".into(),
master_looter: None,
loot_threshold: 2,
}
}
/// `DoReadyCheck` flags every roster member but the caller and arms 30 s; the tick is a no-op
/// before the deadline, prints the `", "`-joined unanswered names on it, and — the only
/// clear-to-zero — disarms, so the next tick prints nothing.
#[test]
fn the_leaders_check_times_out_into_the_afk_list_and_disarms() {
let mut s = UiScript::new().unwrap();
let party = raid_we_lead(&s);
s.set_party(party);
s.run("__benilla_now = 100 DoReadyCheck()").unwrap();
assert_eq!(s.take_party_requests(), vec![PartyRequest::ReadyCheckStart]);
s.run("__benilla_now = 129.9 CheckReadyCheckTime()")
.unwrap();
assert!(
s.take_ready_check_lines().is_empty(),
"not before the deadline"
);
s.run("__benilla_now = 130 CheckReadyCheckTime()").unwrap();
assert_eq!(
s.take_ready_check_lines(),
vec!["The following players are AFK: Alice, Bob".to_string()]
);
s.run("__benilla_now = 200 CheckReadyCheckTime()").unwrap();
assert!(
s.take_ready_check_lines().is_empty(),
"disarmed by the summary"
);
}
/// An answer — ready or not — clears its member's flag; the answer that leaves nobody pending
/// closes the check at once (`0x4ba4d9`), well before the deadline.
#[test]
fn an_answer_clears_its_member_and_the_last_one_closes_the_check_at_once() {
let mut s = UiScript::new().unwrap();
let party = raid_we_lead(&s);
s.set_party(party);
s.run("__benilla_now = 100 DoReadyCheck()").unwrap();
s.ready_check_answered(0xA11CE, false);
assert!(
s.take_ready_check_lines().is_empty(),
"Bob is still pending"
);
s.ready_check_answered(0xB0B, true);
assert_eq!(
s.take_ready_check_lines(),
vec!["No players are AFK".to_string()],
"nobody left pending ⇒ the summary now, not at 130"
);
s.run("__benilla_now = 130 CheckReadyCheckTime()").unwrap();
assert!(s.take_ready_check_lines().is_empty());
}
/// The tick is leader-gated: a member's client arms the same 30 s on the open form and the
/// four conjuncts fail on the guid compare, every frame, forever — the deadline stays armed
/// (nothing on the member's side ever clears it) and nothing is ever printed.
#[test]
fn the_tick_is_inert_for_a_non_leader() {
let mut s = UiScript::new().unwrap();
s.run(r#"READY_CHECK_NO_AFK = "No players are AFK""#)
.unwrap();
s.set_party(two_member_party()); // Alice leads; we are 0x5E1F
s.run("__benilla_now = 100").unwrap();
s.ready_check_request(false);
s.run("__benilla_now = 130 CheckReadyCheckTime()").unwrap();
assert!(s.take_ready_check_lines().is_empty());
assert_eq!(s.model_mut().ready_check.deadline, Some(130.0));
}
/// `SMSG_GROUP_LIST`'s leg: a member who left took their pending flag with them, and a roster
/// scan that finds nobody pending force-closes the check.
#[test]
fn a_roster_change_with_nobody_left_pending_closes_the_check() {
let mut s = UiScript::new().unwrap();
let party = raid_we_lead(&s);
s.set_party(party.clone());
s.run("__benilla_now = 100 DoReadyCheck()").unwrap();
s.ready_check_answered(0xA11CE, true);
// Bob leaves: the roster without him.
let mut without_bob = party;
without_bob.raid.retain(|m| m.guid != 0xB0B);
without_bob.members.retain(|m| m.guid != 0xB0B);
s.set_party(without_bob);
assert_eq!(
s.take_ready_check_lines(),
vec!["No players are AFK".to_string()]
);
}
/// A plain party has no raid roster, so `DoReadyCheck` flags nobody — and the leader's own
/// echo of the open form, taking the response-collection arm, closes it on the spot.
#[test]
fn the_leaders_own_echo_closes_an_empty_check_at_once() {
let mut s = UiScript::new().unwrap();
let mut party = raid_we_lead(&s);
party.raid.clear();
s.set_party(party);
s.run("__benilla_now = 100 DoReadyCheck()").unwrap();
s.ready_check_request(true);
assert_eq!(
s.take_ready_check_lines(),
vec!["No players are AFK".to_string()]
);
}
/// The two keys are read raw off `_G` with an empty-string default — a missing GlobalStrings
/// prints an empty line and never raises.
#[test]
fn a_missing_summary_string_prints_an_empty_line() {
let mut s = UiScript::new().unwrap();
let party = raid_we_lead(&s);
s.set_party(party);
s.run("RAID_MEMBERS_AFK = nil __benilla_now = 100 DoReadyCheck()")
.unwrap();
s.run("__benilla_now = 130 CheckReadyCheckTime()").unwrap();
assert_eq!(s.take_ready_check_lines(), vec![String::new()]);
}
}
+28 -11
View File
@@ -86,10 +86,13 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
lua.create_function(|lua, this: Table| Ok(measured_wh(lua, &this)?.1))?,
)?;
// GetLeft/GetRight/GetTop/GetBottom — the region's RESOLVED edges (y-up UI units; frame twin
// in object.rs). Every drawable region carries anchors (authored or the creation-path
// implicit anchor, decision 1310) and reads its resolved rect; a templateless Lua region
// nobody anchored never resolves → nil, same as pre-resolve.
// GetLeft/GetRight/GetTop/GetBottom — the region's RESOLVED edges in its OWNER's units (y-up;
// screen ÷ the owner's effective scale, the frame twin's law in `object/layout_methods.rs` —
// a region shares its owner's scale, and a texture inside the scaled world map answered
// screen units here while its owner answered local ones, decision 1985). Every drawable
// region carries anchors (authored or the creation-path implicit anchor, decision 1310) and
// reads its resolved rect; a templateless Lua region nobody anchored never resolves → nil,
// same as pre-resolve.
for (name, pick) in [
("GetLeft", 0u8),
("GetRight", 1u8),
@@ -101,11 +104,14 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
lua.create_function(move |lua, this: Table| {
let rh = region_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(model.region_resolved.get(&rh).map(|r| match pick {
0 => r.left,
1 => r.right,
2 => r.top,
_ => r.bottom,
let inv = 1.0 / owner_scale(&model, rh);
Ok(model.region_resolved.get(&rh).map(|r| {
inv * match pick {
0 => r.left,
1 => r.right,
2 => r.top,
_ => r.bottom,
}
}))
})?,
)?;
@@ -201,10 +207,11 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
lua.create_function(|lua, this: Table| {
let rh = region_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
let inv = 1.0 / owner_scale(&model, rh);
Ok(match model.region_resolved.get(&rh) {
Some(r) => (
Value::Number(f64::from((r.left + r.right) * 0.5)),
Value::Number(f64::from((r.bottom + r.top) * 0.5)),
Value::Number(f64::from(inv * (r.left + r.right) * 0.5)),
Value::Number(f64::from(inv * (r.bottom + r.top) * 0.5)),
),
None => (Value::Nil, Value::Nil),
})
@@ -273,3 +280,13 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
)?;
Ok(())
}
/// The region's owner frame's effective scale (1 for an orphan) — the divisor that turns a
/// resolved screen rect into the owner's units, the space every region getter answers in.
fn owner_scale(model: &Model, rh: crate::widget::RegionHandle) -> f32 {
model
.arena
.region(rh)
.map(|r| crate::script::object::eff_scale(model, r.owner))
.unwrap_or(1.0)
}
@@ -588,3 +588,34 @@ fn a_title_region_drag_swallows_the_press_and_ends_on_release() {
// that tells it apart from `StartMoving`, which raises "Frame %s is not movable".
assert!(!s.eval::<bool>("return TP:IsMovable()").unwrap());
}
/// A region's rect getters answer in its OWNER's units — screen ÷ the owner's effective scale —
/// exactly as the frame getters do (decision 1985): a texture inside a frame scaled to 0.5 that
/// covers the frame answers the frame's own width, not half of it.
#[test]
fn region_getters_answer_in_the_owners_units_under_scale() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run(
r#"f = CreateFrame("Frame", "ScaledOwner") f:SetWidth(200) f:SetHeight(100)
f:SetPoint("BOTTOMLEFT", 100, 50) f:SetScale(0.5)
t = f:CreateTexture("ScaledTex") t:SetAllPoints(f)"#,
)
.unwrap();
s.resolve();
let (fl, fr, tl, tr, cx) = s
.eval::<(f64, f64, f64, f64, f64)>(
"local cx = t:GetCenter() return f:GetLeft(), f:GetRight(), t:GetLeft(), t:GetRight(), cx",
)
.unwrap();
assert!(
(fl - tl).abs() < 1e-3 && (fr - tr).abs() < 1e-3,
"frame {fl}..{fr} vs region {tl}..{tr}"
);
assert!(
(fr - fl - 200.0).abs() < 1e-3,
"the owner's own width, {}",
fr - fl
);
assert!((cx - (fl + fr) * 0.5).abs() < 1e-3);
}
@@ -1262,6 +1262,7 @@ fn unit_is_party_leader_ors_two_legs_and_answers_one_when_solo() {
// comparison against our group's leader could ever express.
s.set_party(PartyState {
leader_guid: 0x9999,
own_guid: 0,
..Default::default()
});
s.set_unit(
@@ -1285,6 +1286,7 @@ fn unit_is_party_leader_ors_two_legs_and_answers_one_when_solo() {
// out-of-range member — the client holds no object, so leg 1 has nothing to read.
s.set_party(PartyState {
leader_guid: THEM,
own_guid: 0,
..Default::default()
});
s.set_unit(