diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml
index 7f435f33c..9b425483a 100644
--- a/crates/benilla-app/assets/ui/OptionsFrame.xml
+++ b/crates/benilla-app/assets/ui/OptionsFrame.xml
@@ -110,7 +110,8 @@
BenillaOptionsFrame:SetScale(k)
end
- -- The era tab width, label + 40, measured after OnShow sets the scale.
+ -- The era tab width, label + 40, measured after the window's OnShow sets the scale:
+ -- the tabs' own OnShows run before it, children first.
function BenillaOptionsTab_FitWidth(tab)
tab:SetWidth(getglobal(tab:GetName() .. "Text"):GetStringWidth() + 40)
end
@@ -1341,9 +1342,6 @@
-
- BenillaOptionsTab_FitWidth(this)
-
@@ -3068,6 +3065,8 @@
BenillaOptionsFrame_UpdateScale()
+ BenillaOptionsTab_FitWidth(BenillaOptionsFrameGameTab)
+ BenillaOptionsTab_FitWidth(BenillaOptionsFrameAddOnsTab)
BenillaOptionsFrame_SelectCategory(this.selectedCategory or "Controls")
if UpdateMicroButtons then UpdateMicroButtons() end
diff --git a/crates/benilla-app/assets/ui/ScriptLogFrame.xml b/crates/benilla-app/assets/ui/ScriptLogFrame.xml
index 9a2abc309..13fd9dcc0 100644
--- a/crates/benilla-app/assets/ui/ScriptLogFrame.xml
+++ b/crates/benilla-app/assets/ui/ScriptLogFrame.xml
@@ -232,8 +232,9 @@
-
-
+
+
diff --git a/crates/benilla-app/src/capture/fixtures.rs b/crates/benilla-app/src/capture/fixtures.rs
index 6ae24160a..f5965a3c9 100644
--- a/crates/benilla-app/src/capture/fixtures.rs
+++ b/crates/benilla-app/src/capture/fixtures.rs
@@ -1358,15 +1358,23 @@ pub(super) fn seed_ui_fixture(
warn!("capture: ui-chat-tabhover select failed: {e}");
}
script.resolve();
+ // Each in the root's units the cursor is fed in: the frame's own times its scale.
let expr = match mode.as_str() {
- "2" | "3" => "return ChatFrame2Tab:GetCenter()",
+ "2" | "3" => {
+ "local x, y = ChatFrame2Tab:GetCenter() \
+ local k = ChatFrame2Tab:GetEffectiveScale() return x * k, y * k"
+ }
"0" => {
- "return (ChatFrame2:GetLeft() + ChatFrame2:GetRight()) / 2, \
- (ChatFrame2:GetBottom() + ChatFrame2:GetTop()) / 2"
+ "local k = ChatFrame2:GetEffectiveScale() \
+ return (ChatFrame2:GetLeft() + ChatFrame2:GetRight()) / 2 * k, \
+ (ChatFrame2:GetBottom() + ChatFrame2:GetTop()) / 2 * k"
}
// 9: park far away, so the dock conceals itself.
"9" => "return 2000, 2000",
- _ => "return ChatFrame1Tab:GetCenter()",
+ _ => {
+ "local x, y = ChatFrame1Tab:GetCenter() \
+ local k = ChatFrame1Tab:GetEffectiveScale() return x * k, y * k"
+ }
};
let centre: Result<(f32, f32), _> = script.eval(expr);
match centre {
diff --git a/crates/benilla-app/src/capture/probes/act.rs b/crates/benilla-app/src/capture/probes/act.rs
index bfad967f9..ddc4ea146 100644
--- a/crates/benilla-app/src/capture/probes/act.rs
+++ b/crates/benilla-app/src/capture/probes/act.rs
@@ -550,13 +550,13 @@ struct ProbeDrag {
to: (f32, f32),
}
-/// A frame's centre from its live `GetLeft`/`GetRight`/`GetTop`/`GetBottom`, in the space
-/// [`crate::ui_script::input`] feeds the cursor in.
+/// A frame's centre from its live `GetLeft`/`GetRight`/`GetTop`/`GetBottom`, times its effective
+/// scale: the edges answer in the frame's own units, the cursor is fed in the root's.
fn frame_centre(script: &benilla_ui::script::UiScript, name: &str) -> Option<(f32, f32)> {
let read = |edge: &str| {
script
.eval::(&format!(
- "local f = getglobal(\"{name}\") return f and f:Get{edge}()"
+ "local f = getglobal(\"{name}\") return f and f:Get{edge}() * f:GetEffectiveScale()"
))
.ok()
};
diff --git a/crates/benilla-app/src/chat_bubble.rs b/crates/benilla-app/src/chat_bubble.rs
index 2ad3a8f7e..1a91b7402 100644
--- a/crates/benilla-app/src/chat_bubble.rs
+++ b/crates/benilla-app/src/chat_bubble.rs
@@ -553,7 +553,7 @@ fn draw_bubble(
border_color: [1.0; 4],
};
let up = GxRect::new(-frame.max.y, frame.min.x, -frame.min.y, frame.max.x);
- for p in pieces(up, &bd) {
+ for p in pieces(up, &bd, 1.0) {
// Equal insets keep every piece axis-aligned: a y-down rect from its TL and BR corners.
let rect = Rect::new(
p.corners[0][0],
diff --git a/crates/benilla-app/src/minimap/blips/mod.rs b/crates/benilla-app/src/minimap/blips/mod.rs
index 4d5bf2ced..463d85958 100644
--- a/crates/benilla-app/src/minimap/blips/mod.rs
+++ b/crates/benilla-app/src/minimap/blips/mod.rs
@@ -160,7 +160,6 @@ pub(super) type BlipInputs<'w, 's> = (
Query<'w, 's, &'static GlobalTransform, With>,
Query<'w, 's, &'static Window, With>,
ResMut<'w, MinimapBlipHover>,
- Res<'w, crate::ui_script::UiScaleCvar>,
Res<'w, crate::ui_party::GroupState>,
TrackedCandidates<'w, 's>,
Query<'w, 's, (&'static ObjectStore, &'static Guid), With>,
@@ -200,11 +199,11 @@ pub(super) struct BlipCtx {
pub(super) wz: f32,
/// The cursor in the quads' y-down logical px.
pub(super) cursor: Option,
- /// The cursor in y-up UI space; the reference seats the blip tooltip at the cursor, its exact
- /// offset untraced.
+ /// The cursor in the y-up root units; the reference seats the blip tooltip at the cursor, its
+ /// exact offset untraced.
pub(super) cursor_ui: Option,
- /// Window px per UI unit: everything else here is window px, while Lua
- /// ([`Minimap:PingLocation`](super::ping)) speaks UI units.
+ /// Window px per root unit: everything else here is window px, while Lua
+ /// ([`Minimap:PingLocation`](super::ping)) hands root units.
pub(super) seam: f32,
}
diff --git a/crates/benilla-app/src/minimap/mod.rs b/crates/benilla-app/src/minimap/mod.rs
index 10f72ccfc..c39099465 100644
--- a/crates/benilla-app/src/minimap/mod.rs
+++ b/crates/benilla-app/src/minimap/mod.rs
@@ -351,7 +351,6 @@ fn emit_minimap(
unit_pos,
window,
mut blip_hover,
- ui_scale,
group,
tracked,
self_store,
@@ -621,7 +620,7 @@ fn emit_minimap(
}
let win = window.iter().next();
let cursor = win.and_then(|w| w.cursor_position());
- let seam = win.map_or(1.0, |w| crate::ui_script::seam_scale(w.height(), ui_scale.0));
+ let seam = win.map_or(1.0, |w| crate::ui_script::seam_scale(w.height()));
// The pan origin snapped to a half-logical-pixel grid: every blip offset and rim bearing
// derives from `wx`/`wy`, so the layer steps together instead of rewriting the batch mesh
// every frame while walking. Blip world positions stay exact.
@@ -637,11 +636,11 @@ fn emit_minimap(
wy: (wy / q).round() * q,
wz: wow[2],
cursor,
- // The cursor in UI space (y-up, ÷ the seam scale): the tooltip anchor resolves in
- // 768-virtual units, not window px.
+ // The cursor in the root's units (y-up, ÷ the seam scale): the tooltip anchor resolves
+ // in them, not window px.
cursor_ui: cursor
.zip(win)
- .map(|(c, w)| Vec2::new(c.x / seam, (w.height() - c.y) / seam)),
+ .map(|(c, w)| crate::ui_script::window_to_ui(w.height(), c)),
seam,
}
});
diff --git a/crates/benilla-app/src/minimap/ping.rs b/crates/benilla-app/src/minimap/ping.rs
index c218c7560..bf4e6ba28 100644
--- a/crates/benilla-app/src/minimap/ping.rs
+++ b/crates/benilla-app/src/minimap/ping.rs
@@ -5,9 +5,10 @@
//! [`drive_minimap_ping`] republishes against the live view radius, so the marker follows the pan
//! and the zoom with no second copy to fall out of step.
//!
-//! - In: the stock `Minimap_OnClick` calls `Minimap:PingLocation(dx, dy)` in UI units from the
-//! centre; the renderer converts it with the geometry of the frame it draws: UI units × the seam
-//! scale = window px, ÷ `px_per_yd` = yards.
+//! - In: the stock `Minimap_OnClick` calls `Minimap:PingLocation(dx, dy)` in the minimap's units
+//! from the centre, which the engine hands over in root units; the renderer converts them with
+//! the geometry of the frame it draws: root units × the seam scale = window px, ÷ `px_per_yd` =
+//! yards.
//! - Across: our ping sends `MSG_MINIMAP_PING` (raw world floats, relayed to the rest of the group
//! only, `GroupHandler.cpp:384-391`); a member's seats the same way. A ping seats locally at
//! click time, so a solo ping works.
@@ -59,8 +60,8 @@ impl MinimapPing {
}
}
-/// A `Minimap:PingLocation(x, y)` click to the world point it names: `ui` is centre-relative UI
-/// units (x right, y up), `seam` window px per UI unit ([`crate::ui_script::seam_scale`]), `ctx`
+/// A `Minimap:PingLocation(x, y)` click to the world point it names: `ui` is centre-relative root
+/// units (x right, y up), `seam` window px per root unit ([`crate::ui_script::seam_scale`]), `ctx`
/// the map as drawn this frame. The inverse of [`BlipCtx::offset`]: screen right is −WoW y, screen
/// up +WoW x. `None` outside the disc, the stock test (`Minimap.lua:135`) in yards.
fn click_to_world(ctx: &BlipCtx, ui: (f32, f32), seam: f32) -> Option<(f32, f32)> {
@@ -193,13 +194,13 @@ mod tests {
}
}
- /// The click is in UI units and `px_per_yd` in window px: at 0.9 uiScale on a 1080p window the
- /// seam is ≈1.27, and a conversion that skips it seats every ping ≈27 % too far out.
+ /// The click is in root units and `px_per_yd` in window px: on a 1080p window the seam is
+ /// ≈1.41, and a conversion that skips it seats every ping ≈41 % too far out.
#[test]
fn a_click_converts_through_the_seam_scale() {
let c = ctx();
- let seam = 1080.0 / 768.0 * 0.9; // the shipped default at 1080p
- // 20 UI units right → 20·seam px → yards west (−y).
+ let seam = 1080.0 / 768.0;
+ // 20 root units right → 20·seam px → yards west (−y).
let (x, y) = click_to_world(&c, (20.0, 0.0), seam).expect("inside the disc");
let expect_yd = 20.0 * seam / c.px_per_yd;
assert!((x - 0.0).abs() < 1e-3, "no northing from a due-east click");
diff --git a/crates/benilla-app/src/perf/hud.rs b/crates/benilla-app/src/perf/hud.rs
index ec59c09b6..d963a0a02 100644
--- a/crates/benilla-app/src/perf/hud.rs
+++ b/crates/benilla-app/src/perf/hud.rs
@@ -113,9 +113,9 @@ pub(super) fn refresh_hud_snapshot(
/// clear). The always-up world-state readout (`WorldStateAlwaysUpFrame`, stock
/// `WorldStateFrame.xml`) shares the band, and the dev pill is the one that yields.
///
-/// The layout answers in UI units, a screen `768/uiScale` units tall whatever the window, while
-/// the pill draws in window px, so the chunk returns a fraction of the screen and the caller
-/// scales it by the window height. Reading an edge settles the layout, one graph solve at most.
+/// The layout answers in `UIParent`'s units, a screen `768/uiScale` units tall whatever the
+/// window, while the pill draws in window px, so the chunk returns a fraction of the screen and the
+/// caller scales it by the window height. Reading an edge settles the layout, one graph solve at most.
pub(crate) fn top_centre_claimed(script: &UiScript, win_h: f32) -> f32 {
// `WorldStateAlwaysUpFrame` is always shown; the readout occupies its `AlwaysUpFrame` rows,
// built on demand and hidden when empty, so the claim is the lowest shown row's bottom.
diff --git a/crates/benilla-app/src/ui_chat/channels.rs b/crates/benilla-app/src/ui_chat/channels.rs
index 82175b243..12eb95848 100644
--- a/crates/benilla-app/src/ui_chat/channels.rs
+++ b/crates/benilla-app/src/ui_chat/channels.rs
@@ -444,8 +444,8 @@ pub(super) fn auto_join_zone_channels(
walk.at = Some(zone_id);
}
-/// Feed the VM the zone-less catalog before any interface file runs: world entry runs FrameXML,
-/// the addons and `PLAYER_LOGIN` in one call, ahead of the walk's first tick.
+/// Feed the VM the zone-less catalog before any interface file runs: world entry runs FrameXML
+/// and the addons in one call, ahead of the walk's first tick.
pub(crate) fn seed_zone_channel_catalog(
world: &mut World,
script: &mut benilla_ui::script::UiScript,
diff --git a/crates/benilla-app/src/ui_chat/settings.rs b/crates/benilla-app/src/ui_chat/settings.rs
index 9286ae725..dfa25fa70 100644
--- a/crates/benilla-app/src/ui_chat/settings.rs
+++ b/crates/benilla-app/src/ui_chat/settings.rs
@@ -623,8 +623,9 @@ pub(crate) fn fold_dying_vm_chat_cache(world: &mut World) {
pub(super) fn plugin(app: &mut App) {
app.init_resource::()
- // The restore runs from the world-entry UI load, not here: its events must precede the
- // first chat line and `PLAYER_LOGIN`, which no `Update` ordering guarantees.
+ // The restore runs from the world-entry UI load, not here: its events must follow
+ // `VARIABLES_LOADED` inside the load, where the reference registers its reader
+ // (`0x4900d6`), and precede the first chat line and `PLAYER_LOGIN`.
.add_systems(
Update,
// After the tick: every write it watches is Lua's.
diff --git a/crates/benilla-app/src/ui_chat/tests.rs b/crates/benilla-app/src/ui_chat/tests.rs
index 9d5e22f4a..f41b8b894 100644
--- a/crates/benilla-app/src/ui_chat/tests.rs
+++ b/crates/benilla-app/src/ui_chat/tests.rs
@@ -2397,8 +2397,9 @@ fn the_free_professions_line_is_printed_once() {
// ───────────── the chat cache restores inside the login ─────────
/// `UPDATE_CHAT_WINDOWS` registers the chat frames for `CHAT_MSG_*`, and the `UPDATE_CHAT_COLOR`
-/// burst repaints id-0 lines through `ChatTypeInfo["REPLY"]`, so both come before `PLAYER_LOGIN`.
-/// The probe is a loose addon, because the entry load builds the VM it runs on.
+/// burst repaints id-0 lines through `ChatTypeInfo["REPLY"]`, so both come before `PLAYER_LOGIN`,
+/// which the load arms and the world-enter cascade fires. The probe is a loose addon, because the
+/// entry load builds the VM it runs on.
#[test]
fn the_chat_cache_restore_is_finished_before_player_login() {
let _data = benilla_formats::wow_data_or_skip!();
@@ -2485,6 +2486,10 @@ fn the_chat_cache_restore_is_finished_before_player_login() {
1,
));
crate::ui_script::load_ingame_ui_on_world_entry(&mut world);
+ // The cascade, as the unit feed runs it once the player is in the world.
+ world
+ .non_send_resource_mut::()
+ .fire_world_enter();
let read = |expr: &str| -> String {
world
@@ -2512,7 +2517,7 @@ fn the_chat_cache_restore_is_finished_before_player_login() {
read("ChatOrderProbe.loginRegistered")
);
// The reference's login order: `ADDON_LOADED` (`0x4900a3`), `VARIABLES_LOADED` (`0x4900b2`),
- // the chat-cache burst (`0x4900d6`), `PLAYER_LOGIN` (`0x490959`).
+ // the chat-cache burst (`0x4900d6`), the arm (`0x49011d`), `PLAYER_LOGIN` (`0x490959`).
assert_eq!(
read("ChatOrderProbe.order"),
"VARIABLES_LOADED UPDATE_CHAT_WINDOWS UPDATE_CHAT_COLOR PLAYER_LOGIN ",
diff --git a/crates/benilla-app/src/ui_script/addons.rs b/crates/benilla-app/src/ui_script/addons.rs
index 81fba5a66..5ba49f7de 100644
--- a/crates/benilla-app/src/ui_script/addons.rs
+++ b/crates/benilla-app/src/ui_script/addons.rs
@@ -1692,9 +1692,10 @@ mod tests {
);
}
- /// `UI_Init 0x48fbf0` loads the addons at `0x4900a3` (each `ADDON_LOADED` at `0x51f5ad`) and
- /// fires `VARIABLES_LOADED` at `0x4900b2`; `PLAYER_LOGIN` comes after, on a fresh login from
- /// the player's create (`0x5deb60` → `0x4908c0`), on a `/reload` from `0x490168`.
+ /// `UI_Init 0x48fbf0` loads the addons at `0x4900a3` (each `ADDON_LOADED` at `0x51f5ad`),
+ /// fires `VARIABLES_LOADED` at `0x4900b2` and arms `PLAYER_LOGIN` (`0x49011d`), which the
+ /// world-enter cascade fires: on a fresh login from the player's create (`0x5deb60` →
+ /// `0x4908c0`), on a `/reload` from `0x490168`.
#[test]
fn the_ui_init_events_fire_in_the_reference_order() {
let _l = crate::local_state::test_env::ENV_LOCK
@@ -1728,6 +1729,12 @@ mod tests {
let failures = load_third_party(&mut script, None, &[], true);
assert!(failures.is_empty(), "load errors: {failures:?}");
crate::ui_script::finish_ui_load(&mut script);
+ assert_eq!(
+ event_log(&script),
+ vec!["ADDON_LOADED:EventProbe", "VARIABLES_LOADED:nil"],
+ "the load only arms PLAYER_LOGIN"
+ );
+ script.fire_world_enter();
assert_eq!(
event_log(&script),
diff --git a/crates/benilla-app/src/ui_script/extract/mod.rs b/crates/benilla-app/src/ui_script/extract/mod.rs
index 972afda46..4fb1ef097 100644
--- a/crates/benilla-app/src/ui_script/extract/mod.rs
+++ b/crates/benilla-app/src/ui_script/extract/mod.rs
@@ -425,7 +425,11 @@ pub(super) fn tick_script(
mut last_seam: Local,
// The `scale_factor` measures were last answered under; 0 until the first frame.
mut last_dpi: Local,
- ui_scale: Res,
+ // The dial, `UIParent`'s scale in the VM, and the automatic scale's last display.
+ (ui_scale, mut auto_cache): (
+ Res,
+ Option>,
+ ),
mut ui_cost: ResMut,
mut pass: ResMut,
) {
@@ -442,9 +446,9 @@ pub(super) fn tick_script(
};
let (w, h) = (window.width(), window.height());
// The client's UI space is 768 units tall at every aspect (`f(screenH) = 768`, `0x41ad10`; the
- // caret's `H_px/192`, `0x77b8c0`), `768/uiScale` with the dial. The VM lives in it: quads
- // scale ×s out, and the mouse and the measures ÷s in.
- let s = super::seam_scale(h, ui_scale.0);
+ // caret's `H_px/192`, `0x77b8c0`), the dial being `UIParent`'s scale inside it. The VM lives in
+ // it: quads scale ×s out, and the mouse and the measures ÷s in.
+ let s = super::seam_scale(h);
// A moved seam scale or DPI stales every text metric the engine caches: integer-stepped
// advances do not rescale, and the size snap moves a string's width by several percent,
// enough for the ellipsis to eat fitting text. A monitor hop moves the DPI alone.
@@ -486,8 +490,17 @@ 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 resize re-runs what the interface computed from the old screen size.
+ // 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());
@@ -1713,9 +1726,10 @@ mod clip_plumb_tests {
);
}
- /// At dial 0.5 the VM sees a 2048×1536 screen, and every window-px rect halves.
+ /// The dial is `UIParent`'s scale, so a ScrollFrame with no parent keeps its px rect and clip
+ /// at dial 0.5: the root stays 768 units tall.
#[test]
- fn the_uiscale_dial_scales_the_extracted_rects() {
+ fn the_uiscale_dial_leaves_a_parentless_frame_alone() {
let mut app = app_with_scrolled_marker();
app.insert_resource(crate::ui_script::UiScaleCvar(0.5));
app.update();
@@ -1726,12 +1740,10 @@ mod clip_plumb_tests {
.find(|q| q.color[0] == 1.0 && q.color[1] == 0.0 && q.color[2] == 0.0)
.expect("the colored marker quad extracted");
- // WoW space: top 1536 − 100 = 1436, bottom 1236, right 300; ×0.5 is y-up px 618..718,
- // and y-down through the 768 window the top is 50 and the bottom 150.
assert_eq!(
marker.clip,
- Some(Rect::new(0.0, 50.0, 150.0, 150.0)),
- "dial 0.5 halves the px rect and re-hangs the frame from the taller virtual top"
+ Some(Rect::new(0.0, 100.0, 300.0, 300.0)),
+ "dial 0.5 moves nothing outside UIParent"
);
}
diff --git a/crates/benilla-app/src/ui_script/extract/text.rs b/crates/benilla-app/src/ui_script/extract/text.rs
index 00d95d385..e7bd93126 100644
--- a/crates/benilla-app/src/ui_script/extract/text.rs
+++ b/crates/benilla-app/src/ui_script/extract/text.rs
@@ -40,10 +40,11 @@ pub(super) struct TextHost<'a> {
/// The focused editbox's text UI, unfiltered: [`emit`] matches it to this quad by `target`.
pub ebox: Option<&'a EditBoxTextUi>,
pub screen_h: f32,
- /// The seam scale `windowH/768 × uiScale`, for the unit-space inputs; rects arrive in px.
+ /// The seam scale `windowH/768`, for the root-unit inputs; rects arrive in px.
pub scale: f32,
/// The frame's effective scale, already in the rect: glyph size and shadow offset ride it, as
- /// the 1.12 client's text rides `SetScale`; the editbox x-offsets arrive in screen UI units.
+ /// the 1.12 client's text rides `SetScale` and the UI scale; the editbox x-offsets arrive in
+ /// root units.
pub font_scale: f32,
/// Captures pin the caret on for deterministic pixels; live, the engine's blink decides.
pub caret_pinned: bool,
@@ -387,7 +388,7 @@ mod tests {
assert_eq!(band_clip_slack(Some(2.0)), 3.0);
}
- /// `1.0546875` is the seam scale of a 1600×900 window at uiScale 0.9.
+ /// `1.0546875` is a 1600×900 window's seam times the 0.9 UI scale.
#[test]
fn a_shadow_offset_is_a_whole_pixel_and_never_vanishes() {
assert_eq!(shadow_offset_px(1.0), 1.0);
diff --git a/crates/benilla-app/src/ui_script/input.rs b/crates/benilla-app/src/ui_script/input.rs
index dd1b58019..2a296cc60 100644
--- a/crates/benilla-app/src/ui_script/input.rs
+++ b/crates/benilla-app/src/ui_script/input.rs
@@ -94,8 +94,6 @@ pub(super) fn feed_ui_input(
),
// The characters the active layout makes, which name a key for a keyboard frame.
layout: Res,
- // The uiScale dial folded into the seam scale.
- ui_scale: Res,
) {
let (keyboard, keys, capture, clipboard) = (&mut kbd.0, &kbd.1, &mut kbd.2, &mut kbd.3);
let world_pick = pointer.world_pick();
@@ -139,10 +137,8 @@ pub(super) fn feed_ui_input(
.or_else(crate::target::hover_probe_point)
.filter(|_| !synthetic)
{
- // The window cursor is logical px, y-down from the top left; the UI is y-up in 768-high
- // units under uiScale: flip through the window height, then undo the extract seam's scale.
- let s = super::seam_scale(window.height(), ui_scale.0);
- let (x, y) = (cursor.x / s, (window.height() - cursor.y) / s);
+ // The window cursor in the root's y-up 768-high units, as `GetCursorPosition` answers it.
+ let Vec2 { x, y } = super::window_to_ui(window.height(), cursor);
// `WOW_HIT_COST=1` meters this call's per-frame hit-test rebuild, one line a second.
static HIT_COST: std::sync::OnceLock = std::sync::OnceLock::new();
let metering =
diff --git a/crates/benilla-app/src/ui_script/inspect_tests.rs b/crates/benilla-app/src/ui_script/inspect_tests.rs
index 0dfc3175d..dcc593128 100644
--- a/crates/benilla-app/src/ui_script/inspect_tests.rs
+++ b/crates/benilla-app/src/ui_script/inspect_tests.rs
@@ -492,16 +492,19 @@ fn toggle_inspect_reopens_after_a_close_and_raises_like_the_reference() {
s.eval::("return InspectFrame:IsVisible()").unwrap(),
"the window still shows — Show() runs before OnShow raises"
);
- // Two raises: `InspectFrame` and `InspectPaperDollFrame` each run their own `OnShow` under
- // their own pcall, the first dying at the portrait's gate, the second at `UnitLevel`'s.
+ // Two raises: `InspectPaperDollFrame` and `InspectFrame` each run their own `OnShow` under
+ // their own pcall, the page first, since the show cascade notifies children before their
+ // parent (`0x76aed5`, then `0x76aef5`): the page dies at `UnitLevel`'s gate, the window at
+ // the portrait's.
let errors = s.errors();
assert_eq!(errors.len(), 2, "one raise per OnShow handler: {errors:?}");
assert!(
- errors[0].contains(r#"Usage: SetPortraitTexture(texture, "unit")"#),
- "the portrait's gate, before the name line is ever reached: {errors:?}"
+ errors[0].contains(r#"Usage: UnitLevel("unit")"#),
+ "the paper doll's own handler, dying at its first getter: {errors:?}"
);
assert!(
- errors[1].contains(r#"Usage: UnitLevel("unit")"#),
- "and the paper doll's own handler, dying at its first getter: {errors:?}"
+ errors[1].contains(r#"Usage: SetPortraitTexture(texture, "unit")"#),
+ "then the window's, at the portrait's gate, before the name line is ever reached: \
+ {errors:?}"
);
}
diff --git a/crates/benilla-app/src/ui_script/lifecycle.rs b/crates/benilla-app/src/ui_script/lifecycle.rs
index eafdcd069..d91c92bb9 100644
--- a/crates/benilla-app/src/ui_script/lifecycle.rs
+++ b/crates/benilla-app/src/ui_script/lifecycle.rs
@@ -230,26 +230,22 @@ pub(crate) fn run_pending_entry_load(world: &mut World) {
);
}
-/// Seat the VM's screen size and text measurer for the load, under the current window and
-/// `uiScale`: the load-edge half of [`super::extract::tick_script`]'s per-frame pair. A fresh VM
-/// answers 1024×768 until then, and a value computed once at load keeps it: stock
-/// `WorldMapFrame_OnLoad` sizes the full-screen `BlackoutWorld` from `GetScreenWidth()` and
-/// `GetScreenHeight()` and never again (`WorldMapFrame.lua:19-28`). With no atlas yet (it bakes on
-/// the first `Update`) the per-frame pass seats the measurer.
+/// Seat the VM's screen size and text measurer for the load, under the current window: the
+/// load-edge half of [`super::extract::tick_script`]'s per-frame pair. A fresh VM answers 1024×768
+/// until then, and a value computed once at load keeps it: stock `WorldMapFrame_OnLoad` sizes the
+/// full-screen `BlackoutWorld` from `GetScreenWidth()` and `GetScreenHeight()` and never again
+/// (`WorldMapFrame.lua:19-28`), which answer the root while `UIParent` is still at scale 1. With no
+/// atlas yet (it bakes on the first `Update`) the per-frame pass seats the measurer.
fn seat_raster_seam_for_load(world: &mut World, script: &mut UiScript) {
- let ui_scale = world
- .get_resource::()
- .map_or(1.0, |c| c.0);
let (w, h) = {
let mut q = world.query_filtered::<&Window, With>();
q.single(world)
.map_or((0.0, 0.0), |win| (win.width(), win.height()))
};
- let s = super::seam_scale(h, ui_scale);
+ let s = super::seam_scale(h);
if w > 0.0 && h > 0.0 {
- // The VM lives in 768-tall virtual space, so logical size over the seam scale is its
- // screen. No `UIParent_ManageFramePositions` here, unlike `tick_script`: nothing is laid
- // out yet.
+ // The VM lives in a 768-tall root, so logical size over the seam scale is its screen. No
+ // `UIParent_ManageFramePositions` here, unlike `tick_script`: nothing is laid out yet.
script.set_screen_size(w / s, h / s);
}
let Some(atlas) = world.get_resource::() else {
@@ -258,12 +254,29 @@ fn seat_raster_seam_for_load(world: &mut World, script: &mut UiScript) {
super::extract::seat_text_measurer(script, atlas, s);
}
+/// Which `"player"` a UI load runs its files under, by the edge that runs it.
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum LoadSeat {
+ /// World entry: `UI_Init` runs before `CMSG_PLAYER_LOGIN` goes out (the load at `0x46c236`,
+ /// the send in a later step at `0x46c272`), so no player object exists and the roster's
+ /// record stands in ([`seat_from_roster`]).
+ Roster,
+ /// `/reload`: the player object stays in the world through the teardown and the load, so the
+ /// live one is seated ([`crate::ui_unit::live_player_seat`]), the roster's if none exists.
+ Live,
+}
+
/// Load the in-game UI for this session onto a VM built for it, once per world entry. The
/// reference builds the UI in `UI_Init` (`0x48fbf0`, re-making the Lua state at `0x48fe97`) and
/// destroys it at `0x490bd0`, so every login runs every addon's file scope afresh. Safe on the
/// state edge because the initial transition runs after `PostStartup`: a capture booting straight
/// into `InWorld` still loads after [`benilla_assets::AssetSet::Open`].
pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
+ load_ingame_ui_seated(world, LoadSeat::Roster);
+}
+
+/// [`load_ingame_ui_on_world_entry`] under the `"player"` its edge has.
+fn load_ingame_ui_seated(world: &mut World, seat: LoadSeat) {
// The parked VM goes back into the world, where the CVar fold reads it; a reload has none
// parked and uses the live one.
unpark_boot_vm(world);
@@ -322,19 +335,31 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
}
// The player too, before the addon walk. The record first: the reference's
// `UnitName`/`UnitRace`/`UnitClass`/`UnitSex` read only a copy of the char-enum row made at the
- // Enter World commit (`0x5abd9e`) and never cleared, and each rebuilt VM is told once. The
- // snapshot stands in for the descriptor until that streams in.
+ // Enter World commit (`0x5abd9e`) and never cleared, and each rebuilt VM is told once. Then
+ // the unit: the live player on a `/reload`, else the roster's stand-in until the descriptor
+ // streams in.
if let Some(record) = world
.get_resource::()
.and_then(record_from_roster)
{
script.set_player_record(record);
}
- if let Some(seat) = world
- .get_resource::()
- .and_then(seat_from_roster)
+ match (seat == LoadSeat::Live)
+ .then(|| live_player_seat(world))
+ .flatten()
{
- script.set_unit("player", Some(seat));
+ Some(live) => {
+ script.set_unit("player", Some(live.state));
+ script.set_player_xp(live.xp.0, live.xp.1);
+ }
+ None => {
+ if let Some(seat) = world
+ .get_resource::()
+ .and_then(seat_from_roster)
+ {
+ script.set_unit("player", Some(seat));
+ }
+ }
}
// The addon version gate, read from the persisted base the fold above just made current, so a
// character-screen "Load out of date AddOns" click counts. A bare test world is check on.
@@ -358,6 +383,9 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
// text width, `UIPanelTemplates.xml:370-373`), which answers 0 with no measurer.
seat_raster_seam_for_load(world, &mut script);
// Read out of the world first: the bracket below borrows `world` for the chat-cache restore.
+ let ui_scale = world
+ .get_resource::()
+ .map_or(1.0, |c| c.0);
let zoom = {
let z = world.resource::();
(z.outdoor, z.inside)
@@ -392,6 +420,21 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
|script| crate::vplates::seat_legacy_settings(script, &legacy_plates),
|script| {
crate::ui_chat::restore_chat_looks(world, script);
+ // The UI scale lands on `UIParent` once the files, the addons and
+ // `VARIABLES_LOADED` are done, after the chat reader (`0x4900d6`) and before the
+ // `PLAYER_LOGIN` arm (`0x49011d`), through the display handler (`0x49010e` →
+ // `0x492e90`). So every `OnLoad` sees `UIParent` at scale 1 and the root's size.
+ let display = {
+ let mut q =
+ world.query_filtered::<&Window, With>();
+ q.single(world).map_or((0, 0), |w| {
+ (w.width().round() as u32, w.height().round() as u32)
+ })
+ };
+ let mut none = None;
+ let mut cache = world.get_resource_mut::();
+ let cache = cache.as_deref_mut().map_or(&mut none, |c| &mut c.0);
+ script.seat_ui_scale(ui_scale, display, cache);
},
);
});
@@ -418,6 +461,17 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
}
}
+/// The live player's seat off the world, through the unit feed's own builder; `None` before our
+/// own create. A world without the feed's resources (a bare test world) has none to read.
+fn live_player_seat(world: &mut World) -> Option {
+ world
+ .run_system_cached(crate::ui_unit::live_player_seat)
+ .unwrap_or_else(|e| {
+ warn!("ui_script: no live player to seat for the load ({e}); the roster stands in");
+ None
+ })
+}
+
/// The wire's gender byte (0 male, 1 female) on `UnitSex`'s 2/3 scale, as `ui_unit::snapshot`
/// maps the descriptor's; the record and the seat share it.
fn roster_sex(gender: u8) -> u8 {
@@ -648,11 +702,13 @@ pub(crate) fn end_ui_session(world: &mut World) {
pub(crate) struct ReloadUiPending(pub(crate) bool);
/// Run a pending `ReloadUI()`: the reference's teardown and rebuild (`0x495664 call 0x490bd0`,
-/// `0x495669 call 0x48fbf0`), for us [`end_ui_session`] then [`load_ingame_ui_on_world_entry`]
-/// without leaving the world, so a `DisableAddOn` staged in the dying VM reaches `AddOns.txt`
-/// before the rebuild reads it. `PLAYER_ENTERING_WORLD` then refires from [`crate::ui_unit`]'s
-/// feed on the new VM. In-world only, as the reference's gate (`0x494a50(0xa)`) is; elsewhere the
-/// request is dropped.
+/// `0x495669 call 0x48fbf0`), for us [`end_ui_session`] then the load without leaving the world,
+/// so a `DisableAddOn` staged in the dying VM reaches `AddOns.txt` before the rebuild reads it.
+/// The load runs under the live player, which never left. `UI_Init` then enters the world-enter
+/// cascade itself (`0x490168`), firing the `PLAYER_LOGIN` it armed and `PLAYER_ENTERING_WORLD`;
+/// ours fire from [`crate::ui_unit`]'s feed on the new VM's first `Update`, once it has re-seated
+/// the player. In-world only, as the reference's gate (`0x494a50(0xa)`) is; elsewhere the request
+/// is dropped.
pub(crate) fn run_pending_reload(world: &mut World) {
if !std::mem::take(&mut world.resource_mut::().0) {
return;
@@ -667,7 +723,7 @@ pub(crate) fn run_pending_reload(world: &mut World) {
}
info!("ui_script: ReloadUI — ending the UI session and building a new one");
end_ui_session(world);
- load_ingame_ui_on_world_entry(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
// `SetActiveMover`, which only the player's create runs.
@@ -710,13 +766,13 @@ pub(crate) fn shutdown_on_exit(
/// saved file and fires `VARIABLES_LOADED` (`0x4900b2` → `0x4913b0`), and arms `PLAYER_LOGIN`
/// (`[0xb4e260]`, set at `0x49011d`). The world-enter cascade `0x4908c0` fires `PLAYER_LOGIN` if
/// armed (read at `0x49094b`, fired at `0x490959`, disarmed at `0x49095e`), then
-/// `PLAYER_ENTERING_WORLD` (`0x49096a`).
+/// `PLAYER_ENTERING_WORLD` (`0x49096a`): [`UiScript::fire_world_enter`].
///
/// On a `/reload`, `UI_Init` enters the cascade itself (`0x490168`). On a fresh login that call is
/// skipped (`0x490166 je`, the active-player GUID still 0) and the cascade runs from the local
-/// player's own create (`0x5deb60 call 0x4908c0`). This fires `PLAYER_LOGIN` at the end of the
-/// load on both, the `/reload` shape; `PLAYER_ENTERING_WORLD` fires from [`crate::ui_unit`] once
-/// the self descriptor lands.
+/// player's own create (`0x5deb60 call 0x4908c0`). Either way the player object exists when it
+/// runs, so this only arms, and [`crate::ui_unit`]'s feed fires both once it has seated the
+/// player.
#[cfg(test)]
pub(crate) fn finish_ui_load(script: &mut UiScript) {
finish_ui_load_with(script, |_| {}, |_| {});
@@ -724,10 +780,10 @@ pub(crate) fn finish_ui_load(script: &mut UiScript) {
/// [`finish_ui_load`] with its two host seams. `host_settings` runs between the saved-variables
/// chunk and `VARIABLES_LOADED`, carrying over a setting `config.toml` held before it moved into
-/// the saved variables. `between`
-/// is the chat-cache restore's `UPDATE_CHAT_WINDOWS` + `UPDATE_CHAT_COLOR` burst, after
-/// `VARIABLES_LOADED` and before `PLAYER_LOGIN`, where the reference registers its reader
-/// (`0x4900d6` → `0x498a20`). On a fresh login the reference's cache (`0x5afe50`) defers that
+/// the saved variables. `between` runs after `VARIABLES_LOADED` and before the `PLAYER_LOGIN` arm
+/// (so before the event too): the chat-cache restore's `UPDATE_CHAT_WINDOWS` +
+/// `UPDATE_CHAT_COLOR` burst, where the reference registers its reader (`0x4900d6` → `0x498a20`),
+/// then the UI scale (`0x49010e`). On a fresh login the reference's cache (`0x5afe50`) defers that
/// reader until the server's account data reconciles (`0x5afa42`/`0x5afd01`), after `UI_Init` has
/// returned. Either way a `VARIABLES_LOADED` handler sees the boot chat colours, not the file's.
pub(crate) fn finish_ui_load_with(
@@ -736,11 +792,12 @@ pub(crate) fn finish_ui_load_with(
between: impl FnOnce(&mut UiScript),
) {
// Still the load edge, so still bounded: re-armed so the saved variables and every
- // `PLAYER_LOGIN` handler get the full allowance; the entry edge disarms it after.
+ // `VARIABLES_LOADED` handler get the full allowance; the entry edge disarms it after. The
+ // world-enter events fire later, unbounded, as every event the feeds fire is.
script.set_instruction_budget(addons::LOAD_INSTRUCTION_BUDGET);
crate::ui_saved::load_saved_variables(script, host_settings);
between(script);
- script.fire_event("PLAYER_LOGIN", vec![]);
+ script.arm_player_login();
}
/// Run the patch chain's `Interface\FrameXML\GlobalStrings.lua`, the first file in the reference's
diff --git a/crates/benilla-app/src/ui_script/manifest.rs b/crates/benilla-app/src/ui_script/manifest.rs
index 269d5f59c..3a1da8b8e 100644
--- a/crates/benilla-app/src/ui_script/manifest.rs
+++ b/crates/benilla-app/src/ui_script/manifest.rs
@@ -68,13 +68,13 @@ pub(super) fn apply_buff_durations(script: &UiScript) -> Result<(), String> {
.map_err(|e| e.to_string())
}
-/// What a screen-size change re-runs, from `extract::tick_script`'s resize arm: anchors follow the
-/// screen, but not a size or seat computed from the old one. The managed pass re-wraps the open
-/// bags, whose columns `updateContainerFrameAnchors` lays out by `GetScreenHeight()`.
+/// What a screen-size or UI-scale change re-runs, from `extract::tick_script`: anchors follow the
+/// screen, but not a size or seat computed from the old `GetScreenHeight()`. The managed pass
+/// re-wraps the open bags, whose columns `updateContainerFrameAnchors` lays out by it.
/// Deviation: [`FULLSCREEN_QUADS_RESEAT`] re-sizes the full-screen quads, which the stock files
-/// size only in their OnLoads, because a resize or `uiScale` change here keeps the loaded UI and
-/// the blackout would stop short of the new screen. Host-side, not in the layer: it adapts the
-/// engine's resizable window, and changes nothing the stock UI does at a fixed size.
+/// size only in their OnLoads, because a resize here keeps the loaded UI and the blackout would
+/// stop short of the new screen. Host-side, not in the layer: it adapts the engine's resizable
+/// window, and changes nothing the stock UI does at a fixed size.
pub(super) fn on_screen_resized(script: &UiScript) {
let _ = script.run("if UIParent_ManageFramePositions then UIParent_ManageFramePositions() end");
if let Err(e) = script.run(FULLSCREEN_QUADS_RESEAT) {
@@ -85,11 +85,14 @@ pub(super) fn on_screen_resized(script: &UiScript) {
/// The sizing arithmetic of `WorldMapFrame_OnLoad` (`WorldMapFrame.lua:19-28`) and
/// `CinematicFrame_OnLoad` (`CinematicFrame.lua:6-21`), each existence-guarded: the glue screens
/// have neither. Below 4:3 the bars go back to `CinematicFrame.xml`'s declared 1024 x 128, where
-/// the stock OnLoad leaves them.
+/// the stock OnLoad leaves them. The OnLoads read the screen root's size, `UIParent` being still at
+/// scale 1 then, and their frames hang off the root, so this re-reads the root:
+/// `GetScreenWidth()` times `UIParent`'s scale.
const FULLSCREEN_QUADS_RESEAT: &str = r#"
+local rootScale = UIParent and UIParent:GetEffectiveScale() or 1
if BlackoutWorld then
- local width = GetScreenWidth()
- local height = GetScreenHeight()
+ local width = GetScreenWidth() * rootScale
+ local height = GetScreenHeight() * rootScale
if ( width / height < 4 / 3 ) then
width = width * 1.25
height = height * 1.25
@@ -98,8 +101,8 @@ if BlackoutWorld then
BlackoutWorld:SetHeight( height )
end
if UpperBlackBar and LowerBlackBar then
- local width = GetScreenWidth()
- local height = GetScreenHeight()
+ local width = GetScreenWidth() * rootScale
+ local height = GetScreenHeight() * rootScale
local barWidth, blackBarHeight = 1024, 128
if ( width / height > 4 / 3 ) then
local desiredHeight = width / 2
diff --git a/crates/benilla-app/src/ui_script/mod.rs b/crates/benilla-app/src/ui_script/mod.rs
index 47df8f77e..2b01b917c 100644
--- a/crates/benilla-app/src/ui_script/mod.rs
+++ b/crates/benilla-app/src/ui_script/mod.rs
@@ -1,8 +1,9 @@
//! The UI-engine bridge: hosts [`benilla_ui::script::UiScript`] and feeds its
//! [`extract`](benilla_ui::script::UiScript::extract) output into the quad pass
//! ([`crate::ui_pass::UiQuads`]) every frame. The script side is WoW UI space (y-up, origin
-//! bottom-left, a screen `768/uiScale` units tall); [`seam_scale`] carries quads ×s out and the
-//! mouse ÷s in, and extraction flips to y-down window px.
+//! bottom-left, a screen root 768 units tall at scale 1); [`seam_scale`] carries quads ×s out and
+//! the mouse ÷s in, and extraction flips to y-down window px. The UI scale is `UIParent`'s own
+//! frame scale inside the VM ([`UiScaleCvar`]), never part of the seam.
use bevy::prelude::*;
@@ -199,8 +200,10 @@ pub(crate) fn run_or_warn(script: &benilla_ui::script::UiScript, chunk: &str) {
}
}
-/// The reference's `uiScale` CVar: the VM's screen is `768/uiScale` units tall, so
-/// `uiScale = 768/screenH` is pixel-perfect. Tests pin the `Default` 1.0.
+/// The reference's `uiScale` CVar, which the VM makes `UIParent`'s own scale
+/// ([`UiScript::set_ui_scale`]): the screen root stays 768 units tall, so `uiScale = 768/screenH`
+/// is pixel-perfect inside `UIParent`, and a frame outside it is not scaled. Tests pin the
+/// `Default` 1.0.
#[derive(Resource)]
pub(crate) struct UiScaleCvar(pub(crate) f32);
@@ -213,8 +216,17 @@ impl Default for UiScaleCvar {
/// The shipped `uiScale`. Deviation: a flat 0.9, because 1.0 reads oversized. With `useUiScale`
/// off, its default (`0x8430c0`), the reference sets `max(768/H, 0.9)` above 768 px tall and 1.0
/// at or below (`0x492f70`, on a mode set and from `0x4908ad`), so the two agree from ~853 px up.
+/// benilla never reads `useUiScale`: this CVar always applies to `UIParent` alone, as with it on.
+/// A change goes through the CVar callback (`0x490770`: the narrow-aspect cap, the 0.64 floor);
+/// the load and a display change through the display handler's ON leg (`0x492e90`: the cap, and
+/// below 0.64 the automatic scale `0x492f70` instead of the floor).
pub(crate) const DEFAULT_UI_SCALE: f32 = 0.9;
+/// The display the reference's automatic UI scale last ran for (`0x492f70`'s `[0xb4e304]` and
+/// `[0xb4e308]`), which outlives every UI: the display handler skips a display it already ran for.
+#[derive(Resource, Default)]
+pub(crate) struct UiAutoScaleCache(pub(crate) Option<(u32, u32)>);
+
/// `WOW_UI_SCALE=` if set, clamped to the dial's range, else [`DEFAULT_UI_SCALE`].
fn default_ui_scale() -> f32 {
std::env::var("WOW_UI_SCALE")
@@ -224,16 +236,24 @@ fn default_ui_scale() -> f32 {
.unwrap_or(DEFAULT_UI_SCALE)
}
-/// The seam scale `s`, window px per UI unit (`windowH/768 × uiScale`), used at every crossing of
-/// the VM boundary; identity for a degenerate window (h ≤ 0, before winit).
-pub(crate) fn seam_scale(window_h: f32, ui_scale: f32) -> f32 {
+/// The seam scale `s`, window px per screen-root unit (`windowH/768`: the root is 768 units tall at
+/// every aspect, `0x41ad10`), used at every crossing of the VM boundary; identity for a degenerate
+/// window (h ≤ 0, before winit). The UI scale is not in it: it is `UIParent`'s, inside the VM.
+pub(crate) fn seam_scale(window_h: f32) -> f32 {
if window_h > 0.0 {
- window_h / 768.0 * ui_scale
+ window_h / 768.0
} else {
1.0
}
}
+/// A window cursor (logical px, y-down from the top left) in the VM's root units, y up: what the
+/// pointer feed hands the engine and `GetCursorPosition` answers (`0x48b820`, no scale divided out).
+pub(crate) fn window_to_ui(window_h: f32, cursor: Vec2) -> Vec2 {
+ let s = seam_scale(window_h);
+ Vec2::new(cursor.x / s, (window_h - cursor.y) / s)
+}
+
/// The Lua UI host (a `NonSend` resource: an mlua VM is `!Send`) and its per-frame passes.
pub(crate) struct UiScriptPlugin;
@@ -263,6 +283,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()))
+ .init_resource::()
.init_resource::()
.init_resource::()
.init_resource::()
@@ -600,9 +621,9 @@ fn demo_unit_feed(script: Option>, mut fired: Local App {
+ let script = UiScript::new().unwrap();
+ script.run(TREE).unwrap();
+ let mut app = App::new();
+ app.insert_non_send_resource(script);
+ app.init_resource::();
+ app.init_resource::();
+ app.init_resource::>();
+ app.init_resource::();
+ app.init_resource::();
+ app.init_resource::();
+ app.init_resource::();
+ app.init_resource::();
+ app.init_resource::();
+ app.init_resource::