Files
benilla-octo/crates/benilla-ui/src/script/frame_enum.rs
T
Sam SungandClaude Opus 5.5 a21fb2750a The stock video options window opens, saves, restores defaults and cancels clean: its CVars are registered, spellEffectLevel drives the particle and shard rates, and the video pairs, frame enumeration and client queries are defined (#565)
The stock video options window (`OptionsFrame`, off the player's chain) now opens, saves, restores defaults and cancels with no Lua error, as on 1.12.1.

- The CVars it reads are registered at the reference's defaults: `SmallCull` "0.04" (`0x68854a`), `shadowLevel` "1" (`0x6885bc`), `baseMip` "0" (`0x6887aa`), `DesktopGamma` "0" (`0x402d4d`), `spellEffectLevel` "2" (`0x688900`), plus `doodadAnim` and `texLodBias`, which the new getters read. `SetWorldDetail` keeps `SmallCull` in step (`0x488e56`), and the wrong comment that called the getter its only reader is gone.
- `spellEffectLevel`'s handler (`0x689510`): the value is clamped to 0-2, "Spell effect level set to %d." is printed, the particle emission scalar is set to 0.33/0.66/1.0, and the shard emitter's rate follows the level (`0x6eb95e`).
- The 21 functions #337 lists: the six video getter/setter pairs (`0x488ec0`-`0x489300`), `GetNumFrames`/`EnumerateFrames` in creation order (`0x705f00`, `0x705f60`), and the client queries, including `GetDebugStats` and `OpeningCinematic`'s empty `CMSG_OPENING_CINEMATIC` (0xf9, `0x48c8c9`).

Verified: a headless test walks the stock window's 18 boxes and 9 sliders, then Okay, Defaults and Cancel, with no error (it fails on main at `OptionsFrame.lua:146`), plus unit tests for each binding and handler; 12 mutations each fail a test. A live run opened the window, saved, restored defaults and cancelled with no Lua error. `scripts/gates.sh` green. Built on macOS; no platform seam touched.

Fixes #548
Fixes #337
Refs #329 (benilla's own options window has no Spell Detail row yet)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:12:45 +07:00

131 lines
5.1 KiB
Rust

//! `GetNumFrames` (`0x705f00`) and `EnumerateFrames` (`0x705f60`), registered beside `CreateFrame`
//! from the frame-script table `0x872e74`: both walk the UI root's list of every live frame
//! (`root+0xcc4`), which a frame joins at construction and leaves at destruction, hidden or not.
//! Regions are not frames and are not on it. Here the list is the frame ids in ascending order,
//! since every creation path mints its frame's id at once ([`Model::frame_id`]).
use mlua::{Lua, MultiValue, Value};
use super::object::{decode_id, frame_wrapper};
use super::Model;
/// `0x872f08`, verbatim.
const NO_THIS: &str = "EnumerateFrames: Couldn't find 'this' in current object";
/// `0x872ecc`, verbatim.
const NOT_A_FRAME: &str = "EnumerateFrames: Wrong current object type, expected frame";
/// The first live frame with an id above `after`, in creation order.
fn next_frame(model: &Model, after: u32) -> Option<u32> {
(after.saturating_add(1)..model.next_id).find(|id| {
model
.id_to_frame
.get(id)
.is_some_and(|h| model.arena.frame(*h).is_some())
})
}
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let g = lua.globals();
// `GetNumFrames()`: the list's length as one number; any arguments are ignored.
g.set(
"GetNumFrames",
lua.create_function(|lua, _: MultiValue| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(model.arena.iter_frames().count() as f64)
})?,
)?;
// `EnumerateFrames([frame])`: the frame after `frame` on the list, or the first when the
// argument is not a table (`lua_type(L, 1) == 5`, `0x705f6e`); nil past the last. A table is
// read as an object through its `[0]` (`lua_rawgeti`, `0x705f7c`): none raises `0x872f08`,
// and an object that is not a frame, a Texture or FontString, raises `0x872ecc`.
g.set(
"EnumerateFrames",
lua.create_function(|lua, args: MultiValue| {
let after = match args.front() {
Some(Value::Table(t)) => {
let id = match t.raw_get::<Value>(0)? {
Value::LightUserData(l) if !l.0.is_null() => decode_id(t)?,
_ => return Err(mlua::Error::runtime(NO_THIS)),
};
let model = lua.app_data_ref::<Model>().expect("model app_data");
if !model.id_to_frame.contains_key(&id) {
return Err(mlua::Error::runtime(NOT_A_FRAME));
}
id
}
_ => 0,
};
let next = {
let model = lua.app_data_ref::<Model>().expect("model app_data");
next_frame(&model, after)
};
match next {
Some(id) => Ok(Value::Table(frame_wrapper(lua, id)?)),
None => Ok(Value::Nil),
}
})?,
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use crate::script::UiScript;
#[test]
fn enumerate_frames_walks_every_live_frame_in_creation_order() {
let s = UiScript::new().unwrap();
s.run(
"A = CreateFrame('Frame', 'EnumA') \
A:CreateTexture('EnumATex') \
B = CreateFrame('Button', nil, A) B:Hide() \
C = CreateFrame('Frame', 'EnumC')",
)
.unwrap();
let (count, walked, tail): (i64, i64, Vec<bool>) = s
.eval(
"local n, f, seen = 0, EnumerateFrames(), {} \
while f do n = n + 1 table.insert(seen, f) f = EnumerateFrames(f) end \
local k = table.getn(seen) \
return GetNumFrames(), n, { seen[k - 2] == A, seen[k - 1] == B, seen[k] == C }",
)
.unwrap();
assert_eq!(
walked, count,
"the walk visits exactly GetNumFrames() frames"
);
assert_eq!(
tail,
[true, true, true],
"the three newest frames close the walk in creation order, the hidden and nameless \
one included, and the texture is not a frame"
);
assert_eq!(s.arity("GetNumFrames()").unwrap(), 1);
assert_eq!(s.arity("EnumerateFrames(C)").unwrap(), 1);
assert!(s.eval::<bool>("return EnumerateFrames(C) == nil").unwrap());
// A non-table argument starts over.
assert!(s
.eval::<bool>("return EnumerateFrames(5) == EnumerateFrames() and EnumerateFrames('x') == EnumerateFrames()")
.unwrap());
}
#[test]
fn enumerate_frames_refuses_what_is_not_a_frame() {
let s = UiScript::new().unwrap();
s.run("F = CreateFrame('Frame', 'EnumF') T = F:CreateTexture('EnumT')")
.unwrap();
let e = s.run("EnumerateFrames({})").unwrap_err().to_string();
assert!(
e.contains("EnumerateFrames: Couldn't find 'this' in current object"),
"{e}"
);
let e = s.run("EnumerateFrames(T)").unwrap_err().to_string();
assert!(
e.contains("EnumerateFrames: Wrong current object type, expected frame"),
"{e}"
);
}
}