From e665b0bfc46e305b78aca59892175708dab730df Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2026 19:32:44 +0700 Subject: [PATCH] script errors reach the player, and the bars stop lying - an addon error shows the red script-error dialog the reference shows, and a runaway load loop can no longer freeze world entry - food and drink show stack counts from the first login, and a paladin's aura bar lights the aura that is actually on - logging out no longer erases the character from saved-bag addons: the player's absence is no data - MPQ patches are discovered the way the client discovers them, and a slow arrival holds in place instead of falling through the world Co-Authored-By: Claude Fable 5 --- .../benilla-app/assets/ui/BasicControls.xml | 28 +- crates/benilla-app/src/addon_harness/mod.rs | 9 +- .../benilla-app/src/addon_harness/render.rs | 1 + crates/benilla-app/src/player/move_trace.rs | 18 +- crates/benilla-app/src/player/state.rs | 24 +- crates/benilla-app/src/player/wire_in.rs | 3 + crates/benilla-app/src/player/world_focus.rs | 157 ++++++++++- crates/benilla-app/src/ui_action/feed.rs | 24 +- .../benilla-app/src/ui_action/feed_tests.rs | 109 +++++++- crates/benilla-app/src/ui_action/state.rs | 59 +--- crates/benilla-app/src/ui_char.rs | 28 +- crates/benilla-app/src/ui_items/feed.rs | 121 ++++++++- crates/benilla-app/src/ui_items/mod.rs | 2 +- .../src/ui_script/action_bar_tests.rs | 38 +-- crates/benilla-app/src/ui_script/addons.rs | 34 +++ .../src/ui_script/bagnon_render_tests.rs | 84 ++++++ .../src/ui_script/basic_controls_tests.rs | 43 ++- .../benilla-app/src/ui_script/extract/mod.rs | 6 + crates/benilla-app/src/ui_script/lifecycle.rs | 8 + crates/benilla-app/src/ui_script/manifest.rs | 6 + crates/benilla-app/src/ui_script/mod.rs | 7 +- .../src/ui_script/multibar_stance_tests.rs | 4 + .../src/ui_script/options_tests.rs | 1 + .../src/ui_script/world_entry_tests.rs | 93 +++++++ crates/benilla-app/src/ui_shapeshift.rs | 252 ++++++++++++++++-- crates/benilla-app/src/ui_unit.rs | 12 +- crates/benilla-formats/src/bin/mpqx.rs | 4 +- crates/benilla-formats/src/chain.rs | 158 ++++++++++- crates/benilla-formats/src/lib.rs | 29 +- crates/benilla-ui/src/script/action.rs | 22 +- crates/benilla-ui/src/script/cursor/bar.rs | 7 +- crates/benilla-ui/src/script/cursor/pet.rs | 1 + crates/benilla-ui/src/script/event.rs | 3 +- crates/benilla-ui/src/script/mod.rs | 81 +++++- crates/benilla-ui/src/script/model.rs | 19 ++ crates/benilla-ui/src/script/slash.rs | 2 +- crates/benilla-ui/src/script/stdlib.rs | 6 + .../benilla-ui/src/script/tests/cooldown.rs | 16 +- crates/benilla-ui/src/script/tests/mod.rs | 81 ++++++ 39 files changed, 1385 insertions(+), 215 deletions(-) diff --git a/crates/benilla-app/assets/ui/BasicControls.xml b/crates/benilla-app/assets/ui/BasicControls.xml index 619827c2..7744d3cd 100644 --- a/crates/benilla-app/assets/ui/BasicControls.xml +++ b/crates/benilla-app/assets/ui/BasicControls.xml @@ -46,22 +46,18 @@ _ERRORMESSAGE(text) end - -- ── AND ONE LINE OF THE REFERENCE DELIBERATELY NOT TRANSCRIBED ───────────────────────── - -- The reference ends this block with `seterrorhandler(_ERRORMESSAGE);`. We do not. - -- - -- benilla's default handler is `__benilla_script_error` (decision 1195), which pushes into - -- `UiScript::errors()` — the channel the corpus harness's whole blocker table is built on, - -- and the only place a Lua fault is visible to any instrument we own. Installing - -- `_ERRORMESSAGE` would route every runtime error into a modal dialog instead and blind - -- `blockers`, `demand` and every A/B that depends on them, to buy a dialog nobody has asked - -- for yet. - -- - -- The addon-facing behaviour is unchanged either way: `geterrorhandler()` still answers a - -- callable (the 1195 pair), `geterrorhandler()(msg)` still reports, and an addon that - -- installs its OWN handler with `seterrorhandler` still wins outright. What differs is only - -- which handler is installed when nobody has chosen — and that is ours to choose while the - -- error channel is load-bearing for the arc's instruments. Revisit when a player-facing - -- Lua-error surface exists; the one line is the whole change. + -- ── seterrorhandler(_ERRORMESSAGE): the reference's closing line, now transcribed ────── + -- This file withheld exactly this line from when it landed until decision 1305, because + -- the engine's error channel (`UiScript::errors()`) was the only place a Lua fault was + -- visible to any instrument and a modal would have blinded it. That trade is dissolved, + -- not decided differently: the engine now records every caught script error on the host + -- channel FIRST and then dispatches it to whatever `geterrorhandler()` answers + -- (`dispatch_script_errors_to_handler`, 1305) — so the harness's blocker tables read + -- exactly what they always read, and the player gets what the reference always gave them: + -- an addon that errors puts the red ScriptErrors dialog on screen instead of failing + -- somewhere only a terminal can see (B271's report was debugged entirely off WARN lines). + -- An addon that installs its OWN handler still wins outright, exactly as in 1195. + seterrorhandler(_ERRORMESSAGE) -- ── TEXT (ref l.22-26, verbatim) ────────────────────────────────────────────────────── -- "This function is used to translate text (actually it's already translated as-is)" is diff --git a/crates/benilla-app/src/addon_harness/mod.rs b/crates/benilla-app/src/addon_harness/mod.rs index 5b95bc8d..191bfecd 100644 --- a/crates/benilla-app/src/addon_harness/mod.rs +++ b/crates/benilla-app/src/addon_harness/mod.rs @@ -394,9 +394,11 @@ fn manifest_path(root: &Path, name: &str) -> Option { .map(|e| e.path()) } -/// The per-addon VM-instruction bound (see its use in [`survey_one`]). Measured: the heaviest -/// legitimate corpus addon runs 4M, and 214 of 218 run under 1M. -const ADDON_INSTRUCTION_BUDGET: u64 = 200_000_000; +/// The per-addon VM-instruction bound (see its use in [`survey_one`]) — the ONE number, shared +/// with the live client's world-entry arming (decision 1306) so the corpus measurement behind it +/// (heaviest legitimate addon 4M, 214 of 218 under 1M) cannot drift apart from the bound players +/// actually run under. +const ADDON_INSTRUCTION_BUDGET: u64 = crate::ui_script::addons::LOAD_INSTRUCTION_BUDGET; fn survey_one( root: &Path, @@ -1401,6 +1403,7 @@ fn seat_a_session(script: &mut UiScript) { kind: 0x00, action: 100, count: 0, + consumable: false, }), ); script.set_action_state( diff --git a/crates/benilla-app/src/addon_harness/render.rs b/crates/benilla-app/src/addon_harness/render.rs index c5ce0e82..8bf1e496 100644 --- a/crates/benilla-app/src/addon_harness/render.rs +++ b/crates/benilla-app/src/addon_harness/render.rs @@ -215,6 +215,7 @@ pub(super) fn measure_render( kind: 0, action: 403, count: 0, + consumable: false, }), ); script.set_action_state( diff --git a/crates/benilla-app/src/player/move_trace.rs b/crates/benilla-app/src/player/move_trace.rs index aaa379ab..e7980b19 100644 --- a/crates/benilla-app/src/player/move_trace.rs +++ b/crates/benilla-app/src/player/move_trace.rs @@ -118,17 +118,19 @@ pub(super) fn swim(feet_y: f32, surface_y: f32, swimming: bool, h: f32) { /// /// `resident` means the destination's world arrived (scene spawned + collider queue quiet — /// decision 0737's release) and the hold released onto it; `!resident` is the -/// [`super::SETTLE_TIMEOUT`] backstop firing, which switches gravity on with the world never -/// having become resident. The distinction is invisible from inside the game (both look like "the -/// loading screen went away") and it is the difference between a world that streamed in time and -/// one that did not, so the timeout end is also a `warn!` on the ordinary log — a reporter's paste -/// can then name it without owning a trace. +/// [`super::SETTLE_TIMEOUT`] backstop firing, which since decision 1303 (B263) means the stream +/// made **no progress at all** for the whole budget — a genuinely dead stream, never a slow one — +/// and gravity comes on with the world never having become resident. `waited` is measured from +/// the snap either way. The distinction is invisible from inside the game (both look like "the +/// loading screen went away") and it is the difference between a world that arrived and one that +/// died, so the timeout end is also a `warn!` on the ordinary log — a reporter's paste can then +/// name it without owning a trace. pub(super) fn settle(resident: bool, waited: f32, pos: bevy::prelude::Vec3) { if !resident { bevy::log::warn!( - "settle: TIMED OUT after {waited:.2}s with the world never resident at \ - ({:.1},{:.1},{:.1}) — releasing anyway. If a building stands here, its collider \ - had not finished streaming and the body is about to fall through it.", + "settle: TIMED OUT {waited:.2}s after the snap with the stream stalled and the world \ + never resident at ({:.1},{:.1},{:.1}) — releasing anyway. If a building stands \ + here, its collider never streamed and the body is about to fall through it.", pos.x, pos.y, pos.z, diff --git a/crates/benilla-app/src/player/state.rs b/crates/benilla-app/src/player/state.rs index 7511dfbb..004b2686 100644 --- a/crates/benilla-app/src/player/state.rs +++ b/crates/benilla-app/src/player/state.rs @@ -252,11 +252,14 @@ pub(super) const FALL_FAR_TIME: f32 = 0.5; /// Skin width (yd) kept between the capsule and geometry on casts. pub(super) const SKIN_WIDTH: f32 = 0.02; -/// Max seconds to hold the avatar after a teleport while the world streams in (see [`Player::settling`]). -/// Generous — a dense city's spawn + collider queue drains in a couple seconds; this only backstops a -/// world that never becomes resident (missing data, a stalled stream) so we never hang forever. The -/// release itself is the terrain streamer's (decision 0737), which also pushes the deadline while the -/// resident world is still the departed map's (0710's fail-closed law). +/// Seconds of **stalled** streaming before the post-snap hold gives up (see [`Player::settling`]). +/// A *stall* budget, not a load budget: the deadline is pushed forward while the resident world is +/// still the departed map's (0710's fail-closed law) **and while the destination is visibly still +/// arriving** (any load-progress counter moved — B263, decision 1303). It can therefore only fire +/// against a stream that has made no progress at all for this long — missing data, dead IO — never +/// against a slow machine. As a fixed load budget it was measured 0.01 s from firing on a *fast* +/// machine (a Stormwind arrival consumed 5.99 s of the 6.00 s), which is B263: on any slower +/// machine it released gravity mid-stream and the body fell through the not-yet-collided city. pub(crate) const SETTLE_TIMEOUT: f32 = 6.0; /// The player body's collision capsule, built once at startup and swept by avian's `MoveAndSlide` @@ -580,8 +583,15 @@ pub(crate) struct Player { /// its own leak patch before 0737 deleted the class). pub(crate) settling: bool, /// `Time::elapsed_secs` deadline to give up settling and release (see [`Player::settling`]). - /// Pushed forward by the streamer while [`Player::world_stale`] (0710's fail-closed law). + /// Pushed forward by the streamer while [`Player::world_stale`] (0710's fail-closed law) and + /// while any load-progress counter is still moving (B263, decision 1303) — so it measures + /// *stall*, and a slow arrival can never exhaust it. pub(crate) settle_deadline: f32, + /// `Time::elapsed_secs` when the current settle hold began (the snap) — what the `sett` trace + /// reports the wait against. The deadline above stopped measuring this the moment it became a + /// stall budget: it is re-pushed all through a live stream, so "deadline minus timeout" names + /// the last push, not the arrival. + pub(crate) settle_since: f32, /// **The colliders under our feet may still belong to the map we just left.** Set at every /// snap, cleared by the terrain streamer once the destination's own world is resident. /// @@ -753,7 +763,7 @@ impl Player { /// the whole diagnosis of a fall-through report, so it goes through the `sett` trace either way. pub(crate) fn end_settle(&mut self, resident: bool, now: f32) { self.settling = false; - let waited = SETTLE_TIMEOUT - (self.settle_deadline - now); + let waited = now - self.settle_since; super::move_trace::settle(resident, waited, self.pos); } diff --git a/crates/benilla-app/src/player/wire_in.rs b/crates/benilla-app/src/player/wire_in.rs index 4f064ede..2546f0b3 100644 --- a/crates/benilla-app/src/player/wire_in.rs +++ b/crates/benilla-app/src/player/wire_in.rs @@ -254,6 +254,7 @@ pub(super) fn apply_server_moves( player.model_yaw = w.orientation; // a teleport snaps the body — no chase across a loading screen cam.yaw = w.orientation; player.settling = true; // hold (gravity off) until the new map's ground streams in + player.settle_since = time.elapsed_secs(); player.settle_deadline = time.elapsed_secs() + SETTLE_TIMEOUT; // …and the physics world under the new position is still the OLD map's until the // streamer swaps it, one stage from now. See [`Player::world_stale`]. @@ -293,6 +294,7 @@ pub(super) fn apply_server_moves( player.move_flags = 0; player.airborne_since = None; // a snap ends any in-progress jump arc (no phantom FALL_LAND) player.settling = true; // hold (gravity off) until the destination's ground/buildings load + player.settle_since = time.elapsed_secs(); player.settle_deadline = time.elapsed_secs() + SETTLE_TIMEOUT; // A far same-map teleport relocates us over ground that has not streamed either — the // tiles we were standing on are still the resident ones. See [`Player::world_stale`]. @@ -409,6 +411,7 @@ pub(super) fn apply_server_moves( player.active = true; if first { player.settling = true; // settle onto the initial ground once it loads (don't fall through) + player.settle_since = time.elapsed_secs(); player.settle_deadline = time.elapsed_secs() + SETTLE_TIMEOUT; player.world_stale = true; // nothing is streamed yet — see [`Player::world_stale`] // The streamed spawn pose carries the server's facing (the character's logout diff --git a/crates/benilla-app/src/player/world_focus.rs b/crates/benilla-app/src/player/world_focus.rs index 79485490..03b59ae9 100644 --- a/crates/benilla-app/src/player/world_focus.rs +++ b/crates/benilla-app/src/player/world_focus.rs @@ -105,6 +105,7 @@ pub(super) fn release_post_snap_hold( mut player: ResMut, progress: Option>, time: Res