widget identity, a funded purse and a walkable quest log

- GetObjectType and IsObjectType land from the binary's own carve, with the client's exact inheritance answers
- the survey seats money and a quest log — sixteen addons call GetMoney, five walk the quest log, and none of those paths had ever run
- `date("*t")` returns its table form; the missing shape was an infinite loop for any addon walking days backwards
- unit tokens fold case like the client's resolver, and a FontString template carries the font object it inherits through the whole chain

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-14 18:59:47 +07:00
co-authored by Claude Fable 5
parent 7b6a9136f6
commit 6eae5e8507
18 changed files with 1058 additions and 47 deletions
@@ -8,6 +8,7 @@ use mlua::{Lua, Table, Value};
use crate::order::Strata;
use crate::script::{event, Backdrop, Insets, Model};
use crate::widget::FrameKind;
use super::{decode_id, frame_handle_of, frame_wrapper, strata_from_str};
@@ -87,6 +88,98 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
Ok(())
})?,
)?;
// ── GetObjectType / IsObjectType, frame side (wow-re `widget-type-identity.md` §6) ──────────
//
// The region side landed first because a FontString found it; `_Nameplates` is why this half
// exists too — `_Nameplates.lua:164` tests `Nameplate:GetObjectType() ~= "Button"` and `:191`
// tests `== "StatusBar"`, on FRAMES, in the same file whose `Region:GetObjectType()` calls the
// region verbs answer.
//
// The chains are a hardcoded straight-line list per class in the binary, not a runtime parent
// walk, so they are a table here too. Three of them are things a reasonable person invents
// wrongly:
//
// · **`ScrollingMessageFrame` derives from `Frame`, NOT from `MessageFrame`.** The name says
// otherwise and the roster is explicit (`0x787940`).
// · **`SimpleHTML` is spelled with a capitalised HTML.** Our enum variant is `SimpleHtml`, so
// anything derived from the variant name — `format!("{:?}")` and friends — would hand
// addons `"SimpleHtml"`, and `GetObjectType` is compared with `==` (`IsObjectType`'s
// case-folding would hide it; the getter's would not).
// · **`Cooldown` answers `"Model"`.** 1.12.1 has no `Cooldown` type — the census finds 23
// type-name globals and none is that. The reference builds its cooldown as a `Model` playing
// `UI-Cooldown-Indicator.mdx` (`CooldownFrameTemplate`), and OUR `FrameKind::Cooldown` is a
// deliberate Era-shaped divergence that models the mechanism first-class (0137 phase 4). So
// the faithful answer is what the reference's own cooldown widget IS. Answering `"Cooldown"`
// would announce an Era type to a Lua ecosystem that branches on presence — precisely the
// superset 1189 had to take back out.
fn type_chain(kind: FrameKind) -> &'static [&'static str] {
match kind {
FrameKind::Frame => &["Frame", "Region"],
FrameKind::Button => &["Button", "Frame", "Region"],
FrameKind::CheckButton => &["CheckButton", "Button", "Frame", "Region"],
FrameKind::EditBox => &["EditBox", "Frame", "Region"],
FrameKind::StatusBar => &["StatusBar", "Frame", "Region"],
FrameKind::Slider => &["Slider", "Frame", "Region"],
FrameKind::ScrollFrame => &["ScrollFrame", "Frame", "Region"],
FrameKind::Model => &["Model", "Frame", "Region"],
FrameKind::MessageFrame => &["MessageFrame", "Frame", "Region"],
FrameKind::ScrollingMessageFrame => &["ScrollingMessageFrame", "Frame", "Region"],
FrameKind::ColorSelect => &["ColorSelect", "Frame", "Region"],
FrameKind::SimpleHtml => &["SimpleHTML", "Frame", "Region"],
FrameKind::MovieFrame => &["MovieFrame", "Frame", "Region"],
FrameKind::GameTooltip => &["GameTooltip", "Frame", "Region"],
FrameKind::Minimap => &["Minimap", "Frame", "Region"],
FrameKind::Cooldown => &["Model", "Frame", "Region"],
}
}
fn chain_of(lua: &Lua, this: &Table) -> mlua::Result<&'static [&'static str]> {
let h = frame_handle_of(lua, this)?;
let model = lua.app_data_ref::<Model>().expect("model");
Ok(model
.arena
.frame(h)
.map_or(&["Frame", "Region"][..], |f| type_chain(f.kind)))
}
m.set(
"GetObjectType",
lua.create_function(|lua, this: Table| {
Ok(Value::String(lua.create_string(chain_of(lua, &this)?[0])?))
})?,
)?;
// Same four traps as the region twin (see `script::region`): case-insensitive whole-string,
// number 1 on a hit and nil on a miss, exactly one value either way, and a non-string
// non-number argument raises the reference's own `Usage:` text.
m.set(
"IsObjectType",
lua.create_function(|lua, (this, want): (Table, Value)| {
let chain = chain_of(lua, &this)?;
let want = match &want {
Value::String(s) => s.to_str()?.to_string(),
Value::Number(n) => n.to_string(),
Value::Integer(i) => i.to_string(),
_ => {
let h = frame_handle_of(lua, &this)?;
let model = lua.app_data_ref::<Model>().expect("model");
let who = model
.arena
.frame(h)
.and_then(|f| f.name.clone())
.unwrap_or_else(|| "<unnamed>".to_string());
return Err(mlua::Error::runtime(format!(
"Usage: {who}:IsObjectType(\"TYPE\")"
)));
}
};
Ok(if chain.iter().any(|t| want.eq_ignore_ascii_case(t)) {
Value::Number(1.0)
} else {
Value::Nil
})
})?,
)?;
m.set(
"GetParent",
lua.create_function(|lua, this: Table| {