diff --git a/crates/benilla-app/src/char_create/mod.rs b/crates/benilla-app/src/char_create/mod.rs index b0d89aa5..ff13406c 100644 --- a/crates/benilla-app/src/char_create/mod.rs +++ b/crates/benilla-app/src/char_create/mod.rs @@ -48,11 +48,11 @@ impl Plugin for CharCreatePlugin { Update, ( screen::rescale_screen, - create_input, + create_input.run_if(crate::ui_script::glue::native_glue), debug_auto_create, debug_pick, debug_shot, - rotate_model, + rotate_model.run_if(crate::ui_script::glue::native_glue), refresh::refresh_dynamic, refresh_name_box, refresh::refresh_hover, diff --git a/crates/benilla-app/src/char_create/screen.rs b/crates/benilla-app/src/char_create/screen.rs index c7323048..b97b3a12 100644 --- a/crates/benilla-app/src/char_create/screen.rs +++ b/crates/benilla-app/src/char_create/screen.rs @@ -106,6 +106,7 @@ fn spawn_screen( let root = commands .spawn(( + crate::glue::NativeGlue, CharCreateUi { s }, DynTint::Backdrop, GlobalZIndex(SCREEN_Z), diff --git a/crates/benilla-app/src/char_select/addons.rs b/crates/benilla-app/src/char_select/addons.rs index 9c5c0bd9..342de262 100644 --- a/crates/benilla-app/src/char_select/addons.rs +++ b/crates/benilla-app/src/char_select/addons.rs @@ -720,6 +720,7 @@ fn spawn_panel( commands .spawn(( AddonsUi, + crate::glue::NativeGlue, GlobalZIndex(1200), // over the select screen's 1100, like the delete dialog // The reference's full-screen BACKGROUND layer, black at 0.75; it also keeps a stray // click off the screen behind. diff --git a/crates/benilla-app/src/char_select/dialog.rs b/crates/benilla-app/src/char_select/dialog.rs index ce4d150c..75051e9c 100644 --- a/crates/benilla-app/src/char_select/dialog.rs +++ b/crates/benilla-app/src/char_select/dialog.rs @@ -223,6 +223,7 @@ fn spawn_dialog( commands .spawn(( DialogUi, + crate::glue::NativeGlue, GlobalZIndex(1200), // over the select screen's 1100 Node { width: Val::Percent(100.0), diff --git a/crates/benilla-app/src/char_select/mod.rs b/crates/benilla-app/src/char_select/mod.rs index 52934906..f10afe43 100644 --- a/crates/benilla-app/src/char_select/mod.rs +++ b/crates/benilla-app/src/char_select/mod.rs @@ -101,17 +101,18 @@ impl Plugin for CharSelectPlugin { .chain(), ( screen::materialize_screen, - input::select_input, - input::rotate_model, + input::select_input.run_if(crate::ui_script::glue::native_glue), + input::rotate_model.run_if(crate::ui_script::glue::native_glue), debug_select_dialog, - dialog::drive_delete_dialog, + dialog::drive_delete_dialog.run_if(crate::ui_script::glue::native_glue), // Ahead of the dialog driver, so a refused delete shows the frame it lands. delete_result, // The one `GlueDialog`: here, the refused login and the refused delete. - crate::glue::dialog::drive_glue_dialog, + crate::glue::dialog::drive_glue_dialog + .run_if(crate::ui_script::glue::native_glue), debug_select_addons, debug_select_walk, - addons::drive_addons_panel, + addons::drive_addons_panel.run_if(crate::ui_script::glue::native_glue), refresh::refresh_list, refresh::refresh_banner_and_buttons, refresh::feed_glue_preview, diff --git a/crates/benilla-app/src/char_select/screen.rs b/crates/benilla-app/src/char_select/screen.rs index 354ce27f..6602fb61 100644 --- a/crates/benilla-app/src/char_select/screen.rs +++ b/crates/benilla-app/src/char_select/screen.rs @@ -145,6 +145,7 @@ fn spawn_screen( let root = commands .spawn(( + crate::glue::NativeGlue, CharSelectUi { with_art: art.button_up.is_some(), s, diff --git a/crates/benilla-app/src/game_plugins.rs b/crates/benilla-app/src/game_plugins.rs index 81e0ca2f..55d29e6f 100644 --- a/crates/benilla-app/src/game_plugins.rs +++ b/crates/benilla-app/src/game_plugins.rs @@ -637,8 +637,10 @@ pub(crate) mod schedule_tests { } /// `PostUpdate`'s undeclared-order pairs on the declared graph; `GlobalTransform` and the - /// particle `EffectQuads` are most of it. - const POST_UPDATE_CEILING: usize = 371; + /// particle `EffectQuads` are most of it. Six are `glue::hide_native_glue`'s `Visibility` + /// against the world's and the fade cover's writers, populations it never shares an entity + /// with (the native glue roots alone). + const POST_UPDATE_CEILING: usize = 377; const POST_UPDATE_SLACK: usize = 20; /// The actionable pairs in `Update`: conflicting access, no declared order, and nothing /// [`Classes`] explains, so the executor orders them however the graph falls. The count may diff --git a/crates/benilla-app/src/glue/dialog.rs b/crates/benilla-app/src/glue/dialog.rs index 6b10171e..9a8fd37a 100644 --- a/crates/benilla-app/src/glue/dialog.rs +++ b/crates/benilla-app/src/glue/dialog.rs @@ -196,6 +196,7 @@ pub(crate) fn spawn_dialog( let (caption, caption2) = kind.buttons(strings); commands .spawn(( + super::NativeGlue, GlobalZIndex(1200), // over the screen's 1100 Node { width: Val::Percent(100.0), diff --git a/crates/benilla-app/src/glue/mod.rs b/crates/benilla-app/src/glue/mod.rs index 5cda7d00..c98db844 100644 --- a/crates/benilla-app/src/glue/mod.rs +++ b/crates/benilla-app/src/glue/mod.rs @@ -15,6 +15,22 @@ use bevy::ui::FocusPolicy; use art::{tc_rect, GlueArt, BTN_BG, BTN_HOVER, BUTTON_TC, GOLD}; use widgets::{ArtSwap, GlueBtn, GlueCaption, GlueDisabled, OutlineCopy}; +/// The root of a native glue screen or dialog, which [`hide_native_glue`] keeps hidden while the +/// stock glue screens stand in its place. +#[derive(Component)] +pub(crate) struct NativeGlue; + +/// Under `WOW_GLUE_STOCK=1`, keep every native glue tree hidden, after any screen system shows one +/// and before visibility propagates: their state machine still drives the network, unseen, and a +/// hidden node takes no click. +pub(crate) fn hide_native_glue(mut roots: Query<&mut Visibility, With>) { + for mut v in &mut roots { + if *v != Visibility::Hidden { + *v = Visibility::Hidden; + } + } +} + /// The glue widgets' look passes, registered once for every glue screen. /// /// A screen orders its own refresh `.before(GlueVisuals)` so the flags and captions it writes @@ -50,6 +66,12 @@ impl Plugin for GluePlugin { PostUpdate, fit_glue_canvas.before(bevy::ui::UiSystems::Layout), ) + .add_systems( + PostUpdate, + hide_native_glue + .before(bevy::camera::visibility::VisibilitySystems::VisibilityPropagate) + .run_if(not(crate::ui_script::glue::native_glue)), + ) .add_systems( Update, (art_swaps, glue_button_visuals, glue_hilights, sync_outlines).in_set(GlueVisuals), diff --git a/crates/benilla-app/src/login/mod.rs b/crates/benilla-app/src/login/mod.rs index 40308fea..243c8473 100644 --- a/crates/benilla-app/src/login/mod.rs +++ b/crates/benilla-app/src/login/mod.rs @@ -63,14 +63,15 @@ impl Plugin for LoginPlugin { (drive_policy, to_select_on_roster, drive_quit).chain(), ( screen::materialize_screen, - login_input, + login_input.run_if(crate::ui_script::glue::native_glue), tick_login_caret, screen::refresh_boxes, screen::refresh_checkbox, // The shared glue dialog stays inside this chain: ordering it from outside // would order a shared painter against its own `SystemTypeSet`, which // panics at schedule build, at boot, where no unit test sees it. - crate::glue::dialog::drive_glue_dialog, + crate::glue::dialog::drive_glue_dialog + .run_if(crate::ui_script::glue::native_glue), answer_dialog, // After the driver: it spawns the dialog's edit box, and a realmlist Okay // changes the address repainted here. diff --git a/crates/benilla-app/src/login/screen.rs b/crates/benilla-app/src/login/screen.rs index 2e48894e..cf630206 100644 --- a/crates/benilla-app/src/login/screen.rs +++ b/crates/benilla-app/src/login/screen.rs @@ -141,6 +141,7 @@ fn spawn_screen( let root = commands .spawn(( + crate::glue::NativeGlue, LoginUi { with_art: art.button_up.is_some(), s, diff --git a/crates/benilla-app/src/realm_select/mod.rs b/crates/benilla-app/src/realm_select/mod.rs index 1f4fefcd..cafa2e74 100644 --- a/crates/benilla-app/src/realm_select/mod.rs +++ b/crates/benilla-app/src/realm_select/mod.rs @@ -295,7 +295,8 @@ impl Plugin for RealmSelectPlugin { ( (input::clicks, input::keys) .chain() - .run_if(|realms: Res| realms.owns_input()), + .run_if(|realms: Res| realms.owns_input()) + .run_if(crate::ui_script::glue::native_glue), (tick_refresh, screen::refresh_list) .chain() .run_if(|realms: Res| realms.shown), diff --git a/crates/benilla-app/src/realm_select/screen.rs b/crates/benilla-app/src/realm_select/screen.rs index e0b74640..1bb17e09 100644 --- a/crates/benilla-app/src/realm_select/screen.rs +++ b/crates/benilla-app/src/realm_select/screen.rs @@ -209,6 +209,7 @@ fn spawn_screen( commands .spawn(( + crate::glue::NativeGlue, RealmListUi { with_art, s }, GlobalZIndex(REALM_Z), // `enableMouse="true"` on a full-screen frame: clicks never reach the screen below. diff --git a/crates/benilla-app/src/ui_chat/commands.rs b/crates/benilla-app/src/ui_chat/commands.rs index 8f187250..316545fc 100644 --- a/crates/benilla-app/src/ui_chat/commands.rs +++ b/crates/benilla-app/src/ui_chat/commands.rs @@ -280,15 +280,26 @@ pub(crate) fn register_dev_commands(script: &benilla_ui::script::UiScript, dev: } } -/// Build the table in `PostStartup`, after `Startup` has inserted the VM and the emote catalog. -pub(crate) fn build_slash_commands( - mut commands: Commands, - script: Option>, - emotes: Option>, -) { +/// Build the table in `PostStartup`, after `Startup` has inserted the VM and the emote catalog, +/// off the boot VM's `GlobalStrings.lua` and emote tokens. Where the stock glue screens hold the +/// world instead, their VM has neither, so a boot VM's contents are built for this read alone +/// ([`crate::ui_script::strings_vm`]). +pub(crate) fn build_slash_commands(world: &mut World) { + let glue_side = world + .get_non_send_resource::() + .is_some_and(|s| s.vm_kind() == benilla_ui::script::VmKind::Glue); + let own = if glue_side { + crate::ui_script::strings_vm(world) + } else { + None + }; + let script = own + .as_ref() + .or_else(|| world.get_non_send_resource::()); + let emotes = world.get_resource::(); let (Some(script), Some(emotes)) = (script, emotes) else { warn!("chat: no UI VM or emote catalog — the slash-command table is EMPTY"); - commands.insert_resource(SlashCommands::default()); + world.insert_resource(SlashCommands::default()); return; }; let globals = script.lua().globals(); @@ -305,5 +316,5 @@ pub(crate) fn build_slash_commands( if emote == 0 { error!("chat: NO emote aliases — every /wave-style command is dead (GlobalStrings?)"); } - commands.insert_resource(table); + world.insert_resource(table); } diff --git a/crates/benilla-app/src/ui_script/extract/mod.rs b/crates/benilla-app/src/ui_script/extract/mod.rs index 7d77c7f3..96e17a88 100644 --- a/crates/benilla-app/src/ui_script/extract/mod.rs +++ b/crates/benilla-app/src/ui_script/extract/mod.rs @@ -489,18 +489,23 @@ pub(super) fn tick_script( { let _span = bevy::log::info_span!("ui_script: tick").entered(); let resized = script.set_screen_size(w / s, if h > 0.0 { h / s } else { 768.0 }); - // A display change re-applies the UI scale (`0x492e90`), whose narrow-aspect cap follows - // the window; a moved dial applies once, as its CVar callback does (`0x490770`). - if resized { - let display = (w.round() as u32, h.round() as u32); - let mut none = None; - let cache = auto_cache.as_deref_mut().map_or(&mut none, |c| &mut c.0); - script.apply_ui_scale(display, cache); - } - let rescaled = script.set_ui_scale(ui_scale.0); - // Either re-runs what the interface computed from the old `GetScreenHeight()`. - if resized || rescaled { - super::manifest::on_screen_resized(&script); + // The UI scale is `UIParent`'s, which only the in-game state has; the glue lays out on + // the root alone (`0x46dd30`). + if script.vm_kind() == benilla_ui::script::VmKind::InGame { + // A display change re-applies the UI scale (`0x492e90`), whose narrow-aspect cap + // follows the window; a moved dial applies once, as its CVar callback does + // (`0x490770`). + if resized { + let display = (w.round() as u32, h.round() as u32); + let mut none = None; + let cache = auto_cache.as_deref_mut().map_or(&mut none, |c| &mut c.0); + script.apply_ui_scale(display, cache); + } + let rescaled = script.set_ui_scale(ui_scale.0); + // Either re-runs what the interface computed from the old `GetScreenHeight()`. + if resized || rescaled { + super::manifest::on_screen_resized(&script); + } } script.tick(time.delta_secs()); } @@ -573,8 +578,13 @@ pub(super) fn tick_script( for w in script.take_warnings() { warn!("ui_script: {w}"); } - // A runtime `LoadAddOn`'s record, appended to `Logs\FrameXML.log`. - super::load_log::write(script.take_load_log_writes()); + // A runtime `LoadAddOn`'s record, appended to `Logs\FrameXML.log`; the glue has none, but + // whatever its VM drains is the glue log's. + let writes = script.take_load_log_writes(); + match script.vm_kind() { + benilla_ui::script::VmKind::InGame => super::load_log::write(writes), + benilla_ui::script::VmKind::Glue => super::load_log::write_glue(writes), + } // ── The handover ──────────────────────────────────────────────────────────────────────── *pass = super::UiPassState { diff --git a/crates/benilla-app/src/ui_script/glue.rs b/crates/benilla-app/src/ui_script/glue.rs new file mode 100644 index 00000000..11748d87 --- /dev/null +++ b/crates/benilla-app/src/ui_script/glue.rs @@ -0,0 +1,354 @@ +//! The stock glue screens, behind `WOW_GLUE_STOCK=1` in a dev build: the player's own +//! `GlueXML.toc` off the patch chain, run into a glue VM ([`UiScript::new_glue`]) that stands in +//! the world while no character is in it, where the boot VM otherwise stands. The native screens +//! stay the default and, under the switch, keep driving the network unseen. +//! +//! The reference's glue builder (`0x46a7b0`) resets the Lua state on a rebuild (`0x46a87b`; the +//! cold first build, `0x46a505`, passes 0 and skips it), registers the glue functions +//! (`0x46abb0`) and the 26 glue events (`0x46a88f`), opens `Logs\GlueXML.log` (`0x46a8ce`), then +//! checks `GlueXML.toc`'s signature (`0x46a90b`). Only a valid one (`0x6f10f0` answering 3) loads +//! the toc through the shared runner (`0x6edb90`, at `0x46a9b3`), and only when the loaded files' +//! MD5 matches the signature's (`0x46a9d3`) does `FRAMES_LOADED` fire (`0x46aa34`). Signature +//! results 0–2 (the table at `0x46aaac`) log their line, missing, corrupt or modified (`0x836fb8`, +//! `0x836f98`, `0x836f78`), and load nothing; an MD5 mismatch logs no line, its frames already +//! built, and skips `FRAMES_LOADED`. Either way the client quits with +//! error 9 (`0x401560`: the exit code, then the quit post `ForceQuit` makes with code 0). benilla +//! checks no signature, as for `FrameXML.toc` (`0x48ff4d`): the chain's toc always loads and +//! `FRAMES_LOADED` follows. +//! +//! The screen comes after the build: at startup the reference fires `SET_GLUE_SCREEN` with +//! `"movie"` while the `movie` CVar is 1 (clearing it), else `"login"` (`0x402b3b`–`0x402b65`); +//! back from the world `0x402090` fires it with `"charselect"` or `"login"`. benilla plays no movie +//! yet (#968), so its boot screen is `"login"`. +//! +//! The screens' `ModelFFX` panes (P4) must drive the glue booth's scene rig, the scene M2's own +//! point lights (`portrait::glue_booth::scene_rig`) and the `FfxGlow::GLUE_SCENE` pass, never the +//! `ui_models` tile path, where every lit batch draws black. + +use bevy::prelude::*; + +use benilla_ui::script::{ScriptValue, UiScript}; + +use super::reference_ui; + +/// The switch's variable. +const SWITCH: &str = "WOW_GLUE_STOCK"; + +/// Whether this run puts the stock glue screens up, read once at plugin build: a dev build with +/// `WOW_GLUE_STOCK=1`. A player build never does. +#[derive(Resource, Default, Clone, Copy)] +pub(crate) struct GlueStock(pub(crate) bool); + +impl GlueStock { + /// The switch as this process has it. + pub(crate) fn from_env() -> Self { + Self( + crate::run_mode::dev_affordances() + && std::env::var(SWITCH).ok().as_deref() == Some("1"), + ) + } +} + +/// Run condition: the stock glue screens are up, the switch on and no character in the world. +pub(crate) fn glue_ui_up( + stock: Option>, + state: Option>>, +) -> bool { + stock.is_some_and(|s| s.0) + && state.is_none_or(|s| *s.get() != crate::char_select::ClientState::InWorld) +} + +/// Run condition: the native glue screens draw and take input, the switch off. +pub(crate) fn native_glue(stock: Option>) -> bool { + !stock.is_some_and(|s| s.0) +} + +/// The glue screen `SET_GLUE_SCREEN` names, `GlueParent.lua`'s `GlueScreenInfo` key. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum GlueScreen { + Login, + CharSelect, +} + +impl GlueScreen { + /// The key, as the reference's literals spell it (`0x82e3e0`, `0x82e3e8`). + pub(crate) fn key(self) -> &'static str { + match self { + GlueScreen::Login => "login", + GlueScreen::CharSelect => "charselect", + } + } +} + +/// Load the chain's `GlueXML.toc` into `script`, a glue VM, and fire `FRAMES_LOADED`: the glue +/// builder's steps from the toc on (`0x46a9b3`, `0x46aa34`). Returns every failure; a loader error +/// is tagged `"GlueXML/: "`, a handler that raised during the walk is one too. The +/// load's record is left for [`finish_glue_load_log`]. +pub(super) fn load_glue(script: &mut UiScript) -> Vec { + let mut failures = match reference_ui::glue() { + Some(glue) => { + let mut toc = benilla_ui::status::Status::default(); + let failures = glue.load_files_into(script, &glue.toc.files, &mut toc); + let mut log = benilla_ui::status::Status::default(); + let banner = benilla_ui::status::toc_banner(reference_ui::GLUE_TOC); + toc.close_into(&mut log, script.framexml_debug(), banner); + script.report_load_status(log); + failures + } + None => { + // The runner's `"Couldn't open %s"` (`0x846ff4`) at severity 2; no glue frame exists. + let e = format!( + "{}: not found — the stock glue screens are not built", + reference_ui::GLUE_TOC + ); + error!("ui_script: {e}"); + script.report_load_failure(&e); + let mut log = benilla_ui::status::Status::default(); + log.report( + benilla_ui::status::FAILURE, + benilla_ui::status::missing(reference_ui::GLUE_TOC, false), + ); + script.report_load_status(log); + vec![e] + } + }; + script.fire_event("FRAMES_LOADED", vec![]); + failures.extend( + script + .errors() + .into_iter() + .map(|e| format!("a handler raised during the glue load: {e}")), + ); + failures +} + +/// Drain the glue load's record into `Logs\GlueXML.log`, as the builder does after +/// `FRAMES_LOADED` (`0x46aa61`-`0x46aa94`). +pub(super) fn finish_glue_load_log(script: &mut UiScript) { + script.finish_ui_load_log(); + super::load_log::write_glue(script.take_load_log_writes()); +} + +/// Put `screen` up: `SET_GLUE_SCREEN` with its key as `arg1` (`0x46b840`, event 0). +pub(super) fn set_glue_screen(script: &mut UiScript, screen: GlueScreen) { + script.fire_event( + "SET_GLUE_SCREEN", + vec![ScriptValue::Str(screen.key().to_string())], + ); +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use super::*; + + /// The glue functions the stock GlueXML calls that benilla has not built yet: each one's call + /// raises "attempt to call global `X' (a nil value)", at the load or on the login screen. The + /// list only shrinks: a name built, or no longer reached, comes off it. + const UNBUILT_CALLED: &[&str] = &[ + "EULAAccepted", + "GetBuildInfo", + "GetNumCharacters", + "GetSavedAccountName", + "SetCharCustomizeFrame", + "SetCharSelectModelFrame", + "SetCurrentScreen", + ]; + + /// The movie frame's three handlers, which benilla's `MovieFrame` does not take: it plays no + /// movie. Each is a loader warning, `: SetScript("") unsupported`; the list only + /// shrinks. + const UNBUILT_HANDLERS: &[(&str, &str)] = &[ + ("MovieFrame", "OnMovieFinished"), + ("MovieFrame", "OnMovieHideSubtitle"), + ("MovieFrame", "OnMovieShowSubtitle"), + ]; + + /// The frames each stock glue screen and dialog is, which the load must build. + const SCREENS: [&str; 8] = [ + "GlueParent", + "AccountLogin", + "CharacterSelect", + "CharacterCreate", + "RealmList", + "GlueDialog", + "RealmWizard", + "AddonList", + ]; + + /// The name a "nil value" call error names, in either of Lua's two spellings of it. + fn nil_call(error: &str) -> Option<&str> { + let (_, rest) = error.split_once("attempt to call ")?; + let (kind, rest) = rest.split_once(' ')?; + if !matches!(kind, "global" | "field" | "method") { + return None; + } + let rest = rest.strip_prefix('`').or_else(|| rest.strip_prefix('\''))?; + let (name, rest) = rest.split_once('\'')?; + rest.starts_with(" (a nil value)").then_some(name) + } + + /// The glue state's functions, `reference/1.12-glue-surface.tsv`'s names. + fn glue_surface() -> BTreeSet { + let path = format!( + "{}/../../reference/1.12-glue-surface.tsv", + env!("CARGO_MANIFEST_DIR") + ); + std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("reading {path}: {e}")) + .lines() + .filter(|l| !l.starts_with('#') && !l.starts_with("scope\t")) + .filter_map(|l| l.split('\t').nth(2).map(str::to_string)) + .collect() + } + + #[test] + fn a_nil_call_names_its_function() { + assert_eq!( + nil_call("x.lua:1: attempt to call global `GetBuildInfo' (a nil value)"), + Some("GetBuildInfo") + ); + assert_eq!( + nil_call("x.lua:1: attempt to call field 'Foo' (a nil value)"), + Some("Foo") + ); + assert_eq!( + nil_call("x.lua:1: attempt to index global `X' (a nil value)"), + None + ); + assert_eq!(nil_call("x.lua:1: bad argument #1"), None); + } + + /// The stock GlueXML loads into the glue VM, `FRAMES_LOADED` fires and the login screen goes + /// up, and a few frames run: every screen's frame exists, and the only Lua errors are calls + /// of the glue functions [`UNBUILT_CALLED`] names, each of which is still unbuilt and still + /// called. Any other error, an XML problem or a missing widget method, fails here. + #[test] + fn the_stock_glue_loads_with_only_unbuilt_verbs_raising() { + let _ = benilla_formats::wow_data_or_skip!(); + let mut s = UiScript::new_glue().unwrap(); + s.set_screen_size(1024.0, 768.0); + let mut errors = load_glue(&mut s); + // `load_glue` reports the walk's handler errors; the rest are the screen's and the ticks'. + let _ = s.take_errors(); + set_glue_screen(&mut s, GlueScreen::Login); + for _ in 0..3 { + s.tick(0.1); + } + s.resolve(); + errors.extend(s.take_errors()); + // Every row the session retained: the load's failures and warnings, and what raised. + errors.extend(s.diagnostics().into_iter().map(|d| d.message)); + + let surface = glue_surface(); + let mut called = BTreeSet::new(); + let mut handlers = BTreeSet::new(); + let other: Vec<&String> = errors + .iter() + .filter(|e| { + if let Some(name) = nil_call(e).filter(|n| UNBUILT_CALLED.contains(n)) { + called.insert(name); + return false; + } + if let Some(h) = UNBUILT_HANDLERS.iter().find(|(frame, handler)| { + e.contains(&format!("{frame}: SetScript(\"{handler}\") unsupported")) + }) { + handlers.insert(*h); + return false; + } + true + }) + .collect(); + assert!( + other.is_empty(), + "the glue load raised {} error(s) or warning(s) that are not a call of an unbuilt \ + glue function on UNBUILT_CALLED or a handler on UNBUILT_HANDLERS:\n{}", + other.len(), + other + .iter() + .map(|e| e.as_str()) + .collect::>() + .join("\n") + ); + let listed: BTreeSet<&str> = UNBUILT_CALLED.iter().copied().collect(); + let stale: Vec<&&str> = listed.difference(&called).collect(); + assert!( + stale.is_empty(), + "UNBUILT_CALLED names no longer called unbuilt; take them off: {stale:?}" + ); + let stale: Vec<&(&str, &str)> = UNBUILT_HANDLERS + .iter() + .filter(|h| !handlers.contains(*h)) + .collect(); + assert!( + stale.is_empty(), + "UNBUILT_HANDLERS entries the load no longer warns about; take them off: {stale:?}" + ); + for name in UNBUILT_CALLED { + assert!( + surface.contains(*name), + "{name} is not a glue-state function" + ); + assert!( + s.eval::(&format!("return {name} == nil")).unwrap(), + "{name} is built; take it off UNBUILT_CALLED" + ); + } + + for name in SCREENS { + assert!( + s.eval::(&format!( + "return type({name}) == 'table' and {name}.GetObjectType ~= nil" + )) + .unwrap(), + "the glue load built no frame {name}" + ); + } + assert_eq!( + s.eval::<(bool, bool)>( + "return AccountLogin:IsShown() == 1, CharacterSelect:IsShown() == 1" + ) + .unwrap(), + (true, false), + "SET_GLUE_SCREEN \"login\" shows the login screen alone" + ); + } + + /// `GlueParent` fills the screen root, 768 tall and `768·a` wide (`0x41ad10`, from `0x46ab40`), + /// up to 16:9; past that `GlueParent_OnLoad` centres it 16:9 wide on the root's height. + #[test] + fn glue_parent_fills_the_root_up_to_sixteen_by_nine() { + let _ = benilla_formats::wow_data_or_skip!(); + let rect = |aspect: f32| { + let mut s = UiScript::new_glue().unwrap(); + s.set_screen_size(768.0 * aspect, 768.0); + let _ = load_glue(&mut s); + s.resolve(); + s.eval::<(f32, f32, f32, f32)>( + "return GlueParent:GetLeft(), GlueParent:GetRight(), GlueParent:GetBottom(), \ + GlueParent:GetTop()", + ) + .unwrap() + }; + let near = |a: (f32, f32, f32, f32), b: (f32, f32, f32, f32)| { + [(a.0, b.0), (a.1, b.1), (a.2, b.2), (a.3, b.3)] + .iter() + .all(|(x, y)| (x - y).abs() < 0.01) + }; + for aspect in [4.0 / 3.0, 16.0 / 10.0, 16.0 / 9.0] { + let got = rect(aspect); + assert!( + near(got, (0.0, 768.0 * aspect, 0.0, 768.0)), + "{aspect}: {got:?}" + ); + } + let wide = 21.0 / 9.0; + let bar = (768.0 * wide - 768.0 * 16.0 / 9.0) / 2.0; + let got = rect(wide); + assert!( + near(got, (bar, 768.0 * wide - bar, 0.0, 768.0)), + "21:9: {got:?}" + ); + } +} diff --git a/crates/benilla-app/src/ui_script/lifecycle.rs b/crates/benilla-app/src/ui_script/lifecycle.rs index d91c92bb..faa75f38 100644 --- a/crates/benilla-app/src/ui_script/lifecycle.rs +++ b/crates/benilla-app/src/ui_script/lifecycle.rs @@ -1,7 +1,8 @@ //! The UI session's lifecycle, all edges and nothing per-frame: `Startup` installs a boot VM -//! (strings and emote tokens); world entry builds a fresh VM and loads the in-game UI and every -//! addon onto it; leaving the world runs the reference's shutdown tail and installs a boot -//! VM again; `ReloadUI()` is the two edges back to back without leaving the world. +//! (strings and emote tokens), or the stock glue screens' VM under `WOW_GLUE_STOCK=1`; world entry +//! builds a fresh VM and loads the in-game UI and every addon onto it; leaving the world runs the +//! reference's shutdown tail and installs the glue-side VM again; `ReloadUI()` is the two edges +//! back to back without leaving the world. use bevy::prelude::*; @@ -13,26 +14,71 @@ use super::{load_ingame_ui, CursorPayloadHeld, PlayerUiHover, UiClock, UiKeyboar use crate::ui_script::addons; pub(crate) fn setup_script(world: &mut World) { - install_boot_vm(world); + install_glue_side_vm(world, super::glue::GlueScreen::Login); +} + +/// Install the VM the world holds outside the world: the stock glue screens' under +/// [`super::glue::GlueStock`], put up on `screen`, else a boot VM. +fn install_glue_side_vm(world: &mut World, screen: super::glue::GlueScreen) { + if world + .get_resource::() + .is_some_and(|s| s.0) + { + install_glue_vm(world, screen); + } else { + install_boot_vm(world); + } } /// Build and install a boot VM, the one the world holds outside an in-game UI session: the /// strings the feeds resolve lines from and the emote tokens `/`-commands are built from /// (`ui_chat::commands`), no frames and no font objects. Installed at `Startup` and at every -/// [`end_ui_session`]. +/// [`end_ui_session`], unless the stock glue screens stand there instead ([`install_glue_vm`]). fn install_boot_vm(world: &mut World) { - let Some(script) = new_vm(world) else { - return; - }; + if let Some(script) = strings_vm(world) { + world.insert_non_send_resource(script); + } +} + +/// A boot VM's contents on a VM of their own: `GlobalStrings.lua` and the emote tokens, for a +/// reader that needs them where the world holds the glue VM. `None` (the world's VM removed) when +/// the VM cannot start. +pub(crate) fn strings_vm(world: &mut World) -> Option { + let script = new_vm(world, UiScript::new)?; load_global_strings(world, &script); load_emote_tokens(world, &script); + Some(script) +} + +/// Build the glue VM, load the stock GlueXML onto it, install it and put `screen` up: the glue +/// builder `0x46a7b0` then `SET_GLUE_SCREEN` (`0x402b65` at startup, `0x402090` back from the +/// world). It holds what the glue state holds and nothing of the boot VM's: no `GlobalStrings.lua`, +/// no emote tokens. +fn install_glue_vm(world: &mut World, screen: super::glue::GlueScreen) { + let Some(mut script) = new_vm(world, UiScript::new_glue) else { + return; + }; + seat_raster_seam_for_load(world, &mut script); + let start = std::time::Instant::now(); + let failures = super::glue::load_glue(&mut script); + super::glue::finish_glue_load_log(&mut script); + super::glue::set_glue_screen(&mut script, screen); + info!( + "ui_script: stock glue up in {:.0} ms on \"{}\" ({} load failure(s))", + start.elapsed().as_secs_f32() * 1000.0, + screen.key(), + failures.len() + ); world.insert_non_send_resource(script); } -/// A fresh VM on the process clock with the addon asset probes wired, or `None` (the world's VM -/// removed) when the VM cannot start. -fn new_vm(world: &mut World) -> Option { - let mut script = match UiScript::new() { +/// A fresh VM from `make` on the process clock with the addon asset probes wired, or `None` (the +/// world's VM removed) when the VM cannot start. +fn new_vm( + world: &mut World, + make: fn() -> Result, +) -> Option { + let mut script = match make() { Ok(s) => s, Err(e) => { error!("ui_script: VM init failed: {e}"); @@ -155,7 +201,7 @@ pub(crate) fn arm_entry_ui_load(world: &mut World) { /// `GlobalStrings.lua` and `Fonts.xml` as its first rows, and `ChatFrame.lua` the emote tokens. fn mint_entry_vm(world: &mut World) { crate::cvars::fold_dying_vm_cvars(world); - if let Some(script) = new_vm(world) { + if let Some(script) = new_vm(world, UiScript::new) { world.insert_non_send_resource(script); } } @@ -648,8 +694,24 @@ pub(crate) fn shutdown_ui_state( /// shutdown tail, then end this session's Lua state, so no frame, global or addon upvalue reaches /// the next login. The reference's state outlives `0x490bd0` and is replaced twice through /// `0x703b80`, in `ShutdownGame` (at `0x491231`) and then in the glue build (`0x46a7b0`), so the -/// character screen runs on a state of its own. Ours is a fresh boot VM. +/// character screen runs on a state of its own. Ours is a fresh boot VM, or the stock glue's +/// ([`install_glue_side_vm`]). pub(crate) fn end_ui_session(world: &mut World) { + close_ui_session(world); + // `0x401ee0`'s glue leg: `"charselect"` after a logout, `"login"` after a disconnect + // (`0x402084`); the state this exit enters says which. + let screen = match world + .get_resource::>() + .map(|s| *s.get()) + { + Some(crate::char_select::ClientState::CharSelect) => super::glue::GlueScreen::CharSelect, + _ => super::glue::GlueScreen::Login, + }; + install_glue_side_vm(world, screen); +} + +/// [`end_ui_session`] up to the new VM: the shutdown tail, the folds and the host values reset. +fn close_ui_session(world: &mut World) { // A VM still parked (the load never ran) goes back into the world first. unpark_boot_vm(world); // An entry load still armed means no in-game UI was built, and the tail would overwrite the @@ -690,8 +752,6 @@ pub(crate) fn end_ui_session(world: &mut World) { if let Some(mut minimap) = world.get_resource_mut::() { minimap.0 = None; } - - install_boot_vm(world); } /// A `ReloadUI()` waiting to run: set when [`crate::ui_logout`] drains @@ -722,7 +782,10 @@ pub(crate) fn run_pending_reload(world: &mut World) { return; } info!("ui_script: ReloadUI — ending the UI session and building a new one"); - end_ui_session(world); + // A boot VM, never the glue's: the reload builds no glue (`0x495664` then `0x495669`), and the + // load replaces this VM at once. + close_ui_session(world); + install_boot_vm(world); load_ingame_ui_seated(world, LoadSeat::Live); // `UI_Init` ends by re-entering the world-enter cascade (`0x490168`), its sends included, but // only with the active player set (`0x490166 je`): our own player's entity exists. Not @@ -985,6 +1048,53 @@ mod tests { ); } + /// Under `WOW_GLUE_STOCK=1` the world holds the glue VM where it holds a boot VM, at startup + /// on the login screen (`0x402b65`) and back from the world on the screen the exit enters + /// (`0x402090`); a `ReloadUI()` never builds the glue. + #[test] + fn the_stock_glue_stands_where_the_boot_vm_does() { + use crate::char_select::ClientState; + use benilla_ui::script::VmKind; + let shown = |world: &World, frame: &str| { + benilla_formats::wow_data().is_none() + || world + .non_send_resource::() + .eval::(&format!("return {frame}:IsShown() == 1")) + .unwrap() + }; + let mut world = World::new(); + world.insert_resource(super::super::glue::GlueStock(true)); + world.insert_resource(State::new(ClientState::Login)); + setup_script(&mut world); + assert_eq!( + world.non_send_resource::().vm_kind(), + VmKind::Glue + ); + assert!(shown(&world, "AccountLogin"), "the boot screen is login"); + + world.insert_resource(State::new(ClientState::CharSelect)); + world.init_resource::(); + end_ui_session(&mut world); + assert_eq!( + world.non_send_resource::().vm_kind(), + VmKind::Glue + ); + assert!( + shown(&world, "CharacterSelect"), + "a logout puts the character screen up" + ); + + world.insert_resource(State::new(ClientState::InWorld)); + world.insert_resource(crate::run_mode::CaptureMode); + world.insert_resource(ReloadUiPending(true)); + run_pending_reload(&mut world); + assert_eq!( + world.non_send_resource::().vm_kind(), + VmKind::InGame, + "the reload's VM is a boot VM, never the glue's" + ); + } + /// The reference re-makes its Lua state inside `UI_Init` (`0x48fe97`). #[test] fn the_world_entry_builds_its_own_vm_rather_than_adopting_the_glue_phases() { diff --git a/crates/benilla-app/src/ui_script/load_log.rs b/crates/benilla-app/src/ui_script/load_log.rs index cab30361..79567cc4 100644 --- a/crates/benilla-app/src/ui_script/load_log.rs +++ b/crates/benilla-app/src/ui_script/load_log.rs @@ -1,7 +1,8 @@ //! `Logs\FrameXML.log` (`0x842ecc`): the UI loads' records ([`benilla_ui::status`]), written a //! line at a time as the reference's log layer writes one (`0x65ac20`): the stamp -//! `"%u/%u %02u:%02u:%02u.%03u "` (`0x866aa0`), the text, then `"\r\n"` (`0x82edfc`). Deviation: -//! the file is `benilla-config/Logs/FrameXML.log`, since the install is read-only. +//! `"%u/%u %02u:%02u:%02u.%03u "` (`0x866aa0`), the text, then `"\r\n"` (`0x82edfc`). The glue +//! build writes its own `Logs\GlueXML.log` (`0x837024`, opened at `0x46a8ce`) the same way. +//! Deviation: the files are under `benilla-config/Logs/`, since the install is read-only. use std::io::Write as _; @@ -9,20 +10,32 @@ use bevy::prelude::*; use benilla_ui::status::LogWrite; -/// The log's name, the reference's. +/// The in-game log's name, the reference's. const FILE: &str = "FrameXML.log"; -/// Write the VM's drains in the order they were made. A drain with no lines opens nothing, so a -/// clean UI load leaves the last load's file in place, as the reference's lazy open does -/// (`0x65a930`). +/// The glue log's name, the reference's. +const GLUE_FILE: &str = "GlueXML.log"; + +/// Write the in-game VM's drains in the order they were made. A drain with no lines opens +/// nothing, so a clean UI load leaves the last load's file in place, as the reference's lazy open +/// does (`0x65a930`). pub(super) fn write(writes: Vec) { + write_named(FILE, writes); +} + +/// [`write`] for the glue VM's drains, into the glue's own log. +pub(super) fn write_glue(writes: Vec) { + write_named(GLUE_FILE, writes); +} + +fn write_named(file: &str, writes: Vec) { if writes.iter().all(|w| lines(w).is_empty()) { return; } let Some(dir) = crate::local_state::logs_dir() else { return; // hermetic capture, or no state folder }; - write_under(&dir, writes); + write_under(&dir, file, writes); } fn lines(write: &LogWrite) -> &[String] { @@ -31,9 +44,9 @@ fn lines(write: &LogWrite) -> &[String] { } } -/// [`write`] into `dir`. -fn write_under(dir: &std::path::Path, writes: Vec) { - let path = dir.join(FILE); +/// [`write_named`] into `dir`. +fn write_under(dir: &std::path::Path, file: &str, writes: Vec) { + let path = dir.join(file); for w in writes { let (append, lines) = match w { LogWrite::Rewrite(lines) => (false, lines), @@ -84,11 +97,19 @@ mod tests { fn a_load_rewrites_a_load_addon_appends_and_a_clean_load_touches_nothing() { let dir = std::env::temp_dir().join(format!("benilla-framexml-log-{}", std::process::id())); std::fs::remove_dir_all(&dir).ok(); - write_under(&dir, vec![LogWrite::Rewrite(vec!["first load".into()])]); - write_under(&dir, vec![LogWrite::Rewrite(vec!["second load".into()])]); - write_under(&dir, vec![LogWrite::Append(vec!["on demand".into()])]); + write_under( + &dir, + FILE, + vec![LogWrite::Rewrite(vec!["first load".into()])], + ); + write_under( + &dir, + FILE, + vec![LogWrite::Rewrite(vec!["second load".into()])], + ); + write_under(&dir, FILE, vec![LogWrite::Append(vec!["on demand".into()])]); assert_eq!(read(&dir), ["second load", "on demand"]); - write_under(&dir, vec![LogWrite::Rewrite(vec![])]); + write_under(&dir, FILE, vec![LogWrite::Rewrite(vec![])]); assert_eq!(read(&dir), ["second load", "on demand"]); let raw = std::fs::read_to_string(dir.join(FILE)).unwrap(); let first = raw.split("\r\n").next().unwrap(); diff --git a/crates/benilla-app/src/ui_script/mod.rs b/crates/benilla-app/src/ui_script/mod.rs index 2ccdedc6..cdb17c7f 100644 --- a/crates/benilla-app/src/ui_script/mod.rs +++ b/crates/benilla-app/src/ui_script/mod.rs @@ -16,6 +16,8 @@ use benilla_world::schedule::WorldStage; pub(crate) mod addons; mod content; pub(crate) mod extract; +/// The stock glue screens behind `WOW_GLUE_STOCK=1`. +pub(crate) mod glue; mod input; #[cfg(test)] mod layer_tests; @@ -289,6 +291,7 @@ impl Plugin for UiScriptPlugin { // The quit root runs in `Last`: the close button's `AppExit` is written in `PostUpdate`. crate::shutdown::on_app_exit(app, shutdown_on_exit.into_configs()); app.insert_resource(UiScaleCvar(default_ui_scale())) + .insert_resource(glue::GlueStock::from_env()) .init_resource::() .init_resource::() .init_resource::() @@ -345,14 +348,17 @@ impl Plugin for UiScriptPlugin { ) // In the feed phase, so an `OnUpdate` reads this frame's `GetFramerate()`. .add_systems(Update, feed_framerate.in_set(UiFeed)) - // The input pass hit-tests the rects the tick just resolved, and runs in-world only. + // The input pass hit-tests the rects the tick just resolved, and runs in-world only or + // on the stock glue screens. .init_resource::() .init_resource::() .add_systems( Update, ( extract::tick_script, - input::feed_ui_input.in_set(crate::char_select::InWorldGated), + input::feed_ui_input.run_if( + in_state(crate::char_select::ClientState::InWorld).or(glue::glue_ui_up), + ), // Off the walk that just ran, for the pick and the arbiter after the pass. crate::vplates::seed_plate_hover, ) @@ -412,8 +418,9 @@ fn arbitrate_pointer_over_ui( /// The session lifecycle: the VM's birth, identity, death and reload. mod lifecycle; pub(crate) use lifecycle::{ - end_ui_session, ingame_ui_up, run_pending_reload, setup_script, AddOnIdentity, LeaveWorldSweep, - LeavingWorldArmed, PendingEntryUiLoad, ReloadUiPending, WorldLeaveSweepMessage, + end_ui_session, ingame_ui_up, run_pending_reload, setup_script, strings_vm, AddOnIdentity, + LeaveWorldSweep, LeavingWorldArmed, PendingEntryUiLoad, ReloadUiPending, + WorldLeaveSweepMessage, }; // Test-only: other modules' tests consume these, and a plain re-export would warn unused. #[cfg(test)] diff --git a/crates/benilla-app/src/ui_script/reference_ui.rs b/crates/benilla-app/src/ui_script/reference_ui.rs index 80ad2df5..9c55306f 100644 --- a/crates/benilla-app/src/ui_script/reference_ui.rs +++ b/crates/benilla-app/src/ui_script/reference_ui.rs @@ -47,9 +47,29 @@ pub(super) fn core() -> Option { /// order, as the runner builds its paths (`0x6edbe5`, `0x6edcf4`, `0x6edd0c`) and dispatches each /// in turn (`0x6edd51`). pub(super) fn core_from(toc: &str) -> Addon { - let dir = TOC.rsplit_once('\\').map_or("", |(d, _)| d); - addon( - Toc::parse(toc) + toc_addon(NAME, TOC, toc) +} + +/// The glue screens' table of contents, which the glue builder (`0x46a7b0`) hands to the same +/// `.toc` runner (`0x6edb90`) at `0x46a9b3`, once per glue build. +pub(super) const GLUE_TOC: &str = r"Interface\GlueXML\GlueXML.toc"; + +/// The name the stock glue files load under, as [`NAME`] is the in-game core's. +pub(super) const GLUE_NAME: &str = "GlueXML"; + +/// The glue screens' interface: the chain's own [`GLUE_TOC`] as an [`Addon`] over the chain, or +/// `None` when the chain has no such file. +pub(super) fn glue() -> Option { + read(GLUE_TOC).map(|bytes| toc_addon(GLUE_NAME, GLUE_TOC, &benilla_ui::source::decode(&bytes))) +} + +/// The interface `toc_text` lists, the `.toc` at chain path `toc_path`: its rows under the toc's +/// own directory, in its order. +fn toc_addon(name: &str, toc_path: &str, toc_text: &str) -> Addon { + let dir = toc_path.rsplit_once('\\').map_or("", |(d, _)| d); + named_addon( + name, + Toc::parse(toc_text) .files .iter() .map(|f| format!("{dir}\\{f}")) @@ -58,9 +78,15 @@ pub(super) fn core_from(toc: &str) -> Addon { } /// The stock interface as an [`Addon`] over the chain; `files` are full chain paths. +#[cfg(test)] pub(super) fn addon(files: Vec) -> Addon { + named_addon(NAME, files) +} + +/// [`addon`] under `name`. +fn named_addon(name: &str, files: Vec) -> Addon { Addon::new( - NAME.to_string(), + name.to_string(), Toc { directives: Vec::new(), files, diff --git a/crates/benilla-ui/src/script/glue.rs b/crates/benilla-ui/src/script/glue.rs index 3be4d523..54aa83da 100644 --- a/crates/benilla-ui/src/script/glue.rs +++ b/crates/benilla-ui/src/script/glue.rs @@ -11,6 +11,10 @@ //! //! [`UiScript::new_glue`]: super::UiScript::new_glue +use mlua::Lua; + +use super::Model; + /// Which of the reference's two FrameScript states a VM is. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum VmKind { @@ -80,3 +84,36 @@ impl VmKind { } } } + +/// The glue core's screen verbs (registrar `0x46ddf0`, table `0x8373b8`): the screen root's size +/// and the cursor in it. The glue and in-game states share the root, `0x41ad10`'s, 768 units tall +/// and `768·a` wide for the aspect `a` (the glue builder sets it at `0x46a828`, from `0x46ab40`: +/// `gxResolution`'s width over height with `widescreen` on, its default, else 4:3). The glue has +/// no `UIParent` and so no UI scale: `GetScreenWidth` (`0x46dd30`) and `GetScreenHeight` +/// (`0x46dd80`) are the in-game pair (`0x48b480`, `0x48b4d0`) without its `0x494590` division, +/// and `GetCursorPosition` (`0x46dad0`) is byte for byte the in-game `0x48b820`. +pub(super) fn install(lua: &Lua) -> mlua::Result<()> { + let g = lua.globals(); + g.set( + "GetScreenWidth", + lua.create_function(|lua, ()| { + let model = lua.app_data_ref::().expect("model"); + Ok(model.screen.width()) + })?, + )?; + g.set( + "GetScreenHeight", + lua.create_function(|lua, ()| { + let model = lua.app_data_ref::().expect("model"); + Ok(model.screen.height()) + })?, + )?; + g.set( + "GetCursorPosition", + lua.create_function(|lua, ()| { + let model = lua.app_data_ref::().expect("model"); + Ok(model.cursor_pos) + })?, + )?; + Ok(()) +} diff --git a/crates/benilla-ui/src/script/mod.rs b/crates/benilla-ui/src/script/mod.rs index 773ef10a..d82225ad 100644 --- a/crates/benilla-ui/src/script/mod.rs +++ b/crates/benilla-ui/src/script/mod.rs @@ -646,10 +646,13 @@ impl UiScript { /// 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. + /// types ([`glue`]), and none of the in-game surface. Of the glue registrar's own four tables + /// (`0x46ddf0`, `0x4736c0`, `0x471a20`, `0x46f480`) only the screen verbs are built + /// ([`glue::install`]). pub fn new_glue() -> mlua::Result { - Ok(Self::host(install_framescript(VmKind::Glue)?)) + let lua = install_framescript(VmKind::Glue)?; + glue::install(&lua)?; + Ok(Self::host(lua)) } fn host(lua: Lua) -> UiScript { diff --git a/crates/benilla-ui/src/script/tests/glue_surface.rs b/crates/benilla-ui/src/script/tests/glue_surface.rs index f9dd545d..59332379 100644 --- a/crates/benilla-ui/src/script/tests/glue_surface.rs +++ b/crates/benilla-ui/src/script/tests/glue_surface.rs @@ -162,14 +162,11 @@ const NOT_YET: &[(&str, &[&str])] = &[ "GetBillingTimeRemaining", "GetBillingTimeRested", "GetBuildInfo", - "GetCursorPosition", "GetLocale", "GetMovieResolution", "GetMovieSubtitles", "GetNumAddOns", "GetSavedAccountName", - "GetScreenHeight", - "GetScreenWidth", "GetScriptMemory", "GetServerName", "HideCursor", @@ -522,3 +519,30 @@ fn model_ffx_is_a_model_flagged_ffx() { ("Test/Xml.mdx".to_string(), 2.0) ); } + +/// The glue's screen verbs answer the screen root with no UI scale (`0x46dd30`, `0x46dd80`): the +/// in-game pair divides by `UIParent`'s scale (`0x494590`), the glue pair has none to divide by, +/// even when a frame is named `UIParent`. The cursor is the root's (`0x46dad0`). +#[test] +fn the_glue_screen_verbs_answer_the_root_unscaled() { + let root = (768.0_f32 * 16.0 / 9.0, 768.0_f32); + let read = |mut s: UiScript| { + s.set_screen_size(root.0, root.1); + s.run(r#"local p = CreateFrame("Frame", "UIParent") p:SetScale(0.5)"#) + .unwrap(); + s.mouse_move(100.0, 200.0); + s.eval::<(f32, f32, f32, f32)>( + "local x, y = GetCursorPosition() return GetScreenWidth(), GetScreenHeight(), x, y", + ) + .unwrap() + }; + assert_eq!( + read(UiScript::new_glue().unwrap()), + (root.0, root.1, 100.0, 200.0) + ); + assert_eq!( + read(UiScript::new().unwrap()), + (root.0 * 2.0, root.1 * 2.0, 100.0, 200.0), + "the in-game pair answers in UIParent's units" + ); +} diff --git a/docs/MAP.md b/docs/MAP.md index 4f3f7305..20b4acc1 100644 --- a/docs/MAP.md +++ b/docs/MAP.md @@ -262,6 +262,7 @@ - `WOW_FX_YAW` — benilla-app/src/capture/mod.rs - `WOW_GHOST_PROBE` — benilla-app/src/death/mod.rs - `WOW_GLUE_ROUNDTRIP` — benilla-app/src/char_select/mod.rs, benilla-world/src/bgwin.rs +- `WOW_GLUE_STOCK` — benilla-app/src/ui_script/glue.rs - `WOW_GLYPH_CACHE` — benilla-app/src/ui_text/engine/gpu.rs - `WOW_GM` — benilla-app/src/probe_shield.rs - `WOW_GOD` — benilla-app/src/preflight.rs, benilla-app/src/probe_shield.rs