merge: absorb samwhosung/main on top of the upstream merge
Same fork policy: upstream structure wins, local-unique re-instated. Cargo.toml unions both measured dev-profile philosophies (codegen-units=1 + lto=off from samwhosung, debug=line-tables-only + split-debuginfo from the fork's disk measurements). glue_booth.rs takes samwhosung's typed GlueScene (token/of_race/from_model_path) and re-adds the fork's TurtleWoW races (Goblin=9, BloodElf=10) as variants; their glue fog stays on Human's row, exactly as the fork's scene_fog fallback did. ui script takes samwhosung's mouseover/hyperlink rework (world_mouseover, minimap_blip, hyperlink.rs) over the fork's inline hover_link/span_at/pointer path — the rework covers the same OnHyperlinkEnter/Leave behaviour. game_plugins.rs keeps the fork's re-measured ceilings (466 / 5,509); mail.rs keeps the explicit 13-tuple with a float daysLeft; MAP.md takes the superset.
This commit is contained in:
@@ -1,38 +1,137 @@
|
||||
//! 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::*;
|
||||
|
||||
use benilla_assets::LockRecover;
|
||||
use benilla_ui::script::UiScript;
|
||||
use benilla_ui::script::{GlueNotice, UiScript};
|
||||
|
||||
use super::manifest::silenced_ui_load;
|
||||
use super::{load_ingame_ui, CursorPayloadHeld, PlayerUiHover, UiClock, UiKeyboardCapture};
|
||||
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.
|
||||
///
|
||||
/// The IO thread's roster policy follows the VM that is up: the screen asks for its roster only
|
||||
/// when the stock glue VM was built and its character select screen loaded, so a run whose stock VM failed to build (logged as an error
|
||||
/// by [`new_vm`]) falls back to the park's own enumeration instead of waiting on a screen that
|
||||
/// never asks.
|
||||
fn install_glue_side_vm(world: &mut World, screen: super::glue::GlueScreen) {
|
||||
let stock_asked = world
|
||||
.get_resource::<super::glue::GlueStock>()
|
||||
.is_some_and(|s| s.0);
|
||||
let stock_up = if stock_asked {
|
||||
install_glue_vm(world, screen)
|
||||
} else {
|
||||
install_boot_vm(world);
|
||||
false
|
||||
};
|
||||
if stock_asked && !stock_up {
|
||||
error!("ui_script: WOW_GLUE_STOCK=1 but the stock glue VM did not come up with its character select screen; the roster is fetched at the park");
|
||||
}
|
||||
if let Some(policy) = world.get_resource::<crate::net::RosterPolicy>() {
|
||||
policy
|
||||
.0
|
||||
.store(stock_up, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
};
|
||||
load_global_strings(world, &script);
|
||||
load_emote_tokens(world, &script);
|
||||
world.insert_non_send_resource(script);
|
||||
if let Some(script) = strings_vm(world) {
|
||||
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 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) -> bool {
|
||||
let Some(mut script) = new_vm(world, UiScript::new_glue) else {
|
||||
return false;
|
||||
};
|
||||
seat_raster_seam_for_load(world, &mut script);
|
||||
seed_agreements(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);
|
||||
start_server_alert(world, &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()
|
||||
);
|
||||
// Up means the screen that asks for the roster exists: the stock files loaded far enough to
|
||||
// define the character select frame and its `OnShow`, not merely that a VM started.
|
||||
let charselect = script
|
||||
.eval::<bool>(
|
||||
"return type(CharacterSelect) == 'table' and type(CharacterSelect_OnShow) == 'function'",
|
||||
)
|
||||
.unwrap_or(false);
|
||||
world.insert_non_send_resource(script);
|
||||
charselect
|
||||
}
|
||||
|
||||
/// Start the server alert's fetch, as the glue manager's init does after the GlueXML load and
|
||||
/// before the first screen (`0x46a541`-`0x46a564`, `0x402b36`): the global `SERVER_ALERT_URL` as
|
||||
/// the glue files left it, so `GlueLocalization` wins over `GlueStrings`, fetched once per
|
||||
/// process ([`super::server_alert::begin_once`]).
|
||||
fn start_server_alert(world: &mut World, script: &UiScript) {
|
||||
let url = script.global_text("SERVER_ALERT_URL");
|
||||
if let Some(fetch) = super::server_alert::begin_once(&url) {
|
||||
if let Some(mut mgr) = world.get_resource_mut::<super::glue_mgr::GlueMgr>() {
|
||||
mgr.set_alert_fetch(fetch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the four agreement CVars into the glue VM and the manager before any screen is set, as
|
||||
/// the reference does (`0x46a400` at `0x402b36`, then `SET_GLUE_SCREEN` at `0x402b65`): the login
|
||||
/// screen's `OnShow` asks them at once, and nothing asks again.
|
||||
fn seed_agreements(world: &mut World, script: &mut UiScript) {
|
||||
let Some(cvars) = world.get_resource::<crate::cvars::Cvars>() else {
|
||||
return;
|
||||
};
|
||||
let notices = super::glue_mgr::read_agreements(cvars);
|
||||
for notice in GlueNotice::ALL {
|
||||
script.set_glue_notice(notice, notices[notice as usize]);
|
||||
}
|
||||
if let Some(mut mgr) = world.get_resource_mut::<super::glue_mgr::GlueMgr>() {
|
||||
mgr.set_notices(notices);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 +254,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);
|
||||
}
|
||||
}
|
||||
@@ -657,8 +756,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
|
||||
@@ -699,8 +814,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
|
||||
@@ -731,7 +844,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
|
||||
@@ -994,6 +1110,108 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The IO thread's roster policy follows the VM that came up: the screen asks only when the
|
||||
/// stock glue VM stands, so a boot VM leaves the park to fetch the roster.
|
||||
#[test]
|
||||
fn the_roster_policy_follows_the_vm_that_stands() {
|
||||
use std::sync::atomic::Ordering;
|
||||
for stock in [true, false] {
|
||||
let mut world = World::new();
|
||||
let policy = crate::net::RosterPolicy(Default::default());
|
||||
policy.0.store(!stock, Ordering::SeqCst);
|
||||
world.insert_resource(policy.clone());
|
||||
world.insert_resource(super::super::glue::GlueStock(stock));
|
||||
world.insert_resource(State::new(crate::char_select::ClientState::Login));
|
||||
setup_script(&mut world);
|
||||
// The stock policy needs the stock files to have loaded: no install, no screen.
|
||||
let up = stock && benilla_formats::wow_data().is_some();
|
||||
assert_eq!(policy.0.load(Ordering::SeqCst), up, "stock {stock}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The agreements are read from their CVars before the first screen is set (`0x46a400` at
|
||||
/// `0x402b36`, `SET_GLUE_SCREEN` at `0x402b65`): a returning player's login shows the boxes,
|
||||
/// not the EULA, and a first launch still shows the EULA.
|
||||
#[test]
|
||||
fn the_login_screen_comes_up_on_the_accepted_agreements() {
|
||||
use crate::char_select::ClientState;
|
||||
let _ = benilla_formats::wow_data_or_skip!();
|
||||
let screens = |cvars: crate::cvars::Cvars| {
|
||||
let mut world = World::new();
|
||||
world.insert_resource(super::super::glue::GlueStock(true));
|
||||
world.insert_resource(State::new(ClientState::Login));
|
||||
world.insert_resource(cvars);
|
||||
world.init_resource::<super::super::glue_mgr::GlueMgr>();
|
||||
setup_script(&mut world);
|
||||
world
|
||||
.non_send_resource::<UiScript>()
|
||||
.eval::<(bool, bool, bool)>(
|
||||
"return TOSFrame:IsShown() == 1, AccountLoginUI:IsShown() == 1, \
|
||||
AccountLoginAccountEdit:IsVisible() == 1",
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
let mut accepted = crate::cvars::Cvars::with_value("readEULA", "1");
|
||||
accepted.set("readTOS", "1");
|
||||
assert_eq!(
|
||||
screens(accepted),
|
||||
(false, true, true),
|
||||
"accepted agreements go straight to the login boxes"
|
||||
);
|
||||
assert_eq!(
|
||||
screens(crate::cvars::Cvars::default()),
|
||||
(true, false, false),
|
||||
"a first launch is asked for the EULA"
|
||||
);
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
|
||||
Reference in New Issue
Block a user