The raise-origin check reads the open scope, never the backtrace (#2913)
The debug-build check on where an event is raised passed an allow-listed site by finding its function's name in a captured backtrace. CI builds tests without debug info, and on x86_64 an allowed function inlined away leaves no frame. So #2900's Linux run failed 368 tests: 367 on `minimap::feed_minimap_inside`, 1 on `drive_glue_mgr`. Apple silicon keeps those frames, which is why the Mac gates stayed green. Each of the 13 allowed sites now opens `Origin::Allowed("<site>")` around the part that raises, and the check reads only the open scope. The backtrace is kept only to name callers in a failure. Tested: - `an_allowed_site_passes_by_its_scope_never_by_the_stack` fails under the old verdict; - a guard ties every allow row to a scope in the function it names, and every scope to a row. Fixes #2901 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
use super::*;
|
||||
use benilla_protocol::messages::trainer_spell_state;
|
||||
use benilla_ui::script::origin::{within, Origin};
|
||||
use benilla_ui::script::UiScript;
|
||||
|
||||
/// The `vplates` fixture's wolf, the reference screenshot's subject (vmangos `creature_template`
|
||||
@@ -1759,7 +1760,11 @@ pub(super) fn seed_ui_fixture(
|
||||
if let Some(lua) = lua {
|
||||
vm.run(|script| {
|
||||
if let Some(script) = script {
|
||||
lua(script);
|
||||
// A capture's fixture, dev tooling outside any session, signals as it seeds.
|
||||
within(
|
||||
Origin::Allowed("capture::fixtures::seed_ui_fixture"),
|
||||
|| lua(script),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1790,9 +1795,15 @@ fn open_character_fixture(
|
||||
let open = s.map(|s| {
|
||||
s.set_player_record(record);
|
||||
s.seat_player_guid(me);
|
||||
s.fire_event(
|
||||
"UNIT_INVENTORY_CHANGED",
|
||||
vec![benilla_ui::script::ScriptValue::Str("player".into())],
|
||||
// The items' creates' signal, in place (`0x5d8606`).
|
||||
within(
|
||||
Origin::Allowed("capture::fixtures::open_character_fixture"),
|
||||
|| {
|
||||
s.fire_event(
|
||||
"UNIT_INVENTORY_CHANGED",
|
||||
vec![benilla_ui::script::ScriptValue::Str("player".into())],
|
||||
)
|
||||
},
|
||||
);
|
||||
s.run("ToggleCharacter(\"PaperDollFrame\")")
|
||||
});
|
||||
|
||||
@@ -2084,13 +2084,11 @@ fn every_call_that_queues_for_the_tick_is_listed() {
|
||||
}
|
||||
|
||||
/// Each entry of the raise check's allow list ([`crate::ui_script::raise_origin::RAISES_AT_THEIR_OWN_PLACE`])
|
||||
/// names a function that exists and gives its reason; one that cites a guard list has that list's
|
||||
/// row for it; and every call that queues for the tick has its entry, as each raises at its own
|
||||
/// place in the frame.
|
||||
/// gives its reason; one that cites a guard list has that list's row for it; and every call that
|
||||
/// queues for the tick has its entry, as each raises at its own place in the frame.
|
||||
#[test]
|
||||
fn every_raise_origin_allowed_names_its_function_and_its_row() {
|
||||
use crate::ui_script::raise_origin::{RAISES_AT_THEIR_OWN_PLACE, ROW_LISTS};
|
||||
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let module = |file: &str| file.trim_end_matches(".rs").replace('/', "::");
|
||||
let queued: BTreeSet<String> = QUEUED_FOR_THE_TICK
|
||||
.iter()
|
||||
@@ -2101,27 +2099,16 @@ fn every_raise_origin_allowed_names_its_function_and_its_row() {
|
||||
.collect();
|
||||
let mut wrong = Vec::new();
|
||||
for a in RAISES_AT_THEIR_OWN_PLACE {
|
||||
let (path, func) = a.frame.rsplit_once("::").expect("a `module::fn` frame");
|
||||
let dir = path.replace("::", "/");
|
||||
let found = [format!("{dir}.rs"), format!("{dir}/mod.rs")]
|
||||
.iter()
|
||||
.filter_map(|f| std::fs::read_to_string(src.join(f)).ok())
|
||||
.any(|code| {
|
||||
code.contains(&format!("fn {func}(")) || code.contains(&format!("fn {func}<"))
|
||||
});
|
||||
if !found {
|
||||
wrong.push(format!("{}: no such function", a.frame));
|
||||
}
|
||||
if a.why.trim().is_empty() {
|
||||
wrong.push(format!("{}: no reason", a.frame));
|
||||
wrong.push(format!("{}: no reason", a.site));
|
||||
}
|
||||
let Some((list, _)) = a.why.split_once(':').filter(|(l, _)| ROW_LISTS.contains(l)) else {
|
||||
continue;
|
||||
};
|
||||
let suffix = format!("::{}", a.frame);
|
||||
let suffix = format!("::{}", a.site);
|
||||
let named = a.why.split('`').nth(1).filter(|n| n.starts_with("::"));
|
||||
let has_row = match list {
|
||||
"QUEUED_FOR_THE_TICK" => queued.contains(a.frame),
|
||||
"QUEUED_FOR_THE_TICK" => queued.contains(a.site),
|
||||
"AT_THEIR_OWN_PLACE" => AT_THEIR_OWN_PLACE.iter().any(|(s, _)| s.ends_with(&suffix)),
|
||||
"NOT_YET_ON_A_LANE" => NOT_YET_ON_A_LANE
|
||||
.iter()
|
||||
@@ -2129,17 +2116,45 @@ fn every_raise_origin_allowed_names_its_function_and_its_row() {
|
||||
_ => false,
|
||||
};
|
||||
if !has_row {
|
||||
wrong.push(format!("{}: {list} has no row for it", a.frame));
|
||||
wrong.push(format!("{}: {list} has no row for it", a.site));
|
||||
}
|
||||
}
|
||||
for q in &queued {
|
||||
if !RAISES_AT_THEIR_OWN_PLACE.iter().any(|a| a.frame == q) {
|
||||
if !RAISES_AT_THEIR_OWN_PLACE.iter().any(|a| a.site == q) {
|
||||
wrong.push(format!("{q}: queues for the tick and has no allow entry"));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
|
||||
/// Each row of the raise check's allow list has its scope (`Origin::Allowed`) opened inside the
|
||||
/// function its key names, and every scope the shipped code opens names a row by a literal key, so
|
||||
/// neither the list nor its sites can drift from the other.
|
||||
#[test]
|
||||
fn every_allow_row_has_its_scope_and_every_scope_its_row() {
|
||||
use crate::ui_script::raise_origin::RAISES_AT_THEIR_OWN_PLACE;
|
||||
let scopes = crate::ui_script::fire_scan::allowed_scopes();
|
||||
let mut wrong = Vec::new();
|
||||
for s in &scopes {
|
||||
match s.site.as_deref() {
|
||||
None => wrong.push(format!("{}: a scope whose key is no literal", s.at)),
|
||||
Some(site) if !RAISES_AT_THEIR_OWN_PLACE.iter().any(|a| a.site == site) => {
|
||||
wrong.push(format!("{}: `{site}` has no row on the allow list", s.at));
|
||||
}
|
||||
Some(site) if s.function != site => {
|
||||
wrong.push(format!("{}: `{site}` opens in `{}`", s.at, s.function));
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
for a in RAISES_AT_THEIR_OWN_PLACE {
|
||||
if !scopes.iter().any(|s| s.site.as_deref() == Some(a.site)) {
|
||||
wrong.push(format!("{}: no scope opens it", a.site));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||
}
|
||||
|
||||
/// The keys of `keys` named more than once, as `"<list>: <key>"`.
|
||||
pub(super) fn named_twice(list: &str, keys: impl IntoIterator<Item = String>) -> Vec<String> {
|
||||
let mut seen = BTreeSet::new();
|
||||
|
||||
@@ -29,6 +29,7 @@ use benilla_formats::{tile_to_world, world_to_tile, MinimapTranslate};
|
||||
|
||||
use interior::{interior_drawable, wmo_minimap_stem, InteriorWindow};
|
||||
|
||||
use benilla_ui::script::origin::{within, Origin};
|
||||
use benilla_ui::widget::{MinimapArt, MinimapArtSlot, MINIMAP_DEFAULT_ZOOM};
|
||||
|
||||
use crate::player::Player;
|
||||
@@ -748,7 +749,10 @@ fn feed_minimap_inside(
|
||||
if *was_inside.get(script) != Some(inside) {
|
||||
script.set_minimap_inside(inside);
|
||||
*was_inside.get(script) = Some(inside);
|
||||
script.fire_event("MINIMAP_UPDATE_ZOOM", vec![]);
|
||||
// The update signals the change itself, at frame time (`0x6d95b8`).
|
||||
within(Origin::Allowed("minimap::feed_minimap_inside"), || {
|
||||
script.fire_event("MINIMAP_UPDATE_ZOOM", vec![])
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use benilla_protocol::{SessionEvent, SessionEventKind};
|
||||
use benilla_ui::script::origin::{within, Origin};
|
||||
use bevy::ecs::system::SystemId;
|
||||
use bevy::prelude::*;
|
||||
|
||||
@@ -85,12 +86,16 @@ impl NetHandlers {
|
||||
}
|
||||
|
||||
fn call(world: &mut World, id: SystemId<In<SessionEvent>>, name: &str, ev: SessionEvent) {
|
||||
if let Err(e) = world.run_system_with(id, ev) {
|
||||
// A plugin bug (most likely a missing resource), logged loud so the smoke gate sees it.
|
||||
error!("net: handler `{name}` did not run: {e}");
|
||||
}
|
||||
// The unit edits a handler's seams queued, applied with its commands, before the next event.
|
||||
crate::creature_anim::apply_unit_edits(world);
|
||||
// One handler's run signals in place, as the reference's handlers do (`0x703e50`).
|
||||
within(Origin::Allowed("net::handlers::call"), || {
|
||||
if let Err(e) = world.run_system_with(id, ev) {
|
||||
// A plugin bug (most likely a missing resource), logged loud so the smoke gate sees it.
|
||||
error!("net: handler `{name}` did not run: {e}");
|
||||
}
|
||||
// The unit edits a handler's seams queued, applied with its commands, before the next
|
||||
// event.
|
||||
crate::creature_anim::apply_unit_edits(world);
|
||||
});
|
||||
}
|
||||
|
||||
/// `app.net_handler(SessionEventKind::X, on_x)` registers a system taking `In<SessionEvent>`;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use benilla_ui::script::origin::{within, Origin};
|
||||
use benilla_ui::script::SessionRequest;
|
||||
use bevy::ecs::system::SystemParam;
|
||||
use bevy::prelude::*;
|
||||
@@ -349,7 +350,10 @@ pub(crate) fn leave_world(
|
||||
if called_off {
|
||||
vm.run(|script| {
|
||||
if let Some(script) = script {
|
||||
script.fire_event("LOGOUT_CANCEL", Vec::new());
|
||||
// `SetInWorld(0)` signals in place (`0x5aae97`), the teardown timer's first call.
|
||||
within(Origin::Allowed("ui_logout::leave_world"), || {
|
||||
script.fire_event("LOGOUT_CANCEL", Vec::new())
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1194,6 +1194,55 @@ fn queue_writers_of(s: &Scan) -> Vec<QueueWriter> {
|
||||
out
|
||||
}
|
||||
|
||||
/// A scope benilla-app's shipped code opens for a site on the raise check's allow list,
|
||||
/// `Origin::Allowed(<key>)` ([`benilla_ui::script::origin`]).
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AllowedScope {
|
||||
/// The key, when it is written as a string literal.
|
||||
pub site: Option<String>,
|
||||
/// The function the scope sits in, by its path from the crate root: `minimap::feed_minimap`.
|
||||
pub function: String,
|
||||
/// Where the scope opens.
|
||||
pub at: String,
|
||||
}
|
||||
|
||||
/// Every [`AllowedScope`], in file order.
|
||||
pub(crate) fn allowed_scopes() -> Vec<AllowedScope> {
|
||||
const APP: &str = "crates/benilla-app/src/";
|
||||
const OPEN: &str = "Origin::Allowed(";
|
||||
let mut out = Vec::new();
|
||||
for src in scan().sources.iter().filter(|s| s.path.starts_with(APP)) {
|
||||
let module = src.path[APP.len()..]
|
||||
.trim_end_matches(".rs")
|
||||
.trim_end_matches("/mod")
|
||||
.replace('/', "::");
|
||||
let defs = defs_of(src);
|
||||
let mut from = 0;
|
||||
while let Some(at) = find_from(src.code.as_bytes(), OPEN, from) {
|
||||
from = at + OPEN.len();
|
||||
if src.lit[at] {
|
||||
continue;
|
||||
}
|
||||
let site = src.code[from..]
|
||||
.trim_start()
|
||||
.strip_prefix('"')
|
||||
.and_then(|key| key.split_once('"'))
|
||||
.map(|(key, _)| key.to_string());
|
||||
let function = defs
|
||||
.iter()
|
||||
.filter(|d| d.start <= at && at < d.end)
|
||||
.min_by_key(|d| d.end - d.start)
|
||||
.map_or_else(String::new, |d| format!("{module}::{}", d.name));
|
||||
out.push(AllowedScope {
|
||||
site,
|
||||
function,
|
||||
at: format!("{}:{}", src.path, line_of(&src.code, at)),
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_firing_functions_are_found_by_their_signature() {
|
||||
let s = scan();
|
||||
|
||||
@@ -169,6 +169,7 @@ use std::cell::{Ref, RefCell, RefMut};
|
||||
use std::rc::Rc;
|
||||
|
||||
use benilla_protocol::{Character, LoginRefusal, LoginStage, RealmInfo, RosterCause};
|
||||
use benilla_ui::script::origin::{within, Origin};
|
||||
use benilla_ui::script::{
|
||||
GlueCall, GlueCharacter, GlueHost, GlueNotice, GlueRealm, GlueRealmCells, GlueRequest,
|
||||
GlueSelectCells, GlueSignal, GlueText as Text, NoticeState, ScriptValue, UiScript, VmKind,
|
||||
@@ -2122,7 +2123,10 @@ pub(crate) fn drive_glue_mgr(
|
||||
// holds the list for the next one.
|
||||
if let Some(s) = script.as_deref_mut() {
|
||||
s.note_addon_info_records(super::addons::addon_replies(&msg.records));
|
||||
s.fire_event("ADDON_LIST_UPDATE", Vec::new());
|
||||
within(
|
||||
Origin::Allowed("ui_script::glue_mgr::drive_glue_mgr"),
|
||||
|| s.fire_event("ADDON_LIST_UPDATE", Vec::new()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2132,6 +2136,16 @@ pub(crate) fn drive_glue_mgr(
|
||||
classes: sources.classes.as_deref().map(|c| &c.0),
|
||||
creatures: sources.creatures.as_deref(),
|
||||
};
|
||||
// What the manager raised signals into the glue VM at its frame, `CGlueMgr::Update`
|
||||
// (`0x46b930`), outside any packet.
|
||||
let pump_here =
|
||||
|script: Option<&mut UiScript>,
|
||||
roster: Option<&mut crate::glue_session::roster::Roster>| {
|
||||
within(
|
||||
Origin::Allowed("ui_script::glue_mgr::drive_glue_mgr"),
|
||||
|| pump(&shared, script, realmlist, now_ms, &names, roster),
|
||||
)
|
||||
};
|
||||
|
||||
let (lists, latched) = {
|
||||
let mut mgr = shared.get_mut();
|
||||
@@ -2159,14 +2173,7 @@ pub(crate) fn drive_glue_mgr(
|
||||
(lists, latched)
|
||||
};
|
||||
if latched {
|
||||
pump(
|
||||
&shared,
|
||||
script.as_deref_mut(),
|
||||
realmlist,
|
||||
now_ms,
|
||||
&names,
|
||||
asks.roster.as_deref_mut(),
|
||||
);
|
||||
pump_here(script.as_deref_mut(), asks.roster.as_deref_mut());
|
||||
}
|
||||
let auth_waits = {
|
||||
let mut mgr = shared.get_mut();
|
||||
@@ -2191,14 +2198,7 @@ pub(crate) fn drive_glue_mgr(
|
||||
mgr.auth_waits
|
||||
};
|
||||
if auth_waits {
|
||||
pump(
|
||||
&shared,
|
||||
script.as_deref_mut(),
|
||||
realmlist,
|
||||
now_ms,
|
||||
&names,
|
||||
asks.roster.as_deref_mut(),
|
||||
);
|
||||
pump_here(script.as_deref_mut(), asks.roster.as_deref_mut());
|
||||
shared.get_mut().finish_auth();
|
||||
}
|
||||
{
|
||||
@@ -2224,14 +2224,7 @@ pub(crate) fn drive_glue_mgr(
|
||||
}
|
||||
// What the news raised runs out before the frame's arm, as the reference's network handlers
|
||||
// signal inside the net pump, ahead of `CGlueMgr::Update`.
|
||||
pump(
|
||||
&shared,
|
||||
script.as_deref_mut(),
|
||||
realmlist,
|
||||
now_ms,
|
||||
&names,
|
||||
asks.roster.as_deref_mut(),
|
||||
);
|
||||
pump_here(script.as_deref_mut(), asks.roster.as_deref_mut());
|
||||
{
|
||||
let mut mgr = shared.get_mut();
|
||||
mgr.tick();
|
||||
@@ -2248,14 +2241,7 @@ pub(crate) fn drive_glue_mgr(
|
||||
}
|
||||
}
|
||||
}
|
||||
pump(
|
||||
&shared,
|
||||
script.as_deref_mut(),
|
||||
realmlist,
|
||||
now_ms,
|
||||
&names,
|
||||
asks.roster.as_deref_mut(),
|
||||
);
|
||||
pump_here(script.as_deref_mut(), asks.roster.as_deref_mut());
|
||||
let mut mgr = shared.get_mut();
|
||||
if let Some(s) = script.as_deref() {
|
||||
mgr.select_cells = s.glue_select_cells();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
use benilla_assets::{LockRecover, LooseOpen};
|
||||
use benilla_ui::script::origin::{within, Origin};
|
||||
use benilla_ui::script::UiScript;
|
||||
use benilla_world::frame_pace::FrameStep;
|
||||
|
||||
@@ -70,7 +71,11 @@ fn install_glue_vm(world: &mut World, screen: Option<&str>) -> bool {
|
||||
// The billing verbs read the connection's word too (`0x46dbf0`, `0x46dc40`, `0x46dcf0`).
|
||||
seed_billing(world, &mut script);
|
||||
let start = std::time::Instant::now();
|
||||
let failures = super::glue::load_glue(&mut script);
|
||||
// The build signals `FRAMES_LOADED` itself (`0x46aa34`), outside any packet.
|
||||
let failures = within(
|
||||
Origin::Allowed("ui_script::lifecycle::install_glue_vm"),
|
||||
|| super::glue::load_glue(&mut script),
|
||||
);
|
||||
super::glue::finish_glue_load_log(&mut script);
|
||||
start_server_alert(world, &script);
|
||||
// What the glue carries across its rebuild, in place before the screen's `OnShow` reads it.
|
||||
@@ -84,7 +89,11 @@ fn install_glue_vm(world: &mut World, screen: Option<&str>) -> bool {
|
||||
// `SetCurrentScreen`'s buffer is the client's (`[0xb41478]`) and outlives the Lua state: a
|
||||
// rebuild signals what it holds (`0x46b9a2`), and the screen's handler writes the same name.
|
||||
script.set_glue_screen_name(screen);
|
||||
super::glue::signal_glue_screen(&mut script, screen);
|
||||
// And the screen (`0x46b848`).
|
||||
within(
|
||||
Origin::Allowed("ui_script::lifecycle::install_glue_vm"),
|
||||
|| super::glue::signal_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,
|
||||
@@ -669,55 +678,62 @@ fn load_ingame_ui_seated(world: &mut World, seat: LoadSeat) {
|
||||
// Each phase runs with a call host lent, built off the world as a system's would be, so a call
|
||||
// a file or an `ADDON_LOADED` or `VARIABLES_LOADED` handler makes applies inside its verb, as
|
||||
// the reference's do; the world reads between them are the load's own.
|
||||
silenced_ui_load(&mut script, |script| {
|
||||
crate::script_calls::lent_by_world(world, script, |script| {
|
||||
let _ = load_ingame_ui(script, identity.as_ref(), &roster, version_check);
|
||||
crate::ui_chat::commands::register_dev_commands(
|
||||
script,
|
||||
crate::run_mode::dev_affordances(),
|
||||
);
|
||||
// The new VM's zoom cells from the persisted CVars, which the reference's statics hold
|
||||
// across the load; the world entry's minimap initializer reseeds them from the CVars
|
||||
// again.
|
||||
script.set_minimap_zoom(zoom.0, zoom.1);
|
||||
// The saved-variables file is read after the files set their defaults and before
|
||||
// `VARIABLES_LOADED`, the reference's order (`0x4900a3`, `0x4900b2`); reversed, the
|
||||
// defaults always win. Before `VARIABLES_LOADED` too, the plate setting, whose
|
||||
// `UIParent_OnEvent` arm runs the first `UpdateNameplates()` (`UIParent.lua:231-234`).
|
||||
read_saved_variables(script, |script| {
|
||||
crate::vplates::seat_legacy_settings(script, &legacy_plates)
|
||||
// The load signals its own events, `VARIABLES_LOADED` among them (`0x49156a`), outside any
|
||||
// packet.
|
||||
within(
|
||||
Origin::Allowed("ui_script::lifecycle::load_ingame_ui_seated"),
|
||||
|| {
|
||||
silenced_ui_load(&mut script, |script| {
|
||||
crate::script_calls::lent_by_world(world, script, |script| {
|
||||
let _ = load_ingame_ui(script, identity.as_ref(), &roster, version_check);
|
||||
crate::ui_chat::commands::register_dev_commands(
|
||||
script,
|
||||
crate::run_mode::dev_affordances(),
|
||||
);
|
||||
// The new VM's zoom cells from the persisted CVars, which the reference's statics hold
|
||||
// across the load; the world entry's minimap initializer reseeds them from the CVars
|
||||
// again.
|
||||
script.set_minimap_zoom(zoom.0, zoom.1);
|
||||
// The saved-variables file is read after the files set their defaults and before
|
||||
// `VARIABLES_LOADED`, the reference's order (`0x4900a3`, `0x4900b2`); reversed, the
|
||||
// defaults always win. Before `VARIABLES_LOADED` too, the plate setting, whose
|
||||
// `UIParent_OnEvent` arm runs the first `UpdateNameplates()` (`UIParent.lua:231-234`).
|
||||
read_saved_variables(script, |script| {
|
||||
crate::vplates::seat_legacy_settings(script, &legacy_plates)
|
||||
});
|
||||
});
|
||||
// The account-data readers register after `VARIABLES_LOADED`, in `CGGameUI::Initialize`'s
|
||||
// order: the layout's (`0x4900c2`), the bindings' (`0x4900c7` → `0x4b62b0`), the macros'
|
||||
// (`0x4900d1` → `0x4f0a70`), then the chat setup's below (`0x4900d6`). Each reads now on a
|
||||
// settled section, a `/reload`'s, else once it settles.
|
||||
crate::ui_layout::register_layout_reader(world, script);
|
||||
crate::bindings::register_binding_readers(world, script);
|
||||
crate::ui_macro::register_macro_readers(world, script);
|
||||
// The chat cache restores after `VARIABLES_LOADED` and before any chat is routed: its
|
||||
// `UPDATE_CHAT_WINDOWS` registers each chat frame for its `CHAT_MSG_*` events
|
||||
// (`ChatFrame.lua:1261-1273`).
|
||||
crate::ui_chat::restore_chat_looks(world, script);
|
||||
// The trainer rows the engine holds through a reload, read with the files' strings.
|
||||
if seat == LoadSeat::Live {
|
||||
crate::ui_trainer::seed_reloaded_vm(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 = interface_screen(world).display();
|
||||
let cached = world.get_resource::<super::UiAutoScaleCache>().map(|c| c.0);
|
||||
let mut cache = cached.flatten();
|
||||
crate::script_calls::lent_by_world(world, script, |script| {
|
||||
script.seat_ui_scale(ui_scale, display, &mut cache)
|
||||
});
|
||||
if let Some(mut slot) = world.get_resource_mut::<super::UiAutoScaleCache>() {
|
||||
slot.0 = cache;
|
||||
}
|
||||
script.arm_player_login();
|
||||
});
|
||||
});
|
||||
// The account-data readers register after `VARIABLES_LOADED`, in `CGGameUI::Initialize`'s
|
||||
// order: the layout's (`0x4900c2`), the bindings' (`0x4900c7` → `0x4b62b0`), the macros'
|
||||
// (`0x4900d1` → `0x4f0a70`), then the chat setup's below (`0x4900d6`). Each reads now on a
|
||||
// settled section, a `/reload`'s, else once it settles.
|
||||
crate::ui_layout::register_layout_reader(world, script);
|
||||
crate::bindings::register_binding_readers(world, script);
|
||||
crate::ui_macro::register_macro_readers(world, script);
|
||||
// The chat cache restores after `VARIABLES_LOADED` and before any chat is routed: its
|
||||
// `UPDATE_CHAT_WINDOWS` registers each chat frame for its `CHAT_MSG_*` events
|
||||
// (`ChatFrame.lua:1261-1273`).
|
||||
crate::ui_chat::restore_chat_looks(world, script);
|
||||
// The trainer rows the engine holds through a reload, read with the files' strings.
|
||||
if seat == LoadSeat::Live {
|
||||
crate::ui_trainer::seed_reloaded_vm(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 = interface_screen(world).display();
|
||||
let cached = world.get_resource::<super::UiAutoScaleCache>().map(|c| c.0);
|
||||
let mut cache = cached.flatten();
|
||||
crate::script_calls::lent_by_world(world, script, |script| {
|
||||
script.seat_ui_scale(ui_scale, display, &mut cache)
|
||||
});
|
||||
if let Some(mut slot) = world.get_resource_mut::<super::UiAutoScaleCache>() {
|
||||
slot.0 = cache;
|
||||
}
|
||||
script.arm_player_login();
|
||||
});
|
||||
},
|
||||
);
|
||||
// The load's record goes to `Logs\FrameXML.log` (`0x490187`-`0x4901b6`), after any
|
||||
// `LoadAddOn` block the load ran; nothing reaches chat.
|
||||
script.finish_ui_load_log();
|
||||
@@ -1051,8 +1067,11 @@ pub(crate) fn install_session_glue(world: &mut World) {
|
||||
fn close_ui_session(world: &mut World, root: ShutdownRoot) {
|
||||
// The front runs before the unpark, so a VM still parked (the load never ran) hears none of
|
||||
// it; the sweep's latch is spent at the root, so of this edge and the worldport's, the first
|
||||
// to a departure owns it.
|
||||
let silenced = run_shutdown_front(world, root);
|
||||
// to a departure owns it. The shutdown signals itself, at its root (`0x490b4d`, `0x490c2a`).
|
||||
let silenced = within(
|
||||
Origin::Allowed("ui_script::lifecycle::close_ui_session"),
|
||||
|| run_shutdown_front(world, root),
|
||||
);
|
||||
// A VM still parked goes back into the world first.
|
||||
unpark_glue_vm(world);
|
||||
// An entry load still armed means no in-game UI was built, and the tail would overwrite the
|
||||
@@ -1067,7 +1086,10 @@ fn close_ui_session(world: &mut World, root: ShutdownRoot) {
|
||||
if !ui_never_loaded {
|
||||
crate::script_calls::with_hosted_vm(world, |script| {
|
||||
if let Some(script) = script {
|
||||
shutdown_ui_state(script, identity.as_ref(), layout_held);
|
||||
within(
|
||||
Origin::Allowed("ui_script::lifecycle::close_ui_session"),
|
||||
|| shutdown_ui_state(script, identity.as_ref(), layout_held),
|
||||
);
|
||||
}
|
||||
});
|
||||
// The shutdown's account-data writes (`0x490c55`-`0x490c79` through `0x5afed0`) arm
|
||||
|
||||
@@ -8,77 +8,80 @@ use bevy::app::MainScheduleOrder;
|
||||
use bevy::ecs::schedule::{ExecutorKind, ScheduleLabel};
|
||||
use bevy::prelude::*;
|
||||
|
||||
/// Each origin, by a suffix of its function's path, with the reference fact that puts its raise
|
||||
/// there, or the guard row that names it as remaining work: a reason that opens with a list's
|
||||
/// name ([`ROW_LISTS`]) cites that list's row for the same function, which a test holds to it.
|
||||
/// Each site, by the key its scope opens around the part that raises ([`Origin::Allowed`]), the
|
||||
/// path of the function that opens it, with the reference fact that puts its raise there, or the
|
||||
/// guard row that names it as remaining work: a reason that opens with a list's name
|
||||
/// ([`ROW_LISTS`]) cites that list's row for the same function, which a test holds to it.
|
||||
///
|
||||
/// [`Origin::Allowed`]: benilla_ui::script::origin::Origin::Allowed
|
||||
pub(crate) const RAISES_AT_THEIR_OWN_PLACE: &[Allow] = &[
|
||||
// Their reference places, outside every packet and call.
|
||||
Allow {
|
||||
frame: "net::handlers::call",
|
||||
site: "net::handlers::call",
|
||||
why: "a packet's handler that signals in place, inside its packet, as the reference's \
|
||||
handlers run whole (`0x537ae5`) and signal inside themselves (`0x703e50`): one \
|
||||
handler's run, `NetHandlers::run`'s, not the dispatch's flushes, packet ends and \
|
||||
span starts around it",
|
||||
},
|
||||
Allow {
|
||||
frame: "minimap::feed_minimap_inside",
|
||||
site: "minimap::feed_minimap_inside",
|
||||
why: "the indoor byte's one writer is the minimap's update (`0x6d93a0`), which signals \
|
||||
`MINIMAP_UPDATE_ZOOM` on its change (`0x6d95b8`) at frame time",
|
||||
},
|
||||
Allow {
|
||||
frame: "ui_script::sweep::sweep_on_worldport",
|
||||
site: "ui_script::sweep::sweep_on_worldport",
|
||||
why: "a worldport's teardown is a timer (`0x401bc0`, posted at `0x401ba5`), and the tick \
|
||||
runs its timers ahead of the input and the packets (`0x420d0c`); its sweep signals \
|
||||
`PLAYER_LEAVING_WORLD` (`0x490b4d`)",
|
||||
},
|
||||
Allow {
|
||||
frame: "ui_logout::leave_world",
|
||||
site: "ui_logout::leave_world",
|
||||
why: "`SetInWorld(0)` (`0x5aae70`), the teardown timer's first call (`0x401bd4`), \
|
||||
signals `LOGOUT_CANCEL` in place (`0x5aae97`)",
|
||||
},
|
||||
Allow {
|
||||
frame: "ui_session::latch::walk",
|
||||
site: "ui_session::latch::walk",
|
||||
why: "the interaction watchdog (`0x493230`), every frame, closes the latched window \
|
||||
through `0x493310` (`0x493305`), whose close core signals; ours applies from the \
|
||||
watchdog's commands",
|
||||
},
|
||||
Allow {
|
||||
frame: "ui_script::lifecycle::install_glue_vm",
|
||||
site: "ui_script::lifecycle::install_glue_vm",
|
||||
why: "the glue's build (`0x46a7b0`) signals `FRAMES_LOADED` (`0x46aa34`) and the screen \
|
||||
(`0x46b848`) itself, outside any packet",
|
||||
},
|
||||
Allow {
|
||||
frame: "ui_script::lifecycle::load_ingame_ui_seated",
|
||||
site: "ui_script::lifecycle::load_ingame_ui_seated",
|
||||
why: "the interface's load signals its own events, `VARIABLES_LOADED` among them \
|
||||
(`0x49156a`), outside any packet",
|
||||
},
|
||||
Allow {
|
||||
frame: "ui_script::lifecycle::close_ui_session",
|
||||
site: "ui_script::lifecycle::close_ui_session",
|
||||
why: "the interface's shutdown (`0x490bd0`) signals `PLAYER_LEAVING_WORLD` (`0x490b4d`) \
|
||||
and `PLAYER_LOGOUT` (`0x490c2a`) itself, at a logout's, a disconnect's or a \
|
||||
reload's root",
|
||||
},
|
||||
// Rows of the guard's lists.
|
||||
Allow {
|
||||
frame: "ui_script::glue_mgr::drive_glue_mgr",
|
||||
site: "ui_script::glue_mgr::drive_glue_mgr",
|
||||
why: "AT_THEIR_OWN_PLACE: the login and character screens' answers",
|
||||
},
|
||||
Allow {
|
||||
frame: "capture::fixtures::seed_ui_fixture",
|
||||
site: "capture::fixtures::seed_ui_fixture",
|
||||
why: "QUEUED_FOR_THE_TICK: a capture's fixture",
|
||||
},
|
||||
Allow {
|
||||
frame: "capture::fixtures::open_character_fixture",
|
||||
site: "capture::fixtures::open_character_fixture",
|
||||
why: "a capture's fixture, dev tooling outside any session: the character window's open \
|
||||
stands in for a login's items' creates, whose set-up signals \
|
||||
`UNIT_INVENTORY_CHANGED` in place (`0x5d8606`)",
|
||||
},
|
||||
Allow {
|
||||
frame: "ui_world_map::feed_world_map",
|
||||
site: "ui_world_map::feed_world_map",
|
||||
why: "QUEUED_FOR_THE_TICK: the first world enter's selection",
|
||||
},
|
||||
Allow {
|
||||
frame: "ui_world_map::install_world_loc_projector",
|
||||
site: "ui_world_map::install_world_loc_projector",
|
||||
why: "QUEUED_FOR_THE_TICK: the projector's install",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -26,6 +26,7 @@ use bevy::ecs::schedule::{ExecutorKind, ScheduleConfigs, ScheduleLabel};
|
||||
use bevy::ecs::system::ScheduleSystem;
|
||||
use bevy::prelude::*;
|
||||
|
||||
use benilla_ui::script::origin::{within, Origin};
|
||||
use benilla_ui::script::UiScript;
|
||||
|
||||
/// The world latch, the reference's `[0xb4b424]`: set at `0x4908ce` in the world-enter cascade
|
||||
@@ -282,7 +283,11 @@ pub(crate) fn sweep_on_worldport(world: &mut World) {
|
||||
.get_resource::<crate::net::PendingWorldports>()
|
||||
.is_some_and(|p| p.is_posted())
|
||||
{
|
||||
run_leave_world_sweep(world);
|
||||
// The teardown timer's sweep signals in place (`0x490b4d`).
|
||||
within(
|
||||
Origin::Allowed("ui_script::sweep::sweep_on_worldport"),
|
||||
|| run_leave_world_sweep(world),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use benilla_ui::script::origin::{within, Origin};
|
||||
use benilla_ui::script::UiScript;
|
||||
|
||||
/// The windows `0x493310` tests, in its order (`0x493341` to `0x4934de`): the first whose cell
|
||||
@@ -343,7 +344,10 @@ fn walk(world: &mut World, left: usize) {
|
||||
.into_iter()
|
||||
.find(|e| (e.cell)(world) == Some(guid))
|
||||
{
|
||||
(e.dispatched)(world);
|
||||
// The close core signals inside the walk (`0x493310`), the watchdog's every frame.
|
||||
within(Origin::Allowed("ui_session::latch::walk"), || {
|
||||
(e.dispatched)(world)
|
||||
});
|
||||
}
|
||||
debug_assert_one_holder(world, guid, left);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use benilla_formats::{
|
||||
load_world_map_area_catalog, load_world_map_continent_catalog, load_world_map_overlay_catalog,
|
||||
load_zone_map, WorldMapArea,
|
||||
};
|
||||
use benilla_ui::script::origin::{within, Origin};
|
||||
use benilla_ui::script::{
|
||||
UiScript, WorldMapContinentView, WorldMapLandmarkSource, WorldMapLevel, WorldMapOverlayView,
|
||||
WorldMapSheetRow, WorldMapZoneView,
|
||||
@@ -512,9 +513,15 @@ pub(crate) fn project_on_displayed(
|
||||
/// Hand the engine `GetWorldLocMapPosition`'s projection (`0x4a7360`) over a copy of the catalog.
|
||||
fn install_world_loc_projector(script: &mut UiScript, data: &WorldMapUiData) {
|
||||
let copy = data.clone();
|
||||
script.set_world_loc_projector(Box::new(move |selection, map, x, y| {
|
||||
project_on_displayed(©, selection, map, x, y)
|
||||
}));
|
||||
// Its first landmark list queues `WORLD_MAP_UPDATE`: the projector's install.
|
||||
within(
|
||||
Origin::Allowed("ui_world_map::install_world_loc_projector"),
|
||||
|| {
|
||||
script.set_world_loc_projector(Box::new(move |selection, map, x, y| {
|
||||
project_on_displayed(©, selection, map, x, y)
|
||||
}))
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// [`feed_world_map`]'s memos under one [`crate::ui_script::VmMemo`], so a new VM resets them all.
|
||||
@@ -838,7 +845,10 @@ fn feed_world_map(
|
||||
if let Some(sel) = world_enter_selection(memo.map_synced, top_zone, player_sel) {
|
||||
memo.map_synced = true;
|
||||
let (c, z) = sel.zone.unwrap_or((0, 0));
|
||||
script.sync_world_map_to_player_zone(c, z, sel.direct);
|
||||
// The first world enter's selection, its signal queued (the module's deviation).
|
||||
within(Origin::Allowed("ui_world_map::feed_world_map"), || {
|
||||
script.sync_world_map_to_player_zone(c, z, sel.direct)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
//! raise here is right inside a script call (a verb segment, a handler, the host's apply of a
|
||||
//! call), a lane's firing or the VM's own tick. One made anywhere else is a system raising at its
|
||||
//! own place in the frame, which the source scans cannot see when it goes through a helper in
|
||||
//! another module: it fails, naming its caller, unless a frame of its stack is on the host's allow
|
||||
//! list with its reason. Each raise is judged where it enters the VM, at one of its two doors: the
|
||||
//! fire (`tick::fire_event_into`) or the push onto the host queue (`Model::queue_event`); a queued
|
||||
//! one fires later inside the tick, and a held one inside the host's drain.
|
||||
//! another module: it fails, naming its caller, unless the scope open around it is a site on the
|
||||
//! host's allow list ([`Origin::Allowed`]), which names it with its reason. The verdict reads only
|
||||
//! the open scopes, never the stack, so it is the same in every build profile. Each raise is
|
||||
//! judged where it enters the VM, at one of its two doors: the fire (`tick::fire_event_into`) or
|
||||
//! the push onto the host queue (`Model::queue_event`); a queued one fires later inside the tick,
|
||||
//! and a held one inside the host's drain.
|
||||
//!
|
||||
//! The check runs only while the host has armed it ([`arm`]), around its frame's schedules, so a
|
||||
//! test that drives a VM by hand is not judged. How an unlisted raise fails is the host's
|
||||
@@ -24,14 +26,19 @@ pub enum Origin {
|
||||
Lane,
|
||||
/// The VM's own tick: the queued events, the `OnUpdate` walk and the pump.
|
||||
Tick,
|
||||
/// A site on the host's allow list, by its row's key ([`Allow::site`]): a system that raises
|
||||
/// at its own place in the frame, around the part that raises. A raise inside it passes only
|
||||
/// while the armed list holds the key.
|
||||
Allowed(&'static str),
|
||||
}
|
||||
|
||||
/// An origin outside the three [`Origin`]s that may raise: a stack frame's function path, matched
|
||||
/// as a suffix of a frame's symbol (`ui_world_map::feed_landmarks`), with why it may.
|
||||
/// A row of the host's allow list: a site outside a script call, a lane and the tick that may
|
||||
/// raise, by the key its scope opens ([`Origin::Allowed`]), with why it may.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Allow {
|
||||
/// The function, by a suffix of its path.
|
||||
pub frame: &'static str,
|
||||
/// The site's key: the path of the function that opens its scope, as
|
||||
/// `ui_world_map::feed_world_map`.
|
||||
pub site: &'static str,
|
||||
/// Why it raises at its own place: a reference fact, or the guard row that names it.
|
||||
pub why: &'static str,
|
||||
}
|
||||
@@ -90,28 +97,37 @@ mod imp {
|
||||
let Some((allow, failure)) = ARMED.get() else {
|
||||
return;
|
||||
};
|
||||
if current().is_some() {
|
||||
return;
|
||||
}
|
||||
// The innermost open origin decides; an allowed site only while the armed list holds it.
|
||||
let unlisted = match current() {
|
||||
Some(Origin::Allowed(site)) if !allow.iter().any(|a| a.site == site) => Some(site),
|
||||
Some(_) => return,
|
||||
None => None,
|
||||
};
|
||||
// A Rust function a script called: what it raises is the script call's.
|
||||
if lua.is_some_and(|lua| lua.inspect_stack(0, |_| ()).is_some()) {
|
||||
return;
|
||||
}
|
||||
let at = std::panic::Location::caller();
|
||||
// The stack only names the callers: without debug info an inlined one has no frame.
|
||||
let trace = std::backtrace::Backtrace::force_capture().to_string();
|
||||
let symbols: Vec<&str> = trace
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter_map(|l| l.split_once(": ").map(|(_, s)| s))
|
||||
.collect();
|
||||
if allow.iter().any(|a| allowed(a.frame, &symbols)) {
|
||||
return;
|
||||
}
|
||||
let at = std::panic::Location::caller();
|
||||
let from = callers(&symbols);
|
||||
let head = match unlisted {
|
||||
Some(site) => format!(
|
||||
"`{event}` was raised inside the site `{site}`, which the host's allow list lacks"
|
||||
),
|
||||
None => format!(
|
||||
"`{event}` was raised outside every script call, lane firing, tick and allowed site"
|
||||
),
|
||||
};
|
||||
let line = format!(
|
||||
"`{event}` was raised outside every script call, lane firing and tick, at {at}, \
|
||||
from {from}: put it on a lane or inside the call that causes it, or give its origin \
|
||||
a row on the host's allow list with its reason"
|
||||
"{head}, at {at}, from {from}: put it on a lane or inside the call that causes it, or \
|
||||
give its site a row on the host's allow list with its reason and open that site's \
|
||||
scope (`Origin::Allowed`) around the raise"
|
||||
);
|
||||
match failure {
|
||||
Failure::Panic => panic!("{line}"),
|
||||
@@ -127,14 +143,6 @@ mod imp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a frame of `symbols` is `frame`'s function, or a closure inside it.
|
||||
pub(super) fn allowed(frame: &str, symbols: &[&str]) -> bool {
|
||||
symbols.iter().any(|s| {
|
||||
let s = path_of(s);
|
||||
s.strip_suffix(frame).is_some_and(|p| p.ends_with("::"))
|
||||
})
|
||||
}
|
||||
|
||||
/// A symbol's function path: its generic arguments, its closures and its hash cut.
|
||||
fn path_of(symbol: &str) -> String {
|
||||
let mut out = String::with_capacity(symbol.len());
|
||||
@@ -270,21 +278,6 @@ pub(crate) fn check(event: &str, lua: Option<&mlua::Lua>) {
|
||||
let _ = (event, lua);
|
||||
}
|
||||
|
||||
/// Whether one of `symbols` (a backtrace's frame symbols) is `frame`'s function or a closure in
|
||||
/// it, as the allow list matches.
|
||||
#[cfg(test)]
|
||||
fn frame_matches(frame: &str, symbols: &[&str]) -> bool {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
imp::allowed(frame, symbols)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
let _ = (frame, symbols);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -353,33 +346,40 @@ mod tests {
|
||||
s.queue_event("PLAYER_MONEY", Vec::new());
|
||||
}
|
||||
|
||||
/// A raise whose stack holds an allowed function passes.
|
||||
/// An allowed site's raise passes inside its scope and fails outside it, though both are made
|
||||
/// from the very function the row's key names: a verdict that reads the stack passes the one
|
||||
/// outside, and fails the site wherever the function is inlined away (a build with no debug
|
||||
/// info). A scope whose key the armed list lacks fails too, naming it.
|
||||
#[test]
|
||||
#[cfg_attr(not(debug_assertions), ignore = "the check compiles out")]
|
||||
fn an_allowed_frame_passes() {
|
||||
fn an_allowed_site_passes_by_its_scope_never_by_the_stack() {
|
||||
const SITE: &str =
|
||||
"script::origin::tests::an_allowed_site_passes_by_its_scope_never_by_the_stack";
|
||||
const ALLOW: &[Allow] = &[Allow {
|
||||
frame: "origin::tests::an_allowed_frame_passes",
|
||||
site: SITE,
|
||||
why: "the test's own",
|
||||
}];
|
||||
let s = UiScript::new().unwrap();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
let _armed = arm(ALLOW, Failure::Panic);
|
||||
fire_event_into(&s.lua, "PLAYER_MONEY", Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(debug_assertions), ignore = "the check compiles out")]
|
||||
fn a_frame_matches_its_function_and_its_closures_by_path_suffix() {
|
||||
let symbols = [
|
||||
"std::rt::lang_start",
|
||||
"benilla_app::ui_world_map::feed_landmarks::{{closure}}",
|
||||
"benilla_app::lanes::fire_in_order::h0123456789abcdef",
|
||||
"benilla_app::script_calls::hosted::<(), benilla_app::ui_social::feed::feed_social::{closure#0}>::{closure#0}",
|
||||
];
|
||||
assert!(frame_matches("script_calls::hosted", &symbols));
|
||||
assert!(!frame_matches("ui_social::feed::feed_social", &symbols));
|
||||
assert!(frame_matches("ui_world_map::feed_landmarks", &symbols));
|
||||
assert!(frame_matches("lanes::fire_in_order", &symbols));
|
||||
assert!(!frame_matches("map::feed_landmarks", &symbols));
|
||||
assert!(!frame_matches("ui_world_map::feed", &symbols));
|
||||
within(Origin::Allowed(SITE), || {
|
||||
fire_event_into(&s.lua, "PLAYER_MONEY", Vec::new());
|
||||
s.queue_event("PLAYER_MONEY", Vec::new());
|
||||
});
|
||||
let message = |f: &dyn Fn()| {
|
||||
let failed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f))
|
||||
.expect_err("a raise outside its site passed");
|
||||
failed.downcast_ref::<String>().cloned().unwrap_or_default()
|
||||
};
|
||||
let outside = message(&|| fire_event_into(&s.lua, "PLAYER_MONEY", Vec::new()));
|
||||
assert!(outside.contains("outside every"), "{outside}");
|
||||
let unlisted = message(&|| {
|
||||
within(Origin::Allowed("script::origin::tests::unlisted"), || {
|
||||
fire_event_into(&s.lua, "PLAYER_MONEY", Vec::new())
|
||||
})
|
||||
});
|
||||
assert!(
|
||||
unlisted.contains("`script::origin::tests::unlisted`"),
|
||||
"{unlisted}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -443,17 +443,17 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
- The V-plate border is resampled sharp to the quad's physical size rather than GPU-magnified, because that way its 1 px bevel stays crisp; keyed by that size, so only a resize re-rasterises it. ([`src/ui_script/extract/mod.rs:1463`](../crates/benilla-app/src/ui_script/extract/mod.rs#L1463))
|
||||
- No signature or digest check (`0x46a90b`, `0x46a9d3`, `0x401560(9)`), so the chain's toc always loads and `FRAMES_LOADED` follows, because the check verifies Blizzard's signature with a key carried in the client binary, which benilla does not ship, and a modified interface file is loaded rather than refused. ([`src/ui_script/glue.rs:14`](../crates/benilla-app/src/ui_script/glue.rs#L14))
|
||||
- The reference's size callback only re-scales `GlueParent` and re-lays it out (`0x46ac51`, `0x46ac56`), as its root keeps `gxResolution`'s aspect until a rebuild (`0x41ad10` at `0x46a828`) and its window keeps that shape while dragged (`0x5989d7`); here the root is the window's, which takes any shape, so the box follows it on every resize, without a rebuild because that would reset the screen (the create screen's character and name, the typed password, the selection). ([`src/ui_script/glue.rs:346`](../crates/benilla-app/src/ui_script/glue.rs#L346))
|
||||
- The screen state is not set to 1 after the report (`0x473ef9`), because that store returns the glue from the survey state 11, which benilla never enters, and from the idle screen it would leave a logon standing with none running. ([`src/ui_script/glue_mgr.rs:725`](../crates/benilla-app/src/ui_script/glue_mgr.rs#L725))
|
||||
- The reference's handler completes whichever operation is current; here only the character list's does, in state 3, and a reply of another cause (a roster nothing of the screen asked for) or of another link (before a realm change) completes nothing, because the reference has one link and one request in flight and so cannot tell those replies from the operation they would be applied to, while here the IO thread's park serves several. ([`src/ui_script/glue_mgr.rs:1544`](../crates/benilla-app/src/ui_script/glue_mgr.rs#L1544))
|
||||
- The screen state is not set to 1 after the report (`0x473ef9`), because that store returns the glue from the survey state 11, which benilla never enters, and from the idle screen it would leave a logon standing with none running. ([`src/ui_script/glue_mgr.rs:726`](../crates/benilla-app/src/ui_script/glue_mgr.rs#L726))
|
||||
- The reference's handler completes whichever operation is current; here only the character list's does, in state 3, and a reply of another cause (a roster nothing of the screen asked for) or of another link (before a realm change) completes nothing, because the reference has one link and one request in flight and so cannot tell those replies from the operation they would be applied to, while here the IO thread's park serves several. ([`src/ui_script/glue_mgr.rs:1545`](../crates/benilla-app/src/ui_script/glue_mgr.rs#L1545))
|
||||
- The character is the platform's Unicode text, because that way a player can type any language on any platform. The reference's window is ANSI (`RegisterClassExA`, `DefWindowProcA`), so `WM_CHAR` carries a byte of the system code page, which `0x42d185`-`0x42d1f5` maps through a table `GetACP` picks; a character outside that code page never arrives. ([`src/ui_script/input.rs:763`](../crates/benilla-app/src/ui_script/input.rs#L763))
|
||||
- A capture run (`scenario_active`) keeps the CRT's default seed, 1, because that way its frames reproduce. ([`src/ui_script/lifecycle.rs:193`](../crates/benilla-app/src/ui_script/lifecycle.rs#L193))
|
||||
- A capture run (`scenario_active`) keeps the CRT's default seed, 1, because that way its frames reproduce. ([`src/ui_script/lifecycle.rs:202`](../crates/benilla-app/src/ui_script/lifecycle.rs#L202))
|
||||
- The files are under `benilla-config/Logs/`, because the install is read-only. ([`src/ui_script/load_log.rs:5`](../crates/benilla-app/src/ui_script/load_log.rs#L5))
|
||||
- `FULLSCREEN_QUADS_RESEAT` re-sizes the full-screen quads, which the stock files 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. ([`src/ui_script/manifest.rs:64`](../crates/benilla-app/src/ui_script/manifest.rs#L64))
|
||||
- No `FrameXML.toc` signature check, where `UI_Init` loads on a failed one and then quits (`0x401560(0xa)` at `0x48ff90`), because the check verifies Blizzard's signature with a key carried in the client binary, which benilla does not ship, and a modified interface file is loaded rather than refused. ([`src/ui_script/manifest.rs:161`](../crates/benilla-app/src/ui_script/manifest.rs#L161))
|
||||
- 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). ([`src/ui_script/mod.rs:275`](../crates/benilla-app/src/ui_script/mod.rs#L275))
|
||||
- The body is read with 0x4000-byte reads filled to the end of the body, because that way what a read returns does not depend on how the network cut the packets; and the reading stops once the buffer is full, because no later byte can change it. ([`src/ui_script/server_alert.rs:23`](../crates/benilla-app/src/ui_script/server_alert.rs#L23))
|
||||
- The connection is direct because the reference opens its session with access type 0 (`InternetOpenA`, `0x7a6d97`), the system's proxy settings, which benilla does not read. ([`src/ui_script/server_alert.rs:26`](../crates/benilla-app/src/ui_script/server_alert.rs#L26))
|
||||
- On a worldport it fires with our descriptor present, so `UnitExists("player")` answers true where the reference's (`0x515970`) misses, because matching it breaks `UnitExists` for no stock reader. ([`src/ui_script/sweep.rs:154`](../crates/benilla-app/src/ui_script/sweep.rs#L154))
|
||||
- On a worldport it fires with our descriptor present, so `UnitExists("player")` answers true where the reference's (`0x515970`) misses, because matching it breaks `UnitExists` for no stock reader. ([`src/ui_script/sweep.rs:155`](../crates/benilla-app/src/ui_script/sweep.rs#L155))
|
||||
|
||||
#### ui_social
|
||||
|
||||
|
||||
Reference in New Issue
Block a user