A glue Lua state with the reference glue surface, events and ModelFFX (#963)

The first piece of running the stock GlueXML: benilla had no glue Lua state, and no reference data for its surface.

In 1.12.1 the glue state holds the base set (`0x7039e0`) and seven registrars (`0x46abb0`): 119 functions, 105 of them glue-only, with 26 glue events (`0xb41e70`, `0x46a88f`). `RegisterEvent` matches names case-insensitively and drops an unknown one (`0x702140`). The glue builds the 13 base frame types plus `ModelFFX` (`0x46a4fe`), a Model with its own show and paint (`0x46fa00`); the in-game types come only at world entry (`0x495940`).

`reference/` gains the glue event and surface tables, generated and checked against the shapes table. `UiScript::new_glue()` builds that state; a gate holds its globals to the table, with the 108 unbuilt names listed. Nothing in the app changes yet.

Part of #856

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-12 07:38:02 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 985dabff44
commit a9dd325bb7
20 changed files with 1449 additions and 214 deletions
+3 -4
View File
@@ -235,11 +235,10 @@ pub fn join_ref(base: &str, path: &str) -> String {
out.join("/")
}
/// A `CSimpleModel` kind, whose `LoadXML` (`0x76cac0`) reads `scale=` as the model's scale.
/// A `CSimpleModel` kind, whose `LoadXML` (`0x76cac0`) reads `scale=` as the model's scale;
/// `ModelFFX` shares it (its second vtable's slot 2).
pub(super) fn model_kind_tag(tag: &str) -> bool {
["Model", "PlayerModel", "DressUpModel", "TabardModel"]
.iter()
.any(|k| k.eq_ignore_ascii_case(tag))
crate::script::frame_kind_from_tag(tag).is_some_and(crate::widget::FrameKind::is_model)
}
impl Loader<'_> {
+29 -23
View File
@@ -75,6 +75,35 @@ impl super::UiScript {
}
}
/// `ConsoleExec` (`0x515f10`) is in the registrar both states call (`0x51bb20`, from the glue
/// registrar `0x46abb0` and the in-game `0x51bb60`), so it installs apart from this file's in-game
/// verbs.
pub(super) fn install_console_exec(lua: &Lua) -> mlua::Result<()> {
// `ConsoleExec("name value")`: a registered CVar with a value is written at once, as `SetCVar`
// writes it; anything else, a bare CVar name included (the reference answers it with
// `CVar "%s" is "%s"`, `0x63dde0`), goes to the app's command registry.
lua.globals().set(
"ConsoleExec",
lua.create_function(|lua, line: Option<String>| {
let Some(line) = line.map(|l| l.trim().to_string()).filter(|l| !l.is_empty()) else {
return Ok(());
};
let (name, value) = line
.split_once(char::is_whitespace)
.map_or((line.as_str(), ""), |(n, v)| (n, v.trim()));
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let is_cvar = model.cvars.contains_key(&name.to_ascii_lowercase());
if is_cvar && !value.is_empty() {
super::cvars::write_cvar(&mut model, name, value.to_string());
} else {
model.console_lines.push(line.clone());
}
Ok(())
})?,
)?;
Ok(())
}
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let g = lua.globals();
@@ -135,29 +164,6 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// `ConsoleExec("name value")`: a registered CVar with a value is written at once, as `SetCVar`
// writes it; anything else, a bare CVar name included (the reference answers it with
// `CVar "%s" is "%s"`, `0x63dde0`), goes to the app's command registry.
g.set(
"ConsoleExec",
lua.create_function(|lua, line: Option<String>| {
let Some(line) = line.map(|l| l.trim().to_string()).filter(|l| !l.is_empty()) else {
return Ok(());
};
let (name, value) = line
.split_once(char::is_whitespace)
.map_or((line.as_str(), ""), |(n, v)| (n, v.trim()));
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let is_cvar = model.cvars.contains_key(&name.to_ascii_lowercase());
if is_cvar && !value.is_empty() {
super::cvars::write_cvar(&mut model, name, value.to_string());
} else {
model.console_lines.push(line.clone());
}
Ok(())
})?,
)?;
for (name, chat) in [("LoggingChat", true), ("LoggingCombat", false)] {
g.set(
name,
+82
View File
@@ -0,0 +1,82 @@
//! The glue screens' FrameScript state, which [`UiScript::new_glue`] builds: what the glue builder
//! (`0x46a7b0`) gives Lua where `UI_Init` (`0x48fbf0`) gives it the world's surface.
//!
//! The reference keeps one Lua state (`0xceef74`). The glue builder starts from the bare base set
//! (`0x7039e0`): on a rebuild it resets the state (`0x703b80` at `0x46a87b`, re-running the base
//! init), while the first build (`0x46a505`, passing 0) skips the reset (`je` at `0x46a879`) and
//! takes the state the startup init left (`0x402aee`). Either way it then runs the glue registrar
//! `0x46abb0` and loads the 26-event table below in place of the in-game one
//! (`0x703d90` at `0x46a88f`). The frame types are a process-wide registry instead
//! ([`super::object::registered_frame_kind`]).
//!
//! [`UiScript::new_glue`]: super::UiScript::new_glue
/// Which of the reference's two FrameScript states a VM is.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VmKind {
/// `UI_Init`'s: the in-game surface over FrameXML.
InGame,
/// The glue builder's: the base set, the three shared registrars and the glue verbs, over
/// GlueXML.
Glue,
}
/// The glue event table by id (`0xb41e70`, written by `0x46cc60`), `reference/1.12-glue-events.tsv`.
/// While the glue state runs these are the only events: `RegisterEvent` matches a name against
/// them case-insensitively (`0x702140` with `0x64a4c0`) and drops one they lack without an error.
pub const GLUE_EVENTS: [&str; 26] = [
"SET_GLUE_SCREEN",
"START_GLUE_MUSIC",
"DISCONNECTED_FROM_SERVER",
"OPEN_STATUS_DIALOG",
"UPDATE_STATUS_DIALOG",
"CLOSE_STATUS_DIALOG",
"ADDON_LIST_UPDATE",
"CHARACTER_LIST_UPDATE",
"UPDATE_SELECTED_CHARACTER",
"OPEN_REALM_LIST",
"GET_PREFERRED_REALM_INFO",
"UPDATE_SELECTED_RACE",
"SELECT_LAST_CHARACTER",
"SELECT_FIRST_CHARACTER",
"GLUE_SCREENSHOT_SUCCEEDED",
"GLUE_SCREENSHOT_FAILED",
"PATCH_UPDATE_PROGRESS",
"PATCH_DOWNLOADED",
"SUGGEST_REALM",
"SUGGEST_REALM_WRONG_PVP",
"SUGGEST_REALM_WRONG_CATEGORY",
"SHOW_SERVER_ALERT",
"FRAMES_LOADED",
"FORCE_RENAME_CHARACTER",
"SHOW_SURVEY_NOTIFICATION",
"PLAYER_ENTER_PIN",
];
impl VmKind {
/// The event table's own spelling of `name` for `RegisterEvent`/`UnregisterEvent` (`0x702140`,
/// `0x702280`), or `None` for a name the table lacks, which registers nothing.
///
/// The in-game VM answers every name as given: benilla's in-game events register by name and
/// carry no table, so a name off the reference's table registers and never fires, and only the
/// exact spelling matches.
pub(crate) fn event_key(self, name: &str) -> Option<&str> {
match self {
VmKind::InGame => Some(name),
VmKind::Glue => GLUE_EVENTS
.iter()
.copied()
.find(|e| e.eq_ignore_ascii_case(name)),
}
}
/// Whether the host may fire `event` into this VM. The reference fires by id
/// (`0x703e50`), so the glue state can only ever see one of its 26; a host's in-game event sent
/// to the glue VM reaches no frame, `RegisterAllEvents` ones included.
pub(crate) fn fires(self, event: &str) -> bool {
match self {
VmKind::InGame => true,
VmKind::Glue => GLUE_EVENTS.contains(&event),
}
}
}
+157 -104
View File
@@ -64,6 +64,8 @@ mod follow;
pub(crate) mod font;
mod font_block;
mod frame_enum;
mod glue;
pub use glue::{VmKind, GLUE_EVENTS};
mod gm_ticket;
mod gossip;
mod guild;
@@ -499,117 +501,168 @@ pub fn addon_chunk_name(folder: &str, file: &str) -> String {
format!("@Interface\\AddOns\\{folder}\\{}", file.replace('/', "\\"))
}
/// What both FrameScript states hold: FrameScript's Lua init (`0x7039e0`), the bare state the glue
/// builder starts from (left by startup `0x402aee` on its first build, re-run by its reset
/// `0x703b80` on a later one) and `UI_Init` rebuilds through its own, and the three registrars
/// the glue registrar `0x46abb0` shares with `UI_Init`: `0x51bb20` (the clock, `ConsoleExec` and
/// the file trio), `0x706350` (the frame factory globals and the 17 widget method tables) and
/// `0x4587f0` (sound).
fn install_framescript(vm: VmKind) -> mlua::Result<Lua> {
let lua = Lua::new();
let mut model = Model::new();
model.vm = vm;
lua.set_app_data(model);
// Before anything can fire a handler and while no app-data borrow is held, as the
// profiler's slot requires.
handler_prof::install(&lua);
// `0x7039e0`: the base array `0x811e28`, the four libraries and the compat chunk `0x8722e8`.
stdlib::sandbox(&lua)?;
// Before the stdlib layer, so its aliases bind the 5.0-shaped functions.
lua50::install(&lua)?;
stdlib::install(&lua)?;
// `0x51bb20` (table `0x8503f4`): `GetTime`, `GetGameTime` and `ConsoleExec`. `ReadFile`,
// `DeleteFile` and `AppendToFile` are not built.
clock::install(&lua)?;
chat_misc::install_console_exec(&lua)?;
// `0x706350`: `CreateFrame`, `CreateFont`, `GetNumFrames`, `EnumerateFrames` (table
// `0x872e74`), then the widget method tables `0x7a2e70`..`0x78dc60`.
object::install(&lua)?;
frame_enum::install(&lua)?;
// After `object`, whose `publish_global` it reuses, and before any FrameXML loads:
// `Loader::do_font` publishes into the tables this builds.
font::install(&lua)?;
messageframe::install(&lua)?;
scrollframe::install(&lua)?;
simplehtml::install(&lua)?;
slider::install(&lua)?;
colorselect::install(&lua)?;
modelframe::install(&lua)?;
// `0x4587f0` (table `0x835a50`): `PlaySound`, `PlayMusic`, `PlaySoundFile`, `StopMusic`.
sound::install(&lua)?;
Ok(lua)
}
/// The in-game surface: everything `UI_Init` registers past the shared registrars (`0x490250` and
/// the calls around it), the in-game widget types' method tables among it.
fn install_in_game(lua: &Lua) -> mlua::Result<()> {
addon::install(lua)?;
addon_message::install(lua)?;
chat_send::install(lua)?;
chat_types::install(lua)?;
chat_misc::install(lua)?;
channel::install(lua)?;
chat_window::install(lua)?;
client::install(lua)?;
screenshot::install(lua)?;
zone_text::install(lua)?;
object::install_in_game(lua)?;
region::install_in_game(lua)?;
modelframe::install_player_model(lua)?;
unit::install(lua)?;
party::install(lua)?;
social::install(lua)?;
guild::install(lua)?;
petition::install(lua)?;
binder::install(lua)?;
summon::install(lua)?;
bind_confirm::install(lua)?;
instance::install(lua)?;
gm_ticket::install(lua)?;
duel::install(lua)?;
follow::install(lua)?;
camera_view::install(lua)?;
input_verbs::install(lua)?;
session::install(lua)?;
pvp::install(lua)?;
worn_display::install(lua)?;
death::install(lua)?;
aura::install(lua)?;
cvars::install(lua)?;
nameplate::install_verbs(lua)?;
saved::install(lua)?;
keybind::install(lua)?;
ui_errors::install(lua)?;
pointer::install(lua)?;
action::install(lua)?;
action_bar_toggles::install(lua)?;
container::install(lua)?;
cursor::install(lua)?;
spellbook::install(lua)?;
macros::install(lua)?;
talent::install(lua)?;
dialog_verbs::install(lua)?;
battlefield_score::install(lua)?;
battlefield_queue::install(lua)?;
battlefield_positions::install(lua)?;
tutorial::install(lua)?;
worldmap_arrow::install(lua)?;
shapeshift::install(lua)?;
pet::install(lua)?;
gossip::install(lua)?;
merchant::install(lua)?;
bank::install(lua)?;
stable::install(lua)?;
item_text::install(lua)?;
mail::install(lua)?;
auction::install(lua)?;
trainer::install(lua)?;
taxi::install(lua)?;
trade::install(lua)?;
inspect::install(lua)?;
dressup::install(lua)?;
tabard::install(lua)?;
tradeskill::install(lua)?;
craft::install(lua)?;
reputation::install(lua)?;
skills::install(lua)?;
item_stats::install(lua)?;
char_stats::install(lua)?;
inventory_verbs::install(lua)?;
weapon_enchant::install(lua)?;
loot::install(lua)?;
loot_roll::install(lua)?;
quest::install(lua)?;
quest_log::install(lua)?;
minimap::install(lua)?;
tooltip::install(lua)?;
worldmap::install(lua)?;
worldstate::install(lua)?;
net_stats::install(lua)?;
Ok(())
}
impl UiScript {
/// Build a fully sandboxed, stdlib- and object-model-equipped host.
/// The in-game VM: FrameScript's base set and the registrars both states share
/// ([`install_framescript`]), then the in-game surface `UI_Init` (`0x48fbf0`) registers.
pub fn new() -> mlua::Result<UiScript> {
let lua = Lua::new();
lua.set_app_data(Model::new());
// Before anything can fire a handler and while no app-data borrow is held, as the
// profiler's slot requires.
handler_prof::install(&lua);
let lua = install_framescript(VmKind::InGame)?;
install_in_game(&lua)?;
Ok(Self::host(lua))
}
addon::install(&lua)?;
addon_message::install(&lua)?;
chat_send::install(&lua)?;
chat_types::install(&lua)?;
chat_misc::install(&lua)?;
channel::install(&lua)?;
chat_window::install(&lua)?;
client::install(&lua)?;
screenshot::install(&lua)?;
stdlib::sandbox(&lua)?;
// Before the stdlib layer, so its aliases bind the 5.0-shaped functions.
lua50::install(&lua)?;
stdlib::install(&lua)?;
object::install(&lua)?;
frame_enum::install(&lua)?;
// After `object`, whose `publish_global` it reuses, and before any FrameXML loads:
// `Loader::do_font` publishes into the tables this builds.
font::install(&lua)?;
unit::install(&lua)?;
party::install(&lua)?;
social::install(&lua)?;
guild::install(&lua)?;
petition::install(&lua)?;
binder::install(&lua)?;
summon::install(&lua)?;
bind_confirm::install(&lua)?;
instance::install(&lua)?;
gm_ticket::install(&lua)?;
duel::install(&lua)?;
follow::install(&lua)?;
camera_view::install(&lua)?;
input_verbs::install(&lua)?;
session::install(&lua)?;
pvp::install(&lua)?;
worn_display::install(&lua)?;
death::install(&lua)?;
aura::install(&lua)?;
cvars::install(&lua)?;
nameplate::install_verbs(&lua)?;
saved::install(&lua)?;
keybind::install(&lua)?;
sound::install(&lua)?;
ui_errors::install(&lua)?;
pointer::install(&lua)?;
action::install(&lua)?;
action_bar_toggles::install(&lua)?;
container::install(&lua)?;
cursor::install(&lua)?;
spellbook::install(&lua)?;
macros::install(&lua)?;
talent::install(&lua)?;
dialog_verbs::install(&lua)?;
battlefield_score::install(&lua)?;
battlefield_queue::install(&lua)?;
battlefield_positions::install(&lua)?;
tutorial::install(&lua)?;
worldmap_arrow::install(&lua)?;
shapeshift::install(&lua)?;
pet::install(&lua)?;
gossip::install(&lua)?;
merchant::install(&lua)?;
bank::install(&lua)?;
stable::install(&lua)?;
item_text::install(&lua)?;
mail::install(&lua)?;
auction::install(&lua)?;
trainer::install(&lua)?;
taxi::install(&lua)?;
trade::install(&lua)?;
inspect::install(&lua)?;
dressup::install(&lua)?;
tabard::install(&lua)?;
tradeskill::install(&lua)?;
craft::install(&lua)?;
reputation::install(&lua)?;
skills::install(&lua)?;
item_stats::install(&lua)?;
char_stats::install(&lua)?;
inventory_verbs::install(&lua)?;
weapon_enchant::install(&lua)?;
loot::install(&lua)?;
loot_roll::install(&lua)?;
quest::install(&lua)?;
quest_log::install(&lua)?;
messageframe::install(&lua)?;
scrollframe::install(&lua)?;
simplehtml::install(&lua)?;
slider::install(&lua)?;
colorselect::install(&lua)?;
minimap::install(&lua)?;
modelframe::install(&lua)?;
tooltip::install(&lua)?;
worldmap::install(&lua)?;
worldstate::install(&lua)?;
net_stats::install(&lua)?;
/// The glue screens' VM, as the glue builder (`0x46a7b0`) leaves its state before GlueXML
/// loads: the base set and the shared registrars, with the glue event table and the glue frame
/// types ([`glue`]), and none of the in-game surface. The glue registrar's own four tables
/// (`0x46ddf0`, `0x4736c0`, `0x471a20`, `0x46f480`) are not built yet.
pub fn new_glue() -> mlua::Result<UiScript> {
Ok(Self::host(install_framescript(VmKind::Glue)?))
}
let s = UiScript {
fn host(lua: Lua) -> UiScript {
UiScript {
lua,
instructions: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
session: NEXT_SESSION.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
};
Ok(s)
}
}
/// Which FrameScript state this VM is.
pub fn vm_kind(&self) -> VmKind {
self.model_ref().vm
}
/// The embedded VM, for the app and the loader to add their bindings over this object model.
+3
View File
@@ -61,6 +61,8 @@ impl<T: Copy> IdMap<T> {
/// The Rust-side model behind the Lua VM, in `lua.app_data`. It holds no mlua handles, whose
/// persistent count the vendored `LUAI_MAXCSTACK` caps.
pub(crate) struct Model {
/// Which FrameScript state this VM is: it picks the event table and the frame types.
pub(crate) vm: super::glue::VmKind,
/// Every discovered addon in load order: the AddOn API's registry, filled at world entry.
pub(crate) addons: Vec<super::addon::AddOnInfo>,
/// The Lua index space into `addons`, a title-sorted, hidden-filtered list (`0x51da70`).
@@ -1098,6 +1100,7 @@ impl Model {
pub(crate) fn new() -> Model {
Model {
vm: super::glue::VmKind::InGame,
addons: Vec::new(),
addon_index: Vec::new(),
addon_info_hidden: None,
+4 -3
View File
@@ -527,12 +527,13 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
lua.set_named_registry_value(REG_MODEL_METHODS, m)?;
playermodel_install(lua)
lua.set_named_registry_value(REG_MODEL_METHODS, m)
}
/// `PlayerModel`'s own three methods (table `0x84f1fc`); the rest are `Model`'s, reached on a miss.
fn playermodel_install(lua: &Lua) -> mlua::Result<()> {
/// Only the in-game state registers it (`0x506230`, from `0x490260`), where `Model`'s is in the
/// widget set both states install (`0x76f840`, from `0x706350`).
pub(super) fn install_player_model(lua: &Lua) -> mlua::Result<()> {
let m = lua.create_table()?;
// `SetUnit(unit)` (`0x84f1fc[0]` → `0x505d70`) clears the `SetModel` path; the unit token is
+78 -35
View File
@@ -11,10 +11,11 @@ use std::ffi::c_void;
use mlua::{LightUserData, Lua, ObjectLike, Table, Value};
use super::binding_abi::optional_string;
use super::glue::VmKind;
use super::{Model, REG_FRAME_META, REG_FRAME_METHODS, REG_SCRIPTS, REG_WRAPPERS};
use crate::layout::Point;
use crate::order::{DrawLayer, Strata};
use crate::widget::{FrameHandle, FrameKind};
use crate::widget::{FrameHandle, FrameKind, KindState};
// The frame method clusters, split out for size.
pub(crate) mod anchor_args;
@@ -248,10 +249,10 @@ pub(super) fn draw_layer_name(l: DrawLayer) -> &'static str {
}
}
/// A widget tag or `CreateFrame` type → its [`FrameKind`] ([`enum_token`]). Public so the app asks
/// this one mapping rather than keeping a copy.
/// A widget tag or `CreateFrame` type → its [`FrameKind`] ([`enum_token`]), whichever registration
/// makes it known. Public so the app asks this one mapping rather than keeping a copy.
pub fn frame_kind_from_tag(s: &str) -> Option<FrameKind> {
frame_kind_from_str(s)
frame_type(s).map(|t| t.kind)
}
/// The type registry lookup `CreateFrame` and the XML loader share (`0x6ee280`, table
@@ -259,47 +260,78 @@ pub fn frame_kind_from_tag(s: &str) -> Option<FrameKind> {
/// record is released once the first one is made (`0x6ee439`), so a second one misses. On a miss
/// Lua raises and XML logs `Unknown frame type: %s` and skips the node.
pub(crate) fn registered_frame_kind(lua: &Lua, kind: &str) -> Option<FrameKind> {
let frame_kind = frame_kind_from_str(kind)?;
let one_shot_spent = frame_kind == FrameKind::WorldFrame
&& lua
.app_data_ref::<Model>()
.expect("model app_data")
.world_frame_made;
(!one_shot_spent).then_some(frame_kind)
let t = frame_type(kind)?;
let model = lua.app_data_ref::<Model>().expect("model app_data");
let registered = match t.factory {
Factory::Startup => true,
Factory::InGame => model.vm == VmKind::InGame,
};
let one_shot_spent = t.kind == FrameKind::WorldFrame && model.world_frame_made;
(registered && !one_shot_spent).then_some(t.kind)
}
fn frame_kind_from_str(s: &str) -> Option<FrameKind> {
Some(match enum_token(s).as_str() {
"FRAME" => FrameKind::Frame,
/// `ModelFFX`, the glue's `CSimpleModelFFX` (factory `0x46fa00`): a `CSimpleModel` (its ctor
/// `0x46fa30` runs `0x76c8e0`, then swaps the vtable) answering as `Model` (`0x470030`) with
/// `Model`'s methods and `LoadXML` (`0x76cac0`), which differs in its OnShow (`0x46fa60`) and its
/// paint (`0x46fa90`, [`crate::widget::ModelState::ffx`]).
pub(crate) fn is_model_ffx(kind: &str) -> bool {
enum_token(kind) == "MODELFFX"
}
/// When a frame type's factory is registered (`0x6ee880`) into the process-wide registry, which
/// nothing clears before shutdown (`0x6ee800`, from `0x4036fc`).
#[derive(Clone, Copy)]
enum Factory {
/// Before any FrameScript state is built: the 13 base types (`0x6ef070`, from `0x402af3`), and
/// `ModelFFX`, which the glue manager's init registers (`0x46a4fe`) before its first build
/// (`0x46a505`), so the in-game state has it too.
Startup,
/// `UI_Init`'s eight (`0x495940`, from `0x48feb5`). They stay registered after a logout, so
/// only a glue state that never entered the world lacks them, which is the one benilla builds.
InGame,
}
/// A registered type: the [`FrameKind`] its factory builds, and its registration.
struct FrameType {
kind: FrameKind,
factory: Factory,
}
fn frame_type(s: &str) -> Option<FrameType> {
use Factory::{InGame, Startup};
let (kind, factory) = match enum_token(s).as_str() {
"FRAME" => (FrameKind::Frame, Startup),
// Registered at `0x495948`, the one row passing `1` as its third argument.
"WORLDFRAME" => FrameKind::WorldFrame,
"WORLDFRAME" => (FrameKind::WorldFrame, InGame),
// A plain `CSimpleFrame`: factory `0x495ba0` allocates `0x314` like `<Frame>`'s `0x6eec10`,
// the ctor `0x506950` adds no field, and its paint `0x506a90` does nothing, so
// `GetObjectType()` is "Frame" and `IsObjectType("TaxiRouteFrame")` false (`0x484800` is
// not overridden). The flight lines are the FrameXML's own rotated textures.
"TAXIROUTEFRAME" => FrameKind::Frame,
"BUTTON" => FrameKind::Button,
"CHECKBUTTON" => FrameKind::CheckButton,
"TAXIROUTEFRAME" => (FrameKind::Frame, InGame),
"BUTTON" => (FrameKind::Button, Startup),
"CHECKBUTTON" => (FrameKind::CheckButton, Startup),
// Its own registered type (`0x4959a6`), not a Button: 1.12 has no Lua verb to take loot
// slot N (`LootSlot` only continues a bind confirm), so a Button row could loot nothing.
"LOOTBUTTON" => FrameKind::LootButton,
"EDITBOX" => FrameKind::EditBox,
"STATUSBAR" => FrameKind::StatusBar,
"SLIDER" => FrameKind::Slider,
"SCROLLFRAME" => FrameKind::ScrollFrame,
"MODEL" => FrameKind::Model,
"PLAYERMODEL" => FrameKind::PlayerModel,
"DRESSUPMODEL" => FrameKind::DressUpModel,
"TABARDMODEL" => FrameKind::TabardModel,
"MESSAGEFRAME" => FrameKind::MessageFrame,
"SCROLLINGMESSAGEFRAME" => FrameKind::ScrollingMessageFrame,
"COLORSELECT" => FrameKind::ColorSelect,
"SIMPLEHTML" => FrameKind::SimpleHtml,
"MOVIEFRAME" => FrameKind::MovieFrame,
"GAMETOOLTIP" => FrameKind::GameTooltip,
"MINIMAP" => FrameKind::Minimap,
"LOOTBUTTON" => (FrameKind::LootButton, InGame),
"EDITBOX" => (FrameKind::EditBox, Startup),
"STATUSBAR" => (FrameKind::StatusBar, Startup),
"SLIDER" => (FrameKind::Slider, Startup),
"SCROLLFRAME" => (FrameKind::ScrollFrame, Startup),
"MODEL" => (FrameKind::Model, Startup),
"MODELFFX" => (FrameKind::Model, Startup),
"PLAYERMODEL" => (FrameKind::PlayerModel, InGame),
"DRESSUPMODEL" => (FrameKind::DressUpModel, InGame),
"TABARDMODEL" => (FrameKind::TabardModel, InGame),
"MESSAGEFRAME" => (FrameKind::MessageFrame, Startup),
"SCROLLINGMESSAGEFRAME" => (FrameKind::ScrollingMessageFrame, Startup),
"COLORSELECT" => (FrameKind::ColorSelect, Startup),
"SIMPLEHTML" => (FrameKind::SimpleHtml, Startup),
"MOVIEFRAME" => (FrameKind::MovieFrame, Startup),
"GAMETOOLTIP" => (FrameKind::GameTooltip, InGame),
"MINIMAP" => (FrameKind::Minimap, InGame),
_ => return None,
})
};
Some(FrameType { kind, factory })
}
/// A Lua number-ish → f32 (nil/other → 0.0), for offset/color args.
@@ -351,7 +383,13 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let create_frame = lua.create_function(create_frame)?;
lua.globals().set("CreateFrame", create_frame)?;
Ok(())
}
/// The frame globals only the in-game state registers (table `0x83de68`, `UI_Init`'s): the glue
/// state's `GetScreenWidth`/`GetScreenHeight` are other bindings (`0x46dd30`/`0x46dd80`), and it
/// has no modifier-key or fullscreen-scale verb.
pub(super) fn install_in_game(lua: &Lua) -> mlua::Result<()> {
// `GetScreenWidth`/`GetScreenHeight` (`0x48b480`/`0x48b4d0`): the screen root's size divided by
// `UIParent`'s scale (`GetUiScale`, `0x494590`), so they answer in `UIParent`'s units.
lua.globals().set(
@@ -584,6 +622,11 @@ pub(super) fn create_frame(
if frame_kind == FrameKind::WorldFrame {
model.world_frame_made = true;
}
if is_model_ffx(&kind) {
if let Some(KindState::Model(m)) = model.arena.frame_mut(h).map(|f| &mut f.kind_state) {
m.ffx = true;
}
}
// A child enters its parent's stratum at the parent's level + 1; an explicit
// `frameStrata` or `frameLevel` still overrides it afterwards.
if let Some(p) = parent_handle {
@@ -19,6 +19,10 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
lua.create_function(|lua, (this, event): (Table, String)| {
let h = frame_handle_of(lua, &this)?;
let mut model = lua.app_data_mut::<Model>().expect("model");
// A name the VM's event table lacks registers nothing and raises nothing (`0x702140`).
let Some(event) = model.vm.event_key(&event).map(str::to_string) else {
return Ok(());
};
// Dispatch follows registration order, as the reference's listener list does; a
// re-register keeps its place and never fires twice.
let list = model.event_to_frames.entry(event.clone()).or_default();
@@ -34,6 +38,10 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
lua.create_function(|lua, (this, event): (Table, String)| {
let h = frame_handle_of(lua, &this)?;
let mut model = lua.app_data_mut::<Model>().expect("model");
// Matched as `RegisterEvent` matches (`0x702280`).
let Some(event) = model.vm.event_key(&event).map(str::to_string) else {
return Ok(());
};
if let Some(list) = model.event_to_frames.get_mut(&event) {
list.retain(|x| x != &h);
}
+15 -10
View File
@@ -337,10 +337,24 @@ pub(super) fn region_wrapper(lua: &Lua, id: u32) -> mlua::Result<Table> {
Ok(t)
}
/// Install the region method tables and the region globals.
/// Install the region method tables.
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
install_region_methods(lua)?;
// `__index` is the method table itself, here and on the leaf metatables, not a function
// (~9 ns against ~195 ns per method access); sound while nothing replaces these tables.
let region_meta = lua.create_table()?;
region_meta.set(
"__index",
lua.named_registry_value::<Table>(REG_REGION_METHODS)?,
)?;
lua.set_named_registry_value(REG_REGION_META, region_meta)?;
Ok(())
}
/// The two portrait globals, which only the in-game state registers (`SetPortraitTexture` in
/// table `0x850438`, `SetPortraitToTexture` in `0x83de68`).
pub(super) fn install_in_game(lua: &Lua) -> mlua::Result<()> {
// SetPortraitTexture(texture, unit) (`UnitFrame.lua:26`): binds the region to a unit whose
// model the app bakes off-screen, drawn masked to its inscribed circle; a later `SetTexture`
// or `SetPortraitToTexture` clears it.
@@ -392,15 +406,6 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Ok(())
})?,
)?;
// `__index` is the method table itself, here and on the leaf metatables, not a function
// (~9 ns against ~195 ns per method access); sound while nothing replaces these tables.
let region_meta = lua.create_table()?;
region_meta.set(
"__index",
lua.named_registry_value::<Table>(REG_REGION_METHODS)?,
)?;
lua.set_named_registry_value(REG_REGION_META, region_meta)?;
Ok(())
}
+33 -20
View File
@@ -4,10 +4,10 @@
//! `loadfile`, `load`, `module`, `newproxy` and `debug` removed), the reference's `debugstack`,
//! and a text-only `loadstring`.
//! - [`install`]: the 1.12 bare globals (string, table and math aliases, the `debug*` family, the
//! error-handler pair), `time` and `date`, the clock and zone getters ([`super::clock`],
//! [`super::zone_text`]), and a `string.format` that accepts positional `%N$` specs as the 1.12
//! client's does, rewriting them into a sequential format. Mixing positional and sequential
//! specs raises, as in the reference, whose single argument cursor a positional spec desyncs.
//! error-handler pair), `time` and `date`, and a `string.format` that accepts positional `%N$`
//! specs as the 1.12 client's does, rewriting them into a sequential format. Mixing positional
//! and sequential specs raises, as in the reference, whose single argument cursor a positional
//! spec desyncs.
use mlua::{Lua, Value, Variadic};
@@ -101,9 +101,7 @@ pub(super) fn sandbox(lua: &Lua) -> mlua::Result<()> {
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
install_error_handler(lua)?;
install_time(lua)?;
// Before the Lua layer, whose `debugprofile*` pair keeps its own `GetTime`.
super::clock::install(lua)?;
super::zone_text::install(lua)?;
install_profile(lua)?;
lua.load(WOW_STDLIB)
.set_name("=[benilla wow stdlib]")
.set_mode(mlua::ChunkMode::Text)
@@ -111,6 +109,32 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
Ok(())
}
/// `debugprofilestart`/`debugprofilestop` (`0x702840`/`0x702860`), the two real `debug*` verbs (RDTSC
/// via `0x4293d0`): start latches, stop answers the milliseconds since. Ours read the `GetTime()`
/// clock ([`super::clock::now`]), the monotonic session seconds the tick advances, as benilla has
/// no cycle counter; read in Rust, so an addon that replaces the `GetTime` global cannot bend it.
fn install_profile(lua: &Lua) -> mlua::Result<()> {
use std::sync::atomic::{AtomicU64, Ordering};
let start = std::sync::Arc::new(AtomicU64::new(0f64.to_bits()));
let g = lua.globals();
let latch = start.clone();
g.set(
"debugprofilestart",
lua.create_function(move |lua, ()| {
latch.store(super::clock::now(lua).to_bits(), Ordering::Relaxed);
Ok(())
})?,
)?;
g.set(
"debugprofilestop",
lua.create_function(move |lua, ()| {
let since = f64::from_bits(start.load(Ordering::Relaxed));
Ok((super::clock::now(lua) - since) * 1000.0)
})?,
)?;
Ok(())
}
/// The handler `seterrorhandler` holds, `None` until one is given.
pub(super) fn error_handler(lua: &Lua) -> Option<mlua::Function> {
lua.named_registry_value::<Option<mlua::Function>>(super::REG_ERRORHANDLER)
@@ -223,13 +247,9 @@ ldexp = math.ldexp
-- That is what lets `BasicControls.xml` stop guarding its `debuginfo()` call and transcribe
-- `_ERRORMESSAGE` verbatim, which it now does.
--
-- `debugprofilestart`/`debugprofilestop` are the two that are REAL (RDTSC via `0x4293d0`): start
-- latches, stop answers the elapsed milliseconds since it. Modelled on `GetTime`'s own clock —
-- the same monotonic session seconds the tick advances — because benilla has no cycle counter and
-- an addon uses these to time its own work, which milliseconds answer honestly. The clock is
-- captured, so an addon that replaces the `GetTime` global cannot bend it.
-- `debugprofilestart`/`debugprofilestop` are the two that are REAL; they are Rust
-- (`install_profile`).
do
local GetTime = GetTime
local function noop() end
debuginfo = noop
debugload = noop
@@ -237,19 +257,12 @@ do
debugdump = noop
debugbreak = noop
debugtimestamp = noop
local profileStart = 0
function debugprofilestart() profileStart = GetTime() end
function debugprofilestop() return (GetTime() - profileStart) * 1000 end
end
-- ── _G accessors (getglobal/setglobal: _G[name] get/set) ───────────────────────────────
function getglobal(name) return _G[name] end
function setglobal(name, value) _G[name] = value end
-- ── GetLocale: benilla ships/reads enUS data only (the 5875 MPQs + Spell.dbc enUS column) ──────
function GetLocale() return "enUS" end
-- ── NOT here: wipe / tostringall / strsplit / strjoin / strconcat / strtrim ────────────────────
-- Six 2.0+ names benilla used to define. 1.12 has none of them (absent from
-- `reference/1.12-globals.tsv`, from the stock chain, and from every registrar table), and the
@@ -0,0 +1,524 @@
//! The glue screens' Lua surface (`reference/1.12-glue-surface.tsv`) held to its source, the shapes
//! table, and the glue VM ([`UiScript::new_glue`]) held to the surface: every function it puts in
//! `_G` is a glue-state one, and every glue-state function is there or on [`NOT_YET`]. Then the glue
//! state's frame types and event table.
use std::collections::{BTreeMap, BTreeSet};
use crate::script::{UiScript, GLUE_EVENTS};
use crate::widget::KindState;
/// A vendored `reference/` table's data rows, split on tabs, the header row dropped.
fn reference_rows(file: &str, header: &str) -> Vec<Vec<String>> {
let path = format!("{}/../../reference/{file}", env!("CARGO_MANIFEST_DIR"));
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("reading {path}: {e}"));
text.lines()
.filter(|l| !l.starts_with('#') && !l.is_empty())
.map(|l| l.split('\t').map(str::to_string).collect::<Vec<_>>())
.filter(|f| f[0] != header)
.collect()
}
/// One row of the glue surface table.
pub(super) struct GlueRow {
pub scope: String,
pub name: String,
pub fn_va: String,
pub table_va: String,
pub arity: String,
}
/// `reference/1.12-glue-surface.tsv`.
pub(super) fn glue_surface() -> Vec<GlueRow> {
let rows: Vec<GlueRow> = reference_rows("1.12-glue-surface.tsv", "scope")
.into_iter()
.map(|f| GlueRow {
scope: f[0].clone(),
name: f[2].clone(),
fn_va: f[3].clone(),
table_va: f[4].clone(),
arity: f[5].clone(),
})
.collect();
assert!(
rows.len() > 150,
"the glue surface table looks truncated ({} rows) — regenerate it with \
scripts/gen-reference-glue-surface.py",
rows.len()
);
rows
}
/// The tables the glue state installs, as the glue registrar `0x46abb0` and the Lua init
/// `0x7039e0` loop them: each `cmp` bound over the 8-byte pair stride.
const GLUE_TABLES: [(&str, usize); 8] = [
("0x811e28", 36), // the base array, 0x7039e0
("0x8503f4", 6), // 0x51bb20
("0x872e74", 4), // 0x706350
("0x8373b8", 60), // 0x46ddf0
("0x838578", 11), // 0x4736c0
("0x8380f8", 24), // 0x471a20
("0x837ea0", 10), // 0x46f480
("0x835a50", 4), // 0x4587f0
];
/// Each registrar table's rows are exactly the shapes table's rows for that table, binding and
/// arity alike; a compat alias names a library binding the shapes table has.
#[test]
fn the_glue_surface_matches_the_shapes_table() {
// (table_va, name) -> (fn, arity): a name is not a key in the shapes table.
let shapes: BTreeMap<(String, String), (String, String)> =
reference_rows("1.12-shapes.tsv", "name")
.into_iter()
.map(|f| ((f[3].clone(), f[0].clone()), (f[1].clone(), f[7].clone())))
.collect();
let surface = glue_surface();
for (table_va, rows) in GLUE_TABLES {
let in_shapes: BTreeSet<&str> = shapes
.keys()
.filter(|(t, _)| t == table_va)
.map(|(_, n)| n.as_str())
.collect();
let in_surface: BTreeSet<&str> = surface
.iter()
.filter(|r| r.table_va == table_va)
.map(|r| r.name.as_str())
.collect();
assert_eq!(
in_shapes.len(),
rows,
"the shapes table's {table_va} has {} rows",
in_shapes.len()
);
assert_eq!(
in_surface, in_shapes,
"table {table_va}: the glue surface and the shapes table differ"
);
}
for r in &surface {
if r.fn_va == "lua" {
continue;
}
if r.table_va == "-" {
// `__pow`, set by `luaopen_math` outside any table.
assert_eq!(r.name, "__pow", "{} has no table", r.name);
continue;
}
let shape = shapes
.iter()
.find(|((t, _), (f, _))| *t == r.table_va && *f == r.fn_va)
.map(|(_, s)| s);
assert_eq!(
shape.map(|(f, a)| (f.as_str(), a.as_str())),
Some((r.fn_va.as_str(), r.arity.as_str())),
"{}: no shapes row in table {} with that binding and arity",
r.name,
r.table_va
);
}
let names: BTreeSet<&str> = surface.iter().map(|r| r.name.as_str()).collect();
assert_eq!(names.len(), surface.len(), "a name appears twice");
// 36 base-array rows, the compat chunk's 39 functions and `__pow`; 6 + 4 + 4 shared; the four
// glue tables' 105.
for (scope, n) in [("base", 76), ("shared", 14), ("glue", 105)] {
assert_eq!(
surface.iter().filter(|r| r.scope == scope).count(),
n,
"{scope} rows"
);
}
}
/// Glue-state functions the glue VM does not answer yet, by registrar table, each with why. The
/// list only shrinks: a verb is absent until built, never stubbed.
const NOT_YET: &[(&str, &[&str])] = &[
(
"0x51bb20's file trio, each only `luaL_error(\"Access Denied\")`; unbuilt in the in-game \
VM too, tracked by #338",
&["ReadFile", "DeleteFile", "AppendToFile"],
),
(
"the glue core (0x46ddf0, table 0x8373b8): login, the notices, the addon list, the cursor \
and the glue music; each a binding of its own, the 14 with an in-game namesake included",
&[
"AcceptContest",
"AcceptEULA",
"AcceptScanning",
"AcceptTOS",
"ContestAccepted",
"DefaultServerLogin",
"DisableAddOn",
"DisableAllAddOns",
"DisconnectFromServer",
"EULAAccepted",
"EnableAddOn",
"EnableAllAddOns",
"EnterWorld",
"GetAddOnDependencies",
"GetAddOnEnableState",
"GetAddOnInfo",
"GetBillingPlan",
"GetBillingTimeRemaining",
"GetBillingTimeRested",
"GetBuildInfo",
"GetCursorPosition",
"GetLocale",
"GetMovieResolution",
"GetMovieSubtitles",
"GetNumAddOns",
"GetSavedAccountName",
"GetScreenHeight",
"GetScreenWidth",
"GetScriptMemory",
"GetServerName",
"HideCursor",
"IsAddonVersionCheckEnabled",
"IsConnectedToServer",
"LaunchAddOnURL",
"LaunchURL",
"PINEntered",
"PatchDownloadApply",
"PatchDownloadCancel",
"PatchDownloadProgress",
"PlayCreditsMusic",
"PlayGlueMusic",
"QuitGame",
"ResetAddOns",
"SaveAddOns",
"ScanningAccepted",
"Screenshot",
"SetAddonVersionCheck",
"SetCurrentScreen",
"SetMovieSubtitles",
"SetSavedAccountName",
"SetScriptMemory",
"ShowContestNotice",
"ShowCursor",
"ShowEULANotice",
"ShowScanningNotice",
"ShowTOSNotice",
"StatusDialogClick",
"StopGlueMusic",
"SurveyNotificationDone",
"TOSAccepted",
],
),
(
"character select (0x4736c0, table 0x838578)",
&[
"DeleteCharacter",
"GetCharacterInfo",
"GetCharacterListUpdate",
"GetCharacterSelectFacing",
"GetNumCharacters",
"RenameCharacter",
"SelectCharacter",
"SetCharSelectBackground",
"SetCharSelectModelFrame",
"SetCharacterSelectFacing",
"UpdateSelectionCustomizationScene",
],
),
(
"character create (0x471a20, table 0x8380f8)",
&[
"CreateCharacter",
"CycleCharCustomization",
"GetAvailableRaces",
"GetCharacterCreateFacing",
"GetClassesForRace",
"GetFacialHairCustomization",
"GetFactionForRace",
"GetHairCustomization",
"GetNameForRace",
"GetRandomName",
"GetSelectedClass",
"GetSelectedRace",
"GetSelectedSex",
"HasCharCustomization",
"RandomizeCharCustomization",
"ResetCharCustomize",
"SetCharCustomizeBackground",
"SetCharCustomizeFrame",
"SetCharacterCreateFacing",
"SetSelectedClass",
"SetSelectedRace",
"SetSelectedSex",
"UpdateCustomizationBackground",
"UpdateCustomizationScene",
],
),
(
"the realm list (0x46f480, table 0x837ea0)",
&[
"CancelRealmListQuery",
"ChangeRealm",
"GetNumRealms",
"GetRealmCategories",
"GetRealmInfo",
"GetSelectedCategory",
"RealmListDialogCancelled",
"RequestRealmList",
"SetPreferredInfo",
"SortRealms",
],
),
];
/// The glue VM's globals by Lua type name.
fn glue_globals() -> BTreeMap<String, String> {
UiScript::new_glue()
.expect("construct the glue VM")
.eval::<Vec<Vec<String>>>(
"local out = {} \
for k, v in pairs(getfenv(0)) do \
if type(k) == 'string' then table.insert(out, {k, type(v)}) end \
end \
return out",
)
.expect("dump _G")
.into_iter()
.map(|kv| (kv[0].clone(), kv[1].clone()))
.collect()
}
/// The glue VM's functions are the glue surface less [`NOT_YET`], exactly.
#[test]
fn the_glue_vm_answers_the_glue_surface() {
let surface: BTreeSet<String> = glue_surface().into_iter().map(|r| r.name).collect();
let not_yet: BTreeSet<&str> = NOT_YET
.iter()
.flat_map(|(_, n)| n.iter().copied())
.collect();
let ours: BTreeSet<String> = glue_globals()
.into_iter()
.filter(|(_, t)| t == "function")
.map(|(k, _)| k)
.collect();
let beyond: Vec<&String> = ours.iter().filter(|n| !surface.contains(*n)).collect();
assert!(
beyond.is_empty(),
"the glue VM exposes {} function(s) the glue state does not have: {beyond:?}. The \
in-game surface stays out of it (`UiScript::new` installs that).",
beyond.len()
);
let missing: Vec<&String> = surface
.iter()
.filter(|n| !ours.contains(*n) && !not_yet.contains(n.as_str()))
.collect();
assert!(
missing.is_empty(),
"glue-state functions the glue VM lacks and NOT_YET does not name: {missing:?}"
);
let stale: Vec<&&str> = not_yet
.iter()
.filter(|n| ours.contains(**n) || !surface.contains(**n))
.collect();
assert!(
stale.is_empty(),
"NOT_YET names that are built, or that are not glue-state functions; take them off: \
{stale:?}"
);
assert_eq!(
not_yet.len(),
NOT_YET.iter().map(|(_, n)| n.len()).sum::<usize>(),
"a NOT_YET name is listed twice"
);
}
/// Past its functions the glue `_G` holds the four library tables, the frame metatable
/// `0x7039e0` publishes, and the compat chunk's `PI`; `_G` itself is benilla's, as in-game
/// (`reference_surface::BEYOND_1_12`).
#[test]
fn the_glue_vm_holds_no_other_global() {
let others: BTreeMap<String, String> = glue_globals()
.into_iter()
.filter(|(_, t)| t != "function")
.collect();
let want: BTreeMap<String, String> = [
("PI", "number"),
("__framescript_meta", "table"),
("_G", "table"),
("bit", "table"),
("math", "table"),
("string", "table"),
("table", "table"),
]
.into_iter()
.map(|(k, t)| (k.to_string(), t.to_string()))
.collect();
assert_eq!(others, want);
}
/// The glue event table in code is `reference/1.12-glue-events.tsv`, by id.
#[test]
fn the_glue_event_table_matches_the_reference() {
let rows = reference_rows("1.12-glue-events.tsv", "id");
assert_eq!(rows.len(), GLUE_EVENTS.len());
for (id, row) in rows.iter().enumerate() {
assert_eq!(row[0], id.to_string(), "ids run 0..26 in order");
assert_eq!(row[1], GLUE_EVENTS[id], "event {id}");
}
}
/// `RegisterEvent` matches a name against the glue table case-insensitively (`0x702140` with
/// `0x64a4c0`) and drops one the table lacks without raising; `UnregisterEvent` matches the same
/// way (`0x702280`). An in-game event fired at the glue VM reaches nothing, an all-events frame
/// included, since the reference fires by id.
#[test]
fn glue_events_register_by_the_glue_table() {
let mut s = UiScript::new_glue().unwrap();
s.run(
r#"
heard = {}
f = CreateFrame("Frame")
f:SetScript("OnEvent", function() table.insert(heard, "f:" .. event) end)
f:RegisterEvent("set_glue_screen")
f:RegisterEvent("PLAYER_LOGIN")
all = CreateFrame("Frame")
all:SetScript("OnEvent", function() table.insert(heard, "all:" .. event) end)
all:RegisterAllEvents()
"#,
)
.expect("an event off the glue table raises nothing");
s.fire_event("SET_GLUE_SCREEN", vec![]);
s.queue_event("PLAYER_LOGIN", vec![]);
// The per-frame advance runs over the glue VM, its queued events included.
s.tick(0.016);
assert_eq!(
s.eval::<Vec<String>>("return heard").unwrap(),
["f:SET_GLUE_SCREEN", "all:SET_GLUE_SCREEN"],
"the glue event reaches both frames under the table's spelling; the in-game one neither"
);
s.run(r#"heard = {} f:UnregisterEvent("Set_Glue_Screen")"#)
.unwrap();
s.fire_event("SET_GLUE_SCREEN", vec![]);
assert_eq!(
s.eval::<Vec<String>>("return heard").unwrap(),
["all:SET_GLUE_SCREEN"]
);
}
/// The in-game VM keeps its by-name registry: an in-game event registers and fires there.
#[test]
fn the_in_game_vm_keeps_its_events() {
let mut s = UiScript::new().unwrap();
s.run(
r#"heard = 0 f = CreateFrame("Frame") f:RegisterEvent("PLAYER_LOGIN")
f:SetScript("OnEvent", function() heard = heard + 1 end)"#,
)
.unwrap();
s.fire_event("PLAYER_LOGIN", vec![]);
assert_eq!(s.eval::<i64>("return heard").unwrap(), 1);
}
/// The frame types: the 13 registered at startup (`0x6ef070`) and `ModelFFX` (`0x46a4fe`) in both
/// states; `UI_Init`'s eight (`0x495940`) only in-game.
#[test]
fn the_glue_vm_makes_the_startup_frame_types_only() {
const STARTUP: [&str; 14] = [
"Button",
"CheckButton",
"EditBox",
"Frame",
"MessageFrame",
"Model",
"ScrollFrame",
"ScrollingMessageFrame",
"Slider",
"SimpleHTML",
"StatusBar",
"ColorSelect",
"MovieFrame",
"ModelFFX",
];
const IN_GAME: [&str; 8] = [
"WorldFrame",
"GameTooltip",
"Minimap",
"PlayerModel",
"DressUpModel",
"TabardModel",
"LootButton",
"TaxiRouteFrame",
];
let glue = UiScript::new_glue().unwrap();
let world = UiScript::new().unwrap();
for t in STARTUP {
for (vm, s) in [("glue", &glue), ("in-game", &world)] {
s.run(&format!(r#"CreateFrame("{t}")"#))
.unwrap_or_else(|e| panic!("{vm} CreateFrame(\"{t}\"): {e}"));
}
}
for t in IN_GAME {
let e = glue
.run(&format!(r#"CreateFrame("{t}")"#))
.expect_err(t)
.to_string();
assert!(e.contains(&format!("unknown frame type '{t}'")), "{t}: {e}");
world.run(&format!(r#"CreateFrame("{t}")"#)).unwrap();
}
}
/// `ModelFFX` is a `Model` to Lua (`0x470030`, `0x46ffd0`) with `Model`'s methods, flagged FFX for
/// the renderer; its XML is `Model`'s (`0x76cac0`), `file=` and `scale=` included.
#[test]
fn model_ffx_is_a_model_flagged_ffx() {
let s = UiScript::new_glue().unwrap();
s.run(
r#"
a = CreateFrame("ModelFFX", "FfxPane")
b = CreateFrame("Model", "PlainPane")
a:SetModel("Test/Scene.mdx")
"#,
)
.unwrap();
assert_eq!(
s.eval::<(String, bool, bool, String)>(
r#"return a:GetObjectType(), a:IsObjectType("Model") == 1,
a:IsObjectType("ModelFFX") == 1, a:GetModel()"#
)
.unwrap(),
(
"Model".to_string(),
true,
false,
"Test/Scene.mdx".to_string()
)
);
let ffx = |name: &str| {
let model = s.model_ref();
let h = model.arena.lookup(name).unwrap();
match &model.arena.frame(h).unwrap().kind_state {
KindState::Model(m) => m.ffx,
other => panic!("{name}: {other:?}"),
}
};
assert!(ffx("FfxPane"));
assert!(!ffx("PlainPane"));
let doc = crate::framexml::parse(
r#"<Ui>
<ModelFFX name="XmlFfx" file="Test/Xml.mdx" scale="2"/>
<PlayerModel name="XmlPlayer"/>
</Ui>"#,
)
.unwrap();
let report = crate::loader::load(&s, &doc, &|_| None);
assert!(
report
.warnings
.iter()
.any(|w| w == "Unknown frame type: PlayerModel"),
"the glue state has no PlayerModel factory: {:?}",
report.warnings
);
assert!(ffx("XmlFfx"));
assert_eq!(
s.eval::<(String, f64)>("return XmlFfx:GetModel(), XmlFfx:GetModelScale()")
.unwrap(),
("Test/Xml.mdx".to_string(), 2.0)
);
}
@@ -25,6 +25,7 @@ mod events;
mod font_object;
mod frame_api;
mod generic_for;
mod glue_surface;
mod gm_ticket;
mod handler_prof;
mod host_state;
+3
View File
@@ -36,6 +36,9 @@ impl UiScript {
/// [`UiScript::fire_event`] for a caller holding `&Lua`, such as a Lua binding.
pub(crate) fn fire_event_into(lua: &Lua, event: &str, args: Vec<ScriptValue>) {
let model_mut = || lua.app_data_mut::<Model>().expect("model app_data set");
if !model_mut().vm.fires(event) {
return;
}
{
let mut at = {
let model = model_mut();
+19 -4
View File
@@ -71,6 +71,17 @@ pub enum KindUpdate {
}
impl FrameKind {
/// A `CSimpleModel` kind, carrying a [`ModelState`].
pub fn is_model(self) -> bool {
matches!(
self,
FrameKind::Model
| FrameKind::PlayerModel
| FrameKind::DressUpModel
| FrameKind::TabardModel
)
}
/// The kind's own update, run in the walk after the frame's `OnUpdate`. The reference
/// overrides the slot on more kinds; benilla models these.
pub fn kind_update(self) -> Option<KindUpdate> {
@@ -80,10 +91,7 @@ impl FrameKind {
FrameKind::MessageFrame => Some(KindUpdate::MessageFrame),
FrameKind::ScrollingMessageFrame => Some(KindUpdate::ScrollingMessageFrame),
FrameKind::ScrollFrame => Some(KindUpdate::ScrollFrame),
FrameKind::Model
| FrameKind::PlayerModel
| FrameKind::DressUpModel
| FrameKind::TabardModel => Some(KindUpdate::Model),
k if k.is_model() => Some(KindUpdate::Model),
_ => None,
}
}
@@ -334,6 +342,12 @@ pub struct ModelState {
pub fog_near: f32,
/// Fog far (`+0x3b0`), shaped like [`Self::fog_near`]; the ctor's is `1.0`.
pub fog_far: f32,
/// A `ModelFFX`, the glue's `CSimpleModelFFX` (`0x46fa00`). Its vtable differs from
/// `CSimpleModel`'s in two slots past the destructor: its OnShow (`0x46fa60`) installs the glue
/// glow pass (`0x6cde60` with `[0xb414c4]`) and pins `LightParams.glow` to `*0x8380b4` (0.30),
/// and its layer-2 paint callback (`0x46fad0`, registered by `0x46fa90`) brackets the model
/// paint `0x76d240` with the full-screen-effect pass pair `0x6cd890`/`0x6cda70`.
pub ffx: bool,
}
/// A model pane's armed fog, all the renderer needs ([`ModelState::armed_fog`]).
@@ -406,6 +420,7 @@ impl Default for ModelState {
fog: false,
fog_near: 0.0,
fog_far: 1.0,
ffx: false,
}
}
}
+2 -11
View File
@@ -556,10 +556,7 @@ impl WidgetArena {
}
// `CGCharacterModelBase` extends `CSimpleModel`, adding only a turn-animation pair,
// which is not modelled.
FrameKind::Model
| FrameKind::PlayerModel
| FrameKind::DressUpModel
| FrameKind::TabardModel => KindState::Model(kinds::ModelState::default()),
k if k.is_model() => KindState::Model(kinds::ModelState::default()),
FrameKind::Minimap => KindState::Minimap(kinds::MinimapState::default()),
FrameKind::GameTooltip => KindState::Tooltip(kinds::TooltipState::default()),
_ => KindState::None,
@@ -570,13 +567,7 @@ impl WidgetArena {
if matches!(kind, FrameKind::Minimap) {
self.minimap_created += 1;
}
let model_kind = matches!(
kind,
FrameKind::Model
| FrameKind::PlayerModel
| FrameKind::DressUpModel
| FrameKind::TabardModel
);
let model_kind = kind.is_model();
let visible = frame.effective_visible;
let (strata, level) = (frame.strata, frame.level);
let (index, generation) = self.frames.insert(frame);
+1
View File
@@ -530,6 +530,7 @@
- `gen-message-catalog.py` — Generate `crates/benilla-ui/src/messages/catalog.rs` from `reference/1.12-message-catalog.tsv`.
- `gen-reference-events.py` — Regenerate `reference/1.12-events.tsv`: each 1.12 FrameScript event and the arguments it carries.
- `gen-reference-globals.py` — Regenerate `reference/1.12-globals.tsv`: the 1.12.1 client's global namespace, by origin.
- `gen-reference-glue-surface.py` — Regenerate `reference/1.12-glue-surface.tsv`: every Lua function the glue screens' VM has in `_G`.
- `gen-reference-verb-events.py` — Regenerate `reference/1.12-verb-events.tsv`: the events 1.12 Lua verbs fire, keyed by verb.
- `mixsum.py` — Read a `$WOW_MIX_TAP` capture and say, in numbers, what the mix was doing when.
- `pass-span-lint.py` — Refuse a second open `pass_span` on the same render pass (a wgpu validation error on Vulkan).
+42
View File
@@ -0,0 +1,42 @@
# The glue screens' FrameScript event table of the 1.12.1 client (WoW.exe build 5875): eventId -> name.
# Source: the 26 `mov dword [0xb41e70 + 4*id], imm32` writes of 0x46cc60 (0x46cc60-0x46cd5a, called
# once at startup from 0x402af8), each imm32 the address of the event's name string. The glue
# builder 0x46a7b0 hands the array to 0x703d90 (`mov edx,0x1a; mov ecx,0xb41e70; call 0x703d90`
# at 0x46a885-0x46a88f), which rebuilds the runtime dispatch table [0xceef68] from it wholesale,
# as UI_Init does with the in-game array 0xbe1198 (`1.12-event-catalog.tsv`). So while the glue
# state runs, these 26 are the only events, the id fired by FrameScript_SignalEvent 0x703e50 /
# 0x703f50 indexes this table, and the same integer names a different event in-game.
# Registration: `RegisterEvent` 0x774a40 -> 0x702140 walks the table and compares names with
# 0x64a4c0 (-> 0x414310, case-insensitive); a name off the table matches nothing and is dropped
# without an error. `UnregisterEvent` 0x774b30 -> 0x702280 matches the same way.
# Vendored from the client's bytes; no generator for it lives in this repo.
#
# name_va the name string's address (the imm32).
# write_va the instruction in 0x46cc60 that stores it.
id name name_va write_va
0 SET_GLUE_SCREEN 0x837bec 0x46cc60
1 START_GLUE_MUSIC 0x837bd8 0x46cc6a
2 DISCONNECTED_FROM_SERVER 0x837bbc 0x46cc74
3 OPEN_STATUS_DIALOG 0x837ba8 0x46cc7e
4 UPDATE_STATUS_DIALOG 0x837b90 0x46cc88
5 CLOSE_STATUS_DIALOG 0x837b7c 0x46cc92
6 ADDON_LIST_UPDATE 0x837b68 0x46cc9c
7 CHARACTER_LIST_UPDATE 0x837b50 0x46cca6
8 UPDATE_SELECTED_CHARACTER 0x837b34 0x46ccb0
9 OPEN_REALM_LIST 0x837b24 0x46ccba
10 GET_PREFERRED_REALM_INFO 0x837b08 0x46ccc4
11 UPDATE_SELECTED_RACE 0x837af0 0x46ccce
12 SELECT_LAST_CHARACTER 0x837ad8 0x46ccd8
13 SELECT_FIRST_CHARACTER 0x837ac0 0x46cce2
14 GLUE_SCREENSHOT_SUCCEEDED 0x837aa4 0x46ccec
15 GLUE_SCREENSHOT_FAILED 0x837a8c 0x46ccf6
16 PATCH_UPDATE_PROGRESS 0x837a74 0x46cd00
17 PATCH_DOWNLOADED 0x837a60 0x46cd0a
18 SUGGEST_REALM 0x837a50 0x46cd14
19 SUGGEST_REALM_WRONG_PVP 0x837a38 0x46cd1e
20 SUGGEST_REALM_WRONG_CATEGORY 0x837a18 0x46cd28
21 SHOW_SERVER_ALERT 0x837a04 0x46cd32
22 FRAMES_LOADED 0x8379f4 0x46cd3c
23 FORCE_RENAME_CHARACTER 0x8379dc 0x46cd46
24 SHOW_SURVEY_NOTIFICATION 0x8379c0 0x46cd50
25 PLAYER_ENTER_PIN 0x8379ac 0x46cd5a
1 # The glue screens' FrameScript event table of the 1.12.1 client (WoW.exe build 5875): eventId -> name.
2 # Source: the 26 `mov dword [0xb41e70 + 4*id], imm32` writes of 0x46cc60 (0x46cc60-0x46cd5a, called
3 # once at startup from 0x402af8), each imm32 the address of the event's name string. The glue
4 # builder 0x46a7b0 hands the array to 0x703d90 (`mov edx,0x1a; mov ecx,0xb41e70; call 0x703d90`
5 # at 0x46a885-0x46a88f), which rebuilds the runtime dispatch table [0xceef68] from it wholesale,
6 # as UI_Init does with the in-game array 0xbe1198 (`1.12-event-catalog.tsv`). So while the glue
7 # state runs, these 26 are the only events, the id fired by FrameScript_SignalEvent 0x703e50 /
8 # 0x703f50 indexes this table, and the same integer names a different event in-game.
9 # Registration: `RegisterEvent` 0x774a40 -> 0x702140 walks the table and compares names with
10 # 0x64a4c0 (-> 0x414310, case-insensitive); a name off the table matches nothing and is dropped
11 # without an error. `UnregisterEvent` 0x774b30 -> 0x702280 matches the same way.
12 # Vendored from the client's bytes; no generator for it lives in this repo.
13 #
14 # name_va the name string's address (the imm32).
15 # write_va the instruction in 0x46cc60 that stores it.
16 id name name_va write_va
17 0 SET_GLUE_SCREEN 0x837bec 0x46cc60
18 1 START_GLUE_MUSIC 0x837bd8 0x46cc6a
19 2 DISCONNECTED_FROM_SERVER 0x837bbc 0x46cc74
20 3 OPEN_STATUS_DIALOG 0x837ba8 0x46cc7e
21 4 UPDATE_STATUS_DIALOG 0x837b90 0x46cc88
22 5 CLOSE_STATUS_DIALOG 0x837b7c 0x46cc92
23 6 ADDON_LIST_UPDATE 0x837b68 0x46cc9c
24 7 CHARACTER_LIST_UPDATE 0x837b50 0x46cca6
25 8 UPDATE_SELECTED_CHARACTER 0x837b34 0x46ccb0
26 9 OPEN_REALM_LIST 0x837b24 0x46ccba
27 10 GET_PREFERRED_REALM_INFO 0x837b08 0x46ccc4
28 11 UPDATE_SELECTED_RACE 0x837af0 0x46ccce
29 12 SELECT_LAST_CHARACTER 0x837ad8 0x46ccd8
30 13 SELECT_FIRST_CHARACTER 0x837ac0 0x46cce2
31 14 GLUE_SCREENSHOT_SUCCEEDED 0x837aa4 0x46ccec
32 15 GLUE_SCREENSHOT_FAILED 0x837a8c 0x46ccf6
33 16 PATCH_UPDATE_PROGRESS 0x837a74 0x46cd00
34 17 PATCH_DOWNLOADED 0x837a60 0x46cd0a
35 18 SUGGEST_REALM 0x837a50 0x46cd14
36 19 SUGGEST_REALM_WRONG_PVP 0x837a38 0x46cd1e
37 20 SUGGEST_REALM_WRONG_CATEGORY 0x837a18 0x46cd28
38 21 SHOW_SERVER_ALERT 0x837a04 0x46cd32
39 22 FRAMES_LOADED 0x8379f4 0x46cd3c
40 23 FORCE_RENAME_CHARACTER 0x8379dc 0x46cd46
41 24 SHOW_SURVEY_NOTIFICATION 0x8379c0 0x46cd50
42 25 PLAYER_ENTER_PIN 0x8379ac 0x46cd5a
+212
View File
@@ -0,0 +1,212 @@
# The glue screens' Lua surface in the 1.12.1 client (build 5875): every function in `_G`
# once the glue builder 0x46a7b0 has installed its registrars, before GlueXML loads.
# Generated by scripts/gen-reference-glue-surface.py from reference/1.12-shapes.tsv and
# reference/1.12-globals.tsv; the generator's header carries the registrars and the method.
#
# scope base = FrameScript's Lua init 0x7039e0, which both states run; shared = a
# registrar the in-game state calls too (0x51bb20, 0x706350, 0x4587f0); glue =
# a glue-only registrar table (0x46ddf0, 0x4736c0, 0x471a20, 0x46f480).
# fn, table_va, arity as 1.12-shapes.tsv has them. A base alias the compat chunk
# 0x8722e8 defines carries the library binding it names (`tinsert` is table
# 0x822d40's `insert`), or fn `lua` for a degree-converting closure; math's
# global `__pow` (0x7fb050) has no table.
# world_fn a glue row's in-game namesake, a different binding; `-` if none.
# note shape-differs = both shapes exact, and the glue binding takes or returns a
# different count than its in-game namesake.
# 195 functions: 76 base, 14 shared, 105 glue; 6 glue rows differ in shape from their in-game namesake.
scope registrar name fn table_va arity world_fn note
base 0x7039e0 assert 0x7031a0 0x811e28 1 -
base 0x7039e0 collectgarbage 0x703250 0x811e28 0 -
base 0x7039e0 date 0x7033a0 0x811e28 1 -
base 0x7039e0 debugbreak 0x702820 0x811e28 0 -
base 0x7039e0 debugdump 0x702810 0x811e28 0 -
base 0x7039e0 debuginfo 0x7027f0 0x811e28 0 -
base 0x7039e0 debugload 0x7027e0 0x811e28 0 -
base 0x7039e0 debugprint 0x702800 0x811e28 0 -
base 0x7039e0 debugprofilestart 0x702840 0x811e28 0 -
base 0x7039e0 debugprofilestop 0x702860 0x811e28 1 -
base 0x7039e0 debugstack 0x703760 0x811e28 1 -
base 0x7039e0 debugtimestamp 0x702830 0x811e28 0 -
base 0x7039e0 error 0x702970 0x811e28 ? -
base 0x7039e0 gcinfo 0x703200 0x811e28 2 -
base 0x7039e0 geterrorhandler 0x702950 0x811e28 1 -
base 0x7039e0 getfenv 0x702ac0 0x811e28 1 -
base 0x7039e0 getglobal 0x7027b0 0x811e28 1 -
base 0x7039e0 getmetatable 0x7029f0 0x811e28 1 -
base 0x7039e0 ipairs 0x702cd0 0x811e28 ? -
base 0x7039e0 loadstring 0x703280 0x811e28 1|2 -
base 0x7039e0 next 0x702c90 0x811e28 1|2 -
base 0x7039e0 pairs 0x702da0 0x811e28 3 -
base 0x7039e0 pcall 0x7032f0 0x811e28 ? -
base 0x7039e0 rawequal 0x7030f0 0x811e28 1 -
base 0x7039e0 rawget 0x703130 0x811e28 1 -
base 0x7039e0 rawset 0x703160 0x811e28 1 -
base 0x7039e0 seterrorhandler 0x702900 0x811e28 0 -
base 0x7039e0 setfenv 0x702be0 0x811e28 0 -
base 0x7039e0 setglobal 0x702770 0x811e28 0 -
base 0x7039e0 setmetatable 0x702a40 0x811e28 1 -
base 0x7039e0 time 0x7035a0 0x811e28 1 -
base 0x7039e0 tonumber 0x702e30 0x811e28 1 -
base 0x7039e0 tostring 0x702f40 0x811e28 1 -
base 0x7039e0 type 0x702df0 0x811e28 1 -
base 0x7039e0 unpack 0x7030a0 0x811e28 ? -
base 0x7039e0 xpcall 0x703340 0x811e28 ? -
shared 0x51bb20 AppendToFile 0x515f90 0x8503f4 ? -
shared 0x51bb20 ConsoleExec 0x515f10 0x8503f4 0 -
shared 0x51bb20 DeleteFile 0x515f70 0x8503f4 ? -
shared 0x51bb20 GetGameTime 0x515ee0 0x8503f4 2 -
shared 0x51bb20 GetTime 0x515ea0 0x8503f4 1 -
shared 0x51bb20 ReadFile 0x515f50 0x8503f4 ? -
shared 0x706350 CreateFont 0x706040 0x872e74 1 -
shared 0x706350 CreateFrame 0x7060b0 0x872e74 1 -
shared 0x706350 EnumerateFrames 0x705f60 0x872e74 1 -
shared 0x706350 GetNumFrames 0x705f00 0x872e74 1 -
glue 0x46ddf0 AcceptContest 0x46d150 0x8373b8 0 -
glue 0x46ddf0 AcceptEULA 0x46d070 0x8373b8 0 -
glue 0x46ddf0 AcceptScanning 0x46d0e0 0x8373b8 0 -
glue 0x46ddf0 AcceptTOS 0x46d000 0x8373b8 0 -
glue 0x46ddf0 ContestAccepted 0x46d120 0x8373b8 1 -
glue 0x46ddf0 DefaultServerLogin 0x46d160 0x8373b8 0 -
glue 0x46ddf0 DisableAddOn 0x46d8a0 0x8373b8 0 0x48e760 shape-differs: argc 2 here, 1 in-game (0x48e760)
glue 0x46ddf0 DisableAllAddOns 0x46d940 0x8373b8 0 0x48e7f0 shape-differs: argc 1 here, 0 in-game (0x48e7f0)
glue 0x46ddf0 DisconnectFromServer 0x46d340 0x8373b8 0 -
glue 0x46ddf0 EULAAccepted 0x46d040 0x8373b8 1 -
glue 0x46ddf0 EnableAddOn 0x46d7b0 0x8373b8 0 0x48e690 shape-differs: argc 2 here, 1 in-game (0x48e690)
glue 0x46ddf0 EnableAllAddOns 0x46d850 0x8373b8 0 0x48e720 shape-differs: argc 1 here, 0 in-game (0x48e720)
glue 0x46ddf0 EnterWorld 0x46d3c0 0x8373b8 0 -
glue 0x46ddf0 GetAddOnDependencies 0x46d650 0x8373b8 ? 0x48e5e0
glue 0x46ddf0 GetAddOnEnableState 0x46d6f0 0x8373b8 1 -
glue 0x46ddf0 GetAddOnInfo 0x46d460 0x8373b8 8 0x48e390 shape-differs: arity 8 here, 7 in-game (0x48e390)
glue 0x46ddf0 GetBillingPlan 0x46dc40 0x8373b8 3 -
glue 0x46ddf0 GetBillingTimeRemaining 0x46dbf0 0x8373b8 1 -
glue 0x46ddf0 GetBillingTimeRested 0x46dcf0 0x8373b8 1 0x48ec50
glue 0x46ddf0 GetBuildInfo 0x46cd70 0x8373b8 5 0x4884a0 shape-differs: arity 5 here, 3 in-game (0x4884a0)
glue 0x46ddf0 GetCursorPosition 0x46dad0 0x8373b8 2 0x48b820
glue 0x46ddf0 GetLocale 0x46ce40 0x8373b8 1 0x48d8b0
glue 0x46ddf0 GetMovieResolution 0x46cf10 0x8373b8 1 -
glue 0x46ddf0 GetMovieSubtitles 0x46dbc0 0x8373b8 1 -
glue 0x46ddf0 GetNumAddOns 0x46d420 0x8373b8 1 0x48e350
glue 0x46ddf0 GetSavedAccountName 0x46cdd0 0x8373b8 1 -
glue 0x46ddf0 GetScreenHeight 0x46dd80 0x8373b8 1 0x48b4d0
glue 0x46ddf0 GetScreenWidth 0x46dd30 0x8373b8 1 0x48b480
glue 0x46ddf0 GetScriptMemory 0x46da10 0x8373b8 1 -
glue 0x46ddf0 GetServerName 0x46d280 0x8373b8 3 -
glue 0x46ddf0 HideCursor 0x46db70 0x8373b8 0 -
glue 0x46ddf0 IsAddonVersionCheckEnabled 0x46d9b0 0x8373b8 1 -
glue 0x46ddf0 IsConnectedToServer 0x46d380 0x8373b8 1 -
glue 0x46ddf0 LaunchAddOnURL 0x46d5e0 0x8373b8 0 -
glue 0x46ddf0 LaunchURL 0x46cf60 0x8373b8 0 -
glue 0x46ddf0 PINEntered 0x46d1c0 0x8373b8 0 -
glue 0x46ddf0 PatchDownloadApply 0x46d410 0x8373b8 0 -
glue 0x46ddf0 PatchDownloadCancel 0x46d400 0x8373b8 0 -
glue 0x46ddf0 PatchDownloadProgress 0x46d3e0 0x8373b8 1 -
glue 0x46ddf0 PlayCreditsMusic 0x46cef0 0x8373b8 0 -
glue 0x46ddf0 PlayGlueMusic 0x46ceb0 0x8373b8 0 -
glue 0x46ddf0 QuitGame 0x46cea0 0x8373b8 0 -
glue 0x46ddf0 ResetAddOns 0x46d9a0 0x8373b8 0 -
glue 0x46ddf0 SaveAddOns 0x46d990 0x8373b8 0 -
glue 0x46ddf0 ScanningAccepted 0x46d0b0 0x8373b8 1 -
glue 0x46ddf0 Screenshot 0x46d3d0 0x8373b8 0 0x4893d0
glue 0x46ddf0 SetAddonVersionCheck 0x46d9e0 0x8373b8 0 -
glue 0x46ddf0 SetCurrentScreen 0x46ce60 0x8373b8 0 -
glue 0x46ddf0 SetMovieSubtitles 0x46db80 0x8373b8 0 -
glue 0x46ddf0 SetSavedAccountName 0x46cdf0 0x8373b8 0 -
glue 0x46ddf0 SetScriptMemory 0x46da50 0x8373b8 0 -
glue 0x46ddf0 ShowContestNotice 0x46d0f0 0x8373b8 1 -
glue 0x46ddf0 ShowCursor 0x46db60 0x8373b8 0 -
glue 0x46ddf0 ShowEULANotice 0x46d010 0x8373b8 1 -
glue 0x46ddf0 ShowScanningNotice 0x46d080 0x8373b8 1 -
glue 0x46ddf0 ShowTOSNotice 0x46cfa0 0x8373b8 1 -
glue 0x46ddf0 StatusDialogClick 0x46d270 0x8373b8 0 -
glue 0x46ddf0 StopGlueMusic 0x46cf00 0x8373b8 0 -
glue 0x46ddf0 SurveyNotificationDone 0x46ddd0 0x8373b8 1 -
glue 0x46ddf0 TOSAccepted 0x46cfd0 0x8373b8 1 -
glue 0x4736c0 DeleteCharacter 0x4734c0 0x838578 0 -
glue 0x4736c0 GetCharacterInfo 0x4732a0 0x838578 8 -
glue 0x4736c0 GetCharacterListUpdate 0x473270 0x838578 0 -
glue 0x4736c0 GetCharacterSelectFacing 0x473650 0x838578 1 -
glue 0x4736c0 GetNumCharacters 0x473280 0x838578 1 -
glue 0x4736c0 RenameCharacter 0x473520 0x838578 0|1 -
glue 0x4736c0 SelectCharacter 0x473470 0x838578 0 -
glue 0x4736c0 SetCharSelectBackground 0x473230 0x838578 0 -
glue 0x4736c0 SetCharSelectModelFrame 0x4731c0 0x838578 0 -
glue 0x4736c0 SetCharacterSelectFacing 0x473670 0x838578 1 -
glue 0x4736c0 UpdateSelectionCustomizationScene 0x473640 0x838578 0 -
glue 0x471a20 CreateCharacter 0x4719e0 0x8380f8 0 -
glue 0x471a20 CycleCharCustomization 0x471890 0x8380f8 0 -
glue 0x471a20 GetAvailableRaces 0x471520 0x8380f8 ? -
glue 0x471a20 GetCharacterCreateFacing 0x471910 0x8380f8 1 -
glue 0x471a20 GetClassesForRace 0x471560 0x8380f8 ? -
glue 0x471a20 GetFacialHairCustomization 0x471620 0x8380f8 1 -
glue 0x471a20 GetFactionForRace 0x471450 0x8380f8 2 -
glue 0x471a20 GetHairCustomization 0x4715d0 0x8380f8 1 -
glue 0x471a20 GetNameForRace 0x4713f0 0x8380f8 2 -
glue 0x471a20 GetRandomName 0x471980 0x8380f8 1 -
glue 0x471a20 GetSelectedClass 0x4716e0 0x8380f8 2 -
glue 0x471a20 GetSelectedRace 0x471680 0x8380f8 1 -
glue 0x471a20 GetSelectedSex 0x4716b0 0x8380f8 1 -
glue 0x471a20 HasCharCustomization 0x471820 0x8380f8 1 -
glue 0x471a20 RandomizeCharCustomization 0x471900 0x8380f8 0 -
glue 0x471a20 ResetCharCustomize 0x4713e0 0x8380f8 0 -
glue 0x471a20 SetCharCustomizeBackground 0x4713a0 0x8380f8 0 -
glue 0x471a20 SetCharCustomizeFrame 0x471330 0x8380f8 0 -
glue 0x471a20 SetCharacterCreateFacing 0x471930 0x8380f8 1 -
glue 0x471a20 SetSelectedClass 0x4717c0 0x8380f8 0 -
glue 0x471a20 SetSelectedRace 0x471740 0x8380f8 0 -
glue 0x471a20 SetSelectedSex 0x471780 0x8380f8 0 -
glue 0x471a20 UpdateCustomizationBackground 0x471800 0x8380f8 0 -
glue 0x471a20 UpdateCustomizationScene 0x471810 0x8380f8 0 -
glue 0x46f480 CancelRealmListQuery 0x46ed10 0x837ea0 0 -
glue 0x46f480 ChangeRealm 0x46f0e0 0x837ea0 0 -
glue 0x46f480 GetNumRealms 0x46ed30 0x837ea0 1 -
glue 0x46f480 GetRealmCategories 0x46f1f0 0x837ea0 ? -
glue 0x46f480 GetRealmInfo 0x46ee40 0x837ea0 8 -
glue 0x46f480 GetSelectedCategory 0x46f400 0x837ea0 1 -
glue 0x46f480 RealmListDialogCancelled 0x46ed20 0x837ea0 0 -
glue 0x46f480 RequestRealmList 0x46ecf0 0x837ea0 0 -
glue 0x46f480 SetPreferredInfo 0x46f2c0 0x837ea0 0 -
glue 0x46f480 SortRealms 0x46f330 0x837ea0 0 -
shared 0x4587f0 PlayMusic 0x458720 0x835a50 0 -
shared 0x4587f0 PlaySound 0x4586d0 0x835a50 0 -
shared 0x4587f0 PlaySoundFile 0x458780 0x835a50 1 -
shared 0x4587f0 StopMusic 0x458770 0x835a50 0 -
base 0x7039e0 foreach 0x7fb590 0x822d40 0|1 -
base 0x7039e0 foreachi 0x7fb4e0 0x822d40 0|1 -
base 0x7039e0 getn 0x7fb630 0x822d40 1 -
base 0x7039e0 tinsert 0x7fb6b0 0x822d40 0 -
base 0x7039e0 tremove 0x7fb770 0x822d40 0|1 -
base 0x7039e0 sort 0x7fb920 0x822d40 0 -
base 0x7039e0 abs 0x7fade0 0x822c60 1 -
base 0x7039e0 acos lua - ? -
base 0x7039e0 asin lua - ? -
base 0x7039e0 atan lua - ? -
base 0x7039e0 atan2 lua - ? -
base 0x7039e0 ceil 0x7faf70 0x822c60 1 -
base 0x7039e0 cos lua - ? -
base 0x7039e0 deg 0x7fb140 0x822c60 1 -
base 0x7039e0 exp 0x7fb100 0x822c60 1 -
base 0x7039e0 floor 0x7fafa0 0x822c60 1 -
base 0x7039e0 frexp 0x7fb1a0 0x822c60 2 -
base 0x7039e0 ldexp 0x7fb1f0 0x822c60 1 -
base 0x7039e0 log 0x7fb0a0 0x822c60 1 -
base 0x7039e0 log10 0x7fb0d0 0x822c60 1 -
base 0x7039e0 max 0x7fb2a0 0x822c60 1 -
base 0x7039e0 min 0x7fb230 0x822c60 1 -
base 0x7039e0 mod 0x7fafd0 0x822c60 1 -
base 0x7039e0 rad 0x7fb170 0x822c60 1 -
base 0x7039e0 random 0x7fb310 0x822c60 1 -
base 0x7039e0 randomseed 0x7fb450 0x822c60 0 -
base 0x7039e0 sin lua - ? -
base 0x7039e0 sqrt 0x7fb020 0x822c60 1 -
base 0x7039e0 tan lua - ? -
base 0x7039e0 strbyte 0x7fc210 0x822d88 0|1 -
base 0x7039e0 strchar 0x7fc280 0x822d88 1 -
base 0x7039e0 strfind 0x7fc3b0 0x822d88 ? -
base 0x7039e0 format 0x7fd3d0 0x822d88 1 -
base 0x7039e0 gsub 0x7fd0e0 0x822d88 2 -
base 0x7039e0 strlen 0x7fbcf0 0x822d88 1 -
base 0x7039e0 strlower 0x7fbdf0 0x822d88 1 -
base 0x7039e0 strrep 0x7fc1a0 0x822d88 1 -
base 0x7039e0 strsub 0x7fbd30 0x822d88 1 -
base 0x7039e0 strupper 0x7fc0c0 0x822d88 1 -
base 0x7039e0 __pow 0x7fb050 - 1 -
1 # The glue screens' Lua surface in the 1.12.1 client (build 5875): every function in `_G`
2 # once the glue builder 0x46a7b0 has installed its registrars, before GlueXML loads.
3 # Generated by scripts/gen-reference-glue-surface.py from reference/1.12-shapes.tsv and
4 # reference/1.12-globals.tsv; the generator's header carries the registrars and the method.
5 #
6 # scope base = FrameScript's Lua init 0x7039e0, which both states run; shared = a
7 # registrar the in-game state calls too (0x51bb20, 0x706350, 0x4587f0); glue =
8 # a glue-only registrar table (0x46ddf0, 0x4736c0, 0x471a20, 0x46f480).
9 # fn, table_va, arity as 1.12-shapes.tsv has them. A base alias the compat chunk
10 # 0x8722e8 defines carries the library binding it names (`tinsert` is table
11 # 0x822d40's `insert`), or fn `lua` for a degree-converting closure; math's
12 # global `__pow` (0x7fb050) has no table.
13 # world_fn a glue row's in-game namesake, a different binding; `-` if none.
14 # note shape-differs = both shapes exact, and the glue binding takes or returns a
15 # different count than its in-game namesake.
16 # 195 functions: 76 base, 14 shared, 105 glue; 6 glue rows differ in shape from their in-game namesake.
17 scope registrar name fn table_va arity world_fn note
18 base 0x7039e0 assert 0x7031a0 0x811e28 1 -
19 base 0x7039e0 collectgarbage 0x703250 0x811e28 0 -
20 base 0x7039e0 date 0x7033a0 0x811e28 1 -
21 base 0x7039e0 debugbreak 0x702820 0x811e28 0 -
22 base 0x7039e0 debugdump 0x702810 0x811e28 0 -
23 base 0x7039e0 debuginfo 0x7027f0 0x811e28 0 -
24 base 0x7039e0 debugload 0x7027e0 0x811e28 0 -
25 base 0x7039e0 debugprint 0x702800 0x811e28 0 -
26 base 0x7039e0 debugprofilestart 0x702840 0x811e28 0 -
27 base 0x7039e0 debugprofilestop 0x702860 0x811e28 1 -
28 base 0x7039e0 debugstack 0x703760 0x811e28 1 -
29 base 0x7039e0 debugtimestamp 0x702830 0x811e28 0 -
30 base 0x7039e0 error 0x702970 0x811e28 ? -
31 base 0x7039e0 gcinfo 0x703200 0x811e28 2 -
32 base 0x7039e0 geterrorhandler 0x702950 0x811e28 1 -
33 base 0x7039e0 getfenv 0x702ac0 0x811e28 1 -
34 base 0x7039e0 getglobal 0x7027b0 0x811e28 1 -
35 base 0x7039e0 getmetatable 0x7029f0 0x811e28 1 -
36 base 0x7039e0 ipairs 0x702cd0 0x811e28 ? -
37 base 0x7039e0 loadstring 0x703280 0x811e28 1|2 -
38 base 0x7039e0 next 0x702c90 0x811e28 1|2 -
39 base 0x7039e0 pairs 0x702da0 0x811e28 3 -
40 base 0x7039e0 pcall 0x7032f0 0x811e28 ? -
41 base 0x7039e0 rawequal 0x7030f0 0x811e28 1 -
42 base 0x7039e0 rawget 0x703130 0x811e28 1 -
43 base 0x7039e0 rawset 0x703160 0x811e28 1 -
44 base 0x7039e0 seterrorhandler 0x702900 0x811e28 0 -
45 base 0x7039e0 setfenv 0x702be0 0x811e28 0 -
46 base 0x7039e0 setglobal 0x702770 0x811e28 0 -
47 base 0x7039e0 setmetatable 0x702a40 0x811e28 1 -
48 base 0x7039e0 time 0x7035a0 0x811e28 1 -
49 base 0x7039e0 tonumber 0x702e30 0x811e28 1 -
50 base 0x7039e0 tostring 0x702f40 0x811e28 1 -
51 base 0x7039e0 type 0x702df0 0x811e28 1 -
52 base 0x7039e0 unpack 0x7030a0 0x811e28 ? -
53 base 0x7039e0 xpcall 0x703340 0x811e28 ? -
54 shared 0x51bb20 AppendToFile 0x515f90 0x8503f4 ? -
55 shared 0x51bb20 ConsoleExec 0x515f10 0x8503f4 0 -
56 shared 0x51bb20 DeleteFile 0x515f70 0x8503f4 ? -
57 shared 0x51bb20 GetGameTime 0x515ee0 0x8503f4 2 -
58 shared 0x51bb20 GetTime 0x515ea0 0x8503f4 1 -
59 shared 0x51bb20 ReadFile 0x515f50 0x8503f4 ? -
60 shared 0x706350 CreateFont 0x706040 0x872e74 1 -
61 shared 0x706350 CreateFrame 0x7060b0 0x872e74 1 -
62 shared 0x706350 EnumerateFrames 0x705f60 0x872e74 1 -
63 shared 0x706350 GetNumFrames 0x705f00 0x872e74 1 -
64 glue 0x46ddf0 AcceptContest 0x46d150 0x8373b8 0 -
65 glue 0x46ddf0 AcceptEULA 0x46d070 0x8373b8 0 -
66 glue 0x46ddf0 AcceptScanning 0x46d0e0 0x8373b8 0 -
67 glue 0x46ddf0 AcceptTOS 0x46d000 0x8373b8 0 -
68 glue 0x46ddf0 ContestAccepted 0x46d120 0x8373b8 1 -
69 glue 0x46ddf0 DefaultServerLogin 0x46d160 0x8373b8 0 -
70 glue 0x46ddf0 DisableAddOn 0x46d8a0 0x8373b8 0 0x48e760 shape-differs: argc 2 here, 1 in-game (0x48e760)
71 glue 0x46ddf0 DisableAllAddOns 0x46d940 0x8373b8 0 0x48e7f0 shape-differs: argc 1 here, 0 in-game (0x48e7f0)
72 glue 0x46ddf0 DisconnectFromServer 0x46d340 0x8373b8 0 -
73 glue 0x46ddf0 EULAAccepted 0x46d040 0x8373b8 1 -
74 glue 0x46ddf0 EnableAddOn 0x46d7b0 0x8373b8 0 0x48e690 shape-differs: argc 2 here, 1 in-game (0x48e690)
75 glue 0x46ddf0 EnableAllAddOns 0x46d850 0x8373b8 0 0x48e720 shape-differs: argc 1 here, 0 in-game (0x48e720)
76 glue 0x46ddf0 EnterWorld 0x46d3c0 0x8373b8 0 -
77 glue 0x46ddf0 GetAddOnDependencies 0x46d650 0x8373b8 ? 0x48e5e0
78 glue 0x46ddf0 GetAddOnEnableState 0x46d6f0 0x8373b8 1 -
79 glue 0x46ddf0 GetAddOnInfo 0x46d460 0x8373b8 8 0x48e390 shape-differs: arity 8 here, 7 in-game (0x48e390)
80 glue 0x46ddf0 GetBillingPlan 0x46dc40 0x8373b8 3 -
81 glue 0x46ddf0 GetBillingTimeRemaining 0x46dbf0 0x8373b8 1 -
82 glue 0x46ddf0 GetBillingTimeRested 0x46dcf0 0x8373b8 1 0x48ec50
83 glue 0x46ddf0 GetBuildInfo 0x46cd70 0x8373b8 5 0x4884a0 shape-differs: arity 5 here, 3 in-game (0x4884a0)
84 glue 0x46ddf0 GetCursorPosition 0x46dad0 0x8373b8 2 0x48b820
85 glue 0x46ddf0 GetLocale 0x46ce40 0x8373b8 1 0x48d8b0
86 glue 0x46ddf0 GetMovieResolution 0x46cf10 0x8373b8 1 -
87 glue 0x46ddf0 GetMovieSubtitles 0x46dbc0 0x8373b8 1 -
88 glue 0x46ddf0 GetNumAddOns 0x46d420 0x8373b8 1 0x48e350
89 glue 0x46ddf0 GetSavedAccountName 0x46cdd0 0x8373b8 1 -
90 glue 0x46ddf0 GetScreenHeight 0x46dd80 0x8373b8 1 0x48b4d0
91 glue 0x46ddf0 GetScreenWidth 0x46dd30 0x8373b8 1 0x48b480
92 glue 0x46ddf0 GetScriptMemory 0x46da10 0x8373b8 1 -
93 glue 0x46ddf0 GetServerName 0x46d280 0x8373b8 3 -
94 glue 0x46ddf0 HideCursor 0x46db70 0x8373b8 0 -
95 glue 0x46ddf0 IsAddonVersionCheckEnabled 0x46d9b0 0x8373b8 1 -
96 glue 0x46ddf0 IsConnectedToServer 0x46d380 0x8373b8 1 -
97 glue 0x46ddf0 LaunchAddOnURL 0x46d5e0 0x8373b8 0 -
98 glue 0x46ddf0 LaunchURL 0x46cf60 0x8373b8 0 -
99 glue 0x46ddf0 PINEntered 0x46d1c0 0x8373b8 0 -
100 glue 0x46ddf0 PatchDownloadApply 0x46d410 0x8373b8 0 -
101 glue 0x46ddf0 PatchDownloadCancel 0x46d400 0x8373b8 0 -
102 glue 0x46ddf0 PatchDownloadProgress 0x46d3e0 0x8373b8 1 -
103 glue 0x46ddf0 PlayCreditsMusic 0x46cef0 0x8373b8 0 -
104 glue 0x46ddf0 PlayGlueMusic 0x46ceb0 0x8373b8 0 -
105 glue 0x46ddf0 QuitGame 0x46cea0 0x8373b8 0 -
106 glue 0x46ddf0 ResetAddOns 0x46d9a0 0x8373b8 0 -
107 glue 0x46ddf0 SaveAddOns 0x46d990 0x8373b8 0 -
108 glue 0x46ddf0 ScanningAccepted 0x46d0b0 0x8373b8 1 -
109 glue 0x46ddf0 Screenshot 0x46d3d0 0x8373b8 0 0x4893d0
110 glue 0x46ddf0 SetAddonVersionCheck 0x46d9e0 0x8373b8 0 -
111 glue 0x46ddf0 SetCurrentScreen 0x46ce60 0x8373b8 0 -
112 glue 0x46ddf0 SetMovieSubtitles 0x46db80 0x8373b8 0 -
113 glue 0x46ddf0 SetSavedAccountName 0x46cdf0 0x8373b8 0 -
114 glue 0x46ddf0 SetScriptMemory 0x46da50 0x8373b8 0 -
115 glue 0x46ddf0 ShowContestNotice 0x46d0f0 0x8373b8 1 -
116 glue 0x46ddf0 ShowCursor 0x46db60 0x8373b8 0 -
117 glue 0x46ddf0 ShowEULANotice 0x46d010 0x8373b8 1 -
118 glue 0x46ddf0 ShowScanningNotice 0x46d080 0x8373b8 1 -
119 glue 0x46ddf0 ShowTOSNotice 0x46cfa0 0x8373b8 1 -
120 glue 0x46ddf0 StatusDialogClick 0x46d270 0x8373b8 0 -
121 glue 0x46ddf0 StopGlueMusic 0x46cf00 0x8373b8 0 -
122 glue 0x46ddf0 SurveyNotificationDone 0x46ddd0 0x8373b8 1 -
123 glue 0x46ddf0 TOSAccepted 0x46cfd0 0x8373b8 1 -
124 glue 0x4736c0 DeleteCharacter 0x4734c0 0x838578 0 -
125 glue 0x4736c0 GetCharacterInfo 0x4732a0 0x838578 8 -
126 glue 0x4736c0 GetCharacterListUpdate 0x473270 0x838578 0 -
127 glue 0x4736c0 GetCharacterSelectFacing 0x473650 0x838578 1 -
128 glue 0x4736c0 GetNumCharacters 0x473280 0x838578 1 -
129 glue 0x4736c0 RenameCharacter 0x473520 0x838578 0|1 -
130 glue 0x4736c0 SelectCharacter 0x473470 0x838578 0 -
131 glue 0x4736c0 SetCharSelectBackground 0x473230 0x838578 0 -
132 glue 0x4736c0 SetCharSelectModelFrame 0x4731c0 0x838578 0 -
133 glue 0x4736c0 SetCharacterSelectFacing 0x473670 0x838578 1 -
134 glue 0x4736c0 UpdateSelectionCustomizationScene 0x473640 0x838578 0 -
135 glue 0x471a20 CreateCharacter 0x4719e0 0x8380f8 0 -
136 glue 0x471a20 CycleCharCustomization 0x471890 0x8380f8 0 -
137 glue 0x471a20 GetAvailableRaces 0x471520 0x8380f8 ? -
138 glue 0x471a20 GetCharacterCreateFacing 0x471910 0x8380f8 1 -
139 glue 0x471a20 GetClassesForRace 0x471560 0x8380f8 ? -
140 glue 0x471a20 GetFacialHairCustomization 0x471620 0x8380f8 1 -
141 glue 0x471a20 GetFactionForRace 0x471450 0x8380f8 2 -
142 glue 0x471a20 GetHairCustomization 0x4715d0 0x8380f8 1 -
143 glue 0x471a20 GetNameForRace 0x4713f0 0x8380f8 2 -
144 glue 0x471a20 GetRandomName 0x471980 0x8380f8 1 -
145 glue 0x471a20 GetSelectedClass 0x4716e0 0x8380f8 2 -
146 glue 0x471a20 GetSelectedRace 0x471680 0x8380f8 1 -
147 glue 0x471a20 GetSelectedSex 0x4716b0 0x8380f8 1 -
148 glue 0x471a20 HasCharCustomization 0x471820 0x8380f8 1 -
149 glue 0x471a20 RandomizeCharCustomization 0x471900 0x8380f8 0 -
150 glue 0x471a20 ResetCharCustomize 0x4713e0 0x8380f8 0 -
151 glue 0x471a20 SetCharCustomizeBackground 0x4713a0 0x8380f8 0 -
152 glue 0x471a20 SetCharCustomizeFrame 0x471330 0x8380f8 0 -
153 glue 0x471a20 SetCharacterCreateFacing 0x471930 0x8380f8 1 -
154 glue 0x471a20 SetSelectedClass 0x4717c0 0x8380f8 0 -
155 glue 0x471a20 SetSelectedRace 0x471740 0x8380f8 0 -
156 glue 0x471a20 SetSelectedSex 0x471780 0x8380f8 0 -
157 glue 0x471a20 UpdateCustomizationBackground 0x471800 0x8380f8 0 -
158 glue 0x471a20 UpdateCustomizationScene 0x471810 0x8380f8 0 -
159 glue 0x46f480 CancelRealmListQuery 0x46ed10 0x837ea0 0 -
160 glue 0x46f480 ChangeRealm 0x46f0e0 0x837ea0 0 -
161 glue 0x46f480 GetNumRealms 0x46ed30 0x837ea0 1 -
162 glue 0x46f480 GetRealmCategories 0x46f1f0 0x837ea0 ? -
163 glue 0x46f480 GetRealmInfo 0x46ee40 0x837ea0 8 -
164 glue 0x46f480 GetSelectedCategory 0x46f400 0x837ea0 1 -
165 glue 0x46f480 RealmListDialogCancelled 0x46ed20 0x837ea0 0 -
166 glue 0x46f480 RequestRealmList 0x46ecf0 0x837ea0 0 -
167 glue 0x46f480 SetPreferredInfo 0x46f2c0 0x837ea0 0 -
168 glue 0x46f480 SortRealms 0x46f330 0x837ea0 0 -
169 shared 0x4587f0 PlayMusic 0x458720 0x835a50 0 -
170 shared 0x4587f0 PlaySound 0x4586d0 0x835a50 0 -
171 shared 0x4587f0 PlaySoundFile 0x458780 0x835a50 1 -
172 shared 0x4587f0 StopMusic 0x458770 0x835a50 0 -
173 base 0x7039e0 foreach 0x7fb590 0x822d40 0|1 -
174 base 0x7039e0 foreachi 0x7fb4e0 0x822d40 0|1 -
175 base 0x7039e0 getn 0x7fb630 0x822d40 1 -
176 base 0x7039e0 tinsert 0x7fb6b0 0x822d40 0 -
177 base 0x7039e0 tremove 0x7fb770 0x822d40 0|1 -
178 base 0x7039e0 sort 0x7fb920 0x822d40 0 -
179 base 0x7039e0 abs 0x7fade0 0x822c60 1 -
180 base 0x7039e0 acos lua - ? -
181 base 0x7039e0 asin lua - ? -
182 base 0x7039e0 atan lua - ? -
183 base 0x7039e0 atan2 lua - ? -
184 base 0x7039e0 ceil 0x7faf70 0x822c60 1 -
185 base 0x7039e0 cos lua - ? -
186 base 0x7039e0 deg 0x7fb140 0x822c60 1 -
187 base 0x7039e0 exp 0x7fb100 0x822c60 1 -
188 base 0x7039e0 floor 0x7fafa0 0x822c60 1 -
189 base 0x7039e0 frexp 0x7fb1a0 0x822c60 2 -
190 base 0x7039e0 ldexp 0x7fb1f0 0x822c60 1 -
191 base 0x7039e0 log 0x7fb0a0 0x822c60 1 -
192 base 0x7039e0 log10 0x7fb0d0 0x822c60 1 -
193 base 0x7039e0 max 0x7fb2a0 0x822c60 1 -
194 base 0x7039e0 min 0x7fb230 0x822c60 1 -
195 base 0x7039e0 mod 0x7fafd0 0x822c60 1 -
196 base 0x7039e0 rad 0x7fb170 0x822c60 1 -
197 base 0x7039e0 random 0x7fb310 0x822c60 1 -
198 base 0x7039e0 randomseed 0x7fb450 0x822c60 0 -
199 base 0x7039e0 sin lua - ? -
200 base 0x7039e0 sqrt 0x7fb020 0x822c60 1 -
201 base 0x7039e0 tan lua - ? -
202 base 0x7039e0 strbyte 0x7fc210 0x822d88 0|1 -
203 base 0x7039e0 strchar 0x7fc280 0x822d88 1 -
204 base 0x7039e0 strfind 0x7fc3b0 0x822d88 ? -
205 base 0x7039e0 format 0x7fd3d0 0x822d88 1 -
206 base 0x7039e0 gsub 0x7fd0e0 0x822d88 2 -
207 base 0x7039e0 strlen 0x7fbcf0 0x822d88 1 -
208 base 0x7039e0 strlower 0x7fbdf0 0x822d88 1 -
209 base 0x7039e0 strrep 0x7fc1a0 0x822d88 1 -
210 base 0x7039e0 strsub 0x7fbd30 0x822d88 1 -
211 base 0x7039e0 strupper 0x7fc0c0 0x822d88 1 -
212 base 0x7039e0 __pow 0x7fb050 - 1 -
+21
View File
@@ -81,3 +81,24 @@ Reading it: `crates/benilla-app/src/ui_script/verb_event_gate.rs` is the gate. F
verb benilla registers, the module that registers the verb fires the event, or the pair is declared
there: `ELSEWHERE` (a feed fires it on the state the verb changes, and the named file is checked
too) or `GAP` (nothing does, with the reason).
## `1.12-glue-events.tsv` and `1.12-glue-surface.tsv`
The glue screens' own FrameScript state. The glue builder (`0x46a7b0`) gives Lua a different
surface and a different event table from the in-game state's, and GlueXML runs against those.
`1.12-glue-events.tsv` is the 26-entry event table (`0xb41e70`) the builder loads in place of the
in-game one (`0x703d90` at `0x46a88f`), so a glue event's id names a different event in-game. It is
vendored from the client's bytes, as `1.12-event-catalog.tsv` is.
`1.12-glue-surface.tsv` is every function in the glue state's `_G` before GlueXML loads: the base
set both states share (`0x7039e0`), the three registrars the in-game state calls too, and the four
glue-only tables, with each glue verb's in-game namesake and the six whose shape differs from it.
Regenerating: `scripts/gen-reference-glue-surface.py`, from the shapes and globals tables, so it
reproduces the committed file from any clone.
Reading them: `crates/benilla-ui/src/script/tests/glue_surface.rs` holds the surface table to the
shapes table, row for row by `table_va`, and the glue VM (`UiScript::new_glue`) to the surface table:
every function it exposes is a glue-state one, and every glue-state function it lacks is on the
file's `NOT_YET` list, which only shrinks. The same file holds the VM's event table to
`1.12-glue-events.tsv`.
+212
View File
@@ -0,0 +1,212 @@
#!/usr/bin/env python3
"""Regenerate `reference/1.12-glue-surface.tsv`: every Lua function the glue screens' VM has in `_G`.
scripts/gen-reference-glue-surface.py [--shapes FILE] [--globals FILE] [--out FILE]
The inputs are vendored under `reference/`, so this runs from any clone and reproduces the
committed table byte for byte.
The glue builder (`0x46a7b0`) starts from the bare base set `0x7039e0`: a rebuild resets the Lua
state (`0x703b80` -> `0x7039e0`, at `0x46a87b`), and the first build (`0x46a505`, passing 0) skips
that reset (`je` at `0x46a879`) and takes the state the startup init left (`0x402aee`). It then
calls the glue registrar `0x46abb0`, which installs seven registrar tables and nothing else:
0x51bb20 table 0x8503f4 6 shared: in-game, `0x51bb60` calls it before its own table
0x706350 table 0x872e74 4 shared: in-game at `0x490251`; then the 17 widget method tables
0x46ddf0 table 0x8373b8 60 glue core
0x4736c0 table 0x838578 11 character select
0x471a20 table 0x8380f8 24 character create
0x46f480 table 0x837ea0 10 realm list
0x4587f0 table 0x835a50 4 shared: in-game at `0x4902a6`
Each registrar is a `0x704120` loop over its table with the bound read from its own `cmp`, the
counts above. `0x7039e0`, FrameScript's Lua init, is the base set both states share: the 36-entry
base array `0x811e28` (`1.12-shapes.tsv`'s `baselib` table), the `string`, `table`, `math` and
`bit` libraries (fields, not globals, so not rows here), `math`'s global `__pow` ([`POW`]), and
the compat chunk `0x8722e8` run at `0x703b26`. The chunk's 39 function assignments are
[`COMPAT`]: 32 bind a library function under a bare name (`tinsert = table.insert`), each row
taking that binding's `fn` and table, and 7 wrap one in a degree-converting Lua closure (`sin`,
`acos`, ...), `fn` `lua`. Its one other assignment, `PI = math.pi`, is a number. Every engine
function `1.12-globals.tsv` has that no `global` or `baselib` row of the shapes table names must be
one of these, so a chunk the list missed fails the generator.
Columns:
scope base (0x7039e0) | shared (a registrar the in-game state calls too) | glue.
registrar the function that installs the row.
fn, table_va the binding and its table; a compat alias carries the library binding it names,
or fn `lua` and table `-` for a closure; `__pow` has no table.
arity the shapes table's return arity, `?` for a closure.
world_fn a glue row's in-game namesake from any other `global` table, else `-`. The in-game
state never sees a glue row, but the name is shared: a glue verb benilla builds is a
different binding from the in-game one of the same name.
note `shape-differs` where both shapes are exact and the argument count or the return
arity differs between the glue binding and its in-game namesake.
"""
import argparse
import os
import sys
# (registrar, table_va, rows, scope), in the order `0x46abb0` calls them.
REGISTRARS = [
(0x51BB20, 0x8503F4, 6, "shared"),
(0x706350, 0x872E74, 4, "shared"),
(0x46DDF0, 0x8373B8, 60, "glue"),
(0x4736C0, 0x838578, 11, "glue"),
(0x471A20, 0x8380F8, 24, "glue"),
(0x46F480, 0x837EA0, 10, "glue"),
(0x4587F0, 0x835A50, 4, "shared"),
]
BASE_INIT = 0x7039E0
BASE_TABLE = 0x811E28
BASE_ROWS = 36
# The libraries `0x7039e0` opens, by the name the compat chunk reaches them under.
LIBRARIES = {"table": 0x822D40, "math": 0x822C60, "string": 0x822D88, "bit": 0x822C18}
# The compat chunk's function assignments, in its order: (global, "library.field"), or None for a
# Lua closure that converts degrees around the `math` function of the same name.
COMPAT = [
("foreach", "table.foreach"), ("foreachi", "table.foreachi"), ("getn", "table.getn"),
("tinsert", "table.insert"), ("tremove", "table.remove"), ("sort", "table.sort"),
("abs", "math.abs"), ("acos", None), ("asin", None), ("atan", None), ("atan2", None),
("ceil", "math.ceil"), ("cos", None), ("deg", "math.deg"), ("exp", "math.exp"),
("floor", "math.floor"), ("frexp", "math.frexp"), ("ldexp", "math.ldexp"), ("log", "math.log"),
("log10", "math.log10"), ("max", "math.max"), ("min", "math.min"), ("mod", "math.mod"),
("rad", "math.rad"), ("random", "math.random"), ("randomseed", "math.randomseed"),
("sin", None), ("sqrt", "math.sqrt"), ("tan", None),
("strbyte", "string.byte"), ("strchar", "string.char"), ("strfind", "string.find"),
("format", "string.format"), ("gsub", "string.gsub"), ("strlen", "string.len"),
("strlower", "string.lower"), ("strrep", "string.rep"), ("strsub", "string.sub"),
("strupper", "string.upper"),
]
# `luaopen_math` 0x7fb470 also sets the global `__pow` to 0x7fb050 (`0x7fb4af`-`0x7fb4d2`), which
# Lua 5.0 calls for `^`.
POW = ("__pow", 0x7FB050)
REFERENCE = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "reference"))
def rows(path, header):
if not os.path.exists(path):
sys.exit(f"no table at {path}")
for line in open(path, encoding="utf-8"):
if line.startswith("#"):
continue
f = line.rstrip("\n").split("\t")
if f and f[0] == header:
continue
yield f
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--shapes", default=os.path.join(REFERENCE, "1.12-shapes.tsv"))
ap.add_argument("--globals", default=os.path.join(REFERENCE, "1.12-globals.tsv"))
ap.add_argument("--out", default=os.path.join(REFERENCE, "1.12-glue-surface.tsv"))
a = ap.parse_args()
# name, fn, pair_va, table_va, table_kind, argc, argc_conf, arity, arity_conf, ...
shapes = [f for f in rows(a.shapes, "name") if len(f) >= 9]
by_table = {}
for f in shapes:
by_table.setdefault(int(f[3], 16), []).append(f)
out = []
def take(table_va, n, registrar, scope):
got = by_table.get(table_va, [])
if len(got) != n:
sys.exit(f"table 0x{table_va:x}: the registrar loops {n} rows, the shapes table has {len(got)}")
for f in got:
out.append({"f": f, "scope": scope, "registrar": registrar})
take(BASE_TABLE, BASE_ROWS, BASE_INIT, "base")
for registrar, table_va, n, scope in REGISTRARS:
take(table_va, n, registrar, scope)
# The base set's Lua side: the compat chunk's aliases and `__pow`, each a binding of a library
# the shapes table carries, or a Lua closure.
fields = {}
for lib, table_va in LIBRARIES.items():
for f in by_table.get(table_va, []):
fields[f"{lib}.{f[0]}"] = f
aliases = []
for name, target in COMPAT:
if target is None:
aliases.append((name, "lua", "-", "?"))
continue
f = fields.get(target)
if f is None:
sys.exit(f"compat alias {name} = {target}: no such library binding in {a.shapes}")
aliases.append((name, f[1], f[3], f[7]))
aliases.append((POW[0], f"0x{POW[1]:x}", "-", "1"))
# A 1.12 engine function no registrar row names is one of the aliases, or the list is short.
registered = {f[0] for f in shapes if f[4] in ("global", "baselib")}
alias_names = {x[0] for x in aliases}
for f in rows(a.globals, "name"):
if len(f) >= 3 and f[1] == "function" and f[2] in ("engine", "lua"):
if f[0] not in registered and f[0] not in alias_names:
sys.exit(f"{f[0]} is a 1.12 engine function no registrar and no compat alias names")
# In-game namesakes: rows of every other `global` table, which only the in-game state installs.
ours = {t for _, t, _, _ in REGISTRARS}
world = {}
for f in shapes:
if f[4] == "global" and int(f[3], 16) not in ours:
world.setdefault(f[0], []).append(f)
lines = []
differs = 0
for r in out:
f = r["f"]
name, fn, table_va, argc, argc_conf, arity, arity_conf = f[0], f[1], f[3], f[5], f[6], f[7], f[8]
world_fn, note = "-", ""
if r["scope"] == "glue" and name in world:
w = world[name]
world_fn = "|".join(x[1] for x in w)
notes = []
for x in w:
if argc_conf == "exact" and x[6] == "exact" and argc != x[5]:
notes.append(f"argc {argc} here, {x[5]} in-game ({x[1]})")
if arity_conf == "exact" and x[8] == "exact" and arity != x[7]:
notes.append(f"arity {arity} here, {x[7]} in-game ({x[1]})")
if notes:
note = "shape-differs: " + "; ".join(notes)
differs += 1
lines.append((r["scope"], f"0x{r['registrar']:x}", name, fn, table_va, arity, world_fn, note))
for name, fn, table_va, arity in aliases:
lines.append(("base", f"0x{BASE_INIT:x}", name, fn, table_va, arity, "-", ""))
names = [l[2] for l in lines]
if len(names) != len(set(names)):
sys.exit("a name appears twice in the glue state; the table is keyed by name")
count = {s: sum(1 for l in lines if l[0] == s) for s in ("base", "shared", "glue")}
path = os.path.normpath(a.out)
with open(path, "w", encoding="utf-8") as fh:
fh.write(
"# The glue screens' Lua surface in the 1.12.1 client (build 5875): every function in `_G`\n"
"# once the glue builder 0x46a7b0 has installed its registrars, before GlueXML loads.\n"
"# Generated by scripts/gen-reference-glue-surface.py from reference/1.12-shapes.tsv and\n"
"# reference/1.12-globals.tsv; the generator's header carries the registrars and the method.\n"
"#\n"
"# scope base = FrameScript's Lua init 0x7039e0, which both states run; shared = a\n"
"# registrar the in-game state calls too (0x51bb20, 0x706350, 0x4587f0); glue =\n"
"# a glue-only registrar table (0x46ddf0, 0x4736c0, 0x471a20, 0x46f480).\n"
"# fn, table_va, arity as 1.12-shapes.tsv has them. A base alias the compat chunk\n"
"# 0x8722e8 defines carries the library binding it names (`tinsert` is table\n"
"# 0x822d40's `insert`), or fn `lua` for a degree-converting closure; math's\n"
"# global `__pow` (0x7fb050) has no table.\n"
"# world_fn a glue row's in-game namesake, a different binding; `-` if none.\n"
"# note shape-differs = both shapes exact, and the glue binding takes or returns a\n"
"# different count than its in-game namesake.\n"
f"# {len(lines)} functions: {count['base']} base, {count['shared']} shared, {count['glue']} glue;"
f" {differs} glue rows differ in shape from their in-game namesake.\n"
"scope\tregistrar\tname\tfn\ttable_va\tarity\tworld_fn\tnote\n"
)
for l in lines:
fh.write("\t".join(l) + "\n")
print(f"wrote {path}: {len(lines)} functions, {differs} shape differences")
if __name__ == "__main__":
main()