NTFS load order, a title-sorted AddOns list, and tooltips at the cursor

- addons load in the order NTFS lists a folder, case-insensitive with an underscore before the letters, the order the vanilla corpus was written against and the one that decides which addon provides a shared library (2178)
- GetNumAddOns and the index form of every AddOn verb walk a title-sorted list that hides the Blizzard addons the server says to hide, and the character-select AddOns list is in title order (2175)
- SetPoint and SetAllPoints raise the reference's own errors for a frame name that does not exist or an omitted relative frame instead of quietly anchoring to the parent, and an explicit nil means the screen root (2176)
- a tooltip owned with ANCHOR_CURSOR follows the mouse, which pfUI's tooltip, xpbar and chat modules, pfQuest's browser and TipBuddy ask for; an omitted or unknown anchor is ANCHOR_LEFT (2176)
- a $parent name handed to CreateFrame, CreateTexture or CreateFontString expands as it does in XML, so FonzAppraiser's thirty dropdowns and pfQuest's search icon get the names they asked for (2176)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-11 21:03:49 +07:00
co-authored by Claude Fable 5.1
parent 5af3a15d0e
commit eebbcc050f
53 changed files with 1978 additions and 541 deletions
+147 -9
View File
@@ -590,6 +590,144 @@ fn an_emptied_pooled_line_drops_its_stale_box_and_the_plate_still_contains_the_c
assert!(s.take_errors().is_empty());
}
/// **`SetOwner`'s omitted `anchorType` is mode 0 = ANCHOR_LEFT**, and the clear-vs-keep split runs
/// between NONE and PRESERVE, not between "points" and "doesn't".
///
/// 2142's open thread 1 predicted the opposite on both counts — a mode-7 default, and a NONE arm
/// that must stop dropping anchors — and the wow-re §5 dispatched for the CURSOR mechanism refuted
/// both at the bytes (`system/ui/scratch/tooltip-cursor-anchor-law.md` §0.2/§0.3/§2; decision
/// 2176). What is actually there:
///
/// * `0x53120d` zeroes the binding's local mode before any compare, and `0x531214`'s
/// `lua_isstring` gate jumps the whole `SStrCmpI` chain when arg 3 is absent, nil, a boolean or
/// a table — so the answer is **0 = ANCHOR_LEFT**, silently, with no `luaL_error` anywhere in
/// `[0x531221, 0x53133a)`. The core's `mov [+0x318],7` at `0x530012` is an unconditional
/// pre-store `0x530031` overwrites; it survives only when the owner is NULL.
/// * `0x52fe90` returns for mode **8** at `0x52fead`, *before* `0x52fec2 call 0x767ed0`, and the
/// mode-7 skip beside that clear is gated on an arg1 `SetOwner`'s core always passes as 1. So
/// every mode 0..7 clears, and only PRESERVE keeps the placement.
#[test]
fn set_owner_defaults_to_anchor_left_and_only_preserve_keeps_the_placement() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
Plate = CreateFrame("GameTooltip", "Plate")
Owner = CreateFrame("Frame", "Owner")
Owner:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 300, 300)
Owner:SetWidth(40) Owner:SetHeight(40)
Plate:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)
Plate:SetWidth(50) Plate:SetHeight(20)
"#,
)
.unwrap();
s.resolve();
assert_eq!(s.eval::<f32>("return Plate:GetLeft()").unwrap(), 100.0);
// ANCHOR_PRESERVE (mode 8) is the ONE mode that leaves the plate alone.
s.run(r#"Plate:SetOwner(Owner, "ANCHOR_PRESERVE")"#)
.unwrap();
s.resolve();
assert_eq!(
s.eval::<f32>("return Plate:GetLeft()").unwrap(),
100.0,
"mode 8 returns before the ClearAllPoints"
);
assert_eq!(s.eval::<i64>("return Plate:GetNumPoints()").unwrap(), 1);
// ANCHOR_NONE (mode 7) clears — which is what makes `GameTooltip_SetDefaultAnchor` safe.
s.run(r#"Plate:SetOwner(Owner, "ANCHOR_NONE")"#).unwrap();
assert_eq!(
s.eval::<i64>("return Plate:GetNumPoints()").unwrap(),
0,
"mode 7 reaches the clear: arg1 is always 1 from SetOwner's core"
);
// `SetOwner(f)` — no anchor string at all. Mode 0, and mode 0 PLACES: ANCHOR_LEFT pins the
// plate's BOTTOMRIGHT to the owner's TOPLEFT, so its right edge sits at the owner's left.
s.run(r#"Plate:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)"#)
.unwrap();
s.run("Plate:SetOwner(Owner)").unwrap();
assert_eq!(
s.eval::<String>("return Plate:GetAnchorType()").unwrap(),
"ANCHOR_LEFT",
"the omitted anchorType is the binding's zero-initialised local, mode 0"
);
s.resolve();
assert_eq!(
s.eval::<f32>("return Plate:GetLeft()").unwrap(),
250.0,
"the owner's left edge (300) less the plate's own 50 wide"
);
// An UNRECOGNISED string is the same silent mode 0 — the compare chain falls through with the
// local untouched, and raises nothing.
s.run(r#"Plate:SetOwner(Owner, "ANCHOR_SIDEWAYS")"#)
.unwrap();
assert_eq!(
s.eval::<String>("return Plate:GetAnchorType()").unwrap(),
"ANCHOR_LEFT"
);
}
/// **`ANCHOR_CURSOR` is a real ninth mode**, not a string to warn about (2142's open thread 2, and
/// nine corpus files ask for it): mode 6 clears at `SetOwner` time like mode 7, and then the
/// per-frame update `0x530b20` pins the plate's **BOTTOM** to the screen's **BOTTOMLEFT** at the
/// cursor's absolute position, divided by the plate's own effective scale. See
/// `script::tooltip::cursor_anchor`.
#[test]
fn anchor_cursor_follows_the_cursor_every_frame() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
Tip = CreateFrame("GameTooltip", "Tip")
Host = CreateFrame("Frame", "Host")
Tip:SetOwner(Host, "ANCHOR_CURSOR")
Tip:AddLine("Copper Ore")
-- An explicit size, because this VM has no text measurer: the auto-size pre-pass leaves a
-- plate with no measured extents at its declared size, and the placement is what is under
-- test, not the width.
Tip:SetWidth(120) Tip:SetHeight(40)
Tip:Show()
"#,
)
.unwrap();
assert_eq!(
s.eval::<String>("return Tip:GetAnchorType()").unwrap(),
"ANCHOR_CURSOR",
"the mode round-trips; it is no longer recorded as whatever it was placed by"
);
assert!(
!s.warnings().iter().any(|w| w.contains("ANCHOR_CURSOR")),
"a mode we honour is not warned about: {:?}",
s.warnings()
);
s.mouse_move(300.0, 200.0);
s.resolve();
let (left, right, bottom): (f32, f32, f32) = s
.eval("return Tip:GetLeft(), Tip:GetRight(), Tip:GetBottom()")
.unwrap();
assert_eq!(bottom, 200.0, "the plate's BOTTOM sits at the cursor");
assert!(
((left + right) / 2.0 - 300.0).abs() < 0.001,
"centred horizontally on the cursor: {left}..{right}"
);
// It follows. The whole point of mode 6 is that no second SetOwner is needed.
s.mouse_move(120.0, 480.0);
s.resolve();
let (left, right, bottom): (f32, f32, f32) = s
.eval("return Tip:GetLeft(), Tip:GetRight(), Tip:GetBottom()")
.unwrap();
assert_eq!(bottom, 480.0);
assert!(
((left + right) / 2.0 - 120.0).abs() < 0.001,
"{left}..{right}"
);
}
/// `GameTooltip:GetAnchorType()` — ONE string, the reference's own spelling, round-tripping
/// whatever `SetOwner` was given (`0x5313e0`, table `0x854198`, argc 1, arity 1, kinds `(string)`,
/// reading `[+0x318]` back through the name table `0x531530`).
@@ -625,7 +763,7 @@ fn tooltip_anchor_type_round_trips_every_reachable_mode() {
"a plate nothing has owned is anchored to nothing"
);
// The eight modes SetOwner accepts, each answered back verbatim — ANCHOR_PRESERVE included,
// All NINE modes SetOwner accepts, each answered back verbatim — ANCHOR_PRESERVE included,
// which the reference stores like any other rather than resolving back to what it preserved.
for mode in [
"ANCHOR_RIGHT",
@@ -634,6 +772,7 @@ fn tooltip_anchor_type_round_trips_every_reachable_mode() {
"ANCHOR_TOPLEFT",
"ANCHOR_BOTTOMRIGHT",
"ANCHOR_BOTTOMLEFT",
"ANCHOR_CURSOR",
"ANCHOR_NONE",
"ANCHOR_PRESERVE",
] {
@@ -656,20 +795,19 @@ fn tooltip_anchor_type_round_trips_every_reachable_mode() {
"ANCHOR_LEFT"
);
// An anchor SetOwner cannot honour is recorded as the ANCHOR_RIGHT it is actually PLACED by,
// never as the string it was handed — the getter reports the plate, not the request.
// `ANCHOR_CURSOR` is the live case (nine corpus files ask for it; mode 6 follows the cursor per
// frame and this engine has no such driver), and SetOwner warns rather than pretending.
s.run(r#"AnchorTip:SetOwner(AnchorOwner, "ANCHOR_CURSOR")"#)
// An anchor SetOwner does not recognise is the binding's zero-initialised local, mode 0 —
// silently, with no raise (`0x53120d`, and no `luaL_error` in the compare chain). Ours adds a
// warning the reference does not have, which is a diagnostic and not API surface.
s.run(r#"AnchorTip:SetOwner(AnchorOwner, "ANCHOR_SIDEWAYS")"#)
.unwrap();
assert_eq!(
s.eval::<String>("return AnchorTip:GetAnchorType()")
.unwrap(),
"ANCHOR_RIGHT"
"ANCHOR_LEFT"
);
assert!(
s.warnings().iter().any(|w| w.contains("ANCHOR_CURSOR")),
"the unhonoured anchor is warned, not silently taken: {:?}",
s.warnings().iter().any(|w| w.contains("ANCHOR_SIDEWAYS")),
"the unrecognised anchor is still reported to us: {:?}",
s.warnings()
);
}