frames: EnumerateFrames, the walk over every frame the client has
A real 1.12 global we did not have (`reference/1.12-globals.tsv`), and the natural way for an addon to ask the live UI which frames carry some script or property — the player's backlog item, raised because nothing answered. Read off `0x705f60`. The frame manager owns a Storm list (`[0xcf0bd8]+0xcc0`, link offset `0x304` written by its ctor at `0x7641b8`) and the `CSimpleFrame` ctor links every new frame through `0x7646a0`, which puts the newcomer in the sentinel's own slot `[0xcc4]` and the frame it displaced in the newcomer's `+0x308`. So the list is walked **newest to oldest**: with no frame the binding answers `[0xcc4]`, with one `[frame+0x308]`, and the first frame ever created holds the tagged sentinel `&head|1` there, which is the `test $0x1,%al` that ends the walk with nil. Three details that are not guesses: - The only type test is `lua_type(L, 1) == LUA_TTABLE` (`cmp $0x5`), so a number, a string or a boolean is not an error — it falls through to the head and starts the walk over. - A table is resolved through `T[0]` exactly as our `decode_id` does, and both rejections raise through `luaL_error` (`0x6f4940`, which never returns) with the client's own wording: `0x872f08` for a table with no object behind it, `0x872ecc` for one that is some other kind of object — a region, which is not in this list at all. - Regions and the walk's order aside, hidden and anonymous frames are in it: the link is the ctor's, not the draw list's. The arena grows the list it already implied. `Frame::insertion_seq` could not serve: that is the draw-order stamp, and a `Show` or a strata change restamps it (`resequence_to_tail`), where creation order never moves. So `created` is the handle list in creation order with `created_seq` the frame's index into it, and a step is one lookup. A destroyed frame's slot is kept, not compacted — its handle stops resolving and the walk skips it — because an index shift would break every other frame's `created_seq`, and nothing in play destroys a frame anyway, as nothing in 1.12 can. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -331,6 +331,48 @@ pub(super) fn as_f64(lua: &Lua, v: &Value) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
/// `EnumerateFrames([frame])` (`0x705f60`): one step along the global frame list, newest first.
|
||||
///
|
||||
/// With a frame, the frame created before it; with nothing — or with any argument that is not a
|
||||
/// table, since the only type test is `lua_type(L, 1) == LUA_TTABLE` (`0x6f3400`, `cmp $0x5`) and
|
||||
/// anything else falls through to the head — the newest frame; nil past the oldest. Regions are
|
||||
/// not in the list: the walk is `CSimpleFrame`'s own, and the reference confirms the argument's
|
||||
/// type through the virtual `IsObjectType` before stepping. See
|
||||
/// [`crate::widget::WidgetArena::newest_frame`] for the list itself.
|
||||
///
|
||||
/// Both rejections raise, as `luaL_error` (`0x6f4940`) does, and with the reference's own wording:
|
||||
/// a table with no object behind it is "Couldn't find 'this' in current object" (`0x872f08`), a
|
||||
/// table that is some other kind of object "Wrong current object type, expected frame"
|
||||
/// (`0x872ecc`).
|
||||
fn enumerate_frames(lua: &Lua, arg: Value) -> mlua::Result<Value> {
|
||||
let from = match arg {
|
||||
// A table is resolved; every other type, nil included, starts the walk over.
|
||||
Value::Table(t) => {
|
||||
let id = decode_id(&t).map_err(|_| {
|
||||
mlua::Error::runtime("EnumerateFrames: Couldn't find 'this' in current object")
|
||||
})?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let h = model.id_to_frame.get(&id).copied().ok_or_else(|| {
|
||||
mlua::Error::runtime("EnumerateFrames: Wrong current object type, expected frame")
|
||||
})?;
|
||||
Some(h)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let next = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let handle = match from {
|
||||
Some(h) => model.arena.frame_created_before(h),
|
||||
None => model.arena.newest_frame(),
|
||||
};
|
||||
handle.map(|h| model.frame_id(h))
|
||||
};
|
||||
match next {
|
||||
Some(id) => Ok(Value::Table(frame_wrapper(lua, id)?)),
|
||||
None => Ok(Value::Nil),
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// install
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
@@ -362,6 +404,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let create_frame = lua.create_function(create_frame)?;
|
||||
lua.globals().set("CreateFrame", create_frame)?;
|
||||
|
||||
let enumerate_frames = lua.create_function(enumerate_frames)?;
|
||||
lua.globals().set("EnumerateFrames", enumerate_frames)?;
|
||||
|
||||
// `GetScreenWidth`/`GetScreenHeight`: the screen root's size in UI units.
|
||||
lua.globals().set(
|
||||
"GetScreenWidth",
|
||||
|
||||
@@ -49,3 +49,78 @@ fn non_overwriting_named_publish() {
|
||||
.unwrap();
|
||||
assert!(ok);
|
||||
}
|
||||
|
||||
/// `EnumerateFrames` walks the global frame list newest to oldest, and nothing else: the
|
||||
/// reference's `[0xcf0bd8]+0xcc4` holds the frame linked last and each frame's `+0x308` the one it
|
||||
/// displaced (`0x7646a0`), so the walk starts at the newest and ends on the sentinel.
|
||||
#[test]
|
||||
fn enumerate_frames_walks_every_frame_newest_first() {
|
||||
let s = script();
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Frame", "EnumA")
|
||||
local b = CreateFrame("Frame", "EnumB", a)
|
||||
local c = CreateFrame("Frame") -- anonymous, and hidden
|
||||
c:Hide()
|
||||
-- The three newest, in reverse creation order.
|
||||
local first = EnumerateFrames()
|
||||
assert(first == c, "the newest frame heads the list, named or not, shown or not")
|
||||
assert(EnumerateFrames(first) == b, "then the one created before it")
|
||||
assert(EnumerateFrames(b) == a, "and the one before that")
|
||||
-- The whole walk terminates, visits each frame once, and reaches the three above.
|
||||
local seen, n, f = {}, 0, EnumerateFrames()
|
||||
while f do
|
||||
assert(not seen[f], "a frame is visited once")
|
||||
seen[f] = 1
|
||||
n = n + 1
|
||||
assert(n < 10000, "the walk terminates")
|
||||
f = EnumerateFrames(f)
|
||||
end
|
||||
assert(seen[a] and seen[b] and seen[c], "every frame is in the list")
|
||||
assert(n >= 3, "and the count is the whole arena, not just these")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// A region is not a frame, and the two rejections carry the reference's own wording
|
||||
/// (`0x872f08`/`0x872ecc`, raised through `luaL_error 0x6f4940`); any non-table argument is not a
|
||||
/// rejection at all — the only type test is `lua_type(L, 1) == LUA_TTABLE`, so it starts over.
|
||||
#[test]
|
||||
fn enumerate_frames_rejects_a_non_frame_and_restarts_on_a_non_table() {
|
||||
let s = script();
|
||||
let restart: bool = s
|
||||
.eval(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "EnumRestart")
|
||||
-- A number, a string and a boolean all fall through to the head of the list.
|
||||
return EnumerateFrames(7) == f and EnumerateFrames("f") == f
|
||||
and EnumerateFrames(true) == f
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(restart, "a non-table argument restarts the walk");
|
||||
|
||||
let plain = s
|
||||
.eval::<bool>(r#"return EnumerateFrames({}) ~= nil"#)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(
|
||||
plain.contains("Couldn't find 'this' in current object"),
|
||||
"a table with no object behind it raises the reference's first message: {plain}"
|
||||
);
|
||||
let region = s
|
||||
.eval::<bool>(
|
||||
r#"
|
||||
local f = CreateFrame("Frame", "EnumRegionOwner")
|
||||
local t = f:CreateTexture()
|
||||
return EnumerateFrames(t) ~= nil
|
||||
"#,
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(
|
||||
region.contains("Wrong current object type, expected frame"),
|
||||
"a region raises the reference's second message: {region}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -214,6 +214,10 @@ pub struct Frame {
|
||||
/// The link position in the `(strata, level)` bucket, re-stamped at the tail on each relink.
|
||||
/// Drawing puts the later-linked frame on top; the hit test probes the earlier-linked first.
|
||||
pub insertion_seq: u32,
|
||||
/// This frame's index in the arena's creation list, which is **not** [`Self::insertion_seq`]:
|
||||
/// that one is the draw-order stamp and a `Show` or a strata change restamps it
|
||||
/// ([`WidgetArena::resequence_to_tail`]). Creation order never moves.
|
||||
pub(crate) created_seq: u32,
|
||||
pub kind_state: KindState,
|
||||
next_decl: u32,
|
||||
}
|
||||
@@ -243,6 +247,11 @@ pub struct WidgetArena {
|
||||
regions: Arena<Region>,
|
||||
names: HashMap<String, FrameHandle>,
|
||||
next_insertion: u32,
|
||||
/// Every frame ever created, in creation order: the reference's own global frame list, which
|
||||
/// `EnumerateFrames` walks ([`Self::newest_frame`]). A destroyed frame's handle stops
|
||||
/// resolving and the walk skips it, so the slot is kept rather than compacted — the client
|
||||
/// never destroys a frame either, and a shifted index would break every `created_seq`.
|
||||
created: Vec<FrameHandle>,
|
||||
/// The draw list's fingerprint cache ([`crate::order::traversal`]).
|
||||
pub(crate) order_cache: crate::order::OrderCache,
|
||||
/// Minimap frames ever created, so the state feed notices a new one without a walk.
|
||||
@@ -294,6 +303,7 @@ impl WidgetArena {
|
||||
regions: Arena::new(),
|
||||
names: HashMap::new(),
|
||||
next_insertion: 0,
|
||||
created: Vec::new(),
|
||||
minimap_created: 0,
|
||||
order_cache: Default::default(),
|
||||
ticked_kinds: Vec::new(),
|
||||
@@ -389,6 +399,36 @@ impl WidgetArena {
|
||||
}
|
||||
}
|
||||
|
||||
/// The newest live frame: where `EnumerateFrames()` with no frame starts.
|
||||
///
|
||||
/// The reference's global frame list is a Storm `TSList` on the frame manager
|
||||
/// (`[0xcf0bd8]+0xcc0`, link offset `0x304` written by its ctor at `0x7641b8`), and the
|
||||
/// `CSimpleFrame` ctor links every new frame through `0x7646a0`, which writes the new frame
|
||||
/// into the sentinel's own slot `[0xcc4]` and the frame it displaced into the newcomer's
|
||||
/// `+0x308`. So `[0xcc4]` is the **newest** frame and `+0x308` is the one created before it:
|
||||
/// `EnumerateFrames` (`0x705f60`) walks newest to oldest and stops on the tagged sentinel
|
||||
/// (`&head|1`, the `test $0x1,%al` at `0x705ffb`), which is what the first frame's `+0x308`
|
||||
/// holds.
|
||||
pub fn newest_frame(&self) -> Option<FrameHandle> {
|
||||
self.created
|
||||
.iter()
|
||||
.rev()
|
||||
.copied()
|
||||
.find(|&h| self.frame(h).is_some())
|
||||
}
|
||||
|
||||
/// The live frame created just before `h`, or `None` at the oldest: one step of
|
||||
/// `EnumerateFrames` ([`Self::newest_frame`] has the reference's list).
|
||||
pub fn frame_created_before(&self, h: FrameHandle) -> Option<FrameHandle> {
|
||||
let seq = self.frame(h)?.created_seq as usize;
|
||||
self.created
|
||||
.get(..seq)?
|
||||
.iter()
|
||||
.rev()
|
||||
.copied()
|
||||
.find(|&h| self.frame(h).is_some())
|
||||
}
|
||||
|
||||
/// The first frame created with `name`; a later duplicate never takes it over (`0x701bd0`).
|
||||
pub fn lookup(&self, name: &str) -> Option<FrameHandle> {
|
||||
self.names.get(name).copied()
|
||||
@@ -459,6 +499,7 @@ impl WidgetArena {
|
||||
ignore_parent_scale: false,
|
||||
wow_id: 0,
|
||||
insertion_seq,
|
||||
created_seq: self.created.len() as u32,
|
||||
kind_state: match kind {
|
||||
FrameKind::StatusBar => KindState::StatusBar(StatusBarState::default()),
|
||||
// `CLootButton` and `CSimpleCheckbox` extend `CSimpleButton` by one field each,
|
||||
@@ -503,6 +544,7 @@ impl WidgetArena {
|
||||
);
|
||||
let (index, generation) = self.frames.insert(frame);
|
||||
let handle = FrameHandle { index, generation };
|
||||
self.created.push(handle);
|
||||
if ticked {
|
||||
self.ticked_kinds.push(handle);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user