Comments in benilla-ui: one line of fact each, no history, no record numbers (#143)

Part of #41: the comments of `benilla-ui` (the Lua UI engine, its FrameXML loader and its widgets) are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 43,140 comment lines become 15,161.

- **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal.
- **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them:
  - `0x64a480` is the case-sensitive compare (a byte `repz cmpsb`), and `0x64a4c0` the case-insensitive one.
  - A frame's id sits at `+0xb0`, not `+0xb4`. `EnableKeyboard`/`IsKeyboardEnabled` are `0x776ec0`/`0x776f90`.
  - A region's unpinned edge has no fallback to its owner's rect: the owner supplies only the scale.
  - `$parent` expands to the nearest named ancestor, or "Top" when none is named (`0x76c5b0`), not simply to the parent.
  - A StatusBar fills from its internal normalised fraction, not from `GetValue()`.
  - The addresses of `StartDuel` and `StartDuelUnit` were swapped.
  - Many "untraced" notes now give the traced reference fact: the drag threshold, the per-grip sizing edges, the empty-`autoFocus` skip, and `SetTexCoordModifiesRect`'s default.
  - Stock FrameXML line citations are checked against the stock 1.12 files and corrected. `ref-` file names become the stock file names.
- **Deviation labels.** "Deviation:" now marks only a choice with a reason. A surface that exists only for the retired Era addon target says so plainly: it is not 1.12.
- **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Nine second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the code, the stock files, vmangos and the reference.

Behaviour differences the rewrite surfaced are filed as their own issues (#131–#142), and more are being validated. The comments here state what the code does. String literals are untouched; they are #92.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-09-25 01:58:49 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 8505187668
commit fd386e75ae
233 changed files with 14108 additions and 42094 deletions
+61 -159
View File
@@ -1,7 +1,5 @@
//! The GameTooltip widget's engine mechanics (decision 0274): the line stack + named line
//! regions, auto-size from the measure round-trip, the right-flush, SetOwner's anchor law +
//! IsOwned, SetText's implicit show, both AddLine shapes, ClearLines/Hide firing
//! `OnTooltipCleared`, SetMinimumWidth's floor, and FadeOut's ramp-then-hide.
//! The GameTooltip widget's engine mechanics: the line stack and its auto-size, `SetOwner`'s
//! anchor modes, the implicit and gated shows, and the fade.
use super::common::script;
use crate::script::*;
@@ -22,8 +20,6 @@ fn measure_all(s: &mut UiScript, sizes: &[(&str, f32, f32)]) {
s.resolve();
}
/// The full stack: three lines (one double), measured, auto-sized, right-flushed, and the named
/// line globals published.
#[test]
fn line_stack_autosize_and_right_flush() {
let mut s = script();
@@ -84,16 +80,8 @@ fn line_stack_autosize_and_right_flush() {
assert_eq!(l2.top, 536.0);
}
/// An EMPTY line mid-stack (the corpus' `AddLine("")`; the live shape was an empty-string
/// subtitle from the wire): a ONE-UNIT row that also charges its 2px slot gap. Two fallbacks died
/// to get here. First the owner-edge one: an empty FontString never measures, so its unpinned
/// bottom edge fell back to the OWNER frame's bottom (0068's v1 region rule), the line stretched
/// to the plate's bottom and the anchor chain marched every later line OUT of the plate (the live
/// NPC-tooltip spill — name inside, "Level 20"/"PvP" below the frame under the health bar). Then
/// the zero-span collapse that replaced it, which the reference does not have either: its size
/// getters floor at one FrameXML unit, so `0.0` is not a span a FontString can have (decision
/// 1664). The row and the plate read that floor off ONE constant, which is what keeps them from
/// drifting apart the way B309's did.
/// An empty line is a one-unit row plus its 2 px gap, as the reference's size getters floor a
/// FontString at one FrameXML unit; the row and the plate read that floor from one constant.
#[test]
fn empty_line_is_a_one_unit_row_and_the_chain_stays_inside_the_plate() {
let mut s = script();
@@ -123,10 +111,7 @@ fn empty_line_is_a_one_unit_row_and_the_chain_stays_inside_the_plate() {
],
);
// Rows 14 + 1 + 12 + 12 with 3 slot gaps ⇒ totalh 45, height 65; maxw 90 ⇒ width 110.
// The blank row is ONE unit, not zero: a FontString's span floors at one FrameXML unit on
// both the plate's metric and the line's own rect, from one constant (decision 1664).
// (That this row exists at all is a separate open question — on the reference `AddLine("")`
// returns without incrementing the line count, `0x530270`.)
// The reference's `AddLine("")` returns without adding a line (`0x530270`); ours adds this row.
s.run(
r#"
assert(TTE:GetWidth() == 110, "auto width, got " .. tostring(TTE:GetWidth()))
@@ -145,8 +130,6 @@ fn empty_line_is_a_one_unit_row_and_the_chain_stays_inside_the_plate() {
assert!(s.take_errors().is_empty());
}
/// SetOwner clears previous content (firing OnTooltipCleared), owns the frame for IsOwned, and
/// Hide drops the owner + content and fires OnTooltipCleared again.
#[test]
fn owner_clear_and_hide_lifecycle() {
let mut s = script();
@@ -179,11 +162,9 @@ fn owner_clear_and_hide_lifecycle() {
assert!(s.take_errors().is_empty());
}
/// SetText shows the tooltip implicitly (the PaperDoll empty-slot flow calls no Show); AddLine
/// does not (the corpus' `AddLine … Show()`). The colour law: positional `r, g, b` apply only
/// when the r-slot is a number — the corpus' archaic `(text, "", r, g, b)` shape has a string
/// there, so the real 1.12 binding drops the whole colour tail and renders the DEFAULT GOLD
/// (the reference's zone tooltip, director-matched; the trailing numbers never shift into place).
/// `SetText` shows the tooltip and `AddLine` does not. `r, g, b` apply only when the r slot is a
/// number, so `AddLine(text, "", r, g, b)` draws in the default gold, as the reference's zone
/// tooltip does.
#[test]
fn settext_shows_and_both_addline_shapes() {
let mut s = script();
@@ -228,20 +209,17 @@ fn settext_shows_and_both_addline_shapes() {
})
.unwrap_or_else(|| panic!("no quad for {needle}"))
};
// The engine default gold 0xffffd200 (both no-colour shapes land on it).
// The default gold `0xffffd200`, where both no-colour shapes land.
let gold = [1.0, 210.0 / 255.0, 0.0, 1.0];
assert_eq!(color_of("Zone Name"), gold);
assert_eq!(color_of("plain gold"), gold);
assert_eq!(color_of("wrapped tail"), [0.2, 0.4, 0.6, 1.0]);
// The partial tail: r gates the block on, missing g/b read as 0.0 (byte-pinned).
// A numeric r gates the block on, and a missing g or b reads as 0.
assert_eq!(color_of("red only"), [1.0, 0.0, 0.0, 1.0]);
assert!(s.take_errors().is_empty());
}
/// A tooltip whose lines haven't been measured yet holds its declared size — the gaps alone must
/// never resolve it to a gaps-only rect (the first cut summed LINE_GAP/DOUBLE_GAP for unmeasured
/// rows and collapsed a fresh 4-line tooltip to 60×26; caught during the 0274 call-site
/// migration).
/// Until its lines are measured, a tooltip keeps its declared size rather than a gaps-only one.
#[test]
fn unmeasured_lines_hold_declared_size() {
let mut s = script();
@@ -260,7 +238,7 @@ fn unmeasured_lines_hold_declared_size() {
"#,
)
.unwrap();
// Resolve WITHOUT answering the measure round-trip: the declared 120×32 must hold.
// Resolve without answering the measure: the declared 120×32 holds.
s.resolve();
s.run(
r#"
@@ -284,7 +262,6 @@ fn unmeasured_lines_hold_declared_size() {
.unwrap();
}
/// SetMinimumWidth floors the auto width (the money row's law), and clears with the content.
#[test]
fn minimum_width_floors_autosize() {
let mut s = script();
@@ -307,8 +284,7 @@ fn minimum_width_floors_autosize() {
.unwrap();
}
/// FadeOut ramps the frame alpha down and hides at the end of the ramp (owner + lines dropped,
/// OnTooltipCleared fired); fresh content mid-fade cancels the ramp at full alpha.
/// Fresh content mid-fade cancels the ramp at full alpha.
#[test]
fn fadeout_ramps_then_hides() {
let mut s = script();
@@ -342,7 +318,6 @@ fn fadeout_ramps_then_hides() {
mid_alpha > 0.2 && mid_alpha < 0.8,
"mid-fade alpha ~0.5, got {mid_alpha}"
);
// Re-content cancels the fade at full alpha.
s.run(
r#"
TT5:SetText("Fresh Hover")
@@ -360,7 +335,7 @@ fn fadeout_ramps_then_hides() {
})
.expect("fresh line draws");
assert_eq!(fresh_alpha, 1.0, "fresh content restored full alpha");
// Run a full ramp to the end: hidden + cleared.
// A full ramp ends hidden and cleared.
s.run("TT5:FadeOut()").unwrap();
s.tick(0.6);
s.run(
@@ -374,12 +349,8 @@ fn fadeout_ramps_then_hides() {
assert!(s.take_errors().is_empty());
}
/// SetOwner's anchor law at the screen edge CLAMPS — the client's geometry-flags-bit4 clamp
/// (`assemble 0x767a20`), carried by every GameTooltip frame **by
/// construction** (decision 0352: no tooltip ever leaves the window). The reproduction is the
/// minimap zone-text hover (MinimapCluster.xml: `ANCHOR_LEFT` on a button at the very top of
/// the screen — plate bottom-right at the owner's top-left seats it wholly ABOVE the window):
/// the reference plate hangs DOWN from the screen top instead, size preserved, X untouched.
/// Every GameTooltip clamps to the screen (the geometry-flags bit 4 clamp, `assemble 0x767a20`), so
/// the zone text's `ANCHOR_LEFT` (`Minimap.lua:39`) hangs down from the screen top, not above it.
#[test]
fn owner_anchored_tooltip_clamps_to_screen() {
let mut s = script();
@@ -407,8 +378,7 @@ fn owner_anchored_tooltip_clamps_to_screen() {
],
);
// Auto-size: maxw 120 ⇒ width 140; totalh 14+2+12 ⇒ height 48. Unclamped, the plate's
// BOTTOMRIGHT sits at the owner's TOPLEFT (610, 600) — top 648, wholly above the window.
// The clamp shifts it back down: top at the screen top, size preserved, X untouched.
// BOTTOMRIGHT at the owner's TOPLEFT (610, 600) puts its top at 648.
s.run(
r#"
assert(TTC:GetTop() == 600, "top clamped to the screen top, got " .. TTC:GetTop())
@@ -419,7 +389,7 @@ fn owner_anchored_tooltip_clamps_to_screen() {
)
.unwrap();
s.resolve();
// The flag routes: unclamped, the plate returns to the raw anchor law above the window.
// Unclamped, the plate goes back above the window.
s.run(
r#"
assert(not TTC:IsClampedToScreen(), "flag readable")
@@ -431,14 +401,11 @@ fn owner_anchored_tooltip_clamps_to_screen() {
assert!(s.take_errors().is_empty());
}
/// `0x52fa50`'s ladder as pure logic — the arm thresholds, the
/// ceil/truncate asymmetry, and the `_P1` pick — over SYNTHETIC templates, so this asserts the
/// mechanism and carries none of the reference's own eight strings. The real ones are read off the
/// player's install and exercised in `benilla::ui_script::buff_tests`.
/// `0x52fa50`'s duration ladder over stand-in templates: the arm edges, the ceiling on every arm
/// but seconds, and the `_P1` plural pick.
#[test]
fn the_duration_ladder_ceils_every_arm_but_seconds() {
// Deliberately not the shipped wording: what is under test is which key is reached and what
// number fills it, never what the string says.
// Not the shipped wording: the test is which key is reached and what number fills it.
let table = |key: &str| -> Option<String> {
Some(
match key {
@@ -457,8 +424,7 @@ fn the_duration_ladder_ceils_every_arm_but_seconds() {
};
let d = |ms: u32| crate::script::tooltip::duration_text(ms, "T", true, &table);
// The three arm boundaries, from the compare instructions — each is exact, and the arm below
// it counts in its own unit right up to the edge.
// The three arm edges are exact, and the arm below counts in its own unit up to the edge.
assert_eq!(d(86_400_000).as_deref(), Some("<1 day>"), "the day edge");
assert_eq!(
d(86_399_999).as_deref(),
@@ -478,8 +444,7 @@ fn the_duration_ladder_ceils_every_arm_but_seconds() {
"the worked example: 61 s ceils to 2, it does not truncate to 1"
);
// The asymmetry that makes this one function rather than four format calls: roundUp reaches
// the top three arms only.
// roundUp reaches the top three arms only.
assert_eq!(
d(59_999).as_deref(),
Some("<59 secs>"),
@@ -498,18 +463,18 @@ fn the_duration_ladder_ceils_every_arm_but_seconds() {
"exactly one is singular"
);
// roundUp = 0 truncates the top arms too (the parameter is real, not a constant we folded).
// roundUp = 0 truncates the top arms too.
assert_eq!(
crate::script::tooltip::duration_text(61_000, "T", false, &table).as_deref(),
Some("<1 min>"),
);
// A key the string table does not carry yields NO LINE rather than an invented one.
// A key the string table lacks yields no line.
assert_eq!(
crate::script::tooltip::duration_text(1_000, "NOPE", true, &table),
None
);
// …and a family shipping only the singular falls back to it rather than vanishing.
// A family with only the singular falls back to it.
let singular_only = |key: &str| (key == "S_SEC").then(|| "<%d s>".to_string());
assert_eq!(
crate::script::tooltip::duration_text(5_000, "S", true, &singular_only).as_deref(),
@@ -517,20 +482,9 @@ fn the_duration_ladder_ceils_every_arm_but_seconds() {
);
}
/// **B309 — a POOLED line cell that goes empty must not keep the box it measured last hover.**
///
/// `measured` is a cache the solver honours without a key check on purpose: it is the last-known
/// box, held so a line whose text just changed does not collapse for the frame its re-measure is
/// in flight. Empty text is the one case where that measure never comes — both measure asks
/// filter empty strings out — so before the fix an emptied cell held its dead box FOREVER, and
/// only the DRAWN geometry was wrong: the plate's own sum calls empty text zero
/// (`tooltip::cell`) and the Lua getters key-check (`region::measured_wh`), so both said 0 while
/// the solver stood a full row there.
///
/// Live, that is the item tooltip's SET block: `ClearLines` keeps the cache (the hover re-enter
/// loop depends on it), so the two blank gold spacers land on cells that carried real text on an
/// earlier hover. Each drew an uncounted row and the set bonuses hung below the backdrop —
/// on Field Marshal's Raiment, two blanks, two rows.
/// The solver keeps a line's last measured box, unkeyed, while a re-measure is in flight, and an
/// empty line is never measured, so a pooled cell that goes empty must drop its old box: the item
/// tooltip's set block puts blank spacers on cells that held text.
#[test]
fn an_emptied_pooled_line_drops_its_stale_box_and_the_plate_still_contains_the_chain() {
let mut s = script();
@@ -556,8 +510,7 @@ fn an_emptied_pooled_line_drops_its_stale_box_and_the_plate_still_contains_the_c
("PvP", 24.0, 12.0),
];
measure_all(&mut s, sizes);
// Hover two, SAME pooled cells: line 2 is now the blank spacer. Nothing new to measure —
// lines 1 and 3 re-validate against their own keys, and an empty string is never asked for.
// Hover two, same pooled cells: line 2 is now the blank spacer, which is never measured.
s.run(
r#"
TTR:ClearLines()
@@ -590,21 +543,11 @@ 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 bytes refute both (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.
/// An omitted `anchorType` is mode 0, ANCHOR_LEFT: `0x53120d` zeroes the local, the `lua_isstring`
/// gate at `0x531214` skips the compare chain, and nothing in `[0x531221, 0x53133a)` raises. The
/// core's pre-store of mode 7 (`0x530012`) survives only a NULL owner (`0x530031`). `0x52fe90`
/// returns for mode 8 (`0x52fead`) before its clear (`0x52fec2 call 0x767ed0`), and `SetOwner`
/// always passes the arg that gates mode 7's skip as 1, so only ANCHOR_PRESERVE keeps the points.
#[test]
fn set_owner_defaults_to_anchor_left_and_only_preserve_keeps_the_placement() {
let mut s = script();
@@ -623,7 +566,7 @@ fn set_owner_defaults_to_anchor_left_and_only_preserve_keeps_the_placement() {
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.
// ANCHOR_PRESERVE (mode 8) is the one mode that leaves the plate alone.
s.run(r#"Plate:SetOwner(Owner, "ANCHOR_PRESERVE")"#)
.unwrap();
s.resolve();
@@ -634,7 +577,7 @@ fn set_owner_defaults_to_anchor_left_and_only_preserve_keeps_the_placement() {
);
assert_eq!(s.eval::<i64>("return Plate:GetNumPoints()").unwrap(), 1);
// ANCHOR_NONE (mode 7) clears — which is what makes `GameTooltip_SetDefaultAnchor` safe.
// ANCHOR_NONE (mode 7) clears, which `GameTooltip.lua:73`'s default anchor relies on.
s.run(r#"Plate:SetOwner(Owner, "ANCHOR_NONE")"#).unwrap();
assert_eq!(
s.eval::<i64>("return Plate:GetNumPoints()").unwrap(),
@@ -642,8 +585,7 @@ fn set_owner_defaults_to_anchor_left_and_only_preserve_keeps_the_placement() {
"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.
// No anchor string: mode 0 places the plate's BOTTOMRIGHT at the owner's TOPLEFT.
s.run(r#"Plate:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 100, 100)"#)
.unwrap();
s.run("Plate:SetOwner(Owner)").unwrap();
@@ -659,8 +601,7 @@ fn set_owner_defaults_to_anchor_left_and_only_preserve_keeps_the_placement() {
"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.
// An unrecognised string falls through the compare chain: the same silent mode 0.
s.run(r#"Plate:SetOwner(Owner, "ANCHOR_SIDEWAYS")"#)
.unwrap();
assert_eq!(
@@ -669,11 +610,7 @@ fn set_owner_defaults_to_anchor_left_and_only_preserve_keeps_the_placement() {
);
}
/// **`SetAlpha(255)` on SetOwner is UNCONDITIONAL** — `0x52fff4`, the first thing the SetOwner
/// core `0x52ffe0` does, before its five stores (`0x52ffe0`). Ours only ever restored the alpha one
/// of OUR OWN fades had taken away, so a plate left dim by any other path — an addon's `SetAlpha`,
/// an inherited parent alpha — stayed dim through every subsequent hover, where the reference
/// stamps it back to full on each SetOwner.
/// The `SetOwner` core `0x52ffe0` first sets alpha 255 (`0x52fff4`), whatever dimmed the plate.
#[test]
fn set_owner_stamps_full_alpha_even_with_no_fade_running() {
let s = script();
@@ -694,19 +631,9 @@ fn set_owner_stamps_full_alpha_even_with_no_fade_running() {
.unwrap();
}
/// **`Show` is an EXISTENCE GATE, not a plain show** — `0x530a80`, the CGameTooltip override of
/// `vtbl+0x88` (the slot a Lua `:Show()` lands in). It shows only when BOTH the owner `+0x314`
/// and the line count `+0x31c` are non-zero, and otherwise calls its own `vtbl+0x84`
/// effective-hide `0x530a60` — the SetOwner core with a NULL owner, so the plate is hidden AND
/// un-owned (`0x530a80`).
///
/// The symptom that found it: `Questie`'s tracker draws an EMPTY plate on hover — correctly
/// sized and bordered, with no text. `QuestieTracker.lua`'s quest-button OnEnter has a dead zone
/// between its two arms (an in-progress quest that has objectives AND whose objective text is in
/// the Questie database matches neither), so it adds no line and then calls `Tooltip:Show()`
/// unconditionally. The reference swallows that; we drew it — and, because `layout_tooltips`
/// skips a zero-line plate instead of collapsing it, we drew it at the LAST hover's size, which
/// is why it looked like a real tooltip with the text missing rather than a stub.
/// `Show` (`0x530a80`, the `vtbl+0x88` override) shows only with an owner (`+0x314`) and a line
/// (`+0x31c`); otherwise it calls `0x530a60` (`vtbl+0x84`), the `SetOwner` core with a NULL owner,
/// which hides and un-owns. `QuestieTracker.lua`'s OnEnter can call `Show` with no line.
#[test]
fn show_with_no_lines_self_hides_and_un_owns() {
let mut s = script();
@@ -726,8 +653,7 @@ fn show_with_no_lines_self_hides_and_un_owns() {
)
.unwrap();
// Questie's exact shape: the re-hover's SetOwner clears the content, the handler's dead zone
// adds nothing, and Show() runs anyway.
// Questie's shape: a re-hover's `SetOwner` clears the lines, nothing is added, `Show` runs.
s.run(
r#"
Tip:SetOwner(Owner, "ANCHOR_RIGHT")
@@ -739,7 +665,7 @@ fn show_with_no_lines_self_hides_and_un_owns() {
)
.unwrap();
// The other half of the gate, on its own: a line but no owner is equally not shown.
// A line but no owner is the other half of the gate.
s.run(
r#"
Tip:AddLine("Orphan", 1, 1, 1)
@@ -750,20 +676,9 @@ fn show_with_no_lines_self_hides_and_un_owns() {
.unwrap();
}
/// **A NON-STRING anchor argument must not raise** — and for a year it did, because the binding
/// took arg 3 as `Option<String>` and let mlua's converter be the gate.
///
/// The reference's gate is `lua_isstring 0x6f3510`, whose entire body is `lua_type` then
/// `cmp eax,4 / cmp eax,3`: LUA_TSTRING or LUA_TNUMBER pass and are read by coercion, and every
/// other tag takes `0x53121b je 0x53133a` over the whole compare chain to the join, leaving the
/// zeroed local — mode 0, `ANCHOR_LEFT`, silently, with no `luaL_error` anywhere in
/// `[0x531221, 0x53133a)`. So a boolean, a table, a function and absent are *indistinguishable*
/// at this position.
///
/// The symptom that found it: `Questie` hovers a world-map note with
/// `Tooltip:SetOwner(this, this)` (`QuestieNotes.lua` `Questie_Tooltip_OnEnter`) — a frame where
/// the anchor goes — and got `bad argument #3: error converting Lua table to String` where the
/// reference draws the tooltip.
/// `lua_isstring 0x6f3510` passes only a string or a number; any other tag jumps the compare chain
/// (`0x53121b je 0x53133a`) to mode 0 and raises nothing. `QuestieNotes.lua` passes a frame here
/// (`SetOwner(this, this)`).
#[test]
fn a_non_string_anchor_is_silently_mode_zero_and_never_raises() {
let s = script();
@@ -775,7 +690,7 @@ fn a_non_string_anchor_is_silently_mode_zero_and_never_raises() {
)
.unwrap();
// A TABLE — Questie's own call shape, a frame passed where the anchor string goes.
// A table, as Questie passes.
s.run("Plate:SetOwner(Owner, Owner)")
.expect("a table anchor takes the isstring gate's jump, it does not raise");
assert_eq!(
@@ -783,7 +698,7 @@ fn a_non_string_anchor_is_silently_mode_zero_and_never_raises() {
"ANCHOR_LEFT"
);
// A BOOLEAN and an explicit nil are the same jump.
// A boolean and an explicit nil take the same jump.
for arg in ["true", "false", "nil"] {
s.run(&format!(
r#"Plate:SetOwner(Owner, "ANCHOR_RIGHT") Plate:SetOwner(Owner, {arg})"#
@@ -796,8 +711,7 @@ fn a_non_string_anchor_is_silently_mode_zero_and_never_raises() {
);
}
// A NUMBER is the one non-string tag the gate DOES admit: it is stringified and run through
// the chain, which no number can match — the same mode 0, by the other route.
// A number passes the gate as a string that matches no mode: mode 0 again.
s.run(r#"Plate:SetOwner(Owner, "ANCHOR_RIGHT") Plate:SetOwner(Owner, 5)"#)
.unwrap();
assert_eq!(
@@ -806,11 +720,8 @@ fn a_non_string_anchor_is_silently_mode_zero_and_never_raises() {
);
}
/// **`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`.
/// `ANCHOR_CURSOR`, mode 6, clears at `SetOwner` like mode 7; the per-frame update `0x530b20` then
/// pins the plate's BOTTOM to the screen's BOTTOMLEFT at the cursor, over its effective scale.
#[test]
fn anchor_cursor_follows_the_cursor_every_frame() {
let mut s = script();
@@ -851,7 +762,7 @@ fn anchor_cursor_follows_the_cursor_every_frame() {
"centred horizontally on the cursor: {left}..{right}"
);
// It follows. The whole point of mode 6 is that no second SetOwner is needed.
// It follows the cursor with no second `SetOwner`.
s.mouse_move(120.0, 480.0);
s.resolve();
let (left, right, bottom): (f32, f32, f32) = s
@@ -864,14 +775,8 @@ fn anchor_cursor_follows_the_cursor_every_frame() {
);
}
/// `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`).
///
/// The round trip is the whole point rather than a nicety: `_Nameplates/_Nameplates.lua:479` is
/// `if GameTooltip:GetAnchorType() ~= Anchor then GameTooltip:SetOwner(Column, Anchor) end`, so an
/// answer that never equals what the setter was handed would re-own the plate every OnUpdate; and
/// `pfUI/modules/tooltip.lua:97` compares against the literal `"ANCHOR_NONE"`.
/// `GetAnchorType` (`0x5313e0`, table `0x854198`) answers one string, `[+0x318]` through the name
/// table `0x531530`; `_Nameplates.lua:479` compares it with what it passed to `SetOwner`.
#[test]
fn tooltip_anchor_type_round_trips_every_reachable_mode() {
let s = script();
@@ -883,8 +788,7 @@ fn tooltip_anchor_type_round_trips_every_reachable_mode() {
)
.unwrap();
// Arity 1, kind string, and NO nil leg — the shapes row is `(string)` with no alternative, so
// a plate nobody has owned still answers a string.
// Never nil: a plate nobody has owned answers a string too.
assert_eq!(s.arity("AnchorTip:GetAnchorType()").unwrap(), 1, "arity 1");
assert_eq!(
s.eval::<String>("return type(AnchorTip:GetAnchorType())")
@@ -899,8 +803,7 @@ fn tooltip_anchor_type_round_trips_every_reachable_mode() {
"a plate nothing has owned is anchored to nothing"
);
// 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.
// All nine modes round-trip, ANCHOR_PRESERVE included: the reference stores it like any other.
for mode in [
"ANCHOR_RIGHT",
"ANCHOR_LEFT",
@@ -922,7 +825,7 @@ fn tooltip_anchor_type_round_trips_every_reachable_mode() {
);
}
// Lower case in, the reference's canonical spelling out — SetOwner already folds the token.
// Lower case in, the canonical spelling out.
s.run(r#"AnchorTip:SetOwner(AnchorOwner, "anchor_left")"#)
.unwrap();
assert_eq!(
@@ -931,9 +834,8 @@ fn tooltip_anchor_type_round_trips_every_reachable_mode() {
"ANCHOR_LEFT"
);
// 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.
// An unrecognised anchor is mode 0, with no raise (`0x53120d`). Deviation: we also log a
// warning so the unknown token shows up in diagnostics; no addon can see it.
s.run(r#"AnchorTip:SetOwner(AnchorOwner, "ANCHOR_SIDEWAYS")"#)
.unwrap();
assert_eq!(