idle sits you down and camps you, and the addons anchor, parse and draw
- five idle minutes sit you down and flag you AFK, thirty log you out through the ordinary countdown; holding a key is idle, moving the mouse is not, and a swimmer stays afloat (2092) - a knockdown no longer wedges: the landing froze the clip that held the base-animation lock, leaving a knocked-down player on their back for the session (2098); the perf cost pill starts hidden (2099) - Lua is exactly 1.12's 5.0 — no vararg expression, no #, no %, the remembered table size — so Cartographer loads without errors and Mik's Scrolling Battle Text shows its text, in its own shipped font now that addon TTFs load (2101, 2102, 2103) - a widget name is a Lua global for SetPoint and its kin, so Bartender2's bars spread out; a button's NormalText disowns its justify, the raise renumbers hidden frames too, and a missing toc file warns instead of erroring (2105, 2100, 2104, 2107, 2106) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -193,6 +193,74 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// ── GetFrameType / IsFrameType: the FRAME-side spellings of the pair above (decision 2106) ──
|
||||
//
|
||||
// 1.12 registers the type-identity pair **twice, under two names**. The Region script
|
||||
// (`CScriptRegion`) publishes `GetObjectType`/`IsObjectType` — that is the pair above, and its
|
||||
// `IsObjectType` binding is `0x7a1290`. The *frame* script (`CSimpleFrameScript.cpp`,
|
||||
// `__FILE__` `0x879504`) publishes `GetFrameType 0x773640` and `IsFrameType 0x773700` as well,
|
||||
// and wow-5875-re carved both (`system/ui/scratch/item17-frameapi-batch1.md`, 76-entry
|
||||
// registrar table verified at the bytes). `GetObjectType`/`IsObjectType` are what LATER
|
||||
// clients kept; `GetFrameType`/`IsFrameType` are 1.12's own, and we shipped only the first
|
||||
// pair. That is 1189's superset argument inverted — not an extra name we invented, a real one
|
||||
// we were missing — and it cost the world map:
|
||||
//
|
||||
// Cartographer 2.02, LookNFeel.lua:368, inside OnEnable:
|
||||
// if v:GetFrameType() == "Model" and not v:GetName() then self.playerModel = v end
|
||||
//
|
||||
// With the verb nil that line raised, AceAddon swallowed it, `self.playerModel` stayed nil,
|
||||
// and the next `Cartographer_ChangeZone` → `SetAlpha` put a red script error on screen from
|
||||
// `LookNFeel.lua:737` every time the map opened. FuBar's own `FuBar.lua:576`
|
||||
// (`type(frame:GetFrameType()) ~= "string"`) is the same call.
|
||||
//
|
||||
// Both delegate to the same `type_chain` the `Object` pair reads: `GetFrameType` is
|
||||
// `call [edx+0x1c]` — the identical per-class type-name slot `GetObjectType` reads — then
|
||||
// `lua_pushstring`, one value, extra arguments ignored. `IsFrameType` walks the chain through
|
||||
// `[eax+0x18]`, the same case-insensitive whole-string compare, and answers the NUMBER 1 or
|
||||
// nil.
|
||||
//
|
||||
// **One edge is INFERRED and named rather than guessed silently:** wow-re's note glosses
|
||||
// `IsFrameType`'s *absent-argument* branch as "pushes the frame's own typename (`call [eax+4]`
|
||||
// GetName)", which is self-contradictory — a typename and a name are different slots — so the
|
||||
// branch is not settled. We take the sibling's behaviour (the `Usage:` raise) pending a byte
|
||||
// read. No caller in the 219-addon corpus, the director's AddOns, or the shipped FrameXML ever
|
||||
// omits the argument: `JIM_toolbox/Config2/Pulse_Config.lua:118` is the corpus's only
|
||||
// `IsFrameType` site and it passes `"Slider"`.
|
||||
m.set(
|
||||
"GetFrameType",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
Ok(Value::String(lua.create_string(chain_of(lua, &this)?[0])?))
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"IsFrameType",
|
||||
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}:IsFrameType(\"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| {
|
||||
@@ -334,6 +402,16 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
.unwrap_or_else(|| "<unnamed>".to_string())
|
||||
};
|
||||
let parent_arg = it.next();
|
||||
// `_G[name]` is read with no guard alive (`object::NamedTarget`), and **without**
|
||||
// `$parent` expansion: `0x7a1550` calls `0x76c760` directly, and only the layout
|
||||
// vtable's `0x76c700` runs the token.
|
||||
let named = match &parent_arg {
|
||||
Some(Value::String(s)) => Some(match s.to_str() {
|
||||
Ok(n) => super::prefetch_named_target(lua, n.as_ref(), None),
|
||||
Err(_) => super::NamedTarget::unreadable(),
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
let new_parent = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
match &parent_arg {
|
||||
@@ -349,11 +427,18 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
))
|
||||
})?,
|
||||
),
|
||||
Some(Value::String(s)) => {
|
||||
let n = s.to_str()?.to_string();
|
||||
Some(model.arena.lookup(&n).ok_or_else(|| {
|
||||
Some(Value::String(_)) => {
|
||||
// `_G[name]` + the **Frame** tag check — the reference's `0x76c760`
|
||||
// (`0x7a1550`'s NAME-string path) type-guards against `[0xcf0c10]`, the
|
||||
// narrow Frame id, not `SetPoint`'s root `[0xcf0c3c]`. So a global naming
|
||||
// a REGION fails here exactly as an absent one does.
|
||||
let nt = named.as_ref().expect("a String argument is prefetched");
|
||||
let hit = super::resolve_named_target(&model, nt)
|
||||
.and_then(|id| model.id_to_frame.get(&id).copied());
|
||||
Some(hit.ok_or_else(|| {
|
||||
mlua::Error::runtime(format!(
|
||||
"{who}:SetParent(): Couldn't find region named '{n}'"
|
||||
"{who}:SetParent(): Couldn't find region named '{}'",
|
||||
nt.name
|
||||
))
|
||||
})?)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,35 @@ use crate::layout::{Anchor, Point};
|
||||
use crate::script::{Model, SCREEN};
|
||||
use crate::widget::FrameHandle;
|
||||
|
||||
use super::{as_f32, decode_id, frame_handle_of, frame_wrapper, point_from_str, point_name};
|
||||
use super::{
|
||||
as_f32, decode_id, frame_handle_of, frame_parent_token_base, frame_wrapper, point_from_str,
|
||||
point_name, prefetch_named_target, resolve_named_target, NamedTarget,
|
||||
};
|
||||
|
||||
/// Phase 1 of a `relativeTo`/`SetAllPoints` target: when the argument is a **name string**, expand
|
||||
/// a leading `$parent` against `h`'s first named ancestor and read `_G` — all of it with **no**
|
||||
/// `Model` guard alive, because the reference's read is a gettable and an `__index` on `_G` can run
|
||||
/// Lua that calls straight back into a binding ([`NamedTarget`]).
|
||||
///
|
||||
/// Anything else needs no prefetch and answers `None`.
|
||||
fn prefetch_relative_to(
|
||||
lua: &Lua,
|
||||
arg: Option<&Value>,
|
||||
h: FrameHandle,
|
||||
) -> mlua::Result<Option<NamedTarget>> {
|
||||
let Some(Value::String(s)) = arg else {
|
||||
return Ok(None);
|
||||
};
|
||||
// A name we cannot even read names nothing; the caller's miss path reports it as before.
|
||||
let Ok(raw) = s.to_str() else {
|
||||
return Ok(Some(NamedTarget::unreadable()));
|
||||
};
|
||||
let base = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
frame_parent_token_base(&model, h)
|
||||
};
|
||||
Ok(Some(prefetch_named_target(lua, raw.as_ref(), Some(&base))))
|
||||
}
|
||||
|
||||
/// Populate `m`'s layout (anchor/size) methods (see the module doc).
|
||||
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
@@ -100,6 +128,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
"SetAllPoints",
|
||||
lua.create_function(|lua, (this, target): (Table, Value)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
// The `_G` read runs before the guard — see `set_point` and `object::NamedTarget`.
|
||||
let named = prefetch_relative_to(lua, Some(&target), h)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let rel_id: u32 = match &target {
|
||||
// Same id_to_region fallback as `set_point` above (and `region.rs`'s own
|
||||
@@ -110,18 +140,13 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
model.id_to_frame.contains_key(id) || model.id_to_region.contains_key(id)
|
||||
})
|
||||
.unwrap_or_else(|| default_parent_id(&mut model, h)),
|
||||
Value::String(s) => {
|
||||
// Frames first, then the region-name registry: the real XML anchors frames to
|
||||
// REGIONS too (gossip option rows → the greeting FontString) — resolve() binds
|
||||
// region targets in its second round.
|
||||
let named = s.to_str().ok().and_then(|n| {
|
||||
model
|
||||
.arena
|
||||
.lookup(n.as_ref())
|
||||
.map(|hh| model.frame_id(hh))
|
||||
.or_else(|| model.region_names.get(n.as_ref()).copied())
|
||||
});
|
||||
named.unwrap_or_else(|| default_parent_id(&mut model, h))
|
||||
Value::String(_) => {
|
||||
// `_G[name]` — the client's one widget namespace, frames and regions alike
|
||||
// (`object::NamedTarget`). The real XML anchors frames to REGIONS too (gossip
|
||||
// option rows → the greeting FontString), and both publish there.
|
||||
let nt = named.as_ref().expect("a String argument is prefetched");
|
||||
let hit = resolve_named_target(&model, nt);
|
||||
hit.unwrap_or_else(|| default_parent_id(&mut model, h))
|
||||
}
|
||||
_ => default_parent_id(&mut model, h),
|
||||
};
|
||||
@@ -358,6 +383,10 @@ fn set_point(lua: &Lua, this: &Table, point: &str, rest: [Value; 4]) -> mlua::Re
|
||||
.ok_or_else(|| mlua::Error::runtime(format!("SetPoint: unknown point '{point}'")))?;
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
|
||||
// A name target is read out of `_G` FIRST, with no model guard alive — the reference's read is
|
||||
// a gettable, so an `__index` on `_G` can run Lua that calls back in (`object::NamedTarget`).
|
||||
let named = prefetch_relative_to(lua, rest.first(), h)?;
|
||||
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
|
||||
// relativeTo: a table (wrapper) or a string (name), else default to the parent/screen. A table
|
||||
@@ -379,23 +408,23 @@ fn set_point(lua: &Lua, this: &Table, point: &str, rest: [Value; 4]) -> mlua::Re
|
||||
})
|
||||
.unwrap_or_else(|| default_parent_id(&mut model, h))
|
||||
}
|
||||
Some(Value::String(s)) => {
|
||||
Some(Value::String(_)) => {
|
||||
cursor = 1;
|
||||
// Frames first, then regions by name (see SetAllPoints above — same rationale).
|
||||
let named = s.to_str().ok().and_then(|n| {
|
||||
model
|
||||
.arena
|
||||
.lookup(n.as_ref())
|
||||
.map(|hh| model.frame_id(hh))
|
||||
.or_else(|| model.region_names.get(n.as_ref()).copied())
|
||||
});
|
||||
named.unwrap_or_else(|| {
|
||||
// The parent fallback is the client's behavior, but a *named* target that
|
||||
// doesn't resolve is almost always a bug (a typo, or an XML forward reference —
|
||||
// anchors resolve at SetPoint time, so a target must be declared before its
|
||||
// dependents). Say so instead of silently misdirecting the anchor: this exact
|
||||
// silence cost a hunt twice (QuestLogFrame's reward rows, ItemTextFrame's
|
||||
// scrollbar track).
|
||||
// `_G[name]`, frames and regions alike (see SetAllPoints above — same rationale), with
|
||||
// a leading `$parent` already expanded against this frame's first named ancestor.
|
||||
let nt = named.as_ref().expect("a String argument is prefetched");
|
||||
let hit = resolve_named_target(&model, nt);
|
||||
hit.unwrap_or_else(|| {
|
||||
// **The reference RAISES here** — `luaL_error(0x87ccd4, "%s:SetPoint(): Couldn't
|
||||
// find region named '%s'")`, no fallback and no no-op (`luaG_errormsg`/`luaD_throw`
|
||||
// contain no `ret`). Ours still falls back to the parent and warns, because we
|
||||
// cannot yet see how many of our own transcriptions and of the addon corpus that
|
||||
// raise would abort — the harness reports no engine warnings, which is the
|
||||
// instrument that has to come first (decision 2105, "Named, not done").
|
||||
//
|
||||
// The warning is worth keeping regardless: a *named* target that doesn't resolve is
|
||||
// almost always a bug, and this exact silence cost a hunt twice (QuestLogFrame's
|
||||
// reward rows, ItemTextFrame's scrollbar track).
|
||||
let who = model
|
||||
.arena
|
||||
.frame(h)
|
||||
@@ -403,7 +432,7 @@ fn set_point(lua: &Lua, this: &Table, point: &str, rest: [Value; 4]) -> mlua::Re
|
||||
.unwrap_or_else(|| "<anonymous>".into());
|
||||
model.warnings.push(format!(
|
||||
"SetPoint({who}): relativeTo '{}' does not resolve — anchored to the parent",
|
||||
s.to_str().ok().as_deref().unwrap_or("<non-utf8>")
|
||||
nt.name
|
||||
));
|
||||
default_parent_id(&mut model, h)
|
||||
})
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
//! pending layout, then scan its stratum bucket from **its own level upward** for a frame that is
|
||||
//! not itself, is not one of its descendants (`is_descendant 0x767010`), and whose screen rect
|
||||
//! intersects non-emptily. The answer is stored as `+0xb4` bit `0x10`.
|
||||
//! 3. **The raise, gated on that bit** (`0x7651ac..0x7651e7`): `level_compact 0x764eb0` renumbers the
|
||||
//! stratum's occupied levels contiguously into `[0, count)`, then
|
||||
//! `set_frame_level(bucket->count, propagate = 1)`.
|
||||
//! 3. **The raise, gated on that bit** (`0x7651ac..0x7651e7`): `level_compact 0x764eb0` squeezes the
|
||||
//! free levels out of the stratum — over **every live frame of it, hidden included**
|
||||
//! (`ui/scratch/level-compact-law.md`) — then `set_frame_level(bucket->count, propagate = 1)`.
|
||||
//!
|
||||
//! Three consequences the tests pin: a raise is **`level := top occupied level + 1`**, not a list
|
||||
//! reshuffle; it is **occlusion-gated** — `Raise()` on a frame that overlaps nothing changes nothing
|
||||
@@ -52,8 +52,15 @@
|
||||
//! regardless of when each was last shown. The arithmetic is pinned in
|
||||
//! `tests/toplevel.rs::a_raise_is_top_occupied_level_plus_one_after_compaction`.
|
||||
//! - **`level_compact`** is [`crate::widget::WidgetArena::compact_levels`] — an order-preserving
|
||||
//! renumber of the visible frames in one stratum. It changes no draw order by itself; it is what
|
||||
//! keeps `level := max + 1` from ratcheting upward for the length of a session.
|
||||
//! renumber of **every frame** in one stratum, hidden ones included: `0x764eb0` walks the client's
|
||||
//! master frame list filtering on strata and level alone, and never reads `+0xd4`
|
||||
//! (`ui/scratch/level-compact-law.md`, decision 2104). It changes no draw order by itself; it is
|
||||
//! what keeps `level := max + 1` from ratcheting upward for the length of a session. Renumbering
|
||||
//! only the *bucket* (the visible half) is the one shape it may not have: the raise's `propagate`
|
||||
//! delta is computed in the new numbering and applied to levels that are still in the old one, so
|
||||
//! a squeeze taken out of a visible child and not out of its hidden sibling inverts a pair that
|
||||
//! `parent.level + 1` made equal — see that method's own doc for the window it broke, and for the
|
||||
//! two places ours is deliberately simpler than the bytes.
|
||||
//! - **The OVERLAPPED bit is computed, never stored** — and that is *exact*, not a shortcut. The bit
|
||||
//! is written only by `0x7650f0` and the per-tick pass `0x7657d0`, and read only by `0x7650f0`,
|
||||
//! which with `force = 1` recomputes it immediately before reading it. The stored value is
|
||||
|
||||
Reference in New Issue
Block a user