From db8a28ec2c78c4e2b6ed5e92581aa66f645bd913 Mon Sep 17 00:00:00 2001 From: Sam Sung <51686767+samwhosung@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:19:04 +0700 Subject: [PATCH] Try-ons, slash targets, RestartGx and the video verbs apply in the call (#2790) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several verbs did their work in a later drain: try-ons, the chat input's /target, /assist and /follow, RestartGx, and the engine CVar verbs (SetGamma, the video pairs, SetMultisampleFormat, the minimap zoom, the glue writes). A script's next line read the old state. LoadBindings also switched the current set, so a Character Specific tick then Cancel left the character set live. Reference: a try-on reads the cache and lands or refuses in its call (`0x504540`, `0x504b2d`). RestartGx runs gxRestart synchronously (`0x48dab0` → `0x63ce00`), and SetMultisampleFormat restarts in its call (`0x48c79b`). LoadBindings never writes the current set (`0x4b8249`-`0x4b8252`). Changed: those verbs apply in the call, and the slash commands call the stock verbs. The drains left at the frame each name why; SaveBindings only marks its set until shutdown (`0x4b6310`, `0x4b65e0`). Tests: per verb, a same-chunk read of its effect. Part of #2202. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 --- crates/benilla-app/src/bindings.rs | 5 +- crates/benilla-app/src/cvars/mod.rs | 83 +++++++ crates/benilla-app/src/player/follow.rs | 11 +- .../benilla-app/src/portrait/panes_tests.rs | 2 + crates/benilla-app/src/screenshot.rs | 3 +- crates/benilla-app/src/script_calls.rs | 37 +++ crates/benilla-app/src/sound/ui.rs | 2 +- crates/benilla-app/src/sound/zone.rs | 3 +- crates/benilla-app/src/target/by_name.rs | 113 ++------- crates/benilla-app/src/target/front_tests.rs | 17 +- crates/benilla-app/src/target/gate_tests.rs | 42 +--- crates/benilla-app/src/target/mod.rs | 17 +- crates/benilla-app/src/target/unheld_tests.rs | 31 +-- crates/benilla-app/src/ui_chat/input/mod.rs | 41 ++-- crates/benilla-app/src/ui_chat/settings.rs | 3 +- crates/benilla-app/src/ui_dressup.rs | 215 +++++++++++++----- crates/benilla-app/src/ui_layout.rs | 4 +- crates/benilla-app/src/ui_macro/mod.rs | 3 +- crates/benilla-app/src/ui_models/mod.rs | 3 +- .../src/ui_script/dressup_tests.rs | 6 + crates/benilla-app/src/video.rs | 58 +++-- crates/benilla-ui/src/script/calls.rs | 9 + crates/benilla-ui/src/script/client.rs | 1 + crates/benilla-ui/src/script/cvars.rs | 122 ++++++---- crates/benilla-ui/src/script/dressup.rs | 47 ++-- crates/benilla-ui/src/script/glue_addons.rs | 4 +- crates/benilla-ui/src/script/glue_core.rs | 12 +- crates/benilla-ui/src/script/glue_realm.rs | 2 + crates/benilla-ui/src/script/gm_ticket.rs | 1 + crates/benilla-ui/src/script/item_stats.rs | 14 +- crates/benilla-ui/src/script/keybind.rs | 42 +++- crates/benilla-ui/src/script/minimap.rs | 4 +- crates/benilla-ui/src/script/model.rs | 5 +- crates/benilla-ui/src/script/pvp/mod.rs | 1 + .../benilla-ui/src/script/tradeskill/mod.rs | 1 + crates/benilla-ui/src/script/unit/mod.rs | 7 +- crates/benilla-ui/src/script/video_pairs.rs | 10 +- docs/DEVIATIONS.md | 41 ++-- 38 files changed, 606 insertions(+), 416 deletions(-) diff --git a/crates/benilla-app/src/bindings.rs b/crates/benilla-app/src/bindings.rs index 3eb5f7192..a4a0ebe7b 100644 --- a/crates/benilla-app/src/bindings.rs +++ b/crates/benilla-app/src/bindings.rs @@ -367,7 +367,10 @@ fn read_diff(path: &Option) -> Option, diff --git a/crates/benilla-app/src/cvars/mod.rs b/crates/benilla-app/src/cvars/mod.rs index 4b5c195ac..966661289 100644 --- a/crates/benilla-app/src/cvars/mod.rs +++ b/crates/benilla-app/src/cvars/mod.rs @@ -3587,4 +3587,87 @@ mod tests { ] ); } + + /// The engine verbs that write a CVar run its callback inside the call, as `SetCVar` does: + /// `SetGamma` (`0x489267`), `SetWorldDetail` (`0x488e51`), a video pair's setter (`0x488f30`) + /// and the minimap's `SetZoom` (`0x6da974`) each call `CVar::Set` (`0x63df50`), and + /// `RestartGx`'s handler commits the latched rows synchronously (`0x48dab0` → `0x639f60`). + /// So each knob has moved, and `GetCVar` reads the committed `gxWindow`, before the chunk's + /// next line, with no frame run. + #[test] + fn the_engine_cvar_verbs_run_their_callbacks_inside_the_call() { + let mut app = crate::game_plugins::schedule_tests::headless_client(); + app.insert_resource(bevy::ecs::error::DefaultErrorHandler( + bevy::ecs::error::ignore, + )); + let (tx, _rx) = crossbeam_channel::unbounded(); + app.insert_resource(crate::net::NetCommands(tx)); + app.update(); + app.insert_non_send_resource(UiScript::new().expect("a VM")); + // The registry seeds the new VM's mirror. + app.update(); + let window: String = crate::script_calls::with_hosted_vm(app.world_mut(), |s| { + s.expect("a VM").eval( + r#" + CreateFrame("Minimap", "ProbeMinimap", UIParent):SetZoom(2) + SetGamma(0.25) + SetWorldDetail(0) + SetFarclip(500) + SetCVar("gxWindow", "1") + RestartGx() + return GetCVar("gxWindow") + "#, + ) + }) + .expect("the chunk runs"); + assert_eq!( + window, "1", + "the restart committed the stage inside its call" + ); + let cvars = res::(&app); + assert_eq!(cvars.get("gamma"), Some("0.750000")); + assert_eq!(cvars.get("frillDensity"), Some("16")); + assert_eq!(cvars.get("farclip"), Some("500.000000")); + assert_eq!(cvars.get("minimapZoom"), Some("2")); + assert_eq!(cvars.get("gxWindow"), Some("1")); + assert_eq!(res::(&app).0, 0.75); + assert_eq!(res::(&app).outdoor, 2); + } + + /// The Video options' Okay sets the resolution, then the multisample format + /// (`OptionsFrame.lua:239-240`), with no `RestartGx` when no other row asked one. Each verb + /// runs `gxRestart` inside its call (`0x48c0bc`, `0x48c79b`), so both latched changes commit. + #[test] + fn an_okay_with_a_new_resolution_and_multisample_commits_both() { + let mut app = crate::game_plugins::schedule_tests::headless_client(); + app.insert_resource(bevy::ecs::error::DefaultErrorHandler( + bevy::ecs::error::ignore, + )); + let (tx, _rx) = crossbeam_channel::unbounded(); + app.insert_resource(crate::net::NetCommands(tx)); + app.insert_resource(benilla_world::view::MsaaFormats { + formats: vec![(24, 24, 1), (24, 24, 4)], + }); + app.update(); + let mut script = UiScript::new().expect("a VM"); + let size = |width, height| benilla_ui::script::ScreenResolution { width, height }; + script.set_screen_resolutions(vec![size(800, 600), size(1024, 768)], Some(size(800, 600))); + app.insert_non_send_resource(script); + app.update(); + let (resolution, samples): (String, String) = + crate::script_calls::with_hosted_vm(app.world_mut(), |s| { + s.expect("a VM").eval( + r#" + SetScreenResolution(2) + SetMultisampleFormat(2) + return GetCVar("gxResolution"), GetCVar("gxMultisample") + "#, + ) + }) + .expect("the chunk runs"); + assert_eq!((resolution.as_str(), samples.as_str()), ("1024x768", "4")); + let cvars = res::(&app); + assert_eq!(cvars.get("gxResolution"), Some("1024x768")); + assert_eq!(cvars.get("gxMultisample"), Some("4")); + } } diff --git a/crates/benilla-app/src/player/follow.rs b/crates/benilla-app/src/player/follow.rs index f00fe3d95..b199a0f7c 100644 --- a/crates/benilla-app/src/player/follow.rs +++ b/crates/benilla-app/src/player/follow.rs @@ -73,9 +73,9 @@ impl FollowState { } } -/// A start-following request: the chat input's `/follow`, or the UI's `FollowUnit`/`FollowByName`. -/// A name finds players only; the start gate refuses a creature either way. -#[derive(bevy::ecs::message::Message, Clone, Debug)] +/// A start-following request: `FollowUnit` or `FollowByName`. A name finds players only; the start +/// gate refuses a creature either way. +#[derive(Clone, Debug)] pub(crate) enum FollowRequest { /// `FollowUnit(unit)`; a bare `/follow` is `FollowUnit("target")`. Unit(String), @@ -301,10 +301,9 @@ fn follow_trace_on() -> bool { *ON.get_or_init(|| std::env::var_os("WOW_FOLLOW_TRACE").is_some()) } -/// Register the follow state and its request message. +/// Register the follow state. pub(super) fn plugin(app: &mut App) { - app.init_resource::() - .add_message::(); + app.init_resource::(); } #[cfg(test)] diff --git a/crates/benilla-app/src/portrait/panes_tests.rs b/crates/benilla-app/src/portrait/panes_tests.rs index 933d0c4c1..8b4db765e 100644 --- a/crates/benilla-app/src/portrait/panes_tests.rs +++ b/crates/benilla-app/src/portrait/panes_tests.rs @@ -554,6 +554,8 @@ struct Harness { impl Harness { fn new(lua: &str) -> Self { let mut script = UiScript::new().unwrap(); + // A dressing-room pane's `SetUnit` is a call no host takes here. + script.keep_calls(); script.set_screen_size(1024.0, 768.0); script.set_unit_guids(&benilla_ui::script::UnitGuids { player: ME, diff --git a/crates/benilla-app/src/screenshot.rs b/crates/benilla-app/src/screenshot.rs index d69a48b7d..28d5b0bfa 100644 --- a/crates/benilla-app/src/screenshot.rs +++ b/crates/benilla-app/src/screenshot.rs @@ -95,7 +95,8 @@ impl ScreenshotState { /// Captures each `Screenshot()` one frame after the call. Spawn last frame's asks before draining /// this frame's: that order is the one-frame hold (module doc). While a movie plays the frame pass -/// skips its present block, the capture in it (`0x44267c`), so the asks wait for its end. +/// skips its present block, the capture in it (`0x44267c`), so the asks wait for its end. Not a +/// call's: the verb only arms the request (`0x4893d0`, `0x442a10`), which the frame pass takes. fn ask_for_captures( mut commands: Commands, mut hosted_vm: crate::script_calls::HostedVm, diff --git a/crates/benilla-app/src/script_calls.rs b/crates/benilla-app/src/script_calls.rs index ea0f20a50..8af223127 100644 --- a/crates/benilla-app/src/script_calls.rs +++ b/crates/benilla-app/src/script_calls.rs @@ -145,6 +145,10 @@ disjoint::disjoint_set! { cvars: crate::cvars::CvarSteps<'static>, /// The camera's five views, which the view verbs seat and save. views: crate::player::camera_view::ViewSteps<'static, 'static>, + /// The display restart's commit of the latched rows. + gx_restart: crate::video::GxRestartSteps<'static>, + /// The dressing room the `DressUpModel` verbs edit. + dressup: crate::ui_dressup::DressUpSteps<'static>, asks: engine::RecordAsks<'static, 'static>, /// What the getters read as they run ([`live::LiveReads`]). live: live::LiveReads<'static, 'static>, @@ -926,6 +930,8 @@ impl CallHost for ScriptCallHost<'_, '_> { let writes = appliers.views().apply(request); appliers.cvars().write(script, &writes); } + ScriptCall::RestartGx => appliers.gx_restart().restart(script, 1), + ScriptCall::DressUp(intent) => appliers.dressup().apply(script, intent), ScriptCall::RecordAsk(benilla_ui::script::RecordAsk::AuctionName { guid, list }) => { appliers.auction().wait_for_name(guid, list); } @@ -2223,6 +2229,37 @@ mod tests { ); } + /// A probe's `/target ` runs the stock handler's `TargetByName` (`ChatFrame.lua:748-751`, + /// `0x489d60`), which selects inside its call, so the next line the drain takes reads the new + /// target. Main wrote the ask for a reader later in the frame, and the next line read the old. + #[test] + fn a_chat_target_line_selects_before_the_next_line() { + let mut f = frame(true); + spawn_mob(&mut f, &[]); + let world = f.app.world_mut(); + let mut names = world.resource_mut::(); + names.insert_player(OLD, "Old Mob".into(), None); + names.insert_player(MOB, "Red Affinity".into(), None); + as_creature(world, MOB); + probe_line_world(world); + world.insert_resource(crate::ui_chat::commands::SlashCommands::build( + |name| match name { + "SLASH_TARGET1" => Some("/target".to_string()), + "SLASH_SCRIPT1" => Some("/script".to_string()), + _ => None, + }, + |_| None, + )); + { + let mut script = world.non_send_resource_mut::(); + script.push_chat_input("/target Red Affinity".into()); + script.push_chat_input("/script NAMED = UnitName(\"target\")".into()); + } + crate::ui_chat::drain_chat_input_once(world); + assert_eq!(eval::(&mut f, "return NAMED"), "Red Affinity"); + assert_eq!(selected(&f), Some(MOB)); + } + /// `TargetByName` (`0x489d60`) on a miss leaves the target alone and shows message `0x127` /// (`ERR_UNIT_NOT_FOUND`) for a name given (`0x489dbd`, tested on the raw string's first byte, /// so blanks count) or `0xb8` (`ERR_GENERIC_NO_TARGET`) for an empty one (`0x489dcf`), inside diff --git a/crates/benilla-app/src/sound/ui.rs b/crates/benilla-app/src/sound/ui.rs index 7a91c109f..8f1c2dfce 100644 --- a/crates/benilla-app/src/sound/ui.rs +++ b/crates/benilla-app/src/sound/ui.rs @@ -24,7 +24,7 @@ use super::{SoundConfig, SoundOutput}; use crate::query_cache::Ask; /// Drain the VM's queued `PlaySound` intents into the kit player; drained even with no catalog, -/// so the queue cannot grow unbounded. +/// so the queue cannot grow unbounded. Left at the frame: no script reads what a play does. fn drain_ui_sounds( // With the volumes, a CVar knob the call host's callbacks write; the drain runs no Lua. mut vm: crate::script_calls::VmWith>, diff --git a/crates/benilla-app/src/sound/zone.rs b/crates/benilla-app/src/sound/zone.rs index b0516fcff..309b46e59 100644 --- a/crates/benilla-app/src/sound/zone.rs +++ b/crates/benilla-app/src/sound/zone.rs @@ -974,7 +974,8 @@ fn fade_out_ambience(zone: &mut ZoneAudio, fade_ms: u64) { } /// The Lua music slot: this frame's queued verbs, the loop, the slider and the watch. -/// Unconditional: the stream is the caller's, not the world soundscape's. +/// Unconditional: the stream is the caller's, not the world soundscape's. Left at the frame: no +/// script reads what `PlayMusic` or `StopMusic` does. fn lua_music( // With the volumes, a CVar knob the call host's callbacks write; the slot runs no Lua. mut vm: crate::script_calls::VmWith>, diff --git a/crates/benilla-app/src/target/by_name.rs b/crates/benilla-app/src/target/by_name.rs index a8922c66d..d16ba644c 100644 --- a/crates/benilla-app/src/target/by_name.rs +++ b/crates/benilla-app/src/target/by_name.rs @@ -80,19 +80,6 @@ impl NameSearch { } } -/// `/target `: resolve and select (`TargetByName`). -#[derive(bevy::ecs::message::Message, Clone, Debug)] -pub(crate) struct TargetByNameRequest { - pub(crate) name: String, -} - -/// `/assist [name]`: select the basis unit's target (`AssistByName`, or bare -/// `AssistUnit("target")`, whose basis is the current selection). -#[derive(bevy::ecs::message::Message, Clone, Debug)] -pub(crate) struct AssistRequest { - pub(crate) name: Option, -} - /// The follow start gate's refusals with lines (`0x60fed0`'s mode-3 chain, after its silent arms) /// as a pure function, its order pinned by test; "we" are the active mover: /// @@ -425,7 +412,7 @@ impl PlayerLookup<'_, '_> { } } -/// `/target ` and the Lua `TargetByName(name, exactMatch)` through [`scan::commit_owing`], the +/// `TargetByName(name, exactMatch)`, the stock `/target`'s, through [`scan::commit_owing`], the /// stop, select, re-swing path a click takes. `exact` is the Lua second argument, which `0x489d8e` /// reads with default 0 as the resolver's exact-only flag (consumed at `0x493cab`). The pick goes /// to `SetSelection` as it is (`0x489daa`), so a party member out of range is selected by guid. A @@ -467,7 +454,7 @@ fn missing_line(name: &str) -> &'static str { } } -/// `AssistByName(name)` (`0x489c40`) and `/assist [name]`: the basis is the player a nonempty name +/// `AssistByName(name)` (`0x489c40`), the stock `/assist`'s: the basis is the player a nonempty name /// resolves to (typemask 0x10, mode 0, `0x489c82`), else the selection (`0x489c89`), looked up as a /// unit (`0x489c9b`, typemask 8); [`SelectCommit::assist`] then selects its target. A basis the /// object manager does not hold, a party member out of range included, shows @@ -499,59 +486,6 @@ pub(super) fn assist_named( commit.assist(basis, how); } -/// What the chat layer's `/target` and `/assist` asks read and write. -type ByNameAsk = ( - ByNameScan<'static, 'static>, - SelectCommit<'static, 'static>, - Res<'static, crate::ui_action::UiErrorKeys>, -); - -/// One by-name ask committed through the one SetSelection path, its signals in place with the -/// host lent, as the binding's `SetSelection` signals inside the call (`0x49389c`), then what it -/// owes past them. -fn ask_by_name( - vm: &mut crate::script_calls::VmWith, - step: impl FnOnce(&ByNameScan, &mut SelectCommit, &crate::ui_action::UiErrorKeys), -) { - let select = { - let (scan_params, mut commit, errors) = vm.own(); - let before = super::click::Standing::of(&commit.selection, &commit.seam); - let owed = commit.owing(|commit| step(&scan_params, commit, &errors)); - before.owing_debt(owed) - }; - vm.engine_select(select, |(_, mut commit, _), owed| commit.pay_now(owed)); -} - -/// Drain the chat layer's `/target ` asks. -pub(super) fn target_by_name_requests( - mut requests: MessageReader, - mut vm: crate::script_calls::VmWith, -) { - for request in requests.read() { - ask_by_name(&mut vm, |scan_params, commit, errors| { - target_named(scan_params, commit, errors, &request.name, false); - }); - } -} - -/// Drain the chat layer's `/assist [name]` asks. -pub(super) fn assist_requests( - mut requests: MessageReader, - mut vm: crate::script_calls::VmWith, -) { - for request in requests.read() { - ask_by_name(&mut vm, |scan_params, commit, errors| { - assist_named( - scan_params, - commit, - errors, - request.name.as_deref().unwrap_or_default(), - "/assist", - ); - }); - } -} - /// What a follow request comes to at the start gate (`0x60fed0`). #[derive(Debug, PartialEq, Eq)] pub(crate) enum FollowStart { @@ -563,8 +497,8 @@ pub(crate) enum FollowStart { Started { name: String }, } -/// The follow's start, from the chat layer's `/follow` and from `FollowUnit` and `FollowByName` -/// inside their calls ([`crate::script_calls::ScriptCallHost`]); follow sends nothing on the wire. +/// The follow's start, from `FollowUnit` and `FollowByName`, the stock `/follow`'s, inside their +/// calls ([`crate::script_calls::ScriptCallHost`]); follow sends nothing on the wire. #[derive(SystemParam)] pub(crate) struct FollowSteps<'w, 's> { scan_params: ByNameScan<'w, 's>, @@ -655,20 +589,6 @@ impl FollowSteps<'_, '_> { } } -/// Drain the chat layer's `/follow [name]` into [`crate::player`]: a refusal's line through the -/// action feed, the signals from [`crate::ui_follow`]'s transitions. -pub(super) fn follow_requests( - mut requests: MessageReader, - mut steps: FollowSteps, - errors: Res, -) { - for request in requests.read() { - if let FollowStart::Refused(key) = steps.start(request) { - errors.push(crate::ui_action::UiError::key(key)); - } - } -} - /// The cue `SetSelection` plays for a guid whose object it does not find (`0x843328`). const UNHELD_SELECT_SOUND: &str = "igCharacterSelect"; @@ -840,7 +760,8 @@ impl SelectCommit<'_, '_> { SelectDebt::default() } - /// Pay all of `debt` now, for a writer whose signals have no listeners to run first. + /// Pay all of `debt` now, for a test's writer whose signals have no listeners to run first. + #[cfg(test)] pub(crate) fn pay_now(&mut self, mut debt: SelectDebt) { while debt != SelectDebt::default() { debt = self.pay(debt); @@ -1260,7 +1181,6 @@ mod tests { world.init_resource::(); world.init_resource::(); world.init_resource::(); - world.init_resource::>(); let mut names = NameCache::default(); names.insert_player(ME, "Me".into(), None); names.insert_player(them.guid, "Them".into(), None); @@ -1320,21 +1240,20 @@ mod tests { selection.guid = target.map(|(_, g)| g); } - /// One request through the follow drain: the error lines it queued, and whom we follow after. + /// One request through the follow's start, as `FollowUnit`'s or `FollowByName`'s call makes + /// it: the line its refusal shows, and whom we follow after. fn drain( world: &mut World, request: crate::player::FollowRequest, ) -> (Vec<&'static str>, Option) { - world.write_message(request); - world - .run_system_cached(follow_requests) - .expect("the drain runs"); - let keys = world - .resource_mut::() - .take() - .into_iter() - .map(|e| e.key) - .collect(); + use bevy::ecs::system::RunSystemOnce; + let start = world + .run_system_once(move |mut steps: FollowSteps| steps.start(&request)) + .expect("the start runs"); + let keys = match start { + FollowStart::Refused(key) => vec![key], + _ => Vec::new(), + }; (keys, world.resource::().guid) } diff --git a/crates/benilla-app/src/target/front_tests.rs b/crates/benilla-app/src/target/front_tests.rs index e17c76299..61c56853e 100644 --- a/crates/benilla-app/src/target/front_tests.rs +++ b/crates/benilla-app/src/target/front_tests.rs @@ -938,9 +938,9 @@ fn an_acquired_attacker_signals_its_select_in_place() { assert_eq!(wire_order(&f.rx), ["RandomRoll"]); } -/// The chat layer's `/target` and `/assist` asks commit through `SetSelection` (`0x493540`), -/// which signals inside it (`0x49389c`): the listener's roll goes out behind the select's send, -/// in the ask's own step, with no lane to wait for. +/// A bare `/assist`'s `AssistByName("")` commits through `SetSelection` (`0x493540`), which +/// signals inside it (`0x49389c`): the listener's roll goes out behind the select's send, inside +/// the call, with no lane to wait for. #[test] fn a_chat_assist_signals_its_select_in_place() { let mut f = fix(false); @@ -948,13 +948,10 @@ fn a_chat_assist_signals_its_select_in_place() { f.make_unit(mob); f.make_unit_with(old, &[(16, MOB as u32), (17, 0)]); rolling_listener(&mut f); - let world = f.app.world_mut(); - world - .resource_mut::>() - .write(super::by_name::AssistRequest { name: None }); - world - .run_system_once(super::by_name::assist_requests) - .expect("the /assist reader runs"); + crate::script_calls::with_hosted_vm(f.app.world_mut(), |s| { + s.expect("a VM").run(r#"AssistByName("")"#) + }) + .expect("the assist runs"); assert_eq!(f.selected(), Some(MOB)); assert_eq!(wire_order(&f.rx), ["SetSelection 0x31", "RandomRoll"]); } diff --git a/crates/benilla-app/src/target/gate_tests.rs b/crates/benilla-app/src/target/gate_tests.rs index 07bf4731b..17044bc63 100644 --- a/crates/benilla-app/src/target/gate_tests.rs +++ b/crates/benilla-app/src/target/gate_tests.rs @@ -15,9 +15,8 @@ use bevy::ecs::system::RunSystemOnce; use bevy::prelude::*; use super::front_tests::{fix, Fix, ME, MOB, OLD}; -use super::{by_name, click, cursor_mode, Hovered, PickOcclusion, PressPick}; +use super::{click, cursor_mode, Hovered, PickOcclusion, PressPick}; use crate::net::{ClientCommand, GuidIndex, ObjectStore}; -use crate::target::TargetByNameRequest; use crate::ui_action::UiErrorKeys; use crate::vplates::PlateClicks; @@ -639,45 +638,6 @@ fn a_right_press_with_a_payload_is_consumed_before_the_world_sees_it() { } } -/// `TargetByName` (`0x489d60`) leaves the target alone on a miss and shows message `0x127` -/// (`ERR_UNIT_NOT_FOUND`) for a name given (`0x489dbd`, tested on the raw string's first byte, so -/// blanks count) or `0xb8` (`ERR_GENERIC_NO_TARGET`) for an empty one (`0x489dcf`); the chat -/// layer's `/target` request is the binding's body. A hit shows nothing. The Lua binding's miss is -/// pinned in `script_calls`, whose call host fires the lines inside the call. -#[test] -fn a_target_name_that_finds_nobody_shows_the_references_line() { - let request = |f: &mut Fix, name: &str| { - f.app - .world_mut() - .resource_mut::>() - .write(TargetByNameRequest { name: name.into() }); - f.app - .world_mut() - .run_system_once(by_name::target_by_name_requests) - .expect("the drain runs"); - }; - for (name, line) in [ - ("Nobody", "ERR_UNIT_NOT_FOUND"), - (" ", "ERR_UNIT_NOT_FOUND"), - ("", "ERR_GENERIC_NO_TARGET"), - ] { - let mut f = fix(false); - request(&mut f, name); - assert_eq!(f.errors(), [line], "{name:?}"); - assert_eq!(f.selected(), Some(OLD), "{name:?}: the target stays"); - assert!(f.sent().is_empty(), "{name:?}"); - } - - let mut f = fix(false); - f.app - .world_mut() - .resource_mut::() - .insert_player(MOB, "Mob".into(), None); - request(&mut f, "Mob"); - assert!(f.errors().is_empty(), "{:?}", f.errors()); - assert_eq!(f.selected(), Some(MOB)); -} - /// Talking to an NPC in reach whose dialog status is 1, unavailable, triggers Unavailable Quest /// Givers (`0x5f0268`-`0x5f0278`) ahead of its service ladder; another status triggers nothing. /// The click's select lit the NPC, which the selection's tutorial ladder then judges diff --git a/crates/benilla-app/src/target/mod.rs b/crates/benilla-app/src/target/mod.rs index 9f8eea68c..e694b94ff 100644 --- a/crates/benilla-app/src/target/mod.rs +++ b/crates/benilla-app/src/target/mod.rs @@ -93,11 +93,8 @@ pub(crate) use scan::{fire_owed_selects_lane, take_owed_deselects, LastEnemy}; pub(crate) use scan::{ Acquire, AttackNearestRequest, AttackPick, CreatureTypes, Owed, OwedSelect, OwedSelects, }; -// The chat layer's by-name asks (`/target`, `/assist`), and the typemask `TargetByName`'s -// candidate feed shares. -pub(crate) use by_name::{ - assistable_alive, AssistRequest, Candidacy, NameSearch, PlayerLookup, TargetByNameRequest, -}; +// The typemask `TargetByName`'s candidate feed shares. +pub(crate) use by_name::{assistable_alive, Candidacy, NameSearch, PlayerLookup}; // The one SetSelection path, for the loot response's select (`0x48f3a0`), and what a selecting // call owes after its signals. pub(crate) use by_name::{SelectCommit, SelectDebt}; @@ -328,8 +325,6 @@ impl Plugin for TargetPlugin { .init_resource::() .init_resource::() .add_message::() - .add_message::() - .add_message::() .add_message::() .add_message::() .add_systems( @@ -400,14 +395,6 @@ impl Plugin for TargetPlugin { crate::ui_action::drain_arrival_casts, ) .chain(), - // The chat layer's by-name asks: `/target` and `/assist` commit through - // `scan::commit`; `/follow` hands its subject to `crate::player`. - ( - by_name::target_by_name_requests, - by_name::assist_requests, - by_name::follow_requests, - ) - .chain(), // The melee probe's press, the player's Attack; the TAB keys and ATTACKTARGET // run their bodies' `TargetNearestEnemy` and `AttackTarget` as script calls. crate::ui_action::attack_nearest_probe, diff --git a/crates/benilla-app/src/target/unheld_tests.rs b/crates/benilla-app/src/target/unheld_tests.rs index e49039dfa..8113376f6 100644 --- a/crates/benilla-app/src/target/unheld_tests.rs +++ b/crates/benilla-app/src/target/unheld_tests.rs @@ -9,7 +9,7 @@ use benilla_protocol::messages::{member_status, GroupMemberEntry, PartyMemberSta use benilla_protocol::SessionEvent; use super::front_tests::{fix, Fix, ME, OLD}; -use super::{by_name, scan, Selection}; +use super::{scan, Selection}; use crate::net::{ClientCommand, GuidIndex, ObjectStore}; use crate::ui_action::{UiError, UiErrorKeys}; use crate::ui_party::{GroupState, GROUPTYPE_RAID}; @@ -242,7 +242,7 @@ fn assist_selects_the_bases_target_out_of_range_by_guid() { /// holds (`0x489c0e`); `AssistByName` shows `ERR_UNIT_NOT_FOUND` for a name whose pick is no held /// unit (`0x489d17`), a party member out of range included (`0x489c9b`), and /// `ERR_GENERIC_NO_TARGET` for an empty name with nothing selected (`0x489d2b`). A call's line is -/// signalled inside it; the stock `/assist`, which no call carries, queues the same lines. +/// signalled inside it, the stock `/assist`'s included. #[test] fn an_assist_that_finds_no_unit_says_so() { let mut f = grouped(); @@ -288,33 +288,6 @@ fn an_assist_that_finds_no_unit_says_so() { "an empty name assists the selection" ); - let assist = |f: &mut Fix, name: Option<&str>| { - let world = f.app.world_mut(); - world - .resource_mut::>() - .write(by_name::AssistRequest { - name: name.map(str::to_string), - }); - world - .run_system_once(by_name::assist_requests) - .expect("the /assist reader runs"); - // A one-shot reader starts at the oldest message, as the frame's reader does not. - world - .resource_mut::>() - .clear(); - }; - assist(&mut f, None); - assert_eq!( - errors(&f), - vec![UiError::key("ERR_GENERIC_NO_TARGET")], - "a bare /assist" - ); - assist(&mut f, Some("Nobody")); - assert_eq!( - errors(&f), - vec![UiError::key("ERR_UNIT_NOT_FOUND")], - "/assist Nobody" - ); assert_eq!(selection(&f), (None, None), "no miss moves the selection"); } diff --git a/crates/benilla-app/src/ui_chat/input/mod.rs b/crates/benilla-app/src/ui_chat/input/mod.rs index 821e50103..29e8e896f 100644 --- a/crates/benilla-app/src/ui_chat/input/mod.rs +++ b/crates/benilla-app/src/ui_chat/input/mod.rs @@ -95,12 +95,9 @@ pub(super) struct ChatProbes<'w, 's> { } /// What the drain hands to another subsystem's setter, bundled for the same ceiling. Chat never -/// writes [`Selection`]: [`crate::target`]'s resolver commits through the click's path. +/// writes [`Selection`]: a line's selection runs the stock verb, whose call commits it. #[derive(bevy::ecs::system::SystemParam)] pub(super) struct ChatOut<'w> { - target: MessageWriter<'w, crate::target::TargetByNameRequest>, - assist: MessageWriter<'w, crate::target::AssistRequest>, - follow: MessageWriter<'w, crate::player::FollowRequest>, /// `/partytest ping` stores and signals a group member's ping as the wire's handler does. ping: ResMut<'w, crate::minimap::MinimapPing>, /// The red error line by GlobalStrings key. The system's only `UiErrorKeys` access: a second @@ -580,32 +577,38 @@ pub(super) fn drain_chat_input( ParsedChat::Forfeit => { script.duel_call(benilla_ui::script::DuelRequest::Cancel); } - // `crate::target`'s resolver (the reference's `0x493aa0`) commits through the one - // SetSelection path. A bare `/target` takes `GetSlashCmdTarget`'s fallback: a no-op - // re-select of a player target, nothing for a creature. + // The stock handler's verb (`ChatFrame.lua:748-751`), `TargetByName` (`0x489d60`), + // which selects inside its call, before the next line is taken. A bare `/target` + // takes `GetSlashCmdTarget`'s fallback: a no-op re-select of a player target, + // nothing for a creature. ParsedChat::Target { name } => { if let Some(name) = name.or_else(|| target_player_name(&selection, &names, &commands)) { - chat_out - .target - .write(crate::target::TargetByNameRequest { name }); + return Some(LineWork::Lua(LineLua::Handler(format!( + "TargetByName({})", + parse::lua_quoted_string(&name) + )))); } } - // Deviation: a probe's bare `/assist` assists the live selection, where the stock - // handler (`ChatFrame.lua:740`) resolves a selected player's name through - // `AssistByName`, because a name resolve can land on a different player. + // Deviation: a probe's bare `/assist` assists the live selection, `AssistByName("")` + // (`0x489c89`), where the stock handler (`ChatFrame.lua:740`) resolves a selected + // player's name through `AssistByName`, because a name resolve can land on a + // different player. ParsedChat::Assist { name } => { - chat_out.assist.write(crate::target::AssistRequest { name }); + return Some(LineWork::Lua(LineLua::Handler(format!( + "AssistByName({})", + parse::lua_quoted_string(name.as_deref().unwrap_or_default()) + )))); } // Bare is `FollowUnit("target")`, where the stock handler (`ChatFrame.lua:754`) // first tries a selected player's name; named is `FollowByName(name)` with no exact - // flag, so prefix matching. + // flag, so prefix matching. Each starts the follow inside its call (`0x60fed0`). ParsedChat::Follow { name } => { - chat_out.follow.write(match name { - Some(name) => crate::player::FollowRequest::Name { name, exact: false }, - None => crate::player::FollowRequest::Unit("target".into()), - }); + return Some(LineWork::Lua(LineLua::Handler(match name { + Some(name) => format!("FollowByName({})", parse::lua_quoted_string(&name)), + None => "FollowUnit(\"target\")".to_string(), + }))); } // `SlashCmdList["PVP"]` is `TogglePVP()`, the same call. ParsedChat::Pvp => script.toggle_pvp(), diff --git a/crates/benilla-app/src/ui_chat/settings.rs b/crates/benilla-app/src/ui_chat/settings.rs index 6d6550d98..30982ecea 100644 --- a/crates/benilla-app/src/ui_chat/settings.rs +++ b/crates/benilla-app/src/ui_chat/settings.rs @@ -638,7 +638,8 @@ fn flush( *file.dirty.get(script) = false; } -/// The debounced save, and the `AppExit` flush. +/// The debounced save, and the `AppExit` flush, at the frame: no script reads the file before the +/// next load. fn save_chat_looks( // The slots beside the VM: the save runs no Lua. mut vm: crate::script_calls::VmWith>, diff --git a/crates/benilla-app/src/ui_dressup.rs b/crates/benilla-app/src/ui_dressup.rs index d975336fa..ff83bdd82 100644 --- a/crates/benilla-app/src/ui_dressup.rs +++ b/crates/benilla-app/src/ui_dressup.rs @@ -1,12 +1,13 @@ -//! The dressing-room drain: applies the stock window's `DressUpModel` verbs ([`DressUpIntent`]) in -//! call order, since `DressUpItem` can reset and try on in one call, and composes the booth's look -//! ([`DressUpPreview`]): the player's own gear with each substitution in its [`equip_slot`], the -//! held triple through [`held_lanes`]. A try-on whose template ([`Items::template`]) is cached -//! lands in its place in that order; one that is not lands when the record arrives. -//! There is no fit check: the reference previews whatever it is handed (`DressUpFrame.lua:2-16`). +//! The dressing room: applies the stock window's `DressUpModel` verbs ([`DressUpIntent`]) inside +//! their calls ([`DressUpSteps`]), in call order, since `DressUpItem` can reset and try on in one +//! call, and composes the booth's look each frame ([`DressUpPreview`]): the player's own gear with +//! each substitution in its [`equip_slot`], the held triple through [`held_lanes`]. A try-on whose +//! template ([`Items::template`]) is cached at the call lands there, or refuses there; one that is +//! not lands when the record arrives. There is no fit check: the reference previews whatever it +//! is handed (`DressUpFrame.lua:2-16`). use benilla_protocol::CharEnumItem; -use benilla_ui::script::DressUpIntent; +use benilla_ui::script::{DressUpIntent, UiScript}; use bevy::prelude::*; use crate::entities::equip_slot; @@ -107,7 +108,7 @@ fn lanes_coexist(main: u8, off: u8, dual_wield: bool) -> bool { /// The room's substitutions over the player's own gear, resolved and pending. #[derive(Resource, Default)] pub(crate) struct DressUpRoom { - /// False after `Close`: the booth is empty. + /// False after [`Self::close`]: the booth is empty. open: bool, /// `Undress()` has taken off the player's own pieces until the next `Dress`: `0x504490` clears /// component slots `0..0xb` (`0x5044b0`) and detaches and zeroes both held lanes @@ -151,20 +152,24 @@ impl DressUpRoom { None => self.pending.push(item), } } - DressUpIntent::Close => { - self.open = false; - self.bare = false; - self.worn = Default::default(); - self.held.clear(); - self.pending.clear(); - } } 0 } - /// One frame: the records that landed for the try-ons still waiting, whose callbacks - /// (`0x504ba0`) ran as each arrived, ahead of the verbs called since; then those verbs in call - /// order. Returns the `ERR_NOT_EQUIPPABLE` count to show. + /// Both panes hidden, so the booth empties. The reference's widget keeps its state while + /// hidden, but `DressUpItem` re-issues `SetUnit("player")` on a hidden frame, so the kept + /// state is never seen. + fn close(&mut self) { + self.open = false; + self.bare = false; + self.worn = Default::default(); + self.held.clear(); + self.pending.clear(); + } + + /// The records that landed for the try-ons still waiting, whose callbacks (`0x504ba0`) ran as + /// each arrived, ahead of the verbs called since; then those verbs in call order. Returns the + /// `ERR_NOT_EQUIPPABLE` count to show. fn step( &mut self, intents: impl IntoIterator, @@ -227,27 +232,98 @@ fn not_equippable(inventory_type: u32) -> bool { matches!(inventory_type, 0 | 18 | 24 | 27) || inventory_type > 28 } +/// Whether a dressing-room pane is shown: the dressing room's or the auction house's +/// `AuctionDressUpFrame`, read off the frames (the stock files carry no hook of ours). A hidden +/// pane has no model, and a verb on it does nothing (`0x504596`). +fn pane_shown(script: &UiScript) -> bool { + script.frame_visible("DressUpFrame") || script.frame_visible("AuctionDressUpFrame") +} + +/// Show `refused` `ERR_NOT_EQUIPPABLE` lines (`0x504b2d`, `push 0x13; call 0x496720`), each as +/// `DisplayError` shows it, under the call's input events. +fn show_refusals(script: &mut UiScript, sink: &mut crate::ui_action::MessageSink, refused: u32) { + let input = script.input_events(); + for _ in 0..refused { + if let Some(line) = crate::ui_action::keyed_line(script, "ERR_NOT_EQUIPPABLE") { + crate::ui_action::show_now(script, sink, "ui_dressup", line.under(input)); + } + } +} + +/// The dressing-room verbs inside their calls ([`benilla_ui::script::ScriptCall::DressUp`]), +/// and the frame's turn for the records that landed since. +#[derive(bevy::ecs::system::SystemParam)] +pub(crate) struct DressUpSteps<'w> { + /// Absent in a harness without the room. + room: Option>, + items: Option>, + commands: Option>, + sink: crate::ui_action::MessageSink<'w>, +} + +impl DressUpSteps<'_> { + /// One verb inside its call: a try-on looks its record up in the cache as it stands at the + /// call (`0x504579`), and a cached one goes on, or refuses, before the verb returns + /// (`0x5045dd`-`0x5045ee`, `0x504b2d`). The callbacks of the records that landed since the + /// last verb run first, as each ran at its arrival. + pub(crate) fn apply(&mut self, script: &mut UiScript, intent: DressUpIntent) { + let (Some(room), Some(items), Some(commands)) = ( + self.room.as_deref_mut(), + self.items.as_deref(), + self.commands.as_deref(), + ) else { + return; + }; + if !pane_shown(script) { + return; + } + let refused = room.step([intent], items, commands); + show_refusals(script, &mut self.sink, refused); + } + + /// The frame's turn: the room emptied when no pane is shown, then the records that landed for + /// the try-ons still waiting, each refusal shown from its callback (`0x504ba0`). + fn frame(&mut self, script: &mut UiScript) { + let (Some(room), Some(items)) = (self.room.as_deref_mut(), self.items.as_deref()) else { + return; + }; + if !pane_shown(script) { + room.close(); + } + let refused = room.resolve_pending(items); + show_refusals(script, &mut self.sink, refused); + } +} + pub(crate) struct DressUpUiPlugin; impl Plugin for DressUpUiPlugin { fn build(&self, app: &mut App) { - // A drain, the frame its verbs are called: a try-on's cache lookup reads the cache as it - // stood at the call, before the next frame's packets land. app.init_resource::() - .add_systems(Update, drain_dressup.in_set(UiDrain)); + .add_systems(Update, compose_dressup.in_set(UiDrain)); } } +/// The booth's look, each frame: the room's turn ([`DressUpSteps::frame`]), then the player's own +/// look with the room's substitutions written in, pushed to the pane when it moved. #[allow(clippy::type_complexity)] -fn drain_dressup( - // The VM, whose call host writes the worn pins, and the player the pane clones, pins read. - mut vm_and_me: ParamSet<( - crate::script_calls::HostedVm, - Query<(&ObjectStore, &NetEntity, Option<&crate::entities::WornPins>), With>, +fn compose_dressup( + // The room, beside the host that applies the verbs to it, and the player the pane clones, + // whose pins the host writes. + mut vm: crate::script_calls::VmWith<( + DressUpSteps<'static>, + Query< + 'static, + 'static, + ( + &'static ObjectStore, + &'static NetEntity, + Option<&'static crate::entities::WornPins>, + ), + With, + >, )>, - mut room: ResMut, mut preview: ResMut, - items: Res, commands: Res, // A miss sends the `CMSG_GUILD_QUERY` whose answer paints the tabard. guilds: Res, @@ -256,28 +332,12 @@ fn drain_dressup( Res, Option>, ), - mut sink: crate::ui_action::MessageSink, ) { - let ran = vm_and_me.p0().run(|script| { + let ran = vm.turn(|script, (mut steps, _)| { let Some(script) = script else { return false; }; - let mut intents = script.take_dressup_intents(); - // The room empties when neither the dressing room nor the auction house's - // `AuctionDressUpFrame` is shown, read off the frames (the stock files carry no hook of - // ours), and a verb on a hidden pane does nothing (`0x504596`). The reference's widget - // keeps its state while hidden, but `DressUpItem` re-issues `SetUnit("player")` on a - // hidden frame, so the kept state is never seen. - if !script.frame_visible("DressUpFrame") && !script.frame_visible("AuctionDressUpFrame") { - intents = vec![DressUpIntent::Close]; - } - // A refusal shows as its record lands: in the call when cached, else from the cache's - // callback (`0x504ba0`). - let refused = room.step(intents, &items, &commands); - let lines = (0..refused) - .filter_map(|_| crate::ui_action::keyed_line(script, "ERR_NOT_EQUIPPABLE")) - .collect::>(); - crate::ui_action::show_messages(script, &mut sink, "ui_dressup", lines); + steps.frame(script); true }); if !ran { @@ -286,14 +346,16 @@ fn drain_dressup( // The crest comes off the guild cache this system holds, not the outfit `player_look` // builds. let dual_wield = known.0.knows_dual_wield(known.1.as_deref()); - let me = vm_and_me.p1(); + let (steps, me) = vm.own(); + let (Some(room), Some(items)) = (steps.room.as_deref(), steps.items.as_deref()) else { + return; + }; let look = match (room.open, me.single().ok()) { - (true, Some((store, net, pins))) => { - player_look(store, net, pins, &room, &items, dual_wield).map(|l| DressUpLook { + (true, Some((store, net, pins))) => player_look(store, net, pins, room, items, dual_wield) + .map(|l| DressUpLook { emblem: crate::ui_guild::unit_guild_emblem(&store.0, &guilds, Ask::New(&commands)), ..l - }) - } + }), _ => None, }; if preview.look != look { @@ -491,7 +553,7 @@ mod tests { "Reset puts the player's own chest back on" ); - room.apply(DressUpIntent::Close, &items, &cmds); + room.close(); assert!(!room.open, "closing empties the room (the booth goes dark)"); } @@ -888,6 +950,55 @@ mod tests { } } + /// A try-on applies inside its call (`0x504540`): a cached bag refuses there, so the chunk's + /// next line reads the `UI_ERROR_MESSAGE` its `DisplayError` signalled (`0x504b2d`), and a + /// cached chest is on the booth before the verb returns. + #[test] + fn a_try_on_refuses_and_lands_inside_its_call() { + let mut app = crate::game_plugins::schedule_tests::headless_client(); + app.insert_resource(bevy::ecs::error::DefaultErrorHandler( + bevy::ecs::error::ignore, + )); + let (tx, _rx) = crossbeam_channel::unbounded(); + app.insert_resource(NetCommands(tx)); + app.update(); + { + let mut items = app.world_mut().resource_mut::(); + items.insert_template(4500, Some(worn("Small Bag", 9100, 18))); + items.insert_template(2000, Some(worn("Shiny Chest", 7000, 5))); + } + let mut s = UiScript::new().expect("a VM"); + s.set_screen_size(800.0, 600.0); + app.insert_non_send_resource(s); + let seen: Option = crate::script_calls::with_hosted_vm(app.world_mut(), |s| { + let s = s.expect("a VM"); + s.run( + r#" + ERR_NOT_EQUIPPABLE = "That item can't be equipped." + local room = CreateFrame("Frame", "DressUpFrame", UIParent) + dm = CreateFrame("DressUpModel", "DressUpModel", room) + local f = CreateFrame("Frame") + f:RegisterEvent("UI_ERROR_MESSAGE") + f:SetScript("OnEvent", function() seen = arg1 end) + "#, + ) + .expect("the room"); + let seen = s + .eval::>("dm:SetUnit('player') dm:TryOn(4500) return seen") + .expect("the chunk runs"); + s.run("dm:TryOn(2000)").expect("the try-on"); + seen + }); + // No frame has run since: the room is as the call left it. + let worn_chest = app.world().resource::().worn[4].is_some(); + assert_eq!( + seen.as_deref(), + Some("That item can't be equipped."), + "the refusal signals inside the call" + ); + assert!(worn_chest, "the cached chest went on inside its call"); + } + /// A bow takes the off lane and clears the main; a crossbow takes the main and clears the off. #[test] fn a_tried_on_ranged_weapon_takes_a_hand_and_clears_the_melee_pair() { diff --git a/crates/benilla-app/src/ui_layout.rs b/crates/benilla-app/src/ui_layout.rs index 14379d9d2..7e0a0a4c9 100644 --- a/crates/benilla-app/src/ui_layout.rs +++ b/crates/benilla-app/src/ui_layout.rs @@ -288,8 +288,8 @@ fn watch_layout(mut hosted_vm: crate::script_calls::HostedVm, mut file: ResMut) { hosted_vm.run(|script| { let Some(script) = script else { return }; diff --git a/crates/benilla-app/src/ui_macro/mod.rs b/crates/benilla-app/src/ui_macro/mod.rs index d47657d00..076e1b299 100644 --- a/crates/benilla-app/src/ui_macro/mod.rs +++ b/crates/benilla-app/src/ui_macro/mod.rs @@ -240,7 +240,8 @@ fn migrate_character_bar( } /// Persist on the engine's dirty edge. The verbs signal `UPDATE_MACROS` themselves, inside their -/// calls. +/// calls. Left at the frame: the file is read only at the next load, so no script sees when it +/// is written. fn save_dirty_macros( mut hosted_vm: crate::script_calls::HostedVm, mut files: ResMut, diff --git a/crates/benilla-app/src/ui_models/mod.rs b/crates/benilla-app/src/ui_models/mod.rs index 45f1223be..94788265d 100644 --- a/crates/benilla-app/src/ui_models/mod.rs +++ b/crates/benilla-app/src/ui_models/mod.rs @@ -792,7 +792,8 @@ fn forget_dead_vm_tiles( } /// The per-frame pass: feed the engine the facts it asked for, keep one tile per visible pane, -/// pack the atlas, place every tile at its cell and its play head, and aim the camera. +/// pack the atlas, place every tile at its cell and its play head, and aim the camera. Not a +/// call's: a tile is the pane's draw, which no getter reads. #[allow(clippy::type_complexity)] // a Bevy system's full input set fn sync_tiles( mut vm: crate::script_calls::VmWith<( diff --git a/crates/benilla-app/src/ui_script/dressup_tests.rs b/crates/benilla-app/src/ui_script/dressup_tests.rs index 157b11cbe..0ca6e71ed 100644 --- a/crates/benilla-app/src/ui_script/dressup_tests.rs +++ b/crates/benilla-app/src/ui_script/dressup_tests.rs @@ -74,6 +74,7 @@ fn load_room_with_bags(s: &UiScript) { /// (`PaperDollFrame.lua:100-104`). fn shown_paper_doll() -> UiScript { let mut s = UiScript::new().unwrap(); + s.keep_calls(); s.set_screen_size(1024.0, 768.0); for file in crate::ui_script::test_ui::production_order(&[ CHARACTER_UI, @@ -146,6 +147,7 @@ fn backpack_with_jerky(s: &mut UiScript) -> String { fn ctrl_click_on_a_bag_item_opens_the_room_wearing_it() { let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); + s.keep_calls(); s.set_screen_size(1024.0, 768.0); load_room_with_bags(&s); let btn = backpack_with_jerky(&mut s); @@ -178,6 +180,7 @@ fn ctrl_click_on_a_bag_item_opens_the_room_wearing_it() { fn shift_click_posts_the_link_with_chat_open_and_splits_with_it_closed() { let _data = benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); + s.keep_calls(); s.set_screen_size(1024.0, 768.0); load_room_with_bags(&s); let btn = backpack_with_jerky(&mut s); @@ -218,6 +221,7 @@ fn shift_click_posts_the_link_with_chat_open_and_splits_with_it_closed() { fn ctrl_clicking_a_chat_link_previews_it() { benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); + s.keep_calls(); s.set_screen_size(1024.0, 768.0); load_room(&s); @@ -339,6 +343,7 @@ fn shift_clicking_an_unresolved_slot_posts_nothing_and_never_raises() { fn reset_re_dresses_close_empties_and_the_arrows_spin_the_pane() { benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); + s.keep_calls(); s.set_screen_size(1024.0, 768.0); load_room(&s); s.run("ShowUIPanel(DressUpFrame)").unwrap(); @@ -391,6 +396,7 @@ fn reset_re_dresses_close_empties_and_the_arrows_spin_the_pane() { fn the_room_binds_the_player() { benilla_formats::wow_data_or_skip!(); let mut s = UiScript::new().unwrap(); + s.keep_calls(); s.set_screen_size(1024.0, 768.0); load_room(&s); s.set_unit_guids(&benilla_ui::script::UnitGuids { diff --git a/crates/benilla-app/src/video.rs b/crates/benilla-app/src/video.rs index ccc1d4cbd..6fa5e6621 100644 --- a/crates/benilla-app/src/video.rs +++ b/crates/benilla-app/src/video.rs @@ -22,7 +22,7 @@ //! "vsync on" is `AutoNoVsync` paced to the monitor's refresh rate ([`present_choice`]), which //! keeps the synced look without the crash. `$WOW_PRESENT=fifo` restores plain `Fifo`. -use benilla_ui::script::ScreenResolution; +use benilla_ui::script::{ScreenResolution, UiScript}; use bevy::prelude::*; use bevy::render::renderer::RenderAdapterInfo; use bevy::window::{ @@ -505,32 +505,50 @@ impl Plugin for VideoPlugin { #[derive(Resource, Default, Clone, Copy, PartialEq, Eq, Debug)] pub(crate) struct GxRestarts(u32); -/// Carry `RestartGx()` calls into [`GxRestarts`] and commit the latched `gx*` rows (flags `3`), -/// where the reference's restart calls `CVar::Update` (`0x63e060`) on each; `GetCVar` answers the -/// applied value until then. -fn drain_restart_gx( - mut vm: crate::script_calls::VmWith>, - mut restarts: ResMut, -) { - vm.turn(|script, mut steps| { - let Some(script) = script else { +/// `gxRestart`'s handler (`0x639f60`) inside the call that ran it +/// ([`benilla_ui::script::ScriptCall::RestartGx`]): the latched `gx*` rows (flags `3`) committed, +/// where the reference's restart calls `CVar::Update` (`0x63e060`) on each (`0x639ec0`), their +/// callbacks run and the VM's mirror synced, so `GetCVar` reads the committed value on the +/// verb's next line. The device switch it asks, a [`GxRestarts`] bump, is the window appliers' in +/// the frame's drain: no script reads the window but through these rows. +#[derive(bevy::ecs::system::SystemParam)] +pub(crate) struct GxRestartSteps<'w> { + cvars: crate::cvars::CvarSteps<'w>, + /// Absent in a harness without the video plugin. + restarts: Option>, +} + +impl GxRestartSteps<'_> { + /// `runs` restarts at once. + pub(crate) fn restart(&mut self, script: &mut UiScript, runs: u32) { + // The VM's writes no call took first, so a stage made before the restart is committed. + self.cvars.sync(Some(script)); + let Some(cvars) = self.cvars.cvars() else { return; }; - let asks = script.take_restart_gx_asks(); - if asks == 0 { - // A `ResMut` deref-mut is a change signal, so a quiet frame touches nothing. - return; - } - let Some(cvars) = steps.cvars() else { - return; - }; - restarts.0 = restarts.0.wrapping_add(asks); let committed = cvars.commit_latched(); - steps.sync(Some(script)); + if let Some(restarts) = self.restarts.as_deref_mut() { + restarts.0 = restarts.0.wrapping_add(runs); + } + self.cvars.sync(Some(script)); info!( "video: RestartGx — {committed} staged setting(s) committed; re-asserting the \ display mode and present mode" ); + } +} + +/// The restarts no call took, a VM's with no host lent ([`GxRestartSteps`]). +fn drain_restart_gx(mut vm: crate::script_calls::VmWith>) { + vm.turn(|script, mut steps| { + let Some(script) = script else { + return; + }; + let runs = script.take_restart_gx_asks(); + // A `ResMut` deref-mut is a change signal, so a quiet frame touches nothing. + if runs > 0 { + steps.restart(script, runs); + } }) } diff --git a/crates/benilla-ui/src/script/calls.rs b/crates/benilla-ui/src/script/calls.rs index 06306195e..5e5017672 100644 --- a/crates/benilla-ui/src/script/calls.rs +++ b/crates/benilla-ui/src/script/calls.rs @@ -277,6 +277,15 @@ pub enum ScriptCall { /// `NextView`, `PrevView` or `FlipCameraYaw`, seated on the camera inside the call, its CVar /// writes with it (`0x512f17`, `0x50fa09`). CameraView(super::CameraViewRequest), + /// `gxRestart`'s handler (`0x639f60`), run inside `RestartGx`, `SetScreenResolution` and + /// `SetMultisampleFormat` (through `0x63ce00`, which calls it synchronously) and + /// `RestoreVideoDefaults` (directly, `0x639a51`): the host commits the latched display rows + /// (`0x639ec0`) and runs their callbacks before the verb goes on. + RestartGx, + /// A dressing-room verb ([`super::DressUpIntent`]): `Dress`, `Undress`, `TryOn`, or `SetUnit` + /// and `RefreshUnit` on a `DressUpModel`, applied to the room inside the call; a cached + /// record's try-on goes on, or refuses with `ERR_NOT_EQUIPPABLE`, there (`0x504540`). + DressUp(super::DressUpIntent), /// `FollowUnit` or `FollowByName` ([`super::FollowRequest`]): the start gate, its refusal's /// line or the follow armed and `AUTOFOLLOW_BEGIN` signalled, inside the call (`0x60fed0`). Follow(super::FollowRequest), diff --git a/crates/benilla-ui/src/script/client.rs b/crates/benilla-ui/src/script/client.rs index dcdaf1115..d9afefe5a 100644 --- a/crates/benilla-ui/src/script/client.rs +++ b/crates/benilla-ui/src/script/client.rs @@ -236,6 +236,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { impl super::UiScript { /// The `RequestTimePlayed()` sends a harness's VM kept ([`Self::keep_calls`]): a count, as the /// request body is empty. + #[cfg(test)] pub fn take_played_time_asks(&mut self) -> u32 { self.take_calls_where(|call| { matches!(call, super::ScriptCall::RequestTimePlayed).then_some(()) diff --git a/crates/benilla-ui/src/script/cvars.rs b/crates/benilla-ui/src/script/cvars.rs index f3b15796c..50d6401f1 100644 --- a/crates/benilla-ui/src/script/cvars.rs +++ b/crates/benilla-ui/src/script/cvars.rs @@ -424,8 +424,8 @@ impl super::UiScript { self.model_mut().video_caps = caps; } - /// Drain the `RestartGx()` calls queued since the last call; the host answers each by - /// re-asserting the staged video settings against the window. + /// Drain the restarts no call took ([`restart_gx`]); the host answers each by committing the + /// staged video settings and re-asserting them against the window. pub fn take_restart_gx_asks(&mut self) -> u32 { std::mem::take(&mut self.model_mut().restart_gx_asks) } @@ -790,10 +790,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { set_from_engine(&mut model, "gxColorBits", f.color_bits.to_string()); set_from_engine(&mut model, "gxDepthBits", f.depth_bits.to_string()); set_from_engine(&mut model, "gxMultisample", f.samples.to_string()); - // Deviation: the reference then runs the console command `gxRestart` (`0x842978` - // through `0x63ce00`); none is queued here, because `gxMultisample` applies at next - // launch anyway: the camera reads its sample count once, at spawn. - Ok(()) + drop(model); + // Each `CVar::Set` (`0x48c72a`, `0x48c75b`, `0x48c78c`) runs its row's callback + // inside the call. + written(lua)?; + // Then `gxRestart`, always (`0x48c79b`). + restart_gx(lua) })?, )?; @@ -848,6 +850,44 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { super::video_pairs::install(lua) } +/// `gxRestart`'s handler (`0x639f60`) inside the call that runs it: `RestartGx` (`0x48dab0`), +/// `SetScreenResolution` (`0x48c0bc`) and `SetMultisampleFormat` (`0x48c79b`) through +/// `ConsoleCommandExecute` (`0x63ce00`), which calls the handler synchronously, and +/// `RestoreVideoDefaults`, which calls it directly (`0x639a51`). The host commits the latched rows +/// (`0x639ec0`) before the verb goes on. With no host open (a bare VM) it is +/// counted for the host's frame drain. +fn restart_gx(lua: &Lua) -> mlua::Result<()> { + if super::host::open(lua) { + return super::calls::submit(lua, super::ScriptCall::RestartGx); + } + lua.app_data_mut::() + .expect("model app_data") + .restart_gx_asks += 1; + Ok(()) +} + +/// `names` each set to its registered default, through the host's callbacks inside the call. +fn restore_defaults(lua: &Lua, names: &[&str]) -> mlua::Result<()> { + let mut model = lua.app_data_mut::().expect("model app_data"); + for name in names { + let Some(default) = model + .cvars + .get(&name.to_ascii_lowercase()) + .map(|slot| slot.default.clone()) + else { + // Unregistered in a bare VM: nothing to restore, nothing to warn about. + continue; + }; + set_from_engine(&mut model, name, default); + } + drop(model); + written(lua) +} + +/// How many of [`VIDEO_DEFAULT_CVARS`] lead it as the device record's `gx*` writes, ahead of +/// `RestoreVideoDefaults`' restart. +const VIDEO_DEFAULT_GX: usize = 6; + /// What `RestoreVideoDefaults` puts back: the CVars among the reference verb's writes that benilla /// registers, each at its registered default. The verb writes no `uiScale`, `useUiScale` or /// `anisotropic`. @@ -957,8 +997,11 @@ fn install_video_verbs(lua: &Lua) -> mlua::Result<()> { return Ok(()); } set_from_engine(&mut model, "gxResolution", want); - model.restart_gx_asks += 1; - Ok(()) + drop(model); + // `CVar::Set` (`0x48c0ad`) runs the row's callback, then `gxRestart` runs + // (`0x48c0bc`), both inside the call. + written(lua)?; + restart_gx(lua) })?, )?; // ── GetRefreshRates ───────────────────────────────────────────────────────────────────── @@ -1003,40 +1046,24 @@ fn install_video_verbs(lua: &Lua) -> mlua::Result<()> { // ── RestartGx ──────────────────────────────────────────────────────────────────────────── // "Apply the staged video settings now": `0x48dab0` runs the console command `gxRestart` // (`0x63ce00`), whose handler `0x639f60` applies the staged device format and commits the - // fourteen latched CVars. Here it queues a counted request, as `Screenshot()` does; the host - // commits its gx stages (`Cvars::commit_latched`) and re-asserts them against the window. - // The reference runs each gx change callback at `SetCVar`, where it can veto the write; the - // host runs it at the commit. No restart applies `gxMultisample`: the camera reads it once. - lua.globals().set( - "RestartGx", - lua.create_function(|lua, ()| { - lua.app_data_mut::() - .expect("model app_data") - .restart_gx_asks += 1; - Ok(()) - })?, - )?; + // fourteen latched CVars, synchronously ([`restart_gx`]). The reference runs each gx change + // callback at `SetCVar`, where it can veto the write; the host runs it at the commit. No + // restart applies `gxMultisample`: the camera reads it once. + lua.globals() + .set("RestartGx", lua.create_function(|lua, ()| restart_gx(lua))?)?; // ── RestoreVideoDefaults ───────────────────────────────────────────────────────────────── // The Defaults button's whole effect: `OptionsFrame_SetDefaults` only sets the widgets, which - // the button's `OnClick` then hides without `_Save` (`OptionsFrame.xml:688-690`). Ends with a - // restart, as the reference calls the `gxRestart` handler between its two passes (`0x639a51`). + // the button's `OnClick` then hides without `_Save` (`OptionsFrame.xml:688-690`). lua.globals().set( "RestoreVideoDefaults", lua.create_function(|lua, ()| { - let mut model = lua.app_data_mut::().expect("model app_data"); - for name in VIDEO_DEFAULT_CVARS { - let Some(default) = model - .cvars - .get(&name.to_ascii_lowercase()) - .map(|slot| slot.default.clone()) - else { - // Unregistered in a bare VM: nothing to restore, nothing to warn about. - continue; - }; - set_from_engine(&mut model, name, default); - } - model.restart_gx_asks += 1; - Ok(()) + // The device record's `gx*` (`0x639c40`), the restart (`0x639a51`), then the quality + // rows (`0x639a60`), each write a `CVar::Set` that runs its row's callback inside the + // call. + let (gx, quality) = VIDEO_DEFAULT_CVARS.split_at(VIDEO_DEFAULT_GX); + restore_defaults(lua, gx)?; + restart_gx(lua)?; + restore_defaults(lua, quality) })?, )?; // ── GetGamma / SetGamma ────────────────────────────────────────────────────────────────── @@ -1067,8 +1094,13 @@ fn install_video_verbs(lua: &Lua) -> mlua::Result<()> { // `lua_isnumber` (`0x4891fe`): a number or a numeric string; anything else raises // (`0x6f4940`). let v = super::binding_abi::float_arg(lua, value, USAGE_SET_GAMMA)?; - let mut model = lua.app_data_mut::().expect("model app_data"); - write_cvar(&mut model, CVAR_GAMMA, format_f(1.0 - v)); + write_cvar( + &mut lua.app_data_mut::().expect("model app_data"), + CVAR_GAMMA, + format_f(1.0 - v), + ); + // `CVar::Set` (`0x489267`) runs the row's callback inside the call. + written(lua)?; // Zero return values, not nil (`eax = 0` at every `ret`). Ok(mlua::MultiValue::new()) })?, @@ -1140,6 +1172,9 @@ fn install_world_detail_verbs(lua: &Lua) -> mlua::Result<()> { ); write_cvar(&mut model, CVAR_SMALL_CULL, small_cull_text(stop)); write_cvar(&mut model, CVAR_WORLD_DETAIL, stop.to_string()); + drop(model); + // Each `CVar::Set` (`0x488e51`, `0x488e8e`) runs its row's callback inside the call. + written(lua)?; // Zero return values, not nil (`eax = 0` at every `ret`). Ok(mlua::MultiValue::new()) })?, @@ -1560,4 +1595,13 @@ mod tests { .unwrap(); assert_eq!(trimmed, "Archimonde"); } + + /// `RestoreVideoDefaults` restarts between the device record's writes and the quality rows' + /// (`0x639a51`), so the split falls after the last `gx*` row. + #[test] + fn the_video_defaults_split_after_the_device_records_rows() { + let (gx, quality) = super::VIDEO_DEFAULT_CVARS.split_at(super::VIDEO_DEFAULT_GX); + assert!(gx.iter().all(|n| n.starts_with("gx")), "{gx:?}"); + assert!(!quality.iter().any(|n| n.starts_with("gx")), "{quality:?}"); + } } diff --git a/crates/benilla-ui/src/script/dressup.rs b/crates/benilla-ui/src/script/dressup.rs index 948587de4..3868a2c28 100644 --- a/crates/benilla-ui/src/script/dressup.rs +++ b/crates/benilla-ui/src/script/dressup.rs @@ -1,8 +1,8 @@ -//! The dressing room: the `DressUpModel` verbs (table `0x84f190`) and the intent queue behind them -//! and behind `SetUnit`/`RefreshUnit` on a dressing-room pane. The reference edits a clone of the -//! unit's model (`0x5059a0`, `0x504350`); here the app composes the look from the intents in -//! order, which matters: on a closed window `DressUpItem` resets, then tries on -//! (`DressUpFrame.lua:3-7`). +//! The dressing room: the `DressUpModel` verbs (table `0x84f190`) and `SetUnit`/`RefreshUnit` on a +//! dressing-room pane, each handed to the host, which applies it to the room inside the call +//! ([`super::ScriptCall::DressUp`]). The reference edits a clone of the unit's model (`0x5059a0`, +//! `0x504350`); here the app composes the look from the verbs in call order, which matters: on a +//! closed window `DressUpItem` resets, then tries on (`DressUpFrame.lua:3-7`). use mlua::{Lua, Table, Value}; @@ -14,7 +14,7 @@ use crate::widget::FrameKind; /// Registry key of the `DressUpModel` method table, its own three; the rest come by the chain. pub(super) const REG_DRESSUPMODEL_METHODS: &str = "__benilla_dressupmodel_methods"; -/// One queued dressing-room intent, applied in order. +/// One dressing-room verb, applied inside its call. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum DressUpIntent { /// `SetUnit`, `RefreshUnit` and `Dress` (the Reset button): the reference's rebuild from the @@ -26,26 +26,24 @@ pub enum DressUpIntent { /// `TryOn(item)`: the item in its `InventoryType`'s slot, a held one in the lane `0x5048e8` /// picks, with no class, level or proficiency check (`DressUpFrame.lua:2-16`). TryOn(u32), - /// The window was hidden, so the booth empties. The reference keeps its state while hidden, - /// but the next `DressUpItem` re-issues `SetUnit("player")`, so that state is never seen. - Close, } impl super::UiScript { - /// Drain the dressing room's intents, oldest first; the app applies them in order. + /// The dressing-room verbs a harness's VM kept ([`Self::keep_calls`]), oldest first. pub fn take_dressup_intents(&mut self) -> Vec { - std::mem::take(&mut self.model_mut().dressup_intents) + self.take_calls_where(|call| match call { + super::ScriptCall::DressUp(intent) => Some(*intent), + _ => None, + }) } } -fn queue(lua: &Lua, intent: DressUpIntent) { - lua.app_data_mut::() - .expect("model app_data") - .dressup_intents - .push(intent); +/// Hand `intent` to the host, which applies it before this returns. +fn submit(lua: &Lua, intent: DressUpIntent) -> mlua::Result<()> { + super::calls::submit(lua, super::ScriptCall::DressUp(intent)) } -/// Queue the rebuild when `PlayerModel`'s `SetUnit`/`RefreshUnit` lands on a `DressUpModel`. +/// The rebuild when `PlayerModel`'s `SetUnit`/`RefreshUnit` lands on a `DressUpModel`. pub(super) fn redress_if_dressup(lua: &Lua, this: &Table) -> mlua::Result<()> { let h = frame_handle_of(lua, this)?; let is_dressup = lua @@ -55,7 +53,7 @@ pub(super) fn redress_if_dressup(lua: &Lua, this: &Table) -> mlua::Result<()> { .frame(h) .is_some_and(|f| f.kind == FrameKind::DressUpModel); if is_dressup { - queue(lua, DressUpIntent::Dress); + submit(lua, DressUpIntent::Dress)?; } Ok(()) } @@ -77,8 +75,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { "Undress", lua.create_function(|lua, this: Table| { frame_handle_of(lua, &this)?; - queue(lua, DressUpIntent::Undress); - Ok(()) + submit(lua, DressUpIntent::Undress) })?, )?; @@ -87,8 +84,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { "Dress", lua.create_function(|lua, this: Table| { frame_handle_of(lua, &this)?; - queue(lua, DressUpIntent::Dress); - Ok(()) + submit(lua, DressUpIntent::Dress) })?, )?; @@ -100,7 +96,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { frame_handle_of(lua, &this)?; if let Ok(id) = u32::try_from(item_arg(lua, &item)) { if id != 0 { - queue(lua, DressUpIntent::TryOn(id)); + submit(lua, DressUpIntent::TryOn(id))?; } } Ok(()) @@ -117,6 +113,7 @@ mod tests { fn room() -> UiScript { let mut s = UiScript::new().unwrap(); + s.keep_calls(); s.set_screen_size(800.0, 600.0); s.run(r#"dm = CreateFrame("DressUpModel", "DM", UIParent)"#) .unwrap(); @@ -167,7 +164,7 @@ mod tests { /// The stock `DressUpItemLink` passes the link's digits: a digit string is an id, a link is 0. #[test] - fn the_verbs_queue_intents_in_order_and_try_on_reads_its_argument_like_the_client() { + fn the_verbs_hand_their_calls_over_in_order_and_try_on_reads_its_argument_like_the_client() { let mut s = room(); s.run( r#" @@ -195,7 +192,7 @@ mod tests { DressUpIntent::Dress, ] ); - // The same two PlayerModel verbs on a paper-doll pane queue nothing. + // The same two PlayerModel verbs on a paper-doll pane make no call. s.run(r#"pm = CreateFrame("PlayerModel", "PMOnly", UIParent) pm:SetUnit("player") pm:RefreshUnit()"#) .unwrap(); assert!(s.take_dressup_intents().is_empty()); diff --git a/crates/benilla-ui/src/script/glue_addons.rs b/crates/benilla-ui/src/script/glue_addons.rs index d78cbcd6a..b6b8a65d1 100644 --- a/crates/benilla-ui/src/script/glue_addons.rs +++ b/crates/benilla-ui/src/script/glue_addons.rs @@ -331,7 +331,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { CVAR_VERSION_CHECK, if on { "1" } else { "0" }.to_string(), ); - Ok(()) + super::cvars::written(lua) })?, )?; @@ -358,7 +358,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { CVAR_SCRIPT_MEMORY, (mb << 10).to_string(), ); - Ok(()) + super::cvars::written(lua) })?, )?; diff --git a/crates/benilla-ui/src/script/glue_core.rs b/crates/benilla-ui/src/script/glue_core.rs index 67a243056..1177292ba 100644 --- a/crates/benilla-ui/src/script/glue_core.rs +++ b/crates/benilla-ui/src/script/glue_core.rs @@ -893,9 +893,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { "SetSavedAccountName", lua.create_function(|lua, name: Value| { let name = string_arg(lua, name, "Usage: SetSavedAccountName(\"accountName\")")?; - let mut model = lua.app_data_mut::().expect("model app_data"); - super::cvars::write_cvar(&mut model, "accountName", name); - Ok(()) + super::cvars::write_cvar( + &mut lua.app_data_mut::().expect("model app_data"), + "accountName", + name, + ); + super::cvars::written(lua) })?, )?; @@ -951,7 +954,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { }, ); super::cvars::set_from_engine(&mut model, notice.cvar(), "1".to_string()); - Ok(()) + drop(model); + super::cvars::written(lua) })?, )?; } diff --git a/crates/benilla-ui/src/script/glue_realm.rs b/crates/benilla-ui/src/script/glue_realm.rs index 6d768fe2d..979684844 100644 --- a/crates/benilla-ui/src/script/glue_realm.rs +++ b/crates/benilla-ui/src/script/glue_realm.rs @@ -621,6 +621,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { } name }; + // `ConnectToRealm`'s `CVar::Set` (`0x5ab7bf`) runs the row's callback inside the call. + super::cvars::written(lua)?; if let Some(name) = name { call(lua, GlueCall::ChangeRealm(name)); } diff --git a/crates/benilla-ui/src/script/gm_ticket.rs b/crates/benilla-ui/src/script/gm_ticket.rs index eca25d0df..1dc504906 100644 --- a/crates/benilla-ui/src/script/gm_ticket.rs +++ b/crates/benilla-ui/src/script/gm_ticket.rs @@ -59,6 +59,7 @@ impl super::UiScript { } /// The `Stuck()` calls a harness's VM kept, each one cast of spell 7355. + #[cfg(test)] pub fn take_stuck_casts(&mut self) -> u32 { self.take_calls_like(&super::ScriptCall::GmTicket(GmTicketCall::Stuck)) as u32 } diff --git a/crates/benilla-ui/src/script/item_stats.rs b/crates/benilla-ui/src/script/item_stats.rs index 3e9ec5cfa..0e21bab8a 100644 --- a/crates/benilla-ui/src/script/item_stats.rs +++ b/crates/benilla-ui/src/script/item_stats.rs @@ -458,7 +458,7 @@ impl super::UiScript { /// The guids of the creators and givers the item builds a harness's VM kept the calls of /// ([`Self::keep_calls`]) missed in the name cache, in ask order. - #[cfg(any(test, feature = "test-support"))] + #[cfg(test)] pub fn take_item_name_asks(&mut self) -> Vec { self.take_calls_where(|call| match call { super::ScriptCall::RecordAsk(super::RecordAsk::ItemBuildName(guid)) => Some(*guid), @@ -468,7 +468,7 @@ impl super::UiScript { /// The charter item guids whose petition record or owner the item builds a harness's VM kept /// the calls of missed, in ask order. - #[cfg(any(test, feature = "test-support"))] + #[cfg(test)] pub fn take_petition_asks(&mut self) -> Vec { self.take_calls_where(|call| match call { super::ScriptCall::RecordAsk(super::RecordAsk::Petition(item)) => Some(*item), @@ -488,16 +488,6 @@ impl super::UiScript { self.model_mut().item_sets.insert(set_id, view); } - /// The member entries the set blocks a harness's VM kept the calls of - /// ([`Self::keep_calls`]) asked the item-name cache for, in ask order. - #[cfg(any(test, feature = "test-support"))] - pub fn take_item_name_record_asks(&mut self) -> Vec { - self.take_calls_where(|call| match call { - super::ScriptCall::RecordAsk(super::RecordAsk::ItemName(entry)) => Some(*entry), - _ => None, - }) - } - /// Pushes the whole `ItemRandomProperties` table once, at load: a clicked tooltip cannot /// repaint on a late answer. pub fn set_random_properties( diff --git a/crates/benilla-ui/src/script/keybind.rs b/crates/benilla-ui/src/script/keybind.rs index a469cb5ba..8cdbc0a50 100644 --- a/crates/benilla-ui/src/script/keybind.rs +++ b/crates/benilla-ui/src/script/keybind.rs @@ -285,8 +285,9 @@ impl KeybindState { } /// `LoadBindings(set)`: 0 defaults, 1 account, 2 character, which falls back to the account - /// set (the reference's starts as a copy), each falling back to the defaults. Loading defaults - /// keeps the current set, as the window's Reset To Default does. + /// set (the reference's starts as a copy), each falling back to the defaults. The current set + /// stays: the verb only copies the set into the live one (`0x4b8249`-`0x4b8252`), so the + /// window's Cancel (`LoadBindings(GetCurrentBindingSet())`) reloads the set saved last. fn load(&mut self, set: u32) { let list = match set { 0 => &self.defaults, @@ -298,9 +299,6 @@ impl KeybindState { _ => return, }; self.live = list.clone(); - if set != 0 { - self.current_set = set; - } self.generation += 1; } @@ -404,9 +402,13 @@ impl super::UiScript { } } - /// Host-side `LoadBindings` (world entry: character set if it exists, else account). + /// Host-side `LoadBindings` (world entry: character set if it exists, else account), which + /// makes `set` current first, as the sets' read does (`0x4b64cb`). pub fn load_binding_set(&mut self, set: u32) { let mut model = self.model_mut(); + if set != 0 { + model.keybinds.current_set = set; + } model.keybinds.load(set); model.keybinds.signal_owed = true; } @@ -999,6 +1001,34 @@ mod tests { assert_eq!(s.take_keybind_requests(), vec![KeybindRequest::Save(1)]); } + /// Ticking Character Specific loads set 2 (`KeyBindingFrame_ChangeBindingProfile`), and + /// Cancel's `LoadBindings(GetCurrentBindingSet())` puts the account set back: `LoadBindings` + /// never writes the current set (`0x4b8249`-`0x4b8252`), so it is still 1. + #[test] + fn a_cancelled_character_tick_leaves_the_account_set_live() { + let mut s = script(); + s.run(r#"SetBinding("F", "JUMP") SaveBindings(1)"#).unwrap(); + s.run(r#"SetBinding("F", "CAMERAZOOMIN") SaveBindings(2)"#) + .unwrap(); + // World entry on the account set. + s.load_binding_set(1); + assert_eq!(s.current_binding_set(), 1); + s.run("LoadBindings(2)").unwrap(); + assert_eq!( + s.eval::(r#"return GetBindingAction("F")"#).unwrap(), + "CAMERAZOOMIN", + "the tick shows the character set" + ); + assert_eq!(s.eval::("return GetCurrentBindingSet()").unwrap(), 1); + s.run("LoadBindings(GetCurrentBindingSet())").unwrap(); + assert_eq!( + s.eval::(r#"return GetBindingAction("F")"#).unwrap(), + "JUMP", + "Cancel puts the account set back" + ); + assert_eq!(s.current_binding_set(), 1); + } + #[test] fn an_addons_bindings_xml_registers_as_ordinary_rows() { let s = script(); diff --git a/crates/benilla-ui/src/script/minimap.rs b/crates/benilla-ui/src/script/minimap.rs index bc22ccd71..6d8b3abf1 100644 --- a/crates/benilla-ui/src/script/minimap.rs +++ b/crates/benilla-ui/src/script/minimap.rs @@ -393,7 +393,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { let text = model.minimap.active_zoom().to_string(); super::cvars::set_from_engine(&mut model, cvar, text); } - Ok(()) + drop(model); + // `set_zoom`'s `CVar::Set` (`0x6da974`) runs the row's callback inside the call. + super::cvars::written(lua) })?, )?; m.set( diff --git a/crates/benilla-ui/src/script/model.rs b/crates/benilla-ui/src/script/model.rs index 2c22422b0..3df79b30c 100644 --- a/crates/benilla-ui/src/script/model.rs +++ b/crates/benilla-ui/src/script/model.rs @@ -559,7 +559,7 @@ pub(crate) struct Model { pub(crate) current_resolution: Option, /// What this device and presentation path offer, behind `GetVideoCaps`. pub(crate) video_caps: super::cvars::VideoCaps, - /// `RestartGx()` calls, the video window's apply button. + /// The `gxRestart` runs no call took, a bare VM's, for the host's frame drain. pub(crate) restart_gx_asks: u32, /// Globals `RegisterForSave` declared, in order: written at logout and read back at load. @@ -1051,8 +1051,6 @@ pub(crate) struct Model { /// slots 1..=19 as the doll numbers them: the slot getters' other-unit leg (`0x4c8420` → /// `0x5f0d60`). pub(crate) visible_equipment: HashMap, - /// `DressUpModel` `SetUnit`, `Dress`, `Undress` and `TryOn` calls, applied in call order. - pub(crate) dressup_intents: Vec, /// Tabard designs per `TabardModel`, the app's preview five, the host facts and queued calls. pub(crate) tabard_designs: HashMap, pub(crate) tabard_preview: Option<[i32; 5]>, @@ -1707,7 +1705,6 @@ impl Model { inventory_alerts: [0; 12], weapon_enchants: [None; 2], visible_equipment: HashMap::new(), - dressup_intents: Vec::new(), tabard_designs: HashMap::new(), tabard_preview: None, tabard_host: Default::default(), diff --git a/crates/benilla-ui/src/script/pvp/mod.rs b/crates/benilla-ui/src/script/pvp/mod.rs index e59292d5e..129744afa 100644 --- a/crates/benilla-ui/src/script/pvp/mod.rs +++ b/crates/benilla-ui/src/script/pvp/mod.rs @@ -122,6 +122,7 @@ pub enum PvpCall { impl super::UiScript { /// The `TogglePVP` calls a harness's VM kept ([`Self::keep_calls`]). + #[cfg(test)] pub fn take_pvp_toggles(&mut self) -> u32 { self.take_calls_like(&super::ScriptCall::Pvp(PvpCall::Toggle)) as u32 } diff --git a/crates/benilla-ui/src/script/tradeskill/mod.rs b/crates/benilla-ui/src/script/tradeskill/mod.rs index d432e5ff8..3b4d49127 100644 --- a/crates/benilla-ui/src/script/tradeskill/mod.rs +++ b/crates/benilla-ui/src/script/tradeskill/mod.rs @@ -333,6 +333,7 @@ impl super::UiScript { } /// The `CloseTradeSkill` calls a harness's VM kept ([`Self::keep_calls`]). + #[cfg(test)] pub fn take_trade_skill_closes(&mut self) -> usize { self.take_calls_where(|call| match call { super::ScriptCall::TradeSkill(TradeSkillCall::Close) => Some(()), diff --git a/crates/benilla-ui/src/script/unit/mod.rs b/crates/benilla-ui/src/script/unit/mod.rs index e980c76bf..8b46b31b3 100644 --- a/crates/benilla-ui/src/script/unit/mod.rs +++ b/crates/benilla-ui/src/script/unit/mod.rs @@ -8,7 +8,7 @@ use std::borrow::Cow; use mlua::Lua; -use super::{Model, NearestMode, ScriptCall}; +use super::{Model, ScriptCall}; /// A selection ask from Lua, queued in call order for the app to resolve and commit. The reference /// routes `TargetUnit`, `AssistUnit` and `TargetLastEnemy` through one helper (`0x489a40`: commit, @@ -361,6 +361,7 @@ impl super::UiScript { /// The guids `UnitName` read off a unit with no object and found no name for, which the /// reference's lookup asks the server for (`0x5171eb`), as a harness's VM kept them /// ([`Self::keep_calls`]), in call order. + #[cfg(test)] pub fn take_unit_name_asks(&mut self) -> Vec { self.take_calls_where(|call| match call { super::ScriptCall::RecordAsk(super::RecordAsk::PlayerName(guid)) => Some(*guid), @@ -536,7 +537,8 @@ impl super::UiScript { /// Take the `TargetNearest*([reverse])` calls out of the call stream, `true` for reverse. /// They name no unit: the reference runs the TAB cycler (`0x493f60`) straight into /// `SetSelection`. - pub fn take_target_nearest_requests(&mut self) -> Vec<(NearestMode, bool)> { + #[cfg(test)] + pub fn take_target_nearest_requests(&mut self) -> Vec<(super::NearestMode, bool)> { self.take_calls_where(|c| match c { ScriptCall::TargetNearest { mode, reverse, .. } => Some((*mode, *reverse)), _ => None, @@ -544,6 +546,7 @@ impl super::UiScript { } /// Take the `TargetByName(name, exactMatch)` calls out of the call stream. + #[cfg(test)] pub fn take_target_by_name_requests(&mut self) -> Vec<(String, bool)> { self.take_calls_where(|c| match c { ScriptCall::TargetByName { name, exact, .. } => Some((name.clone(), *exact)), diff --git a/crates/benilla-ui/src/script/video_pairs.rs b/crates/benilla-ui/src/script/video_pairs.rs index b0f344631..7cc389af9 100644 --- a/crates/benilla-ui/src/script/video_pairs.rs +++ b/crates/benilla-ui/src/script/video_pairs.rs @@ -178,8 +178,14 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { } Write::TruncOneMinus(name) => (name, ftol_low(1.0 - v).to_string()), }; - let mut model = lua.app_data_mut::().expect("model app_data"); - write_cvar(&mut model, name, text); + write_cvar( + &mut lua.app_data_mut::().expect("model app_data"), + name, + text, + ); + // `CVar::Set` (`0x488f30` and its siblings) runs the row's callback inside the + // call. + super::cvars::written(lua)?; // Zero return values, not nil (`xor eax,eax` at every `ret`). Ok(MultiValue::new()) })?, diff --git a/docs/DEVIATIONS.md b/docs/DEVIATIONS.md index eafaf66ab..7759aee5a 100644 --- a/docs/DEVIATIONS.md +++ b/docs/DEVIATIONS.md @@ -36,7 +36,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it - [benilla-lua](#benilla-lua): 11 - [benilla-protocol](#benilla-protocol): 29 - [benilla-srp](#benilla-srp): 2 -- [benilla-ui](#benilla-ui): 135 +- [benilla-ui](#benilla-ui): 134 - [benilla-world](#benilla-world): 49 ### benilla-app @@ -75,8 +75,8 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it #### bindings - A press with Super (Cmd) held runs no binding, so a system chord such as Cmd+Tab or Cmd+Q never acts in game. ([`src/bindings.rs:19`](../crates/benilla-app/src/bindings.rs#L19)) -- A trackpad's `Pixel` deltas are carried until they make a line, and step at most once per event (`crate::ui_script::Record::Wheel`). ([`src/bindings.rs:522`](../crates/benilla-app/src/bindings.rs#L522)) -- Latches die with the VM that made them, because a latch names a command of that VM's table and releasing it against a new one would run another load's up-half. ([`src/bindings.rs:598`](../crates/benilla-app/src/bindings.rs#L598)) +- A trackpad's `Pixel` deltas are carried until they make a line, and step at most once per event (`crate::ui_script::Record::Wheel`). ([`src/bindings.rs:525`](../crates/benilla-app/src/bindings.rs#L525)) +- Latches die with the VM that made them, because a latch names a command of that VM's table and releasing it against a new one would run another load's up-half. ([`src/bindings.rs:601`](../crates/benilla-app/src/bindings.rs#L601)) - The Windows key table stops at `VK_F12` and drops later F-keys; they are named here so a stored `F13`-`F24` can be pressed. ([`src/bindings/chord.rs:165`](../crates/benilla-app/src/bindings/chord.rs#L165)) - On the Mac and Linux a key its platform reports no character for keeps its US name too, where the Mac reference drops it (`0x8a4aa`), and a Linux dead key not yet pressed bare stays bindable. ([`src/bindings/layout.rs:116`](../crates/benilla-app/src/bindings/layout.rs#L116)) - A character above U+00FF names the key by itself. The reference's `MapVirtualKeyA` returns an ANSI code-page byte, which `0x42da4b` drops past `0xff` and the namer's UTF-8 encoder (`0x41abb0`) otherwise reads as Latin-1, so such a key is mislabelled or dropped; named by what it types, it stays bindable and readable. ([`src/bindings/layout.rs:216`](../crates/benilla-app/src/bindings/layout.rs#L216)) @@ -322,7 +322,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it - A missing global prints nothing, where the reference prints an empty line (`0x703c02`), because benilla's other `DisplayError` routes do the same. ([`src/ui_chat/broadcast.rs:167`](../crates/benilla-app/src/ui_chat/broadcast.rs#L167)) - In a capital with a blank city word the walk composes `"Trade - "` (it tests the row pointer, not the string, `0x49a2e4`-`0x49a2ed`); we decline, because a wrong channel name is joined silently and for good. ([`src/ui_chat/channels.rs:141`](../crates/benilla-app/src/ui_chat/channels.rs#L141)) - The reference holds every line that waits on its `$`-macro subject's name in the name cache's callback list (`0x49daea` → `0x55f080`) until the name answers, with no bound; here at most `MAX_HELD_SUBJECT_LINES` wait, the oldest dropped past that, as a subject no player owns never answers. ([`src/ui_chat/feed.rs:391`](../crates/benilla-app/src/ui_chat/feed.rs#L391)) -- A probe's bare `/assist` assists the live selection, where the stock handler (`ChatFrame.lua:740`) resolves a selected player's name through `AssistByName`, because a name resolve can land on a different player. ([`src/ui_chat/input/mod.rs:595`](../crates/benilla-app/src/ui_chat/input/mod.rs#L595)) +- A probe's bare `/assist` assists the live selection, `AssistByName("")` (`0x489c89`), where the stock handler (`ChatFrame.lua:740`) resolves a selected player's name through `AssistByName`, because a name resolve can land on a different player. ([`src/ui_chat/input/mod.rs:594`](../crates/benilla-app/src/ui_chat/input/mod.rs#L594)) - A line of 256 bytes or more, which reaches here only through `chat_send`'s deviation for `0x49f604`, is slurred whole instead of cut to that buffer, so it reaches the server whole for the refusal that deviation leaves to it. ([`src/ui_chat/input/send.rs:232`](../crates/benilla-app/src/ui_chat/input/send.rs#L232)) - The files live in `benilla-config/Logs/`, not the install's `Logs`, because the install is read-only. ([`src/ui_chat/logging.rs:4`](../crates/benilla-app/src/ui_chat/logging.rs#L4)) - No re-run when a remote player streams in, because the reference's re-run only repeats the join, which vmangos swallows, or the leave, whose "Not on channel" notice nothing prints. ([`src/ui_chat/recruitment.rs:7`](../crates/benilla-app/src/ui_chat/recruitment.rs#L7)) @@ -331,7 +331,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it #### ui_dressup -- The base is the player's melee pair, held drawn, because the booth shares the select screen's assembly; the reference clones the live model with its attachments, drawn or sheathed (`0x5059a0`, `0x70ea00`). ([`src/ui_dressup.rs:43`](../crates/benilla-app/src/ui_dressup.rs#L43)) +- The base is the player's melee pair, held drawn, because the booth shares the select screen's assembly; the reference clones the live model with its attachments, drawn or sheathed (`0x5059a0`, `0x70ea00`). ([`src/ui_dressup.rs:44`](../crates/benilla-app/src/ui_dressup.rs#L44)) #### ui_duel @@ -416,7 +416,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it #### ui_models -- The reference's paint (`0x76d240`) reads the eye before it rebuilds the root, so a facing change draws one frame late; not reproduced, because it is an ordering quirk. ([`src/ui_models/mod.rs:1467`](../crates/benilla-app/src/ui_models/mod.rs#L1467)) +- The reference's paint (`0x76d240`) reads the eye before it rebuilds the root, so a facing change draws one frame late; not reproduced, because it is an ordering quirk. ([`src/ui_models/mod.rs:1468`](../crates/benilla-app/src/ui_models/mod.rs#L1468)) #### ui_pet @@ -502,7 +502,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it - `gxWindow "0"` is borderless fullscreen, not 1.12's exclusive mode-set at `gxResolution`, because Wayland has no client mode-setting (winit ignores `Fullscreen::Exclusive` there), and X11's XRandR changes the desktop's mode and a crash leaves it changed. ([`src/video.rs:5`](../crates/benilla-app/src/video.rs#L5)) - On Windows, NVIDIA's Vulkan driver (610.88 seen) crashes inside `vkQueuePresentKHR` on the first present after any swapchain resize while the present mode is `Fifo`, `FifoRelaxed` or `AutoVsync` (a maximize is a resize). ([`src/video.rs:17`](../crates/benilla-app/src/video.rs#L17)) -- Sizes are logical px, because the windowed `gxResolution` is a logical inner size here. ([`src/video.rs:606`](../crates/benilla-app/src/video.rs#L606)) +- Sizes are logical px, because the windowed `gxResolution` is a logical inner size here. ([`src/video.rs:624`](../crates/benilla-app/src/video.rs#L624)) #### vplates @@ -742,7 +742,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it - `pow(10, n)` is the platform's, where the reference's (`0x73f940`, `fyl2x`) rounds its extended-precision steps at 53 bits, so a power or a fraction term can differ in the last place. ([`src/script/binding_abi.rs:281`](../crates/benilla-ui/src/script/binding_abi.rs#L281)) - The disabled-checked leg (`0x785380`) tests and reloads the checked slot (`[+0x4e0]`, `0x785388`/`0x7853a5`), a copy of the checked leg's (`0x785270`), so in 1.12.1 a check box with a checked texture repaints that one and never gets its disabled-checked texture from a path. benilla tests and reloads the slot the method names. ([`src/script/button.rs:621`](../crates/benilla-ui/src/script/button.rs#L621)) - The slot is read before the handler, where the reference reads it after (`[esi+0x4dc]`), because reading first keeps the borrow simple and no stock handler re-slots its row. ([`src/script/button.rs:1209`](../crates/benilla-ui/src/script/button.rs#L1209)) -- The reference's only bound is `luaD_call`'s count of 200 nested calls (`0x6f65ae`), past which it raises `C stack overflow` (`0x6f65bf`); eight here, as each level keeps an apply's frame on the Rust stack. ([`src/script/calls.rs:698`](../crates/benilla-ui/src/script/calls.rs#L698)) +- The reference's only bound is `luaD_call`'s count of 200 nested calls (`0x6f65ae`), past which it raises `C stack overflow` (`0x6f65bf`); eight here, as each level keeps an apply's frame on the Rust stack. ([`src/script/calls.rs:707`](../crates/benilla-ui/src/script/calls.rs#L707)) - The reference composes the Shortcut with `GetRealZoneText` alone (`0x49efe8`), so in a capital `/leave Trade` sends `Trade - Stormwind City` and leaves nothing; ours uses the join's city-aware name, because the reference's form cannot leave the channel it joined. ([`src/script/channel.rs:516`](../crates/benilla-ui/src/script/channel.rs#L516)) - The frame's bottom-left origin is snapped onto the device grid so the border art blits 1:1; the reference ceils the anchor point itself onto it (`0x7666a0`, `0x766750`). ([`src/script/chat_bubble.rs:482`](../crates/benilla-ui/src/script/chat_bubble.rs#L482)) - No truncation at 255 characters (`0x49f604`), because vmangos refuses a longer line itself (`Chat.cpp:2177`): it is dropped where the reference sends its first 255. ([`src/script/chat_send.rs:26`](../crates/benilla-ui/src/script/chat_send.rs#L26)) @@ -765,13 +765,12 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it - The list is the window sizes this client can present, not the device's display modes, because benilla makes no exclusive mode-set. `"WxH"` is a contract: `OptionsFrame.lua:281-284` splits on the `x`, raising on a string without one, and `CT_Viewport.lua:107` matches `"(%d+)x(%d+)"`, failing silently. ([`src/script/cvars.rs:81`](../crates/benilla-ui/src/script/cvars.rs#L81)) - The tie-break is ours, because area alone with the reference's adjacent-only dedupe can leave a duplicate `"WxH"` when two modes share an area (1280x960, 1600x768). ([`src/script/cvars.rs:94`](../crates/benilla-ui/src/script/cvars.rs#L94)) - An id out of range is ignored, because the reference reads one entry past the last (`0x48c67c`) and a clamp would apply a format the player did not pick. ([`src/script/cvars.rs:779`](../crates/benilla-ui/src/script/cvars.rs#L779)) -- The reference then runs the console command `gxRestart` (`0x842978` through `0x63ce00`); none is queued here, because `gxMultisample` applies at next launch anyway: the camera reads its sample count once, at spawn. ([`src/script/cvars.rs:793`](../crates/benilla-ui/src/script/cvars.rs#L793)) -- The reference (`0x48dad0` into `0x639a20`) writes hardware-recommended values from its matched `VideoHardware.dbc` row and the `CGxFormat` preset it names (`[0xc4e6ac]`): twelve `gx*` CVars, a synchronous `RestartGx`, then sixteen more. benilla has no such matching, so its registered defaults are its recommended configuration. ([`src/script/cvars.rs:855`](../crates/benilla-ui/src/script/cvars.rs#L855)) -- The index is of the live window size, not of the `gxResolution` CVar the reference parses (`0x48bf20`), because the window here can be dragged to any size, where the reference's backbuffer stays at `gxResolution`'s, and `CT_Viewport` scales by the live one. ([`src/script/cvars.rs:908`](../crates/benilla-ui/src/script/cvars.rs#L908)) -- 0, a negative or an index past the count clamps to the nearer end, because the reference reads one element past the last there (`0x48c00d`), uninitialised memory. ([`src/script/cvars.rs:926`](../crates/benilla-ui/src/script/cvars.rs#L926)) -- Always empty, because a refresh rate is only selectable through an exclusive mode-set and benilla has none. ([`src/script/cvars.rs:967`](../crates/benilla-ui/src/script/cvars.rs#L967)) -- 1.12 has no such CVar; it exists because our Graphics page needs somewhere to keep the stop, and takes the API's name. ([`src/script/cvars.rs:1086`](../crates/benilla-ui/src/script/cvars.rs#L1086)) -- The getter reads `CVAR_WORLD_DETAIL`, where the reference's reads only `smallCull` (`0x488d77`), because benilla's stop follows the knob that draws: a console `frillDensity` write moves the ground clutter and `WorldDetail` with it, and no renderer reads `smallCull` (its callback's `[0x868620]` has no reader). ([`src/script/cvars.rs:1114`](../crates/benilla-ui/src/script/cvars.rs#L1114)) +- The reference (`0x48dad0` into `0x639a20`) writes hardware-recommended values from its matched `VideoHardware.dbc` row and the `CGxFormat` preset it names (`[0xc4e6ac]`): twelve `gx*` CVars, a synchronous `RestartGx`, then sixteen more. benilla has no such matching, so its registered defaults are its recommended configuration. ([`src/script/cvars.rs:895`](../crates/benilla-ui/src/script/cvars.rs#L895)) +- The index is of the live window size, not of the `gxResolution` CVar the reference parses (`0x48bf20`), because the window here can be dragged to any size, where the reference's backbuffer stays at `gxResolution`'s, and `CT_Viewport` scales by the live one. ([`src/script/cvars.rs:948`](../crates/benilla-ui/src/script/cvars.rs#L948)) +- 0, a negative or an index past the count clamps to the nearer end, because the reference reads one element past the last there (`0x48c00d`), uninitialised memory. ([`src/script/cvars.rs:966`](../crates/benilla-ui/src/script/cvars.rs#L966)) +- Always empty, because a refresh rate is only selectable through an exclusive mode-set and benilla has none. ([`src/script/cvars.rs:1010`](../crates/benilla-ui/src/script/cvars.rs#L1010)) +- 1.12 has no such CVar; it exists because our Graphics page needs somewhere to keep the stop, and takes the API's name. ([`src/script/cvars.rs:1118`](../crates/benilla-ui/src/script/cvars.rs#L1118)) +- The getter reads `CVAR_WORLD_DETAIL`, where the reference's reads only `smallCull` (`0x488d77`), because benilla's stop follows the knob that draws: a console `frillDensity` write moves the ground clutter and `WorldDetail` with it, and no renderer reads `smallCull` (its callback's `[0x868620]` has no reader). ([`src/script/cvars.rs:1146`](../crates/benilla-ui/src/script/cvars.rs#L1146)) - The promotion's IME legs (`0x77d417`–`0x77d431`) and the IME writer's `or 6` (`0x77acde`, in `0x77ac60`) are not modelled: benilla has no IME composition. ([`src/script/editbox/flush.rs:28`](../crates/benilla-ui/src/script/editbox/flush.rs#L28)) - The nameplate glow is shown but never drawn, because its additive rim reads as hard edge lines in our linear-blending pipeline; the lit plate brightens its bar instead. ([`src/script/extract.rs:223`](../crates/benilla-ui/src/script/extract.rs#L223)) - A host of 0x80 bytes or more is refused. The reference copies the host into a 0x80-byte buffer (`0x5abc7c`-`0x5abc9c`) and writes its terminator one byte past the end, and the cut host could end in a whitelisted suffix the real one does not; this takes the safe side. ([`src/script/glue_core.rs:424`](../crates/benilla-ui/src/script/glue_core.rs#L424)) @@ -782,10 +781,10 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it - The backpack, the bank and the keyring are each bounded by their own slots, where `0x4f990f` bounds them only by the player list's count (0x71) from their bases (the bank at `+0x27`, `0x4f98ae`; the keyring at `+0x51`, `0x4f98c0`), a reference bug that lets their index read the next container's slots. ([`src/script/held.rs:74`](../crates/benilla-ui/src/script/held.rs#L74)) - A span count that no longer matches the live set always rebuilds, so a state this model does not flag (a ring wiped by `SetMaxLines`, a hidden frame, a frame not yet laid out, which the reference's rebuild skips at `0x78af7b`) leaves no stale button. ([`src/script/hyperlink.rs:167`](../crates/benilla-ui/src/script/hyperlink.rs#L167)) - `IsInInstance()` then answers `nil, "none"`, because the reference returns two results without pushing either (`0x48a7a9`), a stack bug. ([`src/script/instance.rs:24`](../crates/benilla-ui/src/script/instance.rs#L24)) -- Bounds-checked where the reference indexes unguarded, so none reads past it. ([`src/script/item_stats.rs:588`](../crates/benilla-ui/src/script/item_stats.rs#L588)) -- Nil for a display row with no icon, where the reference answers the unloadable `Interface\Icons\`, on which `SetTexture` keeps the previous image (`0x7702ac`), so a reused button would show its old icon. ([`src/script/item_stats.rs:740`](../crates/benilla-ui/src/script/item_stats.rs#L740)) +- Bounds-checked where the reference indexes unguarded, so none reads past it. ([`src/script/item_stats.rs:578`](../crates/benilla-ui/src/script/item_stats.rs#L578)) +- Nil for a display row with no icon, where the reference answers the unloadable `Interface\Icons\`, on which `SetTexture` keeps the previous image (`0x7702ac`), so a reused button would show its old icon. ([`src/script/item_stats.rs:730`](../crates/benilla-ui/src/script/item_stats.rs#L730)) - A key rebound to the command it already has keeps a duplicate index in 1.12.1 (`0x4b7786` lowers the other keys' indices before `0x4b77ac` returns on the same name), so both getters there answer one key too few; that client fault is not reproduced. ([`src/script/keybind.rs:223`](../crates/benilla-ui/src/script/keybind.rs#L223)) -- An addon file whose first binding has no header opens a `HEADER_` row, where the reference files those rows under the previous file's last header, because under a foreign header the player could not find them. ([`src/script/keybind.rs:326`](../crates/benilla-ui/src/script/keybind.rs#L326)) +- An addon file whose first binding has no header opens a `HEADER_` row, where the reference files those rows under the previous file's last header, because under a foreign header the player could not find them. ([`src/script/keybind.rs:324`](../crates/benilla-ui/src/script/keybind.rs#L324)) - The handle's value is the address of a `CGxTex` (`SMemAlloc(0x54)`, `0x594a96`), which benilla cannot reproduce; each texture ranks instead by a creation stamp, one global sequence number taken when its cache key is first created and taken anew once its last region has let go of it (`super::texel::Texels`). ([`src/script/layer_batch.rs:9`](../crates/benilla-ui/src/script/layer_batch.rs#L9)) - The reference's byte copy can end inside a character and keep the stray bytes; a `String` must stay UTF-8, so the cut stops at the character boundary. ([`src/script/macros.rs:135`](../crates/benilla-ui/src/script/macros.rs#L135)) - A listing that finds no icon stays empty, to be listed again on the next ask, where the reference's dedup then reads below its null list (`0x4f0134`-`0x4f0145`) and faults. ([`src/script/macros.rs:555`](../crates/benilla-ui/src/script/macros.rs#L555)) @@ -798,9 +797,9 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it - The reference adds this rim over the bar; we brighten the bar (`LIT_BOOST`), because the rim reads as hard edge lines in our linear-blending pipeline. ([`src/script/nameplate.rs:991`](../crates/benilla-ui/src/script/nameplate.rs#L991)) - A wrapper that decodes to no live widget takes `default_parent`, because the reference, with no `IsA` check, reads a wild pointer. ([`src/script/object/anchor_args.rs:218`](../crates/benilla-ui/src/script/object/anchor_args.rs#L218)) - An `OnSizeChanged` that resolve produces fires at the next `UiScript::resolve`, because a Lua handler must not run in the middle of a level compaction. ([`src/script/object/toplevel.rs:77`](../crates/benilla-ui/src/script/object/toplevel.rs#L77)) -- A third `%s` gets an empty string, because the reference pushes two varargs (`0x6093f2 add esp,0x14`) and would read uninitialised stack. ([`src/script/pvp/mod.rs:318`](../crates/benilla-ui/src/script/pvp/mod.rs#L318)) -- A token resolving to guid 0 answers nil too. The reference jumps from the guid test (`0x5172f6`) to `return 1` (`0x517343`) with nothing pushed, so the one value is its top argument, the token it read (`0x5172e6`); that is a stack quirk, not an answer. ([`src/script/pvp/mod.rs:567`](../crates/benilla-ui/src/script/pvp/mod.rs#L567)) -- With `UNIT_PVP_NAME` or the title missing from `_G` (no install) this answers the plain name, where the reference would format an empty string, because the decoration is install data. ([`src/script/pvp/mod.rs:571`](../crates/benilla-ui/src/script/pvp/mod.rs#L571)) +- A third `%s` gets an empty string, because the reference pushes two varargs (`0x6093f2 add esp,0x14`) and would read uninitialised stack. ([`src/script/pvp/mod.rs:319`](../crates/benilla-ui/src/script/pvp/mod.rs#L319)) +- A token resolving to guid 0 answers nil too. The reference jumps from the guid test (`0x5172f6`) to `return 1` (`0x517343`) with nothing pushed, so the one value is its top argument, the token it read (`0x5172e6`); that is a stack quirk, not an answer. ([`src/script/pvp/mod.rs:568`](../crates/benilla-ui/src/script/pvp/mod.rs#L568)) +- With `UNIT_PVP_NAME` or the title missing from `_G` (no install) this answers the plain name, where the reference would format an empty string, because the decoration is install data. ([`src/script/pvp/mod.rs:572`](../crates/benilla-ui/src/script/pvp/mod.rs#L572)) - An in-flight name reads `...`, where the reference's reads `" "` (`0x82ee00`), because a blank looks like a nameless objective. ([`src/script/quest_log.rs:54`](../crates/benilla-ui/src/script/quest_log.rs#L54)) - One signal per missed record, where the reference runs every callback of every miss as its record lands (items at `0x55bea8` and `0x55bf39` in `0x55bdb0`, from `0x555140`; `0x4e083a`), 2^k - 1 updates for a walking handler over k first-seen records. ([`src/script/quest_log.rs:1212`](../crates/benilla-ui/src/script/quest_log.rs#L1212)) - A local clock step mid-session (an NTP correction) jumps the reference's countdown but not ours, because the step changes no time the quest has left. ([`src/script/quest_log.rs:1282`](../crates/benilla-ui/src/script/quest_log.rs#L1282))