WOW_GLUE_STOCK=1 loads and draws the stock GlueXML (#969)
With `WOW_GLUE_STOCK=1` (dev builds), benilla now loads the stock GlueXML off the player's own patch chain into the glue state and draws it; with the switch off nothing changes. In 1.12.1 the glue builder (`0x46a7b0`) opens `Logs\GlueXML.log` (`0x46a8ce`), runs `GlueXML.toc` through the shared toc runner (`0x6edb90` at `0x46a9b3`), fires `FRAMES_LOADED` (`0x46aa34`) and opens on "login" or "movie" (`0x402b3b`), or "charselect" back from the world (`0x402084`). Glue `GetScreenWidth`/`GetScreenHeight` answer the root unscaled (`0x46dd30`, `0x46dd80`). Under the switch the glue state replaces the boot state, takes input, and fires `SET_GLUE_SCREEN` on those edges; the native screens are hidden but still drive the network. A data-gated test loads the stock files with only seven unbuilt verbs raising, a list that may only shrink. 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
51a2ce5586
commit
133414f5c1
@@ -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,
|
||||
|
||||
@@ -106,6 +106,7 @@ fn spawn_screen(
|
||||
|
||||
let root = commands
|
||||
.spawn((
|
||||
crate::glue::NativeGlue,
|
||||
CharCreateUi { s },
|
||||
DynTint::Backdrop,
|
||||
GlobalZIndex(SCREEN_Z),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -145,6 +145,7 @@ fn spawn_screen(
|
||||
|
||||
let root = commands
|
||||
.spawn((
|
||||
crate::glue::NativeGlue,
|
||||
CharSelectUi {
|
||||
with_art: art.button_up.is_some(),
|
||||
s,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<NativeGlue>>) {
|
||||
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),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -141,6 +141,7 @@ fn spawn_screen(
|
||||
|
||||
let root = commands
|
||||
.spawn((
|
||||
crate::glue::NativeGlue,
|
||||
LoginUi {
|
||||
with_art: art.button_up.is_some(),
|
||||
s,
|
||||
|
||||
@@ -295,7 +295,8 @@ impl Plugin for RealmSelectPlugin {
|
||||
(
|
||||
(input::clicks, input::keys)
|
||||
.chain()
|
||||
.run_if(|realms: Res<Realms>| realms.owns_input()),
|
||||
.run_if(|realms: Res<Realms>| realms.owns_input())
|
||||
.run_if(crate::ui_script::glue::native_glue),
|
||||
(tick_refresh, screen::refresh_list)
|
||||
.chain()
|
||||
.run_if(|realms: Res<Realms>| realms.shown),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<NonSend<benilla_ui::script::UiScript>>,
|
||||
emotes: Option<Res<crate::sound::EmoteSounds>>,
|
||||
) {
|
||||
/// 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::<benilla_ui::script::UiScript>()
|
||||
.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::<benilla_ui::script::UiScript>());
|
||||
let emotes = world.get_resource::<crate::sound::EmoteSounds>();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Res<GlueStock>>,
|
||||
state: Option<Res<State<crate::char_select::ClientState>>>,
|
||||
) -> 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<Res<GlueStock>>) -> 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/<file>: <error>"`, 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<String> {
|
||||
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, `<frame>: SetScript("<handler>") 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<String> {
|
||||
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::<Vec<_>>()
|
||||
.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::<bool>(&format!("return {name} == nil")).unwrap(),
|
||||
"{name} is built; take it off UNBUILT_CALLED"
|
||||
);
|
||||
}
|
||||
|
||||
for name in SCREENS {
|
||||
assert!(
|
||||
s.eval::<bool>(&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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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::<super::glue::GlueStock>()
|
||||
.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<UiScript> {
|
||||
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<UiScript> {
|
||||
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<E: std::fmt::Display>(
|
||||
world: &mut World,
|
||||
make: fn() -> Result<UiScript, E>,
|
||||
) -> Option<UiScript> {
|
||||
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::<State<crate::char_select::ClientState>>()
|
||||
.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::<crate::minimap::MinimapWidget>() {
|
||||
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::<UiScript>()
|
||||
.eval::<bool>(&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::<UiScript>().vm_kind(),
|
||||
VmKind::Glue
|
||||
);
|
||||
assert!(shown(&world, "AccountLogin"), "the boot screen is login");
|
||||
|
||||
world.insert_resource(State::new(ClientState::CharSelect));
|
||||
world.init_resource::<PendingEntryUiLoad>();
|
||||
end_ui_session(&mut world);
|
||||
assert_eq!(
|
||||
world.non_send_resource::<UiScript>().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::<UiScript>().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() {
|
||||
|
||||
@@ -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<LogWrite>) {
|
||||
write_named(FILE, writes);
|
||||
}
|
||||
|
||||
/// [`write`] for the glue VM's drains, into the glue's own log.
|
||||
pub(super) fn write_glue(writes: Vec<LogWrite>) {
|
||||
write_named(GLUE_FILE, writes);
|
||||
}
|
||||
|
||||
fn write_named(file: &str, writes: Vec<LogWrite>) {
|
||||
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<LogWrite>) {
|
||||
let path = dir.join(FILE);
|
||||
/// [`write_named`] into `dir`.
|
||||
fn write_under(dir: &std::path::Path, file: &str, writes: Vec<LogWrite>) {
|
||||
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();
|
||||
|
||||
@@ -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::<UiAutoScaleCache>()
|
||||
.init_resource::<UiFrameCost>()
|
||||
.init_resource::<crate::bindings::WheelNotches>()
|
||||
@@ -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::<UiPassState>()
|
||||
.init_resource::<PointerOverUiPanel>()
|
||||
.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)]
|
||||
|
||||
@@ -47,9 +47,29 @@ pub(super) fn core() -> Option<Addon> {
|
||||
/// 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<Addon> {
|
||||
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<String>) -> Addon {
|
||||
named_addon(NAME, files)
|
||||
}
|
||||
|
||||
/// [`addon`] under `name`.
|
||||
fn named_addon(name: &str, files: Vec<String>) -> Addon {
|
||||
Addon::new(
|
||||
NAME.to_string(),
|
||||
name.to_string(),
|
||||
Toc {
|
||||
directives: Vec::new(),
|
||||
files,
|
||||
|
||||
@@ -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::<Model>().expect("model");
|
||||
Ok(model.screen.width())
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"GetScreenHeight",
|
||||
lua.create_function(|lua, ()| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(model.screen.height())
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"GetCursorPosition",
|
||||
lua.create_function(|lua, ()| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(model.cursor_pos)
|
||||
})?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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<UiScript> {
|
||||
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 {
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user