Lua bindings now read their arguments the way the 1.12.1 client does:
- Numeric strings parse as Storm's `SStrToInt` (`0x64ac60`) and `SStrToFloat` (`0x64aaa0`). That covers channel keys, `GetNumber` and `item:` fields.
- Flag arguments go through `GetBoolOrDefault` (`0x6f1c10`) with each site's default. That is about 40 bindings, `SetChecked()` and `ShowHelm()` among them.
- Colour channels take numeric strings (`lua_tonumber`), and alphas are `isnumber`-gated: no raise, 1.0 by default. A three-argument `SetVertexColor` keeps the current alpha (`0x79ae0a`). So action-bar rings dimmed by a drag stay dimmed, as in 1.12.1.
- `SetTexture("1")` takes the colour form. MessageFrame channels follow `0x795590`.
- `LoggingChat`/`LoggingCombat` return the flag.
Binding-level tests use the reference's values.
Fixes #135
Fixes #144
Fixes #160
Fixes #161
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
293 lines
13 KiB
Rust
293 lines
13 KiB
Rust
//! The GameTooltip Lua methods over the parent module's line pool, plus the content builders'.
|
|
|
|
use mlua::{Lua, Table, Value, Variadic};
|
|
|
|
use crate::layout::{Anchor, Anchors, Point};
|
|
use crate::script::binding_abi::optional_string;
|
|
use crate::script::object::frame_handle_of;
|
|
use crate::script::region::region_handle_of;
|
|
use crate::script::Model;
|
|
use crate::widget::{KindState, TooltipAnchor, TOOLTIP_LINE_GAP, TOOLTIP_PAD};
|
|
|
|
use super::{
|
|
append_line, clear, color_num, parse_line_color, parse_line_tail, set_owner_core, set_shown,
|
|
text_of, tip_mut, with_tip, wrap_arg, write_cell, REG_TOOLTIP_METHODS,
|
|
};
|
|
|
|
pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
|
let m = lua.create_table()?;
|
|
|
|
// SetOwner(owner, anchorType [, x, y]) (`0x5310d0`): first the tooltip's own hide, the shown
|
|
// bit cleared (`0x531179`) and the hide slot (`0x531183`), so a shown tooltip fires
|
|
// `OnTooltipCleared` and `OnHide` and stays hidden until a content call shows it; then the
|
|
// core with the new owner ([`set_owner_core`], `0x5313cf`), which clears and fires again.
|
|
//
|
|
// A missing, non-string or unknown anchorType is mode 0, ANCHOR_LEFT, with no error: the
|
|
// binding's local starts at 0 (`0x53120d`). The core's pre-store of 7 (`0x530012`) survives
|
|
// only a NULL owner, the un-own path (`0x530a60`) of every hide.
|
|
m.set(
|
|
"SetOwner",
|
|
lua.create_function(
|
|
|lua, (this, owner, anchor, x, y): (Table, Value, Value, Option<f32>, Option<f32>)| {
|
|
let h = frame_handle_of(lua, &this)?;
|
|
set_shown(lua, h, false);
|
|
// The owner is read after the hide (`0x531189`), so a bad one raises on a
|
|
// hidden tooltip.
|
|
let owner: Table = lua.unpack(owner)?;
|
|
let owner_h = frame_handle_of(lua, &owner)?;
|
|
// The gate is `lua_isstring` (`0x6f3510`): a string or number is read, and any
|
|
// other type counts as absent rather than raising.
|
|
let anchor = optional_string(lua, &anchor);
|
|
// The warning on an unknown name is ours; the reference is silent, and Lua sees
|
|
// no difference.
|
|
let anchor = match anchor
|
|
.as_deref()
|
|
.map(str::to_ascii_uppercase)
|
|
.as_deref()
|
|
.unwrap_or("")
|
|
{
|
|
"ANCHOR_LEFT" => TooltipAnchor::Left,
|
|
"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 => {
|
|
lua.app_data_mut::<Model>()
|
|
.expect("model app_data")
|
|
.record_warning(format!(
|
|
"SetOwner: unknown anchor '{other}' (mode 0, ANCHOR_LEFT)"
|
|
));
|
|
TooltipAnchor::Left
|
|
}
|
|
};
|
|
set_owner_core(
|
|
lua,
|
|
h,
|
|
Some(owner_h),
|
|
anchor,
|
|
x.unwrap_or(0.0),
|
|
y.unwrap_or(0.0),
|
|
);
|
|
Ok(())
|
|
},
|
|
)?,
|
|
)?;
|
|
// GetAnchorType(): `[+0x318]` by name (`0x5313e0`, name table `0x531530`), never nil. While
|
|
// owned it is the last SetOwner's mode; a never-owned tooltip, and any tooltip since a hide
|
|
// (whose un-own stores mode 7, `0x530012`), answers "ANCHOR_NONE".
|
|
m.set(
|
|
"GetAnchorType",
|
|
lua.create_function(|lua, this: Table| {
|
|
let anchor = with_tip(lua, &this, |t| t.anchor)?;
|
|
Ok(anchor.name())
|
|
})?,
|
|
)?;
|
|
// IsOwned(frame): the hover loop's OnUpdate gate (`ContainerFrame.lua:657`); the owner drops
|
|
// on hide, so a stale OnUpdate never brings a hidden tooltip back.
|
|
m.set(
|
|
"IsOwned",
|
|
lua.create_function(|lua, (this, frame): (Table, Table)| {
|
|
let owner = with_tip(lua, &this, |t| t.owner)?;
|
|
let fh = frame_handle_of(lua, &frame)?;
|
|
Ok(owner == Some(fh))
|
|
})?,
|
|
)?;
|
|
|
|
// SetText(text, r, g, b, alpha, wrap) (`0x531b90`): alpha has its own number gate, default 1,
|
|
// and a text that is neither string nor number raises the binding's usage error. It clears,
|
|
// writes line 1 and shows: the empty-slot hover calls no Show after it
|
|
// (PaperDollFrame.lua:747).
|
|
m.set(
|
|
"SetText",
|
|
lua.create_function(|lua, (this, args): (Table, Variadic<Value>)| {
|
|
let h = frame_handle_of(lua, &this)?;
|
|
if !matches!(
|
|
args.first(),
|
|
Some(Value::String(_)) | Some(Value::Number(_)) | Some(Value::Integer(_))
|
|
) {
|
|
return Err(mlua::Error::RuntimeError(
|
|
"Usage: SetText(\"text\" [, color])".into(),
|
|
));
|
|
}
|
|
let text = text_of(args.first());
|
|
let [r, g, b, _] = parse_line_color(lua, args.get(1), args.get(2), args.get(3));
|
|
// The alpha is its own `lua_isnumber` gate (`0x531ddc`'s neighbour at idx 6), so a
|
|
// numeric string counts.
|
|
let a = color_num(lua, args.get(4)).unwrap_or(1.0);
|
|
let wrap = wrap_arg(args.get(5));
|
|
clear(lua, h);
|
|
append_line(lua, &this, (text, [r, g, b, a]), None, wrap)?;
|
|
set_shown(lua, h, true);
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
// AppendText(text) (`0x531e30`): appends to line 1's left cell in place, keeping its colour,
|
|
// and does nothing with no lines, as the reference's body does (`0x5305b0`, line 1 at
|
|
// `0x5305cc`). The bag tooltips hang a keybinding on their SetText title with it
|
|
// (ContainerFrame.xml:198, MainMenuBarBagButtons.lua:91).
|
|
m.set(
|
|
"AppendText",
|
|
lua.create_function(|lua, (this, text): (Table, Value)| {
|
|
let h = frame_handle_of(lua, &this)?;
|
|
let extra = text_of(Some(&text));
|
|
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
|
let lh = match model.arena.frame(h).map(|f| &f.kind_state) {
|
|
Some(KindState::Tooltip(t)) if t.num_lines > 0 => t.left_lines[0],
|
|
Some(KindState::Tooltip(_)) => return Ok(()),
|
|
_ => return Err(mlua::Error::runtime("not a GameTooltip")),
|
|
};
|
|
let (current, color) = match model.region_data.get(&lh) {
|
|
Some(d) => (
|
|
d.text.clone().unwrap_or_default(),
|
|
d.vertex_color.unwrap_or([1.0, 1.0, 1.0, 1.0]),
|
|
),
|
|
None => (String::new(), [1.0, 1.0, 1.0, 1.0]),
|
|
};
|
|
write_cell(&mut model, lh, &(current + &extra), color);
|
|
super::owe_relayout(&mut model, h);
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
// AddLine(text [, r, g, b [, wrap]]): positional, and appends without showing; the
|
|
// `(text, "", r, g, b)` shape has a non-number r-slot, so it renders default gold.
|
|
m.set(
|
|
"AddLine",
|
|
lua.create_function(|lua, (this, args): (Table, Variadic<Value>)| {
|
|
let text = text_of(args.first());
|
|
let (color, wrap) = parse_line_tail(lua, &args[1.min(args.len())..]);
|
|
append_line(lua, &this, (text, color), None, wrap)
|
|
})?,
|
|
)?;
|
|
// AddDoubleLine(textL, textR, rL, gL, bL, rR, gR, bR [, wrap]) (`0x531840`): each side's
|
|
// colour gates on its own r-slot, and the wrap flag is the tenth argument.
|
|
m.set(
|
|
"AddDoubleLine",
|
|
lua.create_function(
|
|
|lua, (this, tl, tr, cl): (Table, Value, Value, Variadic<Value>)| {
|
|
let left_color = parse_line_color(lua, cl.first(), cl.get(1), cl.get(2));
|
|
let right_color = parse_line_color(lua, cl.get(3), cl.get(4), cl.get(5));
|
|
let right = text_of(Some(&tr));
|
|
// The line core (`0x530270`) clears the flag when the right text is non-empty
|
|
// (`0x530284`), so only a double line with an empty right side can wrap.
|
|
let wrap = right.is_empty() && wrap_arg(cl.get(6));
|
|
append_line(
|
|
lua,
|
|
&this,
|
|
(text_of(Some(&tl)), left_color),
|
|
Some((right, right_color)),
|
|
wrap,
|
|
)
|
|
},
|
|
)?,
|
|
)?;
|
|
m.set(
|
|
"ClearLines",
|
|
lua.create_function(|lua, this: Table| {
|
|
let h = frame_handle_of(lua, &this)?;
|
|
clear(lua, h);
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
m.set(
|
|
"NumLines",
|
|
lua.create_function(|lua, this: Table| with_tip(lua, &this, |t| t.num_lines as i64))?,
|
|
)?;
|
|
// AddFontStrings(left, right) (`0x530c40`): adopts two caller-made FontStrings as the next
|
|
// line pair, placed like an engine-grown pair but keeping the caller's fonts.
|
|
m.set(
|
|
"AddFontStrings",
|
|
lua.create_function(|lua, (this, left, right): (Table, Table, Table)| {
|
|
let h = frame_handle_of(lua, &this)?;
|
|
let lh = region_handle_of(lua, &left)?;
|
|
let rh = region_handle_of(lua, &right)?;
|
|
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
|
let frame_id = model.frame_id(h);
|
|
let prev_left = match model.arena.frame(h).map(|f| &f.kind_state) {
|
|
Some(KindState::Tooltip(t)) => t.left_lines.last().copied(),
|
|
_ => return Err(mlua::Error::runtime("not a GameTooltip")),
|
|
};
|
|
let anchor = match prev_left {
|
|
None => Anchor::new(
|
|
Point::TopLeft,
|
|
frame_id,
|
|
Point::TopLeft,
|
|
TOOLTIP_PAD,
|
|
-TOOLTIP_PAD,
|
|
),
|
|
Some(prev) => {
|
|
let prev_id = model.region_id(prev);
|
|
Anchor::new(
|
|
Point::TopLeft,
|
|
prev_id,
|
|
Point::BottomLeft,
|
|
0.0,
|
|
-TOOLTIP_LINE_GAP,
|
|
)
|
|
}
|
|
};
|
|
let left_id = model.region_id(lh);
|
|
let (left_drew, right_drew) = (model.region_draws(lh), model.region_draws(rh));
|
|
{
|
|
let d = model.region_data.entry(lh).or_default();
|
|
d.hidden = true;
|
|
d.justify.set_h(crate::script::JustifyH::Left);
|
|
d.anchors = Anchors::from([anchor]);
|
|
}
|
|
{
|
|
let d = model.region_data.entry(rh).or_default();
|
|
d.hidden = true;
|
|
d.justify.set_h(crate::script::JustifyH::Right);
|
|
d.anchors =
|
|
Anchors::from([Anchor::new(Point::Right, left_id, Point::Right, 0.0, 0.0)]);
|
|
}
|
|
model.note_region_draw(lh, left_drew);
|
|
model.note_region_draw(rh, right_drew);
|
|
// Two line rows, their points set with the invalidate flag.
|
|
model.invalidate_region(lh, true);
|
|
model.invalidate_region(rh, true);
|
|
let t = tip_mut(&mut model, h)?;
|
|
t.left_lines.push(lh);
|
|
t.right_lines.push(rh);
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
// SetMinimumWidth(w): a floor on the auto-sized width (`SetTooltipMoney` passes the money
|
|
// row's, GameTooltip.lua:91).
|
|
m.set(
|
|
"SetMinimumWidth",
|
|
lua.create_function(|lua, (this, w): (Table, f32)| {
|
|
with_tip(lua, &this, |t| t.min_width = w.max(0.0))
|
|
})?,
|
|
)?;
|
|
// SetPadding(w): extra auto-size width that survives a content clear (ItemRefTooltip's 16
|
|
// keeps its close button off the text, ItemRef.xml:40).
|
|
m.set(
|
|
"SetPadding",
|
|
lua.create_function(|lua, (this, w): (Table, f32)| {
|
|
with_tip(lua, &this, |t| t.padding = w.max(0.0))
|
|
})?,
|
|
)?;
|
|
// FadeOut() (`0x531f00`): the fade request ([`super::fade_out`]).
|
|
m.set(
|
|
"FadeOut",
|
|
lua.create_function(|lua, this: Table| {
|
|
let h = frame_handle_of(lua, &this)?;
|
|
super::fade_out(lua, h);
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
// The content methods: items, spells and auras, talents, units.
|
|
crate::script::tooltip_item::install_methods(lua, &m)?;
|
|
crate::script::tooltip_spell::install_methods(lua, &m)?;
|
|
crate::script::talent::install_tooltip_method(lua, &m)?;
|
|
crate::script::tooltip_unit::install_methods(lua, &m)?;
|
|
|
|
lua.set_named_registry_value(REG_TOOLTIP_METHODS, m)?;
|
|
Ok(())
|
|
}
|