Files
benilla-octo/crates/benilla-app/src/ui_script/surface_gate.rs
T
Sam SungandClaude Opus 5.5 78a983b00e The VM speaks 1.12 alone: the Era globals and the uncalled hooks are gone, the engine's tooltip seams are Rust calls, strata/draw-layer/state-texture/sound/OnClick/quest-item/pickup take their 1.12 shapes, and the surface gate covers the production load (#573)
The Lua VM keeps no surface the 1.12.1 client lacks.

- **Removed:** the nine Era-only globals: `UnitAura`, `CancelUnitBuff`, `GetCursorInfo`, `GetInventoryItemID`, `GetPlayerFacing`, `GetTradePartnerName`, `IsGossipOptionCoded`, `UnitIsAFK`, `UnitIsDND`.
- **Hooks off Lua:** `GameTooltip:BenillaSetItemById` and `:BenillaGetTooltipOwner` are replaced by Rust-side calls. The uncalled `Benilla*` hooks are deleted.
- **Frame strata:** `SetFrameStrata` knows the reference's eight strata and raises its message for any other (`0x774450`).
- **`SetDrawLayer`** reads the layer name alone (`0x79a780`).
- **`Set<State>Texture`** reads a number as a path (`0x781b23`); there is no colour form.
- **`PlaySound`** names a kit and returns nothing (`0x4586d0`). `PlaySoundFile` returns one value (`0x458780`); benilla answers 1, a recorded deviation, since it plays after the call.
- **`OnClick`** sets only `arg1` (`0x779540`).
- **Quest getters:** `GetQuestItemInfo` and the quest-log reward and choice getters answer the reference's five values and its miss behaviour (`0x501f80`, `0x4e0b00`, `0x4e0cf0`).
- **`PickupContainerItem`** returns nothing (`0x4f9b30`).
- **Surface gates:** the bare VM's globals and every widget class's methods are held to `reference/1.12-globals.tsv` with one commented allowlist. A new gate on the full production load (core plus layer) allows beyond that only the layer's `Benilla`-prefixed globals.

Verified:
- A test per removal and per corrected form; a scripted undo of each fails its test.
- Each gate fails on a re-added name.
- `scripts/gates.sh` and `scripts/smoke.sh` are green.
- A live run showed the same warnings and 0 script errors before and after.
- In the 219-addon corpus, AckisRecipeList's bundled AceGUI-3.0 and Roid-Macros' Grid extension now behave as they do on 1.12.1.

Built on macOS; no platform seam touched.

Fixes #171

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 01:44:52 +07:00

144 lines
5.4 KiB
Rust

//! The 1.12 surface of the production load as a gate: every global and widget method an addon
//! reaches once the core and benilla's layer have loaded is one the 1.12.1 client has
//! (`reference/1.12-globals.tsv`), one the engine exposes, or a `Benilla`-prefixed name the layer
//! defines. The engine's own surface is gated on a bare VM in `benilla-ui`, name by name against
//! the same table (`script::tests::reference_surface`, whose commented `BEYOND_1_12` is the one
//! allowlist) and method by method against each class's registrar tables
//! (`script::tests::widget_surface`); so here the load itself may add nothing else.
use std::collections::{BTreeSet, HashSet};
use benilla_ui::script::{widget_method_census, UiScript};
use crate::local_state::test_env::ENV_LOCK;
/// `reference/1.12-globals.tsv`'s names.
fn reference_globals() -> HashSet<&'static str> {
let tsv = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../reference/1.12-globals.tsv"
));
let names: HashSet<&str> = tsv
.lines()
.filter(|l| !l.starts_with('#'))
.filter_map(|l| l.split('\t').next())
.collect();
assert!(
names.len() > 19_000,
"the reference table looks truncated ({} names)",
names.len()
);
names
}
/// The string keys of `_G`.
fn globals_of(s: &UiScript) -> BTreeSet<String> {
s.eval::<Vec<String>>(
"local out = {} for k in pairs(getfenv(0)) do \
if type(k) == 'string' then table.insert(out, k) end end return out",
)
.expect("dump the globals")
.into_iter()
.collect()
}
/// Every `(class, method)` a VM answers, as `Class:Method`. The WorldFrame type is one-shot
/// (`0x6ee439`), spent once the stock `WorldFrame` exists, so a loaded VM cannot make a second;
/// its methods are a Frame's, which the census asks of a Frame.
fn methods_of(s: &UiScript) -> BTreeSet<String> {
widget_method_census(s)
.expect("the widget census")
.into_iter()
.filter_map(|(class, method)| {
if method == "!NOT-INSTANTIABLE" {
assert_eq!(class, "WorldFrame", "{class} could not be made");
return None;
}
Some(format!("{class}:{method}"))
})
.collect()
}
/// A name the layer may define: its prefix, or a slash alias's `SLASH_BENILLA_` form.
fn layer_prefixed(n: &str) -> bool {
n.starts_with("Benilla") || n.starts_with("BENILLA_") || n.starts_with("SLASH_BENILLA_")
}
/// What the production load adds past 1.12 and past the engine, and the layer's own names: the
/// globals (`Benilla`-prefixed ones the engine does not also define passing) and the methods.
fn production_beyond(s: &UiScript, engine: &UiScript) -> (Vec<String>, Vec<String>) {
let reference = reference_globals();
let engine_globals = globals_of(engine);
let globals = globals_of(s)
.into_iter()
.filter(|n| !reference.contains(n.as_str()) && !engine_globals.contains(n))
.filter(|n| !layer_prefixed(n))
.collect();
let engine_methods = methods_of(engine);
let methods = methods_of(s)
.into_iter()
.filter(|m| !engine_methods.contains(m))
.collect();
(globals, methods)
}
/// The production load, core and layer, adds no global past 1.12 but the layer's own
/// `Benilla`-prefixed names, and no widget method at all.
#[test]
fn the_production_load_stays_inside_the_1_12_surface() {
benilla_formats::wow_data_or_skip!();
let _l = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (s, failures) = super::layer_tests::production_load_with("surface", false, "", |_| {});
assert!(failures.is_empty(), "load failures: {failures:#?}");
let engine = UiScript::new().unwrap();
// The layer's globals are there to be checked: a load that defined none proves nothing.
assert!(
globals_of(&s)
.iter()
.any(|n| layer_prefixed(n) && !globals_of(&engine).contains(n)),
"the layer defined no global: it did not load"
);
let (globals, methods) = production_beyond(&s, &engine);
assert!(
globals.is_empty(),
"the production load exposes {} global(s) the 1.12.1 client does not:\n {}\n\n\
A layer global takes the `Benilla` prefix; anything else gets its 1.12 spelling or goes.",
globals.len(),
globals.join(" ")
);
assert!(
methods.is_empty(),
"the production load adds {} widget method(s) the engine does not register:\n {}",
methods.len(),
methods.join(" ")
);
}
/// The gate bites: a global past 1.12 put back into the loaded VM, an unprefixed layer-style one,
/// an engine-only prefixed one and a method on a class's table are each caught.
#[test]
fn the_production_gate_catches_a_name_put_back() {
let s = UiScript::new().unwrap();
let engine = UiScript::new().unwrap();
assert_eq!(
production_beyond(&s, &engine),
(Vec::new(), Vec::new()),
"two bare VMs differ"
);
s.run(
"UnitAura = function() end \
BenillaLayerThing = 1 \
getmetatable(CreateFrame('GameTooltip')).__index.BenillaSetItemById = function() end",
)
.unwrap();
let (globals, methods) = production_beyond(&s, &engine);
assert_eq!(globals, vec!["UnitAura".to_string()]);
assert!(
methods.contains(&"GameTooltip:BenillaSetItemById".to_string()),
"{methods:?}"
);
}