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:
co-authored by
Claude Opus 5.5
parent
8505187668
commit
fd386e75ae
@@ -1,25 +1,12 @@
|
||||
//! The engine **spell/aura tooltip channel** (decision 0274 P2) — the verified line law of the
|
||||
//! spell builder `0x52e610` and the aura builder `0x52f880` (the 0276 fold-back):
|
||||
//! The spell and aura tooltip content, in the line order of the spell builder `0x52e610` and the
|
||||
//! aura builder `0x52f880`: name and rank; cost and range; cast time and cooldown (omitted for a
|
||||
//! passive spell, with no "Passive" line); the required item class, then form, white when met and
|
||||
//! red when not; reagents, a missing one inline red; the description, gold (white on an aura).
|
||||
//! Only `SetPlayerBuff` adds the gold time-remaining line.
|
||||
//!
|
||||
//! - name | rank (gray) — one double line. The name's colour is the BUILDER's: **white** from
|
||||
//! the spell builder (`0x530270`), **gold** from the aura builder (`0x530380`, the gold
|
||||
//! wrapper) — the same split `SetTrackingSpell` shows;
|
||||
//! - **Cost | Range** — ONE double line (either side may be absent);
|
||||
//! - **CastTime | Cooldown** — ONE double line; a passive spell simply omits it (there is NO
|
||||
//! "Passive" text in the 1.12 builder);
|
||||
//! - required tool / form — the equipped-item-class line then the stance line, each white when
|
||||
//! met and red when not;
|
||||
//! - reagents — inline red per missing item (the one builder that uses the `|cffff2020` escape;
|
||||
//! joins when a reagent feed exists);
|
||||
//! - description — gold, wrapped. An AURA's description is **white** (byte-verified difference),
|
||||
//! and only `SetPlayerBuff` appends the duration-remaining line, which is **gold** again
|
||||
//! (`0xffffd200`, the title's gold — B62; see [`render_spell`]'s tail).
|
||||
//!
|
||||
//! The engine renders VIEWS ([`SpellTooltipView`]) the app resolves at push time — the $-token
|
||||
//! substitution (values off Spell.dbc + the player's level), cast-time/duration/range text —
|
||||
//! because the catalogs and the token engine live app-side; the engine holds no DBC knowledge.
|
||||
//! Views are keyed by spell id in an ask-once store (the item-template store's pattern): a
|
||||
//! renderer miss records the id, the app resolves and pushes, the hover's re-enter repaints.
|
||||
//! The app resolves each spell into a [`SpellTooltipView`] (`$`-tokens, cast, range and duration
|
||||
//! text), kept in an ask-once store by spell id: a miss records the id, the app pushes the view and
|
||||
//! the next hover repaints.
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
|
||||
@@ -28,82 +15,67 @@ use super::tooltip::{append_line, clear_content, fire_cleared};
|
||||
use super::{CraftTooltip, Model, TrainerTooltip};
|
||||
|
||||
const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
|
||||
/// The rank column's gray — byte-verified `0xff808080` (0276).
|
||||
/// The rank column's gray, `0xff808080`.
|
||||
const GRAY: [f32; 4] = [128.0 / 255.0, 128.0 / 255.0, 128.0 / 255.0, 1.0];
|
||||
/// The description gold — byte-verified `0xffffd200` (0276).
|
||||
/// The description gold, `0xffffd200`.
|
||||
const GOLD: [f32; 4] = [1.0, 210.0 / 255.0, 0.0, 1.0];
|
||||
|
||||
/// One spell's tooltip view — every string app-resolved (the $-engine's output for the
|
||||
/// description; the cost/range/cast/cooldown texts off the DBC catalogs).
|
||||
/// One spell's tooltip, every string resolved by the app.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct SpellTooltipView {
|
||||
pub name: String,
|
||||
/// "Rank N" — the gray right column of the name line, on the SPELL variant only.
|
||||
/// "Rank N", the name line's gray right column on the spell variant.
|
||||
pub rank: Option<String>,
|
||||
/// "Magic" / "Curse" / "Disease" / "Poison" — the AURA variant's right column (`0x52f8e5`),
|
||||
/// the `SpellDispelType.dbc` name of the spell's dispel class gated by that table's `[+0x28]`
|
||||
/// flag, so an undispellable aura (Stealth) carries `None`. Rendered GOLD, like the aura name
|
||||
/// it shares its line with — not the spell variant's gray.
|
||||
/// The aura variant's gold right column (`0x52f8e5`): the `SpellDispelType.dbc` name, such as
|
||||
/// "Magic", when that row's `[+0x28]` flag is set, so Stealth has none.
|
||||
pub dispel_type: Option<String>,
|
||||
/// "35 Mana" / "20 Rage" / "20 Health" / "11 Health, plus 5 per sec" — the cost cell: the
|
||||
/// RESOLVED cost through the power-type key array with the health fallback (1074).
|
||||
/// The cost cell, such as "35 Mana" or "11 Health, plus 5 per sec": the resolved cost through
|
||||
/// the power-type keys, falling back to health.
|
||||
pub cost: Option<String>,
|
||||
/// "30 yd range" — the range cell.
|
||||
pub range: Option<String>,
|
||||
/// "1.5 sec cast" / "Instant cast" / "Instant" / "Next melee" / "Attack speed" /
|
||||
/// "Channeled" — `None` = a passive spell: the whole casttime|cooldown line is omitted
|
||||
/// (the verified law; never a "Passive" text line).
|
||||
/// The cast cell ("1.5 sec cast", "Instant", "Next melee", "Channeled"); `None` is a passive
|
||||
/// spell, whose cast and cooldown line is omitted.
|
||||
pub cast_time: Option<String>,
|
||||
/// "15 sec cooldown" — the cooldown cell: `max(RecoveryTime, CategoryRecoveryTime)` (the 0276
|
||||
/// line law, `SPELL_RECAST_TIME_SEC 0x854ed4` — Charge's 15 s lives in the CATEGORY column).
|
||||
/// The cooldown cell (`SPELL_RECAST_TIME_SEC 0x854ed4`): `max(RecoveryTime,
|
||||
/// CategoryRecoveryTime)`, as Charge's 15 s is in the category column.
|
||||
pub cooldown: Option<String>,
|
||||
/// "Requires Wands" — the equipped-item-class half (`SPELL_EQUIPPED_ITEM 0x854e94`), over
|
||||
/// `EquippedItemClass`/`EquippedItemSubClassMask`: white when [`Self::item_met`], red when
|
||||
/// not. Sits ABOVE [`Self::requires_form`] (the law's tool-then-form order).
|
||||
/// "Requires Wands" (`SPELL_EQUIPPED_ITEM 0x854e94`), from `EquippedItemClass` and
|
||||
/// `EquippedItemSubClassMask`.
|
||||
pub requires_item: Option<String>,
|
||||
/// Whether some WORN item satisfies the class + subclass mask (the app re-pushes views when
|
||||
/// the equipped set changes, so the color tracks live swaps).
|
||||
/// A worn item matches the class and subclass mask; re-pushed on an equipment change.
|
||||
pub item_met: bool,
|
||||
/// "Requires Battle Stance" — the required-form line (`SPELL_REQUIRED_FORM 0x854e64` over
|
||||
/// the `Stances` mask): white when [`Self::form_met`], red when not.
|
||||
/// "Requires Battle Stance" (`SPELL_REQUIRED_FORM 0x854e64`), from the `Stances` mask.
|
||||
pub requires_form: Option<String>,
|
||||
/// Whether the player's CURRENT shapeshift form satisfies the mask (the app re-pushes views
|
||||
/// on a form change, so the color tracks live stance switches).
|
||||
/// The current shapeshift form matches the mask; the app re-pushes on a form change.
|
||||
pub form_met: bool,
|
||||
/// "Reagents: Light Feather" — `SPELL_REAGENTS 0x854e54` ("Reagents: ", no format slot) +
|
||||
/// the 8 reagent slots. Rendered WHITE and wrapped; a reagent the player is short of carries
|
||||
/// the builder's own **inline** `|cffff2020` escape (the spell builder is the one builder
|
||||
/// that colors mid-line), so the composed string arrives paint-ready from the app.
|
||||
/// "Reagents: Light Feather" (`SPELL_REAGENTS 0x854e54` and the eight reagent slots); the app
|
||||
/// writes a missing reagent's inline `|cffff2020` escape.
|
||||
pub reagents: Option<String>,
|
||||
/// "2.62% chance to dodge" — `CHANCE_TO_DODGE 0x854e44` and its siblings, selected by the
|
||||
/// spell's `Effect[0]` and rendered white and unwrapped, BELOW the reagents and ABOVE the
|
||||
/// description. The
|
||||
/// percentage is the player's own live avoidance/crit field, so the app re-pushes as it moves.
|
||||
/// "2.62% chance to dodge" (`CHANCE_TO_DODGE 0x854e44` and its siblings, by `Effect[0]`),
|
||||
/// from the player's live stat, which the app re-pushes as it moves.
|
||||
pub chance: Option<String>,
|
||||
/// The $-substituted description — gold + wrapped for spells.
|
||||
/// The `$`-substituted description.
|
||||
pub description: String,
|
||||
/// The $-substituted AURA description (`Spell.dbc AuraDescription`) — the buff hover's
|
||||
/// white text (byte-verified: the aura builder reads the aura column). Falls back to
|
||||
/// `description` when empty.
|
||||
/// The `$`-substituted `AuraDescription`, which the aura builder reads; `description` stands in
|
||||
/// when it is empty.
|
||||
pub aura_description: String,
|
||||
}
|
||||
|
||||
impl super::UiScript {
|
||||
/// Store (or replace) a spell's tooltip view — the app's push half of the ask-once flow.
|
||||
/// Store or replace a spell's view, answering its ask.
|
||||
pub fn set_spell_tooltip(&mut self, spell_id: u32, view: SpellTooltipView) {
|
||||
let mut model = self.model_mut();
|
||||
model.spell_tooltip_asks.remove(&spell_id);
|
||||
model.spell_tooltips.insert(spell_id, view);
|
||||
}
|
||||
|
||||
/// Drain the spell ids the renderers asked for that the store didn't have.
|
||||
/// Drain the spell ids the renderers asked for and the store lacked.
|
||||
pub fn take_spell_tooltip_asks(&mut self) -> Vec<u32> {
|
||||
self.model_mut().spell_tooltip_asks.drain().collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up the store; a miss records the ask. `pub(super)` for the talent tooltip's shared use
|
||||
/// (its display + next-rank spells ride this same ask-once channel — decision 0304).
|
||||
/// Look up a spell's view; a miss records the ask.
|
||||
pub(super) fn spell_view_of(lua: &Lua, spell_id: u32) -> Option<SpellTooltipView> {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let v = model.spell_tooltips.get(&spell_id).cloned();
|
||||
@@ -113,49 +85,35 @@ pub(super) fn spell_view_of(lua: &Lua, spell_id: u32) -> Option<SpellTooltipView
|
||||
v
|
||||
}
|
||||
|
||||
/// The talent interleave for [`render_spell`] (decision 0304; the builder's own talent params —
|
||||
/// `TOOLTIP_TALENT_RANK 0x854a2c` / `TOOLTIP_TALENT_LEARN 0x8549f8`): the white "Rank r/m" after
|
||||
/// the name, the red requirement lines while locked (position CONFIRMED — decision 0305's residue:
|
||||
/// matches the builder law, after the rank line), the "Next rank:" block, and the green learn hint.
|
||||
/// The talent lines [`render_spell`] interleaves (`TOOLTIP_TALENT_RANK 0x854a2c`,
|
||||
/// `TOOLTIP_TALENT_LEARN 0x8549f8`): the white "Rank r/m" after the name, the red requirements
|
||||
/// while locked, the next-rank block and the green learn hint.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(super) struct TalentLines {
|
||||
/// `TOOLTIP_TALENT_RANK` = "Rank %d/%d", already filled by the caller off the player's own
|
||||
/// string table. `None` = the table does not carry the key, and the plate shows no rank row
|
||||
/// (decision 2045 — never an invented one).
|
||||
/// `TOOLTIP_TALENT_RANK` ("Rank %d/%d") filled from the player's strings; `None` when they
|
||||
/// lack the key, and no rank row shows.
|
||||
pub rank_line: Option<String>,
|
||||
pub reqs: Vec<String>,
|
||||
/// The next rank's spell id (0 = none) — asked from the spell store when its description
|
||||
/// hasn't landed yet, so the hover's re-enter completes the block.
|
||||
/// The next rank's spell id, 0 for none, asked from the store while `next_desc` is missing.
|
||||
pub next_spell: u32,
|
||||
pub next_desc: Option<String>,
|
||||
pub learn: bool,
|
||||
}
|
||||
|
||||
/// `TOOLTIP_TALENT_LEARN 0x8549f8`'s green — the shared talent-learn green of the tooltip color
|
||||
/// table (`0xff00ff00`).
|
||||
/// The talent learn hint's green, `0xff00ff00`.
|
||||
const GREEN: [f32; 4] = [0.0, 1.0, 0.0, 1.0];
|
||||
/// The unmet-requirement red — `0xc0d390 = ffff2020` (the item builder's own RED value).
|
||||
/// The unmet-requirement red, `0xffff2020` (`0xc0d390`, the item builder's red).
|
||||
const RED: [f32; 4] = [1.0, 32.0 / 255.0, 32.0 / 255.0, 1.0];
|
||||
|
||||
/// Render one spell view — the verified law (module doc). `aura` renders the aura variant:
|
||||
/// white description, plus the caller-supplied duration-remaining line (`SetPlayerBuff` only).
|
||||
/// `talent` interleaves the talent lines ([`TalentLines`] doc).
|
||||
/// The builder's parameter vector, named as the byte law names it (`0x52e610`'s param3..param8).
|
||||
/// These were three positional `bool`s at a
|
||||
/// 7-argument call site, which is exactly the shape that gets silently transposed; `Default` is the
|
||||
/// plain spell hover every caller but two wants.
|
||||
/// The spell builder's options (`0x52e610`'s parameters); `Default` is the plain spell hover.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub(super) struct SpellRenderOpts {
|
||||
/// Render through the AURA builder (`0x52f880`) rather than the spell builder: gold name, white
|
||||
/// description, the dispel-class right column.
|
||||
/// The aura builder (`0x52f880`): gold name, white description, the dispel class at right.
|
||||
pub(super) aura: bool,
|
||||
/// `param6` showRank — the gray "Rank N" right column. `SetSpell` passes 0 (the spellbook hover
|
||||
/// never shows it), `SetAction` passes 1.
|
||||
/// `param6` showRank, the gray "Rank N" column: `SetSpell` passes 0, `SetAction` 1.
|
||||
pub(super) show_rank: bool,
|
||||
/// `param5` altCaster — **one** gate suppressing **both** the totems and the reagents lines
|
||||
/// (byte-verified at the two branch sites `0x52ed43` and `0x52f393`). Set only by
|
||||
/// `SetTrainerService`, and only when the matched learn-wrapper slot was `LEARN_PET_SPELL`: a
|
||||
/// pet-training service shows neither block.
|
||||
/// `param5` altCaster, which drops the totem and reagent lines (`0x52ed43`, `0x52f393`); only
|
||||
/// `SetTrainerService` sets it, for a `LEARN_PET_SPELL` service.
|
||||
pub(super) alt_caster: bool,
|
||||
}
|
||||
|
||||
@@ -172,24 +130,15 @@ fn render_spell(
|
||||
show_rank,
|
||||
alt_caster,
|
||||
} = opts;
|
||||
// The name colour splits by BUILDER, byte-verified: the spell builder `0x52e610` writes its
|
||||
// name line through `0x530270` (white), the aura builder `0x52f880` through `0x530380` — the
|
||||
// GOLD wrapper. SetTrackingSpell's gold,
|
||||
// already pinned by the director's own A/B, is the same wrapper.
|
||||
// The spell builder writes the name white (`0x530270`), the aura builder gold (`0x530380`).
|
||||
let name_color = if aura { GOLD } else { WHITE };
|
||||
// The name line's RIGHT column splits by builder too. The spell builder's is the gray "Rank N",
|
||||
// and it shows only when the CALLER asks (byte-verified: SetSpell passes param6=0 — the
|
||||
// spellbook hover never shows "Rank N"; SetAction passes 1). The aura builder's is the dispel
|
||||
// class ("Magic" on Ice Armor — `0x52f8e5`), and it is GOLD, not gray: a buff never shows a
|
||||
// rank.
|
||||
let right = if aura {
|
||||
v.dispel_type.clone().map(|t| (t, GOLD))
|
||||
} else {
|
||||
v.rank.clone().filter(|_| show_rank).map(|t| (t, GRAY))
|
||||
};
|
||||
append_line(lua, this, (v.name.clone(), name_color), right, false)?;
|
||||
// The talent head: "Rank r/m" (builder line 2, TOOLTIP_TALENT_RANK white) + the red
|
||||
// requirement lines while locked (position CONFIRMED, decision 0305 — TalentLines doc).
|
||||
// The talent head: the white rank line, then the red requirements while locked.
|
||||
if let Some(t) = talent {
|
||||
if let Some(rank) = &t.rank_line {
|
||||
append_line(lua, this, (rank.clone(), WHITE), None, false)?;
|
||||
@@ -199,7 +148,6 @@ fn render_spell(
|
||||
}
|
||||
}
|
||||
if !aura {
|
||||
// Cost | Range — one line, either side optional.
|
||||
match (&v.cost, &v.range) {
|
||||
(Some(c), Some(r)) => append_line(
|
||||
lua,
|
||||
@@ -212,7 +160,6 @@ fn render_spell(
|
||||
(None, Some(r)) => append_line(lua, this, (r.clone(), WHITE), None, false)?,
|
||||
(None, None) => {}
|
||||
}
|
||||
// CastTime | Cooldown — one line; a passive spell (cast_time None) omits it whole.
|
||||
if let Some(ct) = &v.cast_time {
|
||||
match &v.cooldown {
|
||||
Some(cd) => append_line(
|
||||
@@ -225,9 +172,6 @@ fn render_spell(
|
||||
None => append_line(lua, this, (ct.clone(), WHITE), None, false)?,
|
||||
}
|
||||
}
|
||||
// Required tool / form (`SPELL_EQUIPPED_ITEM 0x854e94` / `SPELL_REQUIRED_FORM 0x854e64`):
|
||||
// the equipped-item-class line first, then the stance line. Each white when met, red when
|
||||
// not.
|
||||
if let Some(req) = &v.requires_item {
|
||||
let color = if v.item_met { WHITE } else { RED };
|
||||
append_line(lua, this, (req.clone(), color), None, false)?;
|
||||
@@ -236,15 +180,12 @@ fn render_spell(
|
||||
let color = if v.form_met { WHITE } else { RED };
|
||||
append_line(lua, this, (req.clone(), color), None, false)?;
|
||||
}
|
||||
// Reagents (`SPELL_REAGENTS 0x854e54`): white + wrapped, the missing entries inline-red
|
||||
// inside the text.
|
||||
// Suppressed wholesale by altCaster — the same gate that hides the totems block, which we
|
||||
// have no feed for yet, so this is the only half of it that is observable here.
|
||||
// Reagents, white and wrapped, unless altCaster drops them; the totem line it also drops
|
||||
// is not built.
|
||||
if let Some(reagents) = v.reagents.as_ref().filter(|_| !alt_caster) {
|
||||
append_line(lua, this, (reagents.clone(), WHITE), None, true)?;
|
||||
}
|
||||
// Chance to dodge/parry/block/crit (`CHANCE_TO_DODGE 0x854e44` and its siblings) — white,
|
||||
// NOT wrapped, and it sits here: below the reagents, above the description.
|
||||
// The chance line, white and unwrapped, between the reagents and the description.
|
||||
if let Some(chance) = &v.chance {
|
||||
append_line(lua, this, (chance.clone(), WHITE), None, false)?;
|
||||
}
|
||||
@@ -258,11 +199,9 @@ fn render_spell(
|
||||
let color = if aura { WHITE } else { GOLD };
|
||||
append_line(lua, this, (desc.clone(), color), None, true)?;
|
||||
}
|
||||
// The talent tail: the TOOLTIP_TALENT_NEXT_RANK header (white, `0x854a10` pushed at
|
||||
// `0x52b2cd`) over the next rank's gold description, and the green learn hint (builder
|
||||
// line 13, TOOLTIP_TALENT_LEARN `0x8549f8` at `0x52b362`). Both are keys into the player's
|
||||
// own string table (decision 2045); an install without them shows the description alone
|
||||
// rather than a sentence of ours.
|
||||
// The talent tail: the white `TOOLTIP_TALENT_NEXT_RANK` header (`0x854a10`, pushed at
|
||||
// `0x52b2cd`) over the next rank's gold description, then the green `TOOLTIP_TALENT_LEARN`
|
||||
// hint (`0x52b362`). Both are the player's own strings; without them the line is skipped.
|
||||
if let Some(t) = talent {
|
||||
if let Some(next) = &t.next_desc {
|
||||
if let Some(header) = crate::strings::global(lua, "TOOLTIP_TALENT_NEXT_RANK") {
|
||||
@@ -276,20 +215,16 @@ fn render_spell(
|
||||
}
|
||||
}
|
||||
}
|
||||
// The duration-remaining line (`SetPlayerBuff` only) is GOLD `0xffffd200` — the same gold as
|
||||
// the aura title it sits under, NOT the description's white. Measured off a 1.12.1 reference
|
||||
// shot of Ice Armor in Wetlands: the
|
||||
// "29 minutes remaining" glyphs read exactly `(255, 210, 0)`, pixel-identical to that shot's
|
||||
// "Ice Armor" / "Magic" title row, while its description rows read `(255, 255, 255)`.
|
||||
// The time-remaining line (`SetPlayerBuff` only) is the title's gold `0xffffd200`, not the
|
||||
// description's white: a 1.12.1 screenshot reads (255, 210, 0) on it.
|
||||
if let Some(rem) = remaining {
|
||||
append_line(lua, this, (rem, GOLD), None, false)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The talent tooltip's entry (decision 0304 — `GameTooltip:SetTalent`'s render half): the
|
||||
/// display spell through the shared store + the talent interleave. A missing next-rank view is
|
||||
/// re-asked so the hover's re-enter completes the block.
|
||||
/// `GameTooltip:SetTalent`'s render: the talent's spell plus its talent lines; a missing next-rank
|
||||
/// view is asked for.
|
||||
pub(super) fn set_spell_with_talent(
|
||||
lua: &Lua,
|
||||
this: &Table,
|
||||
@@ -315,8 +250,7 @@ pub(super) fn set_spell_with_talent(
|
||||
Some(&talent),
|
||||
)?,
|
||||
None => {
|
||||
// The view hasn't landed: show the talent head alone (the ask is recorded; the
|
||||
// hover's re-enter repaints complete) — the spell channel's own fallback shape.
|
||||
// No view yet: the rank line alone until the next hover.
|
||||
if let Some(rank) = &talent.rank_line {
|
||||
append_line(lua, this, (rank.clone(), WHITE), None, false)?;
|
||||
}
|
||||
@@ -326,10 +260,7 @@ pub(super) fn set_spell_with_talent(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shared entry: clear, render (or record the ask and show nothing but the name if the caller
|
||||
/// knows one), show.
|
||||
/// The reward-spell hover: the spell by id with the quest's own name as the fallback, or an
|
||||
/// empty tooltip.
|
||||
/// The reward-spell hover: the spell, its name as the fallback, or an empty tooltip.
|
||||
fn set_reward_spell(
|
||||
lua: &Lua,
|
||||
this: &Table,
|
||||
@@ -375,27 +306,16 @@ pub(super) fn set_spell_by_id(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register the spell/aura content channels into the GameTooltip kind method table.
|
||||
/// Register the spell and aura content methods on the GameTooltip method table.
|
||||
pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// GameTooltip:SetSpell(bookId, bookType) — the spellbook hover: the 1-based book slot resolves
|
||||
// through the named book's state to a spell id.
|
||||
//
|
||||
// **`bookType` decides which book**, exactly like every `bookType`-taking global
|
||||
// (`super::spellbook::book_slot`). Byte-verified in `SetSpell 0x532d10`, which is its own
|
||||
// implementation of the same fork rather than a caller of the shared parser: arg2 → number,
|
||||
// `- 1`, bounded `[0, 0x400)` (`0x532dd4`-`0x532df4`); arg3 → string, compared against the
|
||||
// literal `"pet"` at `0x846960` (`0x532e13`); match takes `[4*i + 0xb6f098]` — the PET book —
|
||||
// and sets `isPet = 1` (`0x532e1c`), everything else takes `[4*i + 0xb700f0]` (`0x532e2a`).
|
||||
// That `isPet` then rides into `0x6e2ea0` as the cooldown BANK (`0x532e50`), the same bank
|
||||
// split 1031 built.
|
||||
//
|
||||
// Before the fork, a pet-book hover indexed the PLAYER's slot list, so hovering the imp's first
|
||||
// spell showed the player's first spell — "Attack", crit line and all (decision 1050).
|
||||
// GameTooltip:SetSpell(bookId, bookType): the spellbook hover. `SetSpell 0x532d10` bounds the
|
||||
// 1-based slot to `[0, 0x400)` (`0x532dd4`) and reads the pet book (`0xb6f098`) when `bookType`
|
||||
// is "pet" (`0x532e13`), else the player's (`0xb700f0`); the pet flag also picks the cooldown
|
||||
// bank it passes to `0x6e2ea0` (`0x532e50`).
|
||||
m.set(
|
||||
"SetSpell",
|
||||
lua.create_function(|lua, (this, book_id, book_type): (Table, u32, Value)| {
|
||||
// The reference requires a STRING third argument and bails otherwise (`0x532dc0`'s
|
||||
// `lua_isstring(3)` → `je` out); a non-string is therefore not "the player's book".
|
||||
// A non-string `bookType` bails (`0x532dc0`) rather than meaning the player's book.
|
||||
let Some(book_type) = book_type.as_string().and_then(|s| s.to_str().ok()) else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -409,7 +329,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
set_spell_by_id(lua, &this, spell_id, name, SpellRenderOpts::default(), None)
|
||||
})?,
|
||||
)?;
|
||||
// GameTooltip:SetShapeshift(index) — the stance-bar hover (the form's own spell tooltip).
|
||||
// GameTooltip:SetShapeshift(index): the stance-bar hover, the form's spell tooltip.
|
||||
m.set(
|
||||
"SetShapeshift",
|
||||
lua.create_function(|lua, (this, index): (Table, usize)| {
|
||||
@@ -423,11 +343,9 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
set_spell_by_id(lua, &this, spell_id, name, SpellRenderOpts::default(), None)
|
||||
})?,
|
||||
)?;
|
||||
// GameTooltip:SetPetAction(index) — the pet-bar hover (decision 0982). Only ever reached for a
|
||||
// SPELL slot: the reference's `PetActionButton_OnEnter` builds a token slot's tooltip inline
|
||||
// from `tooltipName`/`tooltipSubtext` and never calls this. A slot with no spell (a token, an
|
||||
// empty slot, an out-of-range index) is a no-op, leaving whatever was shown — the same shape
|
||||
// as SetSpell's out-of-range.
|
||||
// GameTooltip:SetPetAction(index): the pet-bar hover for a spell slot; the stock
|
||||
// `PetActionButton_OnEnter` builds a token's tooltip itself (`PetActionBarFrame.lua:290`). A
|
||||
// slot with no spell is a no-op.
|
||||
m.set(
|
||||
"SetPetAction",
|
||||
lua.create_function(|lua, (this, index): (Table, usize)| {
|
||||
@@ -449,29 +367,11 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
set_spell_by_id(lua, &this, spell_id, name, SpellRenderOpts::default(), None)
|
||||
})?,
|
||||
)?;
|
||||
// GameTooltip:SetPlayerBuff(buffIndex) — the buff-bar hover: the aura variant (white
|
||||
// AuraDescription) + the duration-remaining line only this entry point appends (byte-verified;
|
||||
// remaining computed live off the aura's GetTime expiry). The line's TEXT is no longer interim:
|
||||
// `0x52fa50`'s four-arm ladder and its rounding pin it, and
|
||||
// `tooltip::duration_text` is it.
|
||||
//
|
||||
// **The argument is a 1.12 CACHE POSITION, not a filtered ordinal** — the same 0-based handle
|
||||
// `GetPlayerBuff` returns and every `GetPlayerBuff*` sibling consumes (see
|
||||
// `super::aura`'s header). `ref-BuffFrame.lua:105` is the pin: `GameTooltip:SetPlayerBuff(buffIndex)`
|
||||
// where `buffIndex` came straight out of `GetPlayerBuff`, never from the button's own id.
|
||||
//
|
||||
// This corrects a real defect: the binding previously read a 1-based index within the
|
||||
// sign-filtered list, so of the corpus's 21 call sites — all of which pass a cache position —
|
||||
// every position >= 1 resolved one aura too early, no debuff was ever reachable (the sign
|
||||
// defaulted to helpful), and `SetPlayerBuff(-1)` showed the FIRST buff instead of nothing.
|
||||
// That last one is load-bearing: `BigWigs/Raids/Naxxramas/Loatheb.lua:260-271` feeds an
|
||||
// unchecked `GetPlayerBuff(i, "HARMFUL")` straight in and terminates its scan on the tooltip's
|
||||
// first line going nil.
|
||||
//
|
||||
// A miss (out of range, negative, empty cache) routes through the shared entry with spell id 0,
|
||||
// exactly like SetUnitBuff's: content clears and the plate hides, never a stale tooltip left
|
||||
// showing. Any surplus argument is ignored — `CT_BuffMod/CT_BuffFrame.lua:151` passes a filter
|
||||
// string the reference's own binding never reads.
|
||||
// GameTooltip:SetPlayerBuff(buffIndex): the aura variant plus the time-remaining line only
|
||||
// this entry appends, its text `0x52fa50`'s (`tooltip::duration_text`). The argument is the
|
||||
// 0-based cache position `GetPlayerBuff` returns (`BuffFrame.lua:105`), not a filtered
|
||||
// ordinal. A miss, negative included, clears and hides the plate as `SetUnitBuff`'s does; a
|
||||
// surplus argument is ignored, as in the reference.
|
||||
m.set(
|
||||
"SetPlayerBuff",
|
||||
lua.create_function(|lua, (this, index): (Table, i64)| {
|
||||
@@ -486,33 +386,25 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
.and_then(|pos| model.auras.get("player").and_then(|a| a.get(pos)));
|
||||
match hit {
|
||||
Some(a) => {
|
||||
// The gate is `untilCancelled`, NOT "does this aura have a duration yet".
|
||||
// `0x532b00` skips the whole duration block when the
|
||||
// cache record's `+0xc` is set (`532bda: 8b 46 0c` / `532bdf: 75 2d`), so
|
||||
// a permanent aura shows title + description and nothing more. That flag
|
||||
// is DBC-derived, so it is already right on the frame an aura appears —
|
||||
// before any `SMSG_UPDATE_AURA_DURATION` lands — which is exactly why
|
||||
// `AuraState::until_cancelled`'s own doc calls it a different question
|
||||
// from `expiration_time == 0.0`. Gating on the duration (what this did)
|
||||
// blanked the line for a timed aura's first frames, and hid the
|
||||
// reference's own "0 seconds remaining" once one lapsed.
|
||||
// The gate is `untilCancelled`, not a known duration: `0x532b00` skips
|
||||
// the line when the record's `+0xc` is set (`0x532bdf`), a DBC flag that
|
||||
// holds before any `SMSG_UPDATE_AURA_DURATION` lands; a lapsed aura shows
|
||||
// "0 seconds remaining".
|
||||
let ms = (!a.until_cancelled).then(|| {
|
||||
// The reference counts integer milliseconds off GetTickCount; ours is
|
||||
// a float second clock, so this is the nearest millisecond to it.
|
||||
// The reference counts integer milliseconds off `GetTickCount`;
|
||||
// this rounds the float clock to the nearest one.
|
||||
((a.expiration_time - now) * 1000.0)
|
||||
.round()
|
||||
.clamp(0.0, f64::from(u32::MAX)) as u32
|
||||
});
|
||||
(a.spell_id, a.name.clone(), ms)
|
||||
}
|
||||
// The miss clears rather than leaving the previous plate up — the same shape
|
||||
// SetUnitBuff uses, and the one Loatheb's scan depends on: it breaks its loop
|
||||
// when TextLeft1 reads nil, so a stale line would never let it terminate.
|
||||
// A miss clears: an addon scan that stops when `TextLeft1` reads nil would
|
||||
// otherwise never end.
|
||||
None => (0, None, None),
|
||||
}
|
||||
};
|
||||
// The text is `0x52fa50`'s, over the player's own GlobalStrings — never a string of
|
||||
// ours. Off an install the table is absent and the line simply does not render.
|
||||
// `0x52fa50`'s text from the player's own strings; without them the line is skipped.
|
||||
let remaining = remaining_ms.and_then(|ms| {
|
||||
let g = lua.globals();
|
||||
super::tooltip::duration_text(ms, "SPELL_TIME_REMAINING", true, &|key| {
|
||||
@@ -532,22 +424,13 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
)
|
||||
})?,
|
||||
)?;
|
||||
// GameTooltip:SetUnitBuff(unit, index) / SetUnitDebuff(unit, index) — the target frame's aura
|
||||
// hover: the same aura variant (white AuraDescription), WITHOUT the duration-remaining line —
|
||||
// byte-verified, only SetPlayerBuff appends it (and no other unit has a duration on the 1.12
|
||||
// wire anyway). The index counts within the sign-filtered list, the UnitBuff/UnitDebuff
|
||||
// convention.
|
||||
// GameTooltip:SetUnitBuff/SetUnitDebuff(unit, index): the aura variant with no time line; the
|
||||
// 1-based index counts within the helpful or harmful list, as `UnitBuff` does.
|
||||
for (verb, helpful) in [("SetUnitBuff", true), ("SetUnitDebuff", false)] {
|
||||
m.set(
|
||||
verb,
|
||||
// `index` is `Value`, not `i64`, and that is a fidelity fix rather than laxity — the
|
||||
// same correction `SetTexture` already carries. A C binding reads what it wants off
|
||||
// the Lua stack: `lua_tonumber` on nil yields 0, which finds no aura and shows
|
||||
// nothing. Typing it `i64` made us RAISE on a call the real client accepts silently.
|
||||
//
|
||||
// Found by the use-probe: `CT_AssistFrameDebuff1:OnEnter` calls
|
||||
// `SetUnitDebuff(unit, this:GetID())` and the id is nil on a frame CT_UnitFrames
|
||||
// created without one. It only fires on hover, so nothing before the probe saw it.
|
||||
// `index` is a `Value`: the reference's `lua_tonumber` reads nil as 0, which finds no
|
||||
// aura, where an `i64` would raise.
|
||||
lua.create_function(move |lua, (this, token, index): (Table, String, Value)| {
|
||||
let index = match &index {
|
||||
Value::Integer(i) => *i,
|
||||
@@ -568,9 +451,8 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
.and_then(|a| a.iter().filter(|a| a.helpful == helpful).nth(idx))
|
||||
.map(|a| (a.spell_id, a.name.clone()))
|
||||
};
|
||||
// A miss (index past the list, unknown token) still routes through the shared
|
||||
// entry with spell id 0: content clears and the empty plate hides — never a
|
||||
// stale previous tooltip left showing. Id 0 records no ask.
|
||||
// A miss goes through with spell id 0, which clears and hides the plate and
|
||||
// records no ask.
|
||||
let (spell_id, name) = hit.unwrap_or((0, None));
|
||||
set_spell_by_id(
|
||||
lua,
|
||||
@@ -586,13 +468,9 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
// GameTooltip:SetTrackingSpell() — the minimap tracking icon's hover: NAME gold over a white
|
||||
// (aura-)description, pinned by the director's reference A/B (2026-07-20: "Find Minerals"
|
||||
// gold over white "Finding Minerals."). That is just the AURA builder's shape — its name line
|
||||
// rides the gold wrapper `0x530380` too — so this is no longer a one-off; `0x532c50`'s body
|
||||
// is still unconfirmed in the binary, so read it before extending BEYOND that shape. No
|
||||
// duration-remaining line (only SetPlayerBuff appends one), and no tracking active clears +
|
||||
// hides, like the SetUnitBuff miss path.
|
||||
// GameTooltip:SetTrackingSpell(): the tracking icon's hover, the name gold over the white
|
||||
// aura description, as the reference shows "Find Minerals" and as the aura builder's gold
|
||||
// wrapper `0x530380` writes it; `0x532c50` itself is untraced. No tracking clears and hides.
|
||||
m.set(
|
||||
"SetTrackingSpell",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
@@ -623,8 +501,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// The view hasn't landed (ask recorded; the re-enter repaints): the name
|
||||
// alone, in the same gold.
|
||||
// No view yet: the name alone, gold.
|
||||
if let Some(name) = fallback_name {
|
||||
append_line(lua, &this, (name, GOLD), None, false)?;
|
||||
}
|
||||
@@ -634,21 +511,9 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// GameTooltip:SetAction(slot) — the action-bar hover: pure delegation by payload kind
|
||||
// (`0x5322a0`, byte-verified: SPELL 0x00 → the spell builder `0x52e610`, ITEM 0x80 → the item
|
||||
// builder `0x52b650`, MACRO 0x40 → `0x52b040`).
|
||||
//
|
||||
// The MACRO arm is the reference's whole `0x52b040`: fetch the record (`0x4f0f40`), and either
|
||||
// hide (no record) or render ONE line — the macro's NAME (`rec+0x24`) — through the
|
||||
// single-coloured-line wrapper `0x5303b0` in the "normal" colour `0xc0cf60`, which is WHITE
|
||||
// (`0xffffffff`, never rewritten). Not the spell title's gold, and nothing about the bound
|
||||
// spell: 1.12 has no
|
||||
// `#showtooltip`. This arm was a `_ => Ok(())` left from before 0983 shipped macros, which is
|
||||
// why a macro on the bar hovered to nothing (the director, 2026-08-27, after 1636).
|
||||
// GameTooltip:SetQuestRewardSpell() / SetQuestLogRewardSpell() — the hover of the reward
|
||||
// slot stock marks `rewardType = "spell"` (QuestFrameTemplates.xml:150, QuestLogFrame.xml:115;
|
||||
// bindings 0x535bb0 / 0x535c60, self only, 0 returns): the quest's reward spell by id through
|
||||
// the spell renderer, an empty tooltip when the quest rewards none.
|
||||
// GameTooltip:SetQuestRewardSpell() / SetQuestLogRewardSpell() (`0x535bb0`, `0x535c60`): the
|
||||
// hover of a reward slot whose `rewardType` is "spell" (`QuestFrameTemplates.xml:150`,
|
||||
// `QuestLogFrame.xml:115`), the quest's reward spell or an empty tooltip.
|
||||
m.set(
|
||||
"SetQuestRewardSpell",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
@@ -676,6 +541,10 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
set_reward_spell(lua, &this, id)
|
||||
})?,
|
||||
)?;
|
||||
// GameTooltip:SetAction(slot): the action-bar hover by payload kind (`0x5322a0`): a spell
|
||||
// (0x00) to the spell builder `0x52e610`, an item (0x80) to the item builder `0x52b650`, a
|
||||
// macro (0x40) to `0x52b040`, which fetches it (`0x4f0f40`) and renders only its name, white
|
||||
// (`0x5303b0` in colour `0xc0cf60`), or hides with none; 1.12 has no `#showtooltip`.
|
||||
m.set(
|
||||
"SetAction",
|
||||
lua.create_function(|lua, (this, slot): (Table, u32)| {
|
||||
@@ -697,7 +566,6 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
None,
|
||||
),
|
||||
0x80 => {
|
||||
// Route through the shared item renderer (the id-keyed entry).
|
||||
let f: mlua::Function = this.get("BenillaSetItemById")?;
|
||||
f.call::<()>((this.clone(), a.action))
|
||||
}
|
||||
@@ -706,8 +574,8 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
let name = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
clear_content(&mut model, h);
|
||||
// The same 1..36 index lookup `GetActionText` and the bar's icon arm
|
||||
// make, so the three can never disagree about which macro a slot holds.
|
||||
// The same 1..36 lookup as `GetActionText` and the bar icon, so the
|
||||
// three agree on a slot's macro.
|
||||
model.macros.get(a.action as usize).map(|m| m.name.clone())
|
||||
};
|
||||
fire_cleared(lua, h);
|
||||
@@ -722,19 +590,10 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GameTooltip:SetTrainerService(index) — the trainer detail-icon hover (ref
|
||||
// `Blizzard_TrainerUI.xml:452`, whose OnEnter is SetOwner(this,"ANCHOR_RIGHT") +
|
||||
// SetTrainerService(ClassTrainerFrame.selectedService) + Show(); the LIST ROWS carry no tooltip
|
||||
// at all).
|
||||
//
|
||||
// The binding is a **selector, not a renderer**: `0x5338b0` emits no line of its own (verified
|
||||
// negative — none of the four AddLine helpers appears in its extent) and hands one of the two
|
||||
// shared builders a subject. The subject is decided app-side, because the law reads `Spell.dbc`
|
||||
// fields the engine cannot see, and arrives pre-resolved as `TrainerService::tooltip`. All this
|
||||
// does is pick the renderer — which is exactly what the reference binding does.
|
||||
//
|
||||
// `index` is a **VISIBLE** row index (headers interleave), resolved through the same mapping
|
||||
// every other trainer getter uses; a header row is a no-op.
|
||||
// GameTooltip:SetTrainerService(index): the trainer detail icon's hover
|
||||
// (`Blizzard_TrainerUI.xml:452`); list rows have no tooltip. `0x5338b0` writes no line itself
|
||||
// and hands a shared builder the subject the app resolved (`TrainerService::tooltip`). `index`
|
||||
// is a visible row, headers included; a header is a no-op.
|
||||
m.set(
|
||||
"SetTrainerService",
|
||||
lua.create_function(|lua, (this, index): (Table, usize)| {
|
||||
@@ -746,9 +605,8 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
}
|
||||
};
|
||||
match subject {
|
||||
// Route through the shared item renderer, the way the reference routes into
|
||||
// `0x52b650`. No fallback name: an item id of 0 or a template still in flight
|
||||
// renders an EMPTY tooltip, which is the builder's own early-out.
|
||||
// The item builder (`0x52b650`), with no fallback name: id 0 or a template in
|
||||
// flight renders empty, the builder's own early-out.
|
||||
TrainerTooltip::Item(item_id) => {
|
||||
let f: mlua::Function = this.get("BenillaSetItemById")?;
|
||||
f.call::<()>((this.clone(), item_id))
|
||||
@@ -770,17 +628,10 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
// GameTooltip:SetCraftSpell(craftIndex) — the Craft window's detail-icon hover (ref
|
||||
// `CraftIcon`'s OnEnter, `Blizzard_CraftUI.xml:566`). `SetTrainerService`'s structural twin and
|
||||
// its law's opposite (`SetCraftSpell 0x533e90`): a selector into
|
||||
// the same two shared builders, deciding on the RECIPE's own effect columns rather than a
|
||||
// taught spell's attributes. The subject arrives pre-resolved as `CraftRecipe::tooltip`.
|
||||
//
|
||||
// This replaced a v1 two-line "name white, description gold" render. That was wrong in both
|
||||
// halves: `0x533e90` funnels into `0x52e610`/`0x52b650` like every other content binding, so
|
||||
// two lines were eight-plus short, and on a `LEARN_SPELL` or `CREATE_ITEM` recipe it was
|
||||
// describing the wrong subject entirely. `craft_index` is a raw recipe position — the Craft
|
||||
// window is FLAT, with no headers to interleave (unlike the tradeskill list).
|
||||
// GameTooltip:SetCraftSpell(craftIndex): the craft detail icon's hover
|
||||
// (`Blizzard_CraftUI.xml:566`). `SetCraftSpell 0x533e90` hands a shared builder the subject the
|
||||
// app resolved from the recipe's own effects (`CraftRecipe::tooltip`). `craftIndex` is a plain
|
||||
// recipe position: the craft list has no headers.
|
||||
m.set(
|
||||
"SetCraftSpell",
|
||||
lua.create_function(|lua, (this, craft_index): (Table, usize)| {
|
||||
|
||||
Reference in New Issue
Block a user