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:
co-authored by
Claude Opus 5.5
parent
985dabff44
commit
a9dd325bb7
@@ -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<'_> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
@@ -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 -
|
||||
|
@@ -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`.
|
||||
|
||||
Executable
+212
@@ -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()
|
||||
Reference in New Issue
Block a user