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:
@@ -33,8 +33,8 @@ use super::{FontObject, Model, RegionData};
|
||||
use crate::layout::{Anchor, Point};
|
||||
use crate::order::DrawLayer;
|
||||
use crate::widget::{
|
||||
FrameHandle, KindState, RegionKind, TooltipState, TOOLTIP_DOUBLE_GAP, TOOLTIP_FADE_SECS,
|
||||
TOOLTIP_LINE_GAP, TOOLTIP_PAD,
|
||||
FrameHandle, KindState, RegionKind, TooltipAnchor, TooltipState, TOOLTIP_DOUBLE_GAP,
|
||||
TOOLTIP_FADE_SECS, TOOLTIP_LINE_GAP, TOOLTIP_PAD,
|
||||
};
|
||||
|
||||
/// Registry key of the GameTooltip method table (the MAXCSTACK discipline: named registry root).
|
||||
@@ -673,6 +673,67 @@ fn cell(model: &Model, rh: crate::widget::RegionHandle) -> Cell {
|
||||
/// (`resolve` solves, fills the pending measures inline, and solves again, so this pre-pass runs
|
||||
/// a second time with real extents); without one it is the host's batch round-trip, one frame
|
||||
/// later, which is the engine-less path every measurer-free VM still takes.
|
||||
/// **ANCHOR_CURSOR — mode 6, re-anchored to the live cursor every frame** (`0x530b20`, the
|
||||
/// `CGameTooltip` override of `vtable+0x38` on `0x808f60`; wow-re
|
||||
/// `system/ui/scratch/tooltip-cursor-anchor-law.md` §1, three independent derivations agreeing
|
||||
/// instruction-for-instruction). Nine corpus files ask for it — `pfUI`'s tooltip, xpbar and chat
|
||||
/// modules, `pfQuest/browser.lua`, `TipBuddy` — and until decision 2176 benilla warned and placed
|
||||
/// them by some other mode.
|
||||
///
|
||||
/// The whole of the re-anchor is one `SetPoint`:
|
||||
///
|
||||
/// ```text
|
||||
/// 530b32 cmp [this+0x318],6 ; jne -- every other mode skips
|
||||
/// 530b3b eax = [this+0xa0] -- the CSimpleTop root == [0xcf0bd8]
|
||||
/// 530b4f ecx = [eax+0x111c] ; edx = [eax+0x1118] -- the SAVED cursor, normalised [0,1]
|
||||
/// 530b63 call 0x41ad80 -- x *= G44 (screen w), y *= G48 (screen h)
|
||||
/// 530b68 fld [this+0x7c] ; fdivr 1.0f -- 1 / the tooltip's own effective scale
|
||||
/// 530b97 call 0x767c70(BOTTOM=7, [0xcf0bd8], BOTTOMLEFT=6, x/scale, y/scale, doResolve=1)
|
||||
/// ```
|
||||
///
|
||||
/// So **the plate's BOTTOM-centre is pinned to the screen's BOTTOMLEFT plus the cursor's absolute
|
||||
/// position** — centred horizontally on the cursor and sitting directly above it. Four details are
|
||||
/// worth keeping because none is guessable from the name:
|
||||
///
|
||||
/// * The `relativeTo` is the screen root passed **raw**, not `+0x24` like every other anchor — the
|
||||
/// `CSimpleTop`'s `CLayoutFrame` base sits at offset 0. Ours is [`SCREEN`](super::SCREEN), which
|
||||
/// is the same object.
|
||||
/// * The **divide by the tooltip's own effective scale** is the "cursor box recip": an anchor
|
||||
/// offset is stored raw and multiplied by the child's scale at resolve time, so dividing here is
|
||||
/// what makes the result land at the cursor's *absolute* position. Ours does the same divide,
|
||||
/// against the same scale [`GetWidth`](super::object::eff_scale) reads.
|
||||
/// * **The `SetOwner`-time offsets `+0x3c4/+0x3c8` do not participate** — 12 reads of them, every
|
||||
/// one on a modes-0..5 arm of `0x52fe90`.
|
||||
/// * There is **no `ClearAllPoints` here** (earned zero over the function's complete call set): one
|
||||
/// anchor slot is overwritten, and `0x767c70`'s own no-op gate — same target, same
|
||||
/// `relativePoint`, both offsets within 2⁻²² — is what keeps a still cursor free.
|
||||
///
|
||||
/// It runs from the per-frame update pump, which a hidden tooltip is not in (`0x76ad9d`
|
||||
/// deregisters on hide), and in the **same frame** as the placement it produces: the layout drain
|
||||
/// `0x768ed0` runs at `0x765799`, right after the `+0x38` loop and before the draw. Hence its home
|
||||
/// here, at the top of the resolve, beside the auto-size pass.
|
||||
fn cursor_anchor(model: &mut Model, h: FrameHandle) {
|
||||
let scale = crate::script::object::eff_scale(model, h);
|
||||
let (cx, cy) = model.cursor_pos;
|
||||
let new = Anchor::new(
|
||||
Point::Bottom,
|
||||
super::SCREEN,
|
||||
Point::BottomLeft,
|
||||
cx / scale,
|
||||
cy / scale,
|
||||
);
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
// `0x767c70`'s change-detect, in our own terms: one slot, replaced only on a real change.
|
||||
let same =
|
||||
input.anchors.len() == 1 && crate::script::object::anchor_bits_eq(&input.anchors[0], &new);
|
||||
if same {
|
||||
return;
|
||||
}
|
||||
let old_targets: Vec<u32> = input.anchors.iter().map(|a| a.relative_to).collect();
|
||||
input.anchors = vec![new];
|
||||
model.touch_layout_retarget_frame(h, &old_targets, &[super::SCREEN]);
|
||||
}
|
||||
|
||||
pub(super) fn layout_tooltips(model: &mut Model) {
|
||||
// The arena's tooltip registry, not the resolve's whole frame roster: this pre-pass runs at
|
||||
// the top of EVERY resolve, and finding two or three tooltips by scanning ~4000 ids was most
|
||||
@@ -686,6 +747,16 @@ pub(super) fn layout_tooltips(model: &mut Model) {
|
||||
.filter(|h| model.frame_to_id.contains_key(h))
|
||||
.collect();
|
||||
for h in tips {
|
||||
// Mode 6 first, and **before the line gate below**: the reference's per-frame update is
|
||||
// not gated on content at all, only on the plate being in the pump's list — which is the
|
||||
// shown list. See [`cursor_anchor`].
|
||||
let cursor_mode = match model.arena.frame(h).map(|f| (&f.kind_state, f.shown)) {
|
||||
Some((KindState::Tooltip(t), shown)) => shown && t.anchor == TooltipAnchor::Cursor,
|
||||
_ => false,
|
||||
};
|
||||
if cursor_mode {
|
||||
cursor_anchor(model, h);
|
||||
}
|
||||
let (num, lefts, rights, min_w, pad_w) = match model.arena.frame(h).map(|f| &f.kind_state) {
|
||||
Some(KindState::Tooltip(t)) => (
|
||||
t.num_lines,
|
||||
|
||||
@@ -20,24 +20,35 @@ use super::{
|
||||
pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let m = lua.create_table()?;
|
||||
|
||||
// SetOwner(owner, anchorType [, x, y]) — remember the owner, clear the content (the engine
|
||||
// fires OnTooltipCleared: a fresh owner never inherits the last hover's lines or money), and
|
||||
// point the plate per the anchor law. The six owner anchors are the documented 1.12 API set
|
||||
// (spec-faithful, same posture as StatusBar's contract; the corpus exercises RIGHT/LEFT/
|
||||
// BOTTOMRIGHT); ANCHOR_NONE leaves pointing to the caller (GameTooltip_SetDefaultAnchor),
|
||||
// ANCHOR_PRESERVE keeps the previous placement. The mode is stored on the tooltip and read back
|
||||
// by `GetAnchorType` below.
|
||||
// SetOwner(owner, anchorType [, x, y]) — the binding `0x5310d0` and the core it calls,
|
||||
// `0x52ffe0(owner_region, mode, dx, dy)`: SetAlpha(255) (`0x52fff4`), fade-disarm
|
||||
// (`0x530002`), `+0x314 = owner` (`0x53000c`), the mode (`0x530031`), the offsets, then the
|
||||
// anchor-apply `0x52fe90`. We clear the content too, because the engine fires
|
||||
// OnTooltipCleared: a fresh owner never inherits the last hover's lines or money.
|
||||
//
|
||||
// **The `anchorType`-omitted default here is ANCHOR_RIGHT and the reference's is ANCHOR_NONE.**
|
||||
// The reference's SetOwner core defaults `[+0x318]` to mode 7 = ANCHOR_NONE (`0x530012`, wow-re
|
||||
// `bag-portrait-and-appendtext.md` §5) — no `SetPoint` at all, the plate left wherever it was.
|
||||
// Two stock 1.12 files reach that leg (`QuestTimerFrame.xml:19`, `WorldStateFrame.xml:681`,
|
||||
// both `GameTooltip:SetOwner(this)`), and neither is on `benilla.toc` yet, so nothing today
|
||||
// takes it. It is left alone deliberately rather than quietly flipped: our ANCHOR_NONE arm
|
||||
// below DROPS the plate's anchors where the reference merely skips the SetPoint, so changing
|
||||
// the default would land those two hovers at the layout origin rather than "wherever it was" —
|
||||
// the two questions have to be settled together, and the second one is a visible-placement
|
||||
// change. Recorded here so the next reader sees a stated divergence, not an oversight.
|
||||
// **The omitted / unreadable / unrecognised anchorType is mode 0 = ANCHOR_LEFT, silently.**
|
||||
// `0x53120d` writes 0 into the binding's local mode *before any compare runs*; `0x531214`'s
|
||||
// `lua_isstring` gate jumps the whole `SStrCmpI` chain when arg 3 is absent, nil, boolean or a
|
||||
// table; the chain's last `jne` reaches the same join untouched; and `[0x531221, 0x53133a)`
|
||||
// contains no `luaL_error` (earned zero, positive-controlled against the five this function
|
||||
// *does* have in its owner-validation prologue). `0x5313c0` then hands that local to the core.
|
||||
//
|
||||
// The core's own `mov [+0x318],7` at `0x530012` is **not** that default: it is an
|
||||
// unconditional pre-store that `0x530031` overwrites with the argument, and it survives only
|
||||
// when the owner pointer is NULL — the un-own path `0x530a60` takes on every hide.
|
||||
//
|
||||
// **The anchor-apply clears, and only ANCHOR_PRESERVE escapes it** (`0x52fe90` read
|
||||
// contiguously): a NULL owner returns at `0x52fe9e`; **mode 8 returns at `0x52fead`, before
|
||||
// the clear**; then `0x52feb8` tests the caller's arg1, which `0x530037` always passes as
|
||||
// **1** (`0x53002d push 0x1`), so the mode-7 skip at `0x52feba` is dead on this path and
|
||||
// `0x52fec2 call 0x767ed0` (ClearAllPoints) runs for **every mode 0..7**; and only then does
|
||||
// `0x52fed0 ja` send 6 and 7 home while 0..5 take the SetPoint jump table `0x52ffbc`.
|
||||
//
|
||||
// So the three arms below are: 0..5 clear-and-point, **6 and 7 clear**, 8 nothing. Decision
|
||||
// 2176 replaces 2142's open thread 1, which had it backwards on both counts — it read the
|
||||
// core's pre-store as the Lua default and read "no SetPoint" as "no clear", and a 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).
|
||||
m.set(
|
||||
"SetOwner",
|
||||
lua.create_function(
|
||||
@@ -59,29 +70,36 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let owner_id = model.frame_id(owner_h);
|
||||
// The anchor mode is resolved ONCE, into the reference's own enum, and both
|
||||
// the placement below and `GetAnchorType`'s answer are taken from that one
|
||||
// value — so the plate and the getter can never tell different stories. An
|
||||
// unrecognised string (`ANCHOR_CURSOR` among them, which this engine cannot
|
||||
// honour — see [`TooltipAnchor::Cursor`]) warns and is recorded as the
|
||||
// ANCHOR_RIGHT it is actually placed by, not as the string it was handed.
|
||||
// value — so the plate and the getter can never tell different stories.
|
||||
//
|
||||
// Absent, unreadable and unrecognised all land on **ANCHOR_LEFT**, which is
|
||||
// the binding's zero-initialised local (`0x53120d`) surviving the
|
||||
// `lua_isstring` gate or the compare chain, with no raise. The warning on the
|
||||
// unrecognised leg is ours and stays: the reference is silent, but nothing
|
||||
// observable to Lua changes, and this exact warning is what made
|
||||
// `ANCHOR_CURSOR` visible as a real ninth mode nine corpus files ask for
|
||||
// rather than a typo (2142's open thread 2).
|
||||
let anchor = match anchor
|
||||
.as_deref()
|
||||
.map(str::to_ascii_uppercase)
|
||||
.as_deref()
|
||||
.unwrap_or("ANCHOR_RIGHT")
|
||||
.unwrap_or("")
|
||||
{
|
||||
"ANCHOR_RIGHT" => TooltipAnchor::Right,
|
||||
"ANCHOR_LEFT" => TooltipAnchor::Left,
|
||||
"ANCHOR_TOPRIGHT" => TooltipAnchor::TopRight,
|
||||
"ANCHOR_TOPLEFT" => TooltipAnchor::TopLeft,
|
||||
"ANCHOR_BOTTOMRIGHT" => TooltipAnchor::BottomRight,
|
||||
"ANCHOR_RIGHT" => TooltipAnchor::Right,
|
||||
"ANCHOR_BOTTOMLEFT" => TooltipAnchor::BottomLeft,
|
||||
"ANCHOR_BOTTOMRIGHT" => TooltipAnchor::BottomRight,
|
||||
"ANCHOR_TOPLEFT" => TooltipAnchor::TopLeft,
|
||||
"ANCHOR_TOPRIGHT" => TooltipAnchor::TopRight,
|
||||
"ANCHOR_CURSOR" => TooltipAnchor::Cursor,
|
||||
"ANCHOR_NONE" => TooltipAnchor::None,
|
||||
"ANCHOR_PRESERVE" => TooltipAnchor::Preserve,
|
||||
"" => TooltipAnchor::Left,
|
||||
other => {
|
||||
model.record_warning(format!(
|
||||
"SetOwner: unknown anchor '{other}' (kept ANCHOR_RIGHT)"
|
||||
"SetOwner: unknown anchor '{other}' (mode 0, ANCHOR_LEFT)"
|
||||
));
|
||||
TooltipAnchor::Right
|
||||
TooltipAnchor::Left
|
||||
}
|
||||
};
|
||||
tip_mut(&mut model, h)?.anchor = anchor;
|
||||
@@ -96,58 +114,56 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
None
|
||||
}
|
||||
};
|
||||
match pts {
|
||||
Some((own, rel)) => {
|
||||
let new =
|
||||
Anchor::new(own, owner_id, rel, x.unwrap_or(0.0), y.unwrap_or(0.0));
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
// The no-op compare keeps the per-frame SetOwner idiom (the bag
|
||||
// hover's OnUpdate re-enter) from dirtying tier 1 by itself; the
|
||||
// content clear above already reports its own writes.
|
||||
let same = input.anchors.len() == 1
|
||||
&& crate::script::object::anchor_bits_eq(&input.anchors[0], &new);
|
||||
if !same {
|
||||
// The plate re-points at the button under the cursor, which moves
|
||||
// an EDGE — structural under 1388, and therefore a whole-graph
|
||||
// derivation on every bag slot and every spellbook button a hover
|
||||
// sweep crossed. It names its node now (decision 1625): the old
|
||||
// and new target lists are both right here, so the cached graph's
|
||||
// edges are re-pointed instead of thrown away.
|
||||
let old_targets: Vec<u32> =
|
||||
input.anchors.iter().map(|a| a.relative_to).collect();
|
||||
input.anchors = vec![new];
|
||||
model.touch_layout_retarget_frame(h, &old_targets, &[owner_id]);
|
||||
// **Modes 6 and 7 CLEAR and stop; mode 8 does neither.** `0x52fe90` reaches
|
||||
// `0x52fec2 call 0x767ed0` for every mode 0..7 — the mode-7 skip beside it is
|
||||
// gated on an arg1 that `SetOwner`'s core always passes as 1 — and mode 8
|
||||
// returned two compares earlier, at `0x52fead`, without touching anything.
|
||||
//
|
||||
// For ANCHOR_NONE that clear is what makes `GameTooltip_SetDefaultAnchor`
|
||||
// (`SetOwner(owner, "ANCHOR_NONE")` → `ClearAllPoints()` → `SetPoint(...)`,
|
||||
// the path of every action-bar hover) safe: a stale owner anchor cannot win
|
||||
// the frame the caller forgets to re-point. For ANCHOR_CURSOR it is the
|
||||
// opening move of a placement the per-frame update finishes
|
||||
// (`script::tooltip::cursor_anchor`).
|
||||
//
|
||||
// Dropping every anchor is a retarget to the EMPTY target set, and it names
|
||||
// its node like any other (decision 1625). It matters here more than
|
||||
// anywhere: left on the conservative touch, this line was a whole-graph
|
||||
// derivation on every action button the cursor crossed and on nothing else —
|
||||
// exactly the shape the director's recorder reported (decision 1630).
|
||||
if matches!(anchor, TooltipAnchor::Cursor | TooltipAnchor::None) {
|
||||
let dropped = match model.layout_inputs.get_mut(&h) {
|
||||
Some(input) if !input.anchors.is_empty() => {
|
||||
Some(input.anchors.drain(..).map(|a| a.relative_to).collect())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(old_targets) = dropped {
|
||||
let old_targets: Vec<u32> = old_targets;
|
||||
model.touch_layout_retarget_frame(h, &old_targets, &[]);
|
||||
}
|
||||
None if anchor == TooltipAnchor::None => {
|
||||
// The caller points it (ClearAllPoints+SetPoint) — drop ours now so a
|
||||
// stale owner anchor never wins the frame the caller forgets to.
|
||||
//
|
||||
// Dropping every anchor is a retarget to the EMPTY target set, and it
|
||||
// names its node like any other (decision 1625). It matters here more
|
||||
// than anywhere: this arm is `GameTooltip_SetDefaultAnchor`'s, which is
|
||||
// the path EVERY action-bar hover takes (stock `ActionButton_SetTooltip`, the
|
||||
// `UberTooltips` default; the stance, pet and bonus bars the same) —
|
||||
// while a bag slot anchors straight to its button and never comes
|
||||
// through here. So a conservative touch on this line was a whole-graph
|
||||
// derivation on every action button the cursor crossed, and on nothing
|
||||
// else, which is exactly the shape the director's recorder reported (decision 1630):
|
||||
// every derive frame owned by a MultiBar/BonusAction button.
|
||||
let dropped = match model.layout_inputs.get_mut(&h) {
|
||||
Some(input) if !input.anchors.is_empty() => {
|
||||
Some(input.anchors.drain(..).map(|a| a.relative_to).collect())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(old_targets) = dropped {
|
||||
let old_targets: Vec<u32> = old_targets;
|
||||
model.touch_layout_retarget_frame(h, &old_targets, &[]);
|
||||
}
|
||||
}
|
||||
if let Some((own, rel)) = pts {
|
||||
let new =
|
||||
Anchor::new(own, owner_id, rel, x.unwrap_or(0.0), y.unwrap_or(0.0));
|
||||
let input = model.layout_inputs.entry(h).or_default();
|
||||
// The no-op compare keeps the per-frame SetOwner idiom (the bag
|
||||
// hover's OnUpdate re-enter) from dirtying tier 1 by itself; the
|
||||
// content clear above already reports its own writes.
|
||||
let same = input.anchors.len() == 1
|
||||
&& crate::script::object::anchor_bits_eq(&input.anchors[0], &new);
|
||||
if !same {
|
||||
// The plate re-points at the button under the cursor, which moves
|
||||
// an EDGE — structural under 1388, and therefore a whole-graph
|
||||
// derivation on every bag slot and every spellbook button a hover
|
||||
// sweep crossed. It names its node now (decision 1625): the old
|
||||
// and new target lists are both right here, so the cached graph's
|
||||
// edges are re-pointed instead of thrown away.
|
||||
let old_targets: Vec<u32> =
|
||||
input.anchors.iter().map(|a| a.relative_to).collect();
|
||||
input.anchors = vec![new];
|
||||
model.touch_layout_retarget_frame(h, &old_targets, &[owner_id]);
|
||||
}
|
||||
// ANCHOR_PRESERVE. `TooltipAnchor::Cursor` shares the arm for
|
||||
// exhaustiveness only — the string match above never produces it (it warns
|
||||
// and records ANCHOR_RIGHT instead), so nothing reaches here that way.
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
fire_cleared(lua, h);
|
||||
|
||||
Reference in New Issue
Block a user