diff --git a/crates/benilla-app/src/bindings.rs b/crates/benilla-app/src/bindings.rs index b61d58d8..2dc2963c 100644 --- a/crates/benilla-app/src/bindings.rs +++ b/crates/benilla-app/src/bindings.rs @@ -1,19 +1,19 @@ -//! Key bindings: the one chord-to-command engine every rebindable input runs through. The table -//! lives engine-side ([`benilla_ui::script::keybind`], which the Key Bindings window's Lua edits); -//! this module derives an exact-match dispatch map from it whenever its generation moves. +//! Key bindings: the one chord-to-command engine every rebindable input runs through. The key +//! table and the commands live engine-side ([`benilla_ui::script::keybind`]): the commands are the +//! `Bindings.xml` rows the core and each addon loaded, the live keys what `SetBinding` and the Key +//! Bindings window write. This module derives an exact-match dispatch map from the keys whenever +//! the table's generation moves, and runs a pressed chord's command. //! -//! Dispatch ([`latch_and_dispatch`], in [`crate::ui_script::UiInput`] after the UI key feed): +//! Dispatch ([`latch_and_dispatch`], in [`crate::ui_script::UiInput`] after the UI key feed), as +//! `CBindings::ExecuteBinding` (`0x4b7990`) and `RunCommand` (`0x4b7b50`) do: //! - a press probes its exact chord, then once more with its leftmost modifier dropped //! ([`Chord::fallback`]); Super held matches nothing; -//! - [`Kind::Held`] commands latch on the press and unlatch on the base key's release (the -//! reference's `runOnUp` law). UI focus suppresses presses and releases nothing already held; -//! a latch ends only on its base key's release, the stuck-latch sweep (OS focus loss, the +//! - the resolved command's body runs with `keystate = "down"`, and the base key's release runs a +//! `runOnUp` body again with `"up"`. UI focus suppresses presses and releases nothing already +//! held; a latch ends only on its base key's release, the stuck-latch sweep (OS focus loss, the //! loading cover) or a VM swap; -//! - [`Kind::Edge`]/[`Kind::EdgeUpDown`] run their 1.12 Lua bodies in the VM; -//! - [`Kind::Host`] lands in [`BindingsState::fired`] for engine consumers. -//! -//! An addon's `Bindings.xml` rows ([`benilla_ui::bindings_xml`]) dispatch here too: a resolved -//! chord names a [`Bound`], either a registry [`Cmd`] or an index into the addon table. +//! - what the bodies call reaches the engine through the Lua binding functions +//! ([`benilla_ui::script::BindingInput`]) as [`BindingsState`]'s [`Input`]s. //! //! Persistence: `benilla-config/bindings/account.txt` and `-.txt` ([`store`]); the //! character file's existence is the character-set state, as in the reference. @@ -23,39 +23,24 @@ use bevy::input::mouse::{AccumulatedMouseScroll, MouseScrollUnit}; use bevy::input::ButtonState; use bevy::prelude::*; -use benilla_ui::script::keybind::{AddonBindingBody, KeybindCommand, KeybindRequest}; +use benilla_ui::script::keybind::KeybindRequest; use benilla_ui::script::UiScript; use crate::char_select::InWorldGated; use crate::ui_script::{PlayerUiHover, PointerOverUiPanel, UiKeyboardCapture}; pub(crate) mod chord; -pub(crate) mod commands; mod script_input; mod store; use chord::{BindKey, Chord}; -pub(crate) use commands::cmd; -use commands::{Cmd, Kind, SPECS}; +pub(crate) use script_input::Input; -/// What a bound chord names: a registry command or an addon's `Bindings.xml` body. An enum, not -/// one index space, so no `SPECS[cmd]` read can be handed an addon index. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum Bound { - /// A registry command, an index into [`SPECS`]. - Spec(Cmd), - /// An addon-declared binding, an index into [`BindingDispatch::addons`]. - Addon(u16), -} - -/// The chord-to-binding map, rebuilt whenever the engine table's generation moves. Probe it +/// The chord-to-command map, rebuilt whenever the engine table's generation moves. Probe it /// through [`BindingDispatch::resolve`]: the lookup is two probes. #[derive(Resource, Default)] struct BindingDispatch { - map: std::collections::HashMap, - /// The addon bindings [`Bound::Addon`] indexes, in registration order; snapshotted with the - /// map so the two agree on what an index means. - addons: Vec, + map: std::collections::HashMap, /// The engine table's generation this map was built from, keyed per VM: a fresh VM restarts /// its counter at 0. seen_generation: crate::ui_script::VmMemo>, @@ -69,33 +54,33 @@ impl BindingDispatch { /// `Ctrl`+`Shift` is the dev overlays' plane ([`benilla_world::modkeys::DEV_CHORD`]) and /// `Ctrl`+`Shift`+`P` would otherwise fall back to `SHIFT-P` (`TOGGLECHARACTER3`). An exact /// `CTRL-SHIFT-` binding still dispatches; mouse and wheel are never suppressed. - fn resolve(&self, chord: Chord, dev_plane: bool) -> Option { - if let Some(&bound) = self.map.get(&chord) { - return Some(bound); + fn resolve(&self, chord: Chord, dev_plane: bool) -> Option<&str> { + if let Some(command) = self.map.get(&chord) { + return Some(command); } if dev_plane { return None; } - self.map.get(&chord.fallback()?).copied() + self.map.get(&chord.fallback()?).map(String::as_str) } } -/// This frame's binding activity, which engine-side consumers read instead of raw keys. +/// This frame's binding activity, which engine-side consumers read instead of raw keys: the +/// [`Input`]s the running bodies' Lua calls set. #[derive(Resource, Default)] pub(crate) struct BindingsState { - /// Live latches, (base key, binding): a [`Kind::Held`], [`Kind::EdgeUpDown`] or `runOnUp` - /// addon press not yet released. - latched: Vec<(BindKey, Bound)>, - /// The held commands a Lua Start set and no Stop has cleared ([`script_input`]); no key backs - /// them, so no key's release ends them but their own command's. - script_held: Vec, - /// Commands whose first latch began this frame (the press edge). - just: Vec, - /// Host-edge commands fired this frame. - fired: Vec, - /// Analog amount per host command this frame (wheel notches; a key press adds the - /// reference's 1.0 step), the camera zoom's input. - amounts: Vec<(Cmd, f32)>, + /// Live latches, (base key, command): a press not yet released, whose release runs the + /// command's `runOnUp` half. The reference replays the press-time chord at key-up (`0x483bd0`); + /// latching the resolved command is equivalent. + latched: Vec<(BindKey, String)>, + /// The held bits a Start set and no Stop has cleared. + held: Vec, + /// Inputs whose Start, or whose one-shot, came this frame (the press edge). + just: Vec, + /// One-shots fired this frame. + fired: Vec, + /// Amount per one-shot this frame: a zoom's yards, else 1.0. + amounts: Vec<(Input, f32)>, /// The keys we believe are down: a press of one is a repeat. The reference classifies /// auto-repeat off its own list (`0x4248b3`), never the Win32 `lParam` repeat bit. /// Normalized like [`latched`](Self::latched) and reconciled against `ButtonInput` every @@ -105,34 +90,36 @@ pub(crate) struct BindingsState { } impl BindingsState { - /// Is a registry command latched right now (the reference's held movement bit), by a key or - /// by a Lua Start? - pub(crate) fn pressed(&self, c: Cmd) -> bool { - self.latched.iter().any(|&(_, l)| l == Bound::Spec(c)) || self.script_held.contains(&c) + /// Is a held bit set right now (the reference's `[InputControl+4]`)? + pub(crate) fn pressed(&self, i: Input) -> bool { + self.held.contains(&i) } - /// Did this command's latch begin this frame (the key-down edge)? - pub(crate) fn just_pressed(&self, c: Cmd) -> bool { - self.just.contains(&c) + /// Did this bit's Start, or this one-shot, come this frame (the key-down edge)? + pub(crate) fn just_pressed(&self, i: Input) -> bool { + self.just.contains(&i) } - /// Did this host-edge command fire this frame? - pub(crate) fn fired(&self, c: Cmd) -> bool { - self.fired.contains(&c) + /// Did this one-shot fire this frame? + pub(crate) fn fired(&self, i: Input) -> bool { + self.fired.contains(&i) } - /// Total analog amount for a host command this frame (0.0 when idle). - pub(crate) fn amount(&self, c: Cmd) -> f32 { + /// Total amount for a one-shot this frame (0.0 when idle): the camera zoom's yards. + pub(crate) fn amount(&self, i: Input) -> f32 { self.amounts .iter() - .filter(|&&(a, _)| a == c) + .filter(|&&(a, _)| a == i) .map(|&(_, v)| v) .sum() } - /// Test seam for consumer systems: a state in which these host commands fired this frame. - #[cfg(test)] - pub(crate) fn test_fired(cmds: &[Cmd]) -> Self { - Self { - fired: cmds.to_vec(), - ..Default::default() - } + /// Both mouse bits held by their Lua Starts, the both-button run: MOVEANDSTEER's body + /// (`Bindings.xml:3-11`) runs `CameraOrSelectOrMoveStart` and `TurnOrActionStart`. + pub(crate) fn steering(&self) -> bool { + self.pressed(Input::TurnOrAction) && self.pressed(Input::CameraOrSelectOrMove) + } + /// [`Self::steering`] began this frame. + pub(crate) fn steering_began(&self) -> bool { + self.steering() + && (self.just_pressed(Input::TurnOrAction) + || self.just_pressed(Input::CameraOrSelectOrMove)) } } @@ -159,58 +146,38 @@ impl Plugin for BindingsPlugin { .add_systems( Update, ( - // The registry and both sets are seeded at the VM's birth, not here: - // `seed_bindings_for_vm` runs in `load_ingame_ui_on_world_entry`, before - // FrameXML and every addon. + // The key sets are seeded at the VM's birth, not here: `seed_bindings_for_vm` + // runs in `load_ingame_ui_on_world_entry`, before FrameXML and every addon; + // the commands come with the load. (sync_dispatch, latch_and_dispatch) .chain() .in_set(crate::ui_script::UiInput) .in_set(BindingSet) .in_set(InWorldGated), - // After the tick: the requests are Lua's own (`SaveBindings`, `RunBinding`), - // queued by handlers the tick dispatched, and a save must not wait a frame. + // After the tick: the requests are Lua's own (`SaveBindings`), queued by + // handlers the tick dispatched, and a save must not wait a frame. drain_binding_requests.after(crate::ui_script::UiInput), ), ); } } -/// The registry as the engine table's registration payload, for the boot seed and the capture -/// fixtures. -pub(crate) fn registry_commands() -> Vec { - SPECS - .iter() - .map(|s| KeybindCommand { - name: s.name, - category: s.category, - run_on_up: s.run_on_up(), - default1: s.d1, - default2: s.d2, - }) - .collect() -} - -/// Seed the keybinding table at the VM's birth, before any interface file runs: the command -/// registry, the account set, and the character's set if it has one. +/// Seed the key sets at the VM's birth, before any interface file runs: the defaults off the +/// player's chain (`WTF\DefaultBindings.wtf`), the account set, and the character's set if it has +/// one. The keys are their own table, so a command a later file declares finds its keys there, +/// and stock `ActionButton_OnLoad` paints its hotkey corner from `GetBindingKey` at load. /// -/// Order matters, and is the reference's: stock `ActionButton_OnLoad` paints its hotkey corner -/// from `GetBindingKey` at load, an addon's `SetBinding` on a stock command needs the command -/// registered, and the registry must hold the low indices the Key Bindings window walks. -/// `seed_binding_set` keeps the diff by name, so a stored addon row binds when the addon -/// registers it later. +/// The reference reads the defaults later in `UI_Init` (`0x4b62b0` at `0x4900c7`); here they are +/// read first because the stored sets are diffs against them ([`store`]'s deviation), and nothing +/// reads set 0 before `LoadBindings(0)`. pub(crate) fn seed_bindings_for_vm(world: &mut World, script: &mut UiScript) { - script.register_bindings(®istry_commands()); + let defaults = crate::ui_script::default_bindings(); + script.set_default_bindings(defaults); + let defaults = script.default_bindings(); let account = crate::local_state::bindings_account_path(); let overrides = read_diff(&account).unwrap_or_default(); - script.seed_binding_set(1, Some(store::resolve(&overrides))); + script.seed_binding_set(1, Some(store::resolve(&overrides, &defaults))); script.load_binding_set(1); - // `SPECS` and `ABSENT` together are the client's whole 1.12 command surface. - info!( - "bindings: {} of {} 1.12 commands registered ({} recorded absent)", - SPECS.len(), - SPECS.len() + commands::ABSENT.len(), - commands::ABSENT.len() - ); // The character's own set: its file existing makes it the active set, the reference's rule. // No roster identity (a rigged or capture run) leaves set 2 unseeded. @@ -222,7 +189,7 @@ pub(crate) fn seed_bindings_for_vm(world: &mut World, script: &mut UiScript) { .and_then(|(realm, name)| crate::local_state::bindings_character_path(realm, name)); match read_diff(&character) { Some(overrides) => { - script.seed_binding_set(2, Some(store::resolve(&overrides))); + script.seed_binding_set(2, Some(store::resolve(&overrides, &defaults))); script.load_binding_set(2); info!("bindings: character-specific set loaded"); } @@ -250,34 +217,13 @@ fn read_diff(path: &Option) -> Option>, files: Res) { let Some(mut script) = script else { return }; for req in script.take_keybind_requests() { - // Only the Lua-bodied kinds can run this way: a `Held`/`Host` action is engine state a Lua - // call cannot assert. The stock callers run `SCREENSHOT` and `TOGGLEWORLDMAP`, both - // `Kind::Edge`. - let which = match req { - KeybindRequest::Save(which) => which, - KeybindRequest::Run(name) => { - match SPECS.iter().find(|s| s.name == name).map(|s| &s.kind) { - Some(Kind::Edge(lua)) | Some(Kind::EdgeUpDown(lua, _)) => { - let lua = *lua; - if let Err(e) = script.run(lua) { - warn!("bindings(RunBinding {name}): {e}"); - } - } - Some(_) => warn!("RunBinding({name}): a held/engine action has no body to run"), - None => warn!("RunBinding({name}): no such command"), - } - continue; - } - }; - let snapshot = script.keybind_snapshot(); - let text = store::to_diff(&snapshot); + let KeybindRequest::Save(which) = req; + let text = store::to_diff(&script.keybind_snapshot(), &script.default_bindings()); let path = match which { 1 => &files.account, 2 => &files.character, @@ -300,8 +246,9 @@ fn drain_binding_requests(script: Option>, files: Res>, mut dispatch: ResMut) { let Some(mut script) = script else { return }; let generation = script.keybinds_generation(); @@ -310,43 +257,14 @@ fn sync_dispatch(script: Option>, mut dispatch: ResMut = SPECS - .iter() - .enumerate() - .map(|(i, s)| (s.name, Bound::Spec(Cmd(i as u16)))) - .collect(); - for (i, a) in addons.iter().enumerate() { - // A registry name is never overwritten. `try_from`, because a wrapped index would - // dispatch the wrong body rather than none. - let Ok(i) = u16::try_from(i) else { break }; - by_name.entry(a.name.as_str()).or_insert(Bound::Addon(i)); - } - for (name, keys) in script.keybind_snapshot() { - let Some(&bound) = by_name.get(name.as_str()) else { - // A name with no home: an uninstalled addon's row (kept on purpose) or a 1.12 - // command benilla does not implement, which `ABSENT` explains in the log. - if let Some(absent) = commands::ABSENT.iter().find(|a| a.name == name) { - if !keys.is_empty() { - warn!( - "bindings: {name} is bound to {keys:?} but benilla does not implement it \u{2014} {}", - absent.why - ); - } - } - continue; - }; - for key in keys { - match Chord::parse(&key) { - Some(ch) => { - dispatch.map.insert(ch, bound); - } - None => warn!("bindings: {name}: unpressable chord '{key}' (unknown token)"), + for (key, command) in script.binding_keys() { + match Chord::parse(&key) { + Some(ch) => { + dispatch.map.insert(ch, command); } + None => warn!("bindings: {command}: unpressable chord '{key}' (unknown token)"), } } - dispatch.addons = addons; script.fire_event("UPDATE_BINDINGS", vec![]); } @@ -400,6 +318,7 @@ fn latch_and_dispatch( dispatch: Res, mut state: ResMut, mut same_vm: Local>, + mut notches: Local, mut focus_lost: MessageReader, cover: Option>, ) { @@ -407,21 +326,26 @@ fn latch_and_dispatch( state.fired.clear(); state.amounts.clear(); - // Deviation: latches die with the VM that made them, because a latch indexes that VM's - // dispatch table and releasing it against a new one would run the wrong addon's up-half. The + // Deviation: 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. The // reference keeps a held key running through `ReloadUI`; here it re-latches on its next press. - // A Lua Start dies with it too, as a key would. + // The held bits die with it, as the key's Stop would have cleared them. if let Some(script) = script.as_ref() { if same_vm.claim(script) { state.latched.clear(); - state.script_held.clear(); + state.held.clear(); } } - // The reference's two bulk clears reach a Lua Start as they reach a held key: the window - // deactivate (`0x514490`, direction bits only; autorun is `Player`'s) and the world enter - // behind the loading cover (`0x5144c0`). A key's latch meets them in the stuck-latch sweep. - if focus_lost.read().count() > 0 || cover.is_some_and(|c| c.covering()) { - state.script_held.clear(); + // The reference's two bulk clears: the window deactivate (`0x514490`'s `and eax,0xfffff00f`, + // the direction bits only, so the mouse pair and autorun survive) and the world enter behind + // the loading cover (`0x5144c0`), which clears every bit. A key's latch meets them in the + // stuck-latch sweep too, whose up-half runs the Stop. + if cover.is_some_and(|c| c.covering()) { + state.held.clear(); + } else if focus_lost.read().count() > 0 { + state + .held + .retain(|&i| matches!(i, Input::TurnOrAction | Input::CameraOrSelectOrMove)); } // Lua's binding calls made since the last pass, before this frame's keys. if let Some(s) = script.as_mut() { @@ -438,15 +362,6 @@ fn latch_and_dispatch( // affordances: a player build keeps the reference's `CTRL-SHIFT-P` fallback to `SHIFT-P`. let dev_plane = ctrl && shift && !alt && crate::run_mode::dev_affordances(); - // A binding body runs as `ExecuteBinding` runs it, so the movement functions' gate passes. - let run_lua = |script: &mut Option>, lua: &str, tag: &str| { - if let Some(s) = script.as_mut() { - if let Err(e) = s.run_binding(lua) { - warn!("bindings({tag}): {e}"); - } - } - }; - // ── Who owns this frame's keys ── a focused EditBox eats every key while it holds focus; a // shown keyboard frame ate the keys in `capture.consumed`. Both suppress a press and nothing // else: in the reference a focused box makes the movement handlers no-ops, so the direction @@ -461,8 +376,8 @@ fn latch_and_dispatch( .down .retain(|&kc| physically_down(BindKey::Key(kc), &keys, &buttons)); - // ── Keyboard ── press edges latch or fire, gated on ownership and the capture arm; release - // edges unlatch and fire the `runOnUp` up-half. + // ── Keyboard ── press edges run their command, gated on ownership and the capture arm; + // release edges unlatch and run the `runOnUp` up-half. let typing = capture.typing; for ev in keyboard.read() { let key = chord::normalize_key(ev.key_code); @@ -490,7 +405,7 @@ fn latch_and_dispatch( if (typing && !arrow_exempt) || eaten || sup || repeat { continue; } - if state.latched.iter().any(|&(k, _)| k == BindKey::Key(key)) { + if state.latched.iter().any(|(k, _)| *k == BindKey::Key(key)) { continue; // already latched (missed release would double-latch) } let chord = Chord { @@ -499,26 +414,13 @@ fn latch_and_dispatch( shift, key: BindKey::Key(key), }; - if let Some(bound) = dispatch.resolve(chord, dev_plane) { - press( - &mut state, - &mut script, - run_lua, - &dispatch, - bound, - BindKey::Key(key), - ); + if let Some(command) = dispatch.resolve(chord, dev_plane) { + press(&mut state, &mut script, command, BindKey::Key(key)); } } ButtonState::Released => { state.down.retain(|&kc| kc != key); - release( - &mut state, - &mut script, - run_lua, - &dispatch, - BindKey::Key(key), - ); + release(&mut state, &mut script, BindKey::Key(key)); } } } @@ -535,7 +437,7 @@ fn latch_and_dispatch( if buttons.just_pressed(b) && !sup && hover.0.is_none() - && !state.latched.iter().any(|&(k, _)| k == BindKey::Mouse(b)) + && !state.latched.iter().any(|(k, _)| *k == BindKey::Mouse(b)) { let chord = Chord { alt, @@ -543,40 +445,28 @@ fn latch_and_dispatch( shift, key: BindKey::Mouse(b), }; - if let Some(bound) = dispatch.resolve(chord, false) { - press( - &mut state, - &mut script, - run_lua, - &dispatch, - bound, - BindKey::Mouse(b), - ); + if let Some(command) = dispatch.resolve(chord, false) { + press(&mut state, &mut script, command, BindKey::Mouse(b)); } } if buttons.just_released(b) { - release( - &mut state, - &mut script, - run_lua, - &dispatch, - BindKey::Mouse(b), - ); + release(&mut state, &mut script, BindKey::Mouse(b)); } } - // ── Wheel ── a notch is a press and its release, back to back: the reference hands one chord - // to `CBindings::ExecuteBinding` with `isDown=1` (`0x483d6f`) then `isDown=0` (`0x483d82`). - // A `runOnUp` command runs both halves in the frame; a plain one's up leg is the - // `RunCommand` (`0x4b7b50`) no-op. + // ── Wheel ── each notch is a press and its release, back to back: the reference hands one + // chord to `CBindings::ExecuteBinding` with `isDown=1` (`0x483d6f`) then `isDown=0` + // (`0x483d82`) per wheel message. A `runOnUp` command runs both halves; a plain one's up leg is + // the `RunCommand` (`0x4b7bf1`) no-op. A trackpad's fractional lines carry to whole notches. let wheel = wheel_lines(scroll.unit, scroll.delta.y); // Over chrome only (`PointerOverUiPanel`): the wheel still zooms with the cursor on a // nameplate, a mouse-enabled widget but not a panel. if wheel != 0.0 && !sup && !over_ui.0 { - let (key, amount) = if wheel > 0.0 { - (BindKey::WheelUp, wheel) + let steps = notches.feed(wheel); + let key = if steps > 0 { + BindKey::WheelUp } else { - (BindKey::WheelDown, -wheel) + BindKey::WheelDown }; let chord = Chord { alt, @@ -584,38 +474,26 @@ fn latch_and_dispatch( shift, key, }; - match dispatch.resolve(chord, false) { - // A host command takes the notch's analog magnitude (the camera zoom), which `press` - // cannot carry: it spends the 1.0 key step. It has no release half. - Some(Bound::Spec(cmd)) if matches!(SPECS[cmd.0 as usize].kind, Kind::Host) => { - state.fired.push(cmd); - state.amounts.push((cmd, amount)); + if let Some(command) = dispatch.resolve(chord, false) { + for _ in 0..steps.unsigned_abs() { + press(&mut state, &mut script, command, key); + release(&mut state, &mut script, key); } - // Everything else is the reference's pair: `Kind::EdgeUpDown` runs both halves (a - // wheel-bound action button casts), an addon's `runOnUp` body runs down then up, and - // `Kind::Held` sets and clears in the one tick, as the reference's movement bit does. - Some(bound) => { - press(&mut state, &mut script, run_lua, &dispatch, bound, key); - release(&mut state, &mut script, run_lua, &dispatch, key); - } - None => {} } } // ── The stuck-latch sweep ── a release the window never saw: a latch whose base key reads up - // unlatches now and fires its up-half. The reference's two bulk clears land here, since bevy - // zeroes `ButtonInput` for both: OS window deactivate (`0x514490`'s `and eax,0xfffff00f`, the - // direction bits released while autorun survives) and the loading cover - // (`loading_screen::input`'s `swallow`, the world-enter `0x5144c0`, which clears everything). - // UI keyboard focus never reaches here. + // unlatches now and runs its up-half. The reference's two bulk clears land here too, since + // bevy zeroes `ButtonInput` for both (see the clears above). UI keyboard focus never reaches + // here. let mut stuck: Vec = Vec::new(); - for &(k, _) in &state.latched { - if !physically_down(k, &keys, &buttons) && !stuck.contains(&k) { - stuck.push(k); + for (k, _) in &state.latched { + if !physically_down(*k, &keys, &buttons) && !stuck.contains(k) { + stuck.push(*k); } } for k in stuck { - release(&mut state, &mut script, run_lua, &dispatch, k); + release(&mut state, &mut script, k); } // The calls this pass's binding bodies made act this frame, as the reference's return from @@ -626,27 +504,6 @@ fn latch_and_dispatch( } } -#[cfg(test)] -impl BindingDispatch { - /// A dispatch seeded from the registry defaults, the no-VM test seam; no addon bindings. - fn test_defaults() -> Self { - let mut map = std::collections::HashMap::new(); - for (i, s) in SPECS.iter().enumerate() { - for d in [s.d1, s.d2].into_iter().flatten() { - map.insert( - Chord::parse(d).expect("default parses"), - Bound::Spec(Cmd(i as u16)), - ); - } - } - Self { - map, - addons: Vec::new(), - seen_generation: crate::ui_script::VmMemo::default(), - } - } -} - /// Is this base key physically down, per bevy's button planes? The stuck-latch sweep and the /// pressed-key reconcile must agree, so both ask here. `ENTER` is down while either physical /// key is, since [`chord::normalize_key`] folds `NUMPADENTER` into it. @@ -666,75 +523,32 @@ fn physically_down( } } -/// One matching press: latch the held kinds, run or fire by kind. +/// One matching press: the command's body with `keystate = "down"`, and a latch on its base key +/// so the release can run the up-half. fn press( state: &mut BindingsState, script: &mut Option>, - run_lua: impl Fn(&mut Option>, &str, &str), - dispatch: &BindingDispatch, - bound: Bound, + command: &str, key: BindKey, ) { - let cmd = match bound { - Bound::Spec(cmd) => cmd, - // Run the body with `keystate = "down"`; latch only if its `runOnUp` asks for the - // release half. - Bound::Addon(i) => { - if let Some(a) = dispatch.addons.get(i as usize) { - run_addon(script, a, "down"); - if a.run_on_up { - state.latched.push((key, bound)); - } - } - return; - } - }; - let spec = &SPECS[cmd.0 as usize]; - match &spec.kind { - Kind::Held => { - if !state.pressed(cmd) { - state.just.push(cmd); - } - state.latched.push((key, bound)); - } - Kind::Edge(lua) => run_lua(script, lua, spec.name), - Kind::EdgeUpDown(down, _) => { - run_lua(script, down, spec.name); - state.latched.push((key, bound)); - } - Kind::Host => { - state.fired.push(cmd); - if !state.pressed(cmd) { - state.just.push(cmd); - } - state.amounts.push((cmd, 1.0)); + if let Some(s) = script.as_mut() { + if let Err(e) = s.execute_binding(command, true) { + warn!("bindings({command}): {e}"); } } + state.latched.push((key, command.to_owned())); } -/// A base key's release: drop its latch and fire a `runOnUp` up-half, even while typing (the -/// reference completes a pressed binding's release regardless of focus). -fn release( - state: &mut BindingsState, - script: &mut Option>, - run_lua: impl Fn(&mut Option>, &str, &str), - dispatch: &BindingDispatch, - key: BindKey, -) { +/// A base key's release: drop its latch and run the command's `runOnUp` up-half, even while +/// typing (the reference completes a pressed binding's release regardless of focus). +fn release(state: &mut BindingsState, script: &mut Option>, key: BindKey) { let mut i = 0; while i < state.latched.len() { if state.latched[i].0 == key { - match state.latched.remove(i).1 { - Bound::Spec(cmd) => match &SPECS[cmd.0 as usize].kind { - Kind::EdgeUpDown(_, up) => run_lua(script, up, SPECS[cmd.0 as usize].name), - Kind::Held => state.release_script_held(cmd), - _ => {} - }, - // The same body again, with `keystate = "up"`. - Bound::Addon(a) => { - if let Some(a) = dispatch.addons.get(a as usize) { - run_addon(script, a, "up"); - } + let (_, command) = state.latched.remove(i); + if let Some(s) = script.as_mut() { + if let Err(e) = s.execute_binding(&command, false) { + warn!("bindings({command}): {e}"); } } } else { @@ -743,30 +557,6 @@ fn release( } } -/// Run one addon binding's body with the `keystate` global set for the call and restored after. -/// -/// A `runOnUp` body is one chunk run on press and release, forking on the bare global `keystate` -/// (`Bindings.xml:4`); it is set in `_G`, not prepended, so the addon's line numbers hold. -/// `keystate` is absent from the 1.12 client's in-world `_G` (`reference/1.12-globals.tsv`), so -/// the reference sets it transiently, as it does `this`/`arg1` (`0x703f50` → `0x704f10`). -/// Save-and-restore because bodies nest: an inner binding must not clear the outer `keystate`. -fn run_addon(script: &mut Option>, bind: &AddonBindingBody, keystate: &str) { - let Some(s) = script.as_mut() else { return }; - let globals = s.lua().globals(); - // `Option`, since benilla-app does not depend on mlua; `None` converts back to nil. - let prior: Option = globals.get("keystate").unwrap_or(None); - if let Err(e) = globals.set("keystate", keystate) { - warn!("bindings({}): setting keystate: {e}", bind.name); - return; - } - if let Err(e) = s.run_binding(&bind.body) { - warn!("bindings({}): {e}", bind.name); - } - if let Err(e) = s.lua().globals().set("keystate", prior) { - warn!("bindings({}): restoring keystate: {e}", bind.name); - } -} - #[cfg(test)] mod tests { use bevy::input::keyboard::{Key, KeyboardInput}; @@ -774,18 +564,53 @@ mod tests { use super::*; - /// A minimal app around [`latch_and_dispatch`] with the registry defaults and no VM, fed real - /// input events through `InputPlugin` as winit feeds them. + /// A core `Bindings.xml` in the reference's shapes over the real binding functions, and bodies + /// that count their runs where the stock one would open a window; plus its defaults. + const CORE: &str = r#" + + if keystate == "down" then CameraOrSelectOrMoveStart() TurnOrActionStart() + else CameraOrSelectOrMoveStop() TurnOrActionStop() end + + + if keystate == "down" then MoveForwardStart() else MoveForwardStop() end + + + if keystate == "down" then TurnLeftStart() else TurnLeftStop() end + + Jump() + SitOrStand() + ToggleSheath() + ToggleAutoRun() + AB1 = keystate + PET1 = keystate + PET10 = keystate + TABS = (TABS or 0) + 1 + STABS = (STABS or 0) + 1 + PETPAPER = 1 + UITOGGLES = (UITOGGLES or 0) + 1 + CameraZoomIn(1.0) + CameraZoomOut(1.0) + "#; + const DEFAULTS: &str = "bind BUTTON3 MOVEANDSTEER\r\nbind W MOVEFORWARD\r\nbind UP MOVEFORWARD\r\n\ + bind A TURNLEFT\r\nbind LEFT TURNLEFT\r\nbind SPACE JUMP\r\nbind X SITORSTAND\r\n\ + bind Z TOGGLESHEATH\r\nbind NUMLOCK TOGGLEAUTORUN\r\nbind BUTTON4 TOGGLEAUTORUN\r\n\ + bind 1 ACTIONBUTTON1\r\nbind CTRL-1 BONUSACTIONBUTTON1\r\nbind CTRL-0 BONUSACTIONBUTTON10\r\n\ + bind TAB TARGETNEARESTENEMY\r\nbind SHIFT-TAB TARGETPREVIOUSENEMY\r\n\ + bind SHIFT-P TOGGLECHARACTER3\r\nbind ALT-Z TOGGLEUI\r\n\ + bind MOUSEWHEELUP CAMERAZOOMIN\r\nbind MOUSEWHEELDOWN CAMERAZOOMOUT\r\n"; + + /// A VM with [`CORE`] declared and [`DEFAULTS`] live, as a first login has them. + fn core_script() -> UiScript { + let mut script = UiScript::new().expect("VM"); + script.set_default_bindings(benilla_ui::script::keybind::parse_bindings_wtf(DEFAULTS)); + script.load_binding_set(1); + script.register_bindings(&benilla_ui::bindings_xml::parse(CORE).expect("well-formed")); + script + } + + /// The dispatch harness over [`core_script`]. fn harness() -> App { - let mut app = App::new(); - app.add_plugins((MinimalPlugins, bevy::input::InputPlugin)) - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .insert_resource(BindingDispatch::test_defaults()) - .add_systems(Update, latch_and_dispatch); - app + vm_harness(core_script()) } fn key(app: &mut App, k: KeyCode, state: bevy::input::ButtonState, repeat: bool) { @@ -811,6 +636,21 @@ mod tests { fn state(app: &App) -> &BindingsState { app.world().resource::() } + fn mouse(app: &mut App, button: MouseButton, state: bevy::input::ButtonState) { + app.world_mut().write_message(MouseButtonInput { + button, + state, + window: Entity::PLACEHOLDER, + }); + } + fn wheel(app: &mut App, y: f32) { + app.world_mut().write_message(MouseWheel { + unit: MouseScrollUnit::Line, + x: 0.0, + y, + window: Entity::PLACEHOLDER, + }); + } /// What a keyboard frame ate this frame. Set, not pushed: `feed_ui_input` rewrites the list /// every pass. @@ -865,16 +705,20 @@ mod tests { .expect("eval") } - /// An addon's `runOnUp` body is one chunk run twice, `keystate` "down" then "up"; our own - /// registry holds two strings ([`Kind::EdgeUpDown`]) instead. - #[test] - fn an_addon_binding_fires_its_lua_and_runs_again_on_release_when_it_asked_to() { - let mut script = UiScript::new().expect("VM"); - script.register_bindings(®istry_commands()); + fn with_addon(bindings: &str) -> UiScript { + let mut script = core_script(); script.register_addon_bindings( "ProbeAddon", - &benilla_ui::bindings_xml::parse(PROBE_BINDINGS).expect("well-formed"), + &benilla_ui::bindings_xml::parse(bindings).expect("well-formed"), ); + script + } + + /// A `runOnUp` body is one chunk run twice, `keystate` "down" then "up"; a plain one runs on + /// the press alone. The core's and an addon's rows dispatch the same way. + #[test] + fn a_binding_fires_its_lua_and_runs_again_on_release_when_it_asked_to() { + let script = with_addon(PROBE_BINDINGS); // A 1.12 `` ships no default chord, so an addon binding starts unbound. script .run(r#"SetBinding("J", "PROBEHOLD"); SetBinding("G", "PROBEEDGE")"#) @@ -887,7 +731,7 @@ mod tests { assert_eq!( lua_count(&app, "PROBE_DOWN"), 1, - "the press must reach the addon's body — this is the phase-4 bug" + "the press reaches the body" ); assert_eq!(lua_str(&app, "PROBE_LAST"), "down"); assert_eq!( @@ -919,23 +763,14 @@ mod tests { "no runOnUp, no second run — an addon that toggled here would toggle back" ); - // The registry dispatches unchanged beside them. + // The core's rows dispatch beside them. press_key(&mut app, KeyCode::KeyW); app.update(); - assert!(state(&app).pressed(cmd::MOVE_FORWARD)); - assert_eq!( - lua_count(&app, "PROBE_DOWN"), - 1, - "a built-in press is nobody else's" - ); + assert!(state(&app).pressed(Input::MoveForward)); + assert_eq!(lua_count(&app, "PROBE_DOWN"), 1, "W is nobody else's"); - // `keystate` does not outlive the call: the 1.12 client's in-world `_G` has none - // (`reference/1.12-globals.tsv`). - assert_eq!( - lua_str(&app, "tostring(keystate)"), - "nil", - "keystate must be restored after a binding body runs, not left standing in _G" - ); + // `keystate` does not outlive the call: `RunCommand` sets it to nil (`0x4b7c42`). + assert_eq!(lua_str(&app, "tostring(keystate)"), "nil"); } /// A focused EditBox swallows every key (the reference's handler returns 1 on every path, @@ -951,7 +786,7 @@ mod tests { press_key(&mut app, KeyCode::ArrowLeft); app.update(); assert!( - !state(&app).pressed(cmd::TURN_LEFT), + !state(&app).pressed(Input::TurnLeft), "an unflagged focused box eats the arrow" ); release_key(&mut app, KeyCode::ArrowLeft); @@ -964,7 +799,7 @@ mod tests { press_key(&mut app, KeyCode::ArrowLeft); app.update(); assert!( - state(&app).pressed(cmd::TURN_LEFT), + state(&app).pressed(Input::TurnLeft), "a flagged box declines the arrow, so TURNLEFT runs" ); @@ -972,7 +807,7 @@ mod tests { press_key(&mut app, KeyCode::KeyW); app.update(); assert!( - !state(&app).pressed(cmd::MOVE_FORWARD), + !state(&app).pressed(Input::MoveForward), "only the arrows are exempt — every other key a focused box still eats" ); } @@ -981,26 +816,21 @@ mod tests { /// direction bits rather than clearing them (`0x514490` is the OS window-deactivate clear). #[test] fn a_held_latch_rides_out_a_box_taking_focus_and_still_releases() { - let mut script = UiScript::new().expect("VM"); - script.register_bindings(®istry_commands()); - script.register_addon_bindings( - "ProbeAddon", - &benilla_ui::bindings_xml::parse(PROBE_BINDINGS).expect("well-formed"), - ); + let script = with_addon(PROBE_BINDINGS); script.run(r#"SetBinding("J", "PROBEHOLD")"#).expect("bind"); let mut app = vm_harness(script); press_key(&mut app, KeyCode::KeyW); press_key(&mut app, KeyCode::KeyJ); app.update(); - assert!(state(&app).pressed(cmd::MOVE_FORWARD)); + assert!(state(&app).pressed(Input::MoveForward)); assert_eq!(lua_count(&app, "PROBE_DOWN"), 1); // A box takes focus; both latches ride it out. app.world_mut().resource_mut::().typing = true; app.update(); assert!( - state(&app).pressed(cmd::MOVE_FORWARD), + state(&app).pressed(Input::MoveForward), "holding W and opening the chat box keeps you running" ); assert_eq!( @@ -1014,7 +844,7 @@ mod tests { release_key(&mut app, KeyCode::KeyJ); app.update(); assert!( - !state(&app).pressed(cmd::MOVE_FORWARD), + !state(&app).pressed(Input::MoveForward), "releasing W stops you, while typing exactly as otherwise" ); assert_eq!( @@ -1024,19 +854,17 @@ mod tests { ); } - /// The whole wheel-bind path: the real Keybindings page, a capsule armed by a click, a notch - /// over the page (its OnMouseWheel, as 1.12's window binds it), then the bound chord - /// dispatching. + /// The whole wheel-bind path: the real Keybindings page over the stock commands, a capsule + /// armed by a click, a notch over the page (its OnMouseWheel, as 1.12's window binds it), then + /// the bound chord dispatching the stock JUMP body. #[test] fn a_wheel_notch_binds_through_the_real_page_and_then_dispatches() { benilla_formats::wow_data_or_skip!(); - let by_name = - |n: &str| Cmd(SPECS.iter().position(|s| s.name == n).expect("registered") as u16); let mut s = crate::ui_script::keybindings_tests::harness(); crate::ui_script::keybindings_tests::on_page(&mut s); const ROW: &str = "BenillaOptionsFrameContainerBodyKeybindingsRow"; - // Expand Movement and arm JUMP's first capsule: JUMP is not `runOnUp`, so the reference - // accepts the wheel on it. + // Expand Movement and arm JUMP's first capsule: the reference accepts the wheel on any + // command. s.run(&format!("{ROW}1Header:Click()")).expect("expand"); s.run(&format!("{ROW}9Key1Button:Click()")).expect("select"); assert_eq!( @@ -1066,47 +894,28 @@ mod tests { let mut app = vm_harness(s); // The bound chord now dispatches: the next notch jumps rather than binding. - app.world_mut().write_message(MouseWheel { - unit: MouseScrollUnit::Line, - x: 0.0, - y: 1.0, - window: Entity::PLACEHOLDER, - }); + wheel(&mut app, 1.0); app.update(); - assert!(state(&app).fired(by_name("JUMP"))); + assert!(state(&app).fired(Input::Jump)); assert!( - !state(&app).fired(cmd::CAMERA_ZOOM_IN), + !state(&app).fired(Input::CameraZoomIn), "JUMP stole the wheel from the camera, the 1.12 steal law" ); } /// A notch is a press and its release (`0x483d6f`, `0x483d82`), so a `runOnUp` binding on the - /// wheel runs both halves in one frame. Seeded through the stored set, since `SetBinding` - /// refuses a wheel chord on a press-and-release command: the hand-edited file case. + /// wheel runs both halves in one frame. #[test] fn a_wheel_notch_runs_both_halves_of_a_press_and_release_binding() { - let mut script = UiScript::new().expect("VM"); - script.register_bindings(®istry_commands()); - script.register_addon_bindings( - "ProbeAddon", - &benilla_ui::bindings_xml::parse(PROBE_BINDINGS).expect("well-formed"), - ); + let mut script = with_addon(PROBE_BINDINGS); script.seed_binding_set( 1, - Some(vec![( - "PROBEHOLD".to_string(), - vec!["MOUSEWHEELUP".to_string()], - )]), + Some(vec![("MOUSEWHEELUP".to_string(), "PROBEHOLD".to_string())]), ); script.load_binding_set(1); let mut app = vm_harness(script); - app.world_mut().write_message(MouseWheel { - unit: MouseScrollUnit::Line, - x: 0.0, - y: 1.0, - window: Entity::PLACEHOLDER, - }); + wheel(&mut app, 1.0); app.update(); assert_eq!(lua_count(&app, "PROBE_DOWN"), 1, "the notch's press half"); assert_eq!( @@ -1130,29 +939,29 @@ mod tests { let mut app = harness(); press_key(&mut app, KeyCode::Space); app.update(); - assert!(state(&app).fired(cmd::JUMP), "the first press jumps"); + assert!(state(&app).fired(Input::Jump), "the first press jumps"); - // (1) JUMP never latches, so only the pressed-key set stops a jump per repeat. + // (1) Only the pressed-key set stops a jump per repeat. repeat_key(&mut app, KeyCode::Space); app.update(); assert!( - !state(&app).fired(cmd::JUMP), + !state(&app).fired(Input::Jump), "a repeat of a key already down is not a press" ); // Movement, so the resume below has something to observe. press_key(&mut app, KeyCode::KeyW); app.update(); - assert!(state(&app).pressed(cmd::MOVE_FORWARD)); + assert!(state(&app).pressed(Input::MoveForward)); // (3) The window is deactivated: bevy empties the keyboard plane, which unlatches through - // the stuck-latch sweep and empties our pressed-key set. + // the stuck-latch sweep, whose up-half runs the Stop, and empties our pressed-key set. app.world_mut() .resource_mut::>() .release_all(); app.update(); assert!( - !state(&app).pressed(cmd::MOVE_FORWARD), + !state(&app).pressed(Input::MoveForward), "the deactivate releases the direction bits (0x514490)" ); @@ -1161,37 +970,25 @@ mod tests { repeat_key(&mut app, KeyCode::KeyW); app.update(); assert!( - state(&app).pressed(cmd::MOVE_FORWARD), + state(&app).pressed(Input::MoveForward), "the first repeat after re-activation re-latches" ); } - /// An addon's bindings that call the movement functions, in the reference's shapes: a - /// `runOnUp` pair like stock MOVEFORWARD, and a Start and a Stop alone. + /// An addon's bindings that call the movement functions: a Start and a Stop alone. const MOVE_BINDINGS: &str = r#" - - if ( keystate == "down" ) then MoveForwardStart(); else MoveForwardStop(); end - MoveForwardStart() MoveForwardStop() "#; - /// `MoveForwardStart`/`Stop` set and clear the bit the W key does (`0x515090(0x10, …)`): a - /// binding body's pair moves while held, a Start with no Stop keeps moving, a Stop clears it - /// whoever set it, and outside a binding body the gate (`0x494a50`) refuses the call. + /// `MoveForwardStart`/`Stop` set and clear the bit the W key's body does (`0x515090(0x10, …)`): + /// a Start with no Stop keeps moving, a Stop clears it whoever set it, and outside a binding + /// body the gate (`0x494a50`) refuses the call. #[test] fn the_movement_functions_drive_the_held_state_as_the_key_does() { - let mut script = UiScript::new().expect("VM"); - script.register_bindings(®istry_commands()); - script.register_addon_bindings( - "ProbeAddon", - &benilla_ui::bindings_xml::parse(MOVE_BINDINGS).expect("well-formed"), - ); + let script = with_addon(MOVE_BINDINGS); script - .run( - r#"SetBinding("J", "PROBEFWD"); SetBinding("K", "PROBEGO"); - SetBinding("L", "PROBESTOP")"#, - ) + .run(r#"SetBinding("K", "PROBEGO"); SetBinding("L", "PROBESTOP")"#) .expect("bind"); let mut app = vm_harness(script); let tap = |app: &mut App, k: KeyCode| { @@ -1202,59 +999,61 @@ mod tests { }; // The `runOnUp` pair: forward from the press frame, stopped on the release. - press_key(&mut app, KeyCode::KeyJ); + press_key(&mut app, KeyCode::KeyW); app.update(); - assert!(state(&app).pressed(cmd::MOVE_FORWARD)); + assert!(state(&app).pressed(Input::MoveForward)); assert!( - state(&app).just_pressed(cmd::MOVE_FORWARD), + state(&app).just_pressed(Input::MoveForward), "the press edge" ); app.update(); - assert!(state(&app).pressed(cmd::MOVE_FORWARD), "held across frames"); - release_key(&mut app, KeyCode::KeyJ); + assert!( + state(&app).pressed(Input::MoveForward), + "held across frames" + ); + assert!(!state(&app).just_pressed(Input::MoveForward)); + release_key(&mut app, KeyCode::KeyW); app.update(); - assert!(!state(&app).pressed(cmd::MOVE_FORWARD)); + assert!(!state(&app).pressed(Input::MoveForward)); // A Start with no Stop keeps moving past its key's release; the Stop ends it. tap(&mut app, KeyCode::KeyK); app.update(); assert!( - state(&app).pressed(cmd::MOVE_FORWARD), + state(&app).pressed(Input::MoveForward), "no Stop, still moving" ); tap(&mut app, KeyCode::KeyL); - assert!(!state(&app).pressed(cmd::MOVE_FORWARD)); + assert!(!state(&app).pressed(Input::MoveForward)); // W's release runs MOVEFORWARD's Stop, which clears a Lua Start too. tap(&mut app, KeyCode::KeyK); tap(&mut app, KeyCode::KeyW); - assert!(!state(&app).pressed(cmd::MOVE_FORWARD)); + assert!(!state(&app).pressed(Input::MoveForward)); // And a Stop clears the bit under a held key, which stays stopped until pressed again. press_key(&mut app, KeyCode::KeyW); app.update(); tap(&mut app, KeyCode::KeyL); - assert!(!state(&app).pressed(cmd::MOVE_FORWARD)); + assert!(!state(&app).pressed(Input::MoveForward)); release_key(&mut app, KeyCode::KeyW); app.update(); // Outside a binding body (a macro, `RunScript`, an addon's own code) nothing moves. app.world() .non_send_resource::() - .run("MoveForwardStart() Jump() ToggleAutoRun()") + .run("MoveForwardStart() Jump() ToggleAutoRun() RunBinding('MOVEFORWARD')") .expect("the calls answer"); app.update(); - assert!(!state(&app).pressed(cmd::MOVE_FORWARD)); - assert!(!state(&app).fired(cmd::JUMP)); - assert!(!state(&app).fired(cmd::TOGGLE_AUTORUN)); + assert!(!state(&app).pressed(Input::MoveForward)); + assert!(!state(&app).fired(Input::Jump)); + assert!(!state(&app).fired(Input::ToggleAutoRun)); } - /// The ungated one-shots fire their commands as the keys do: `CameraZoomIn(yards)` carries its - /// amount to the zoom (1.0 when absent, `0x50b42d`), `SitOrStand` and `ToggleSheath` fire. + /// The ungated one-shots fire as the keys do: `CameraZoomIn(yards)` carries its amount to the + /// zoom (1.0 when absent, `0x50b42d`), `SitOrStand` and `ToggleSheath` fire. #[test] - fn the_one_shot_functions_fire_their_commands() { - let mut script = UiScript::new().expect("VM"); - script.register_bindings(®istry_commands()); - let mut app = vm_harness(script); + fn the_one_shot_functions_fire_their_inputs() { + let mut app = harness(); let run = |app: &mut App, lua: &str| { app.world() .non_send_resource::() @@ -1263,55 +1062,55 @@ mod tests { app.update(); }; run(&mut app, "CameraZoomIn(3)"); - assert!(state(&app).fired(cmd::CAMERA_ZOOM_IN)); - assert_eq!(state(&app).amount(cmd::CAMERA_ZOOM_IN), 3.0); + assert!(state(&app).fired(Input::CameraZoomIn)); + assert_eq!(state(&app).amount(Input::CameraZoomIn), 3.0); run(&mut app, "CameraZoomOut()"); - assert_eq!(state(&app).amount(cmd::CAMERA_ZOOM_OUT), 1.0); + assert_eq!(state(&app).amount(Input::CameraZoomOut), 1.0); assert_eq!( - state(&app).amount(cmd::CAMERA_ZOOM_IN), + state(&app).amount(Input::CameraZoomIn), 0.0, "one frame only" ); run(&mut app, "SitOrStand()"); - assert!(state(&app).fired(cmd::SIT_OR_STAND)); + assert!(state(&app).fired(Input::SitOrStand)); run(&mut app, "ToggleSheath()"); - assert!(state(&app).fired(cmd::TOGGLE_SHEATH)); - assert!(!state(&app).fired(cmd::SIT_OR_STAND)); + assert!(state(&app).fired(Input::ToggleSheath)); + assert!(!state(&app).fired(Input::SitOrStand)); } + /// Two keys on one command share its one bit: W and UP both run MOVEFORWARD's body, so the + /// second Start is no new edge (`0x514840`) and either release's Stop clears the bit whoever + /// set it (`0x514b70`). #[test] - fn held_commands_latch_across_frames_and_release_per_base_key() { + fn two_keys_on_one_command_share_its_bit_so_either_release_stops_it() { let mut app = harness(); - // W and UP are both MOVEFORWARD: press both, release one, still moving. press_key(&mut app, KeyCode::KeyW); app.update(); - assert!(state(&app).pressed(cmd::MOVE_FORWARD)); - assert!(state(&app).just_pressed(cmd::MOVE_FORWARD), "press edge"); + assert!(state(&app).pressed(Input::MoveForward)); + assert!(state(&app).just_pressed(Input::MoveForward), "press edge"); press_key(&mut app, KeyCode::ArrowUp); app.update(); - assert!(state(&app).pressed(cmd::MOVE_FORWARD)); + assert!(state(&app).pressed(Input::MoveForward)); assert!( - !state(&app).just_pressed(cmd::MOVE_FORWARD), + !state(&app).just_pressed(Input::MoveForward), "second key on an already-held command is no new edge" ); release_key(&mut app, KeyCode::KeyW); app.update(); - assert!(state(&app).pressed(cmd::MOVE_FORWARD), "UP still holds it"); + assert!( + !state(&app).pressed(Input::MoveForward), + "W's Stop clears the bit though UP is still down" + ); release_key(&mut app, KeyCode::ArrowUp); app.update(); - assert!(!state(&app).pressed(cmd::MOVE_FORWARD)); - // A repeat press (held-key auto-repeat) neither re-latches nor re-edges. + assert!(!state(&app).pressed(Input::MoveForward)); + // A repeat press (held-key auto-repeat) neither re-runs nor re-edges. press_key(&mut app, KeyCode::KeyW); app.update(); - key( - &mut app, - KeyCode::KeyW, - bevy::input::ButtonState::Pressed, - true, - ); + repeat_key(&mut app, KeyCode::KeyW); app.update(); - assert!(state(&app).pressed(cmd::MOVE_FORWARD)); - assert!(!state(&app).just_pressed(cmd::MOVE_FORWARD)); + assert!(state(&app).pressed(Input::MoveForward)); + assert!(!state(&app).just_pressed(Input::MoveForward)); } /// The two-probe lookup: the exact chord, then one retry with the leftmost modifier dropped, @@ -1319,129 +1118,124 @@ mod tests { #[test] fn a_press_probes_its_chord_then_falls_back_once() { // Shift held, W pressed: `SHIFT-W` is unbound, so the retry drops SHIFT and MOVEFORWARD - // latches (the reference's `strchr` step, `0x4b7990`). + // runs (the reference's `strchr` step, `0x4b7990`). let mut app = harness(); press_key(&mut app, KeyCode::ShiftLeft); press_key(&mut app, KeyCode::KeyW); app.update(); assert!( - state(&app).pressed(cmd::MOVE_FORWARD), + state(&app).pressed(Input::MoveForward), "an unbound SHIFT-W falls back to W, the modifier dropped" ); - // It unlatches on the base key with the modifier still down (the reference replays the - // press-time chord at key-up, `0x483bd0`; latching the resolved command is equivalent). + // It releases on the base key with the modifier still down (the reference replays the + // press-time chord at key-up, `0x483bd0`). release_key(&mut app, KeyCode::KeyW); app.update(); - assert!(!state(&app).pressed(cmd::MOVE_FORWARD)); + assert!(!state(&app).pressed(Input::MoveForward)); // Bare Z is the sheath toggle, ALT-Z is TOGGLEUI: the exact probe runs first, so a // fallback never overrides a real entry. let mut app = harness(); press_key(&mut app, KeyCode::KeyZ); app.update(); - assert!(state(&app).fired(cmd::TOGGLE_SHEATH)); - assert!(!state(&app).fired(cmd::TOGGLE_UI)); + assert!(state(&app).fired(Input::ToggleSheath)); + assert_eq!(lua_count(&app, "UITOGGLES"), 0); release_key(&mut app, KeyCode::KeyZ); press_key(&mut app, KeyCode::AltLeft); press_key(&mut app, KeyCode::KeyZ); app.update(); - assert!( - state(&app).fired(cmd::TOGGLE_UI), + assert_eq!( + lua_count(&app, "UITOGGLES"), + 1, "ALT-Z is TOGGLEUI: the exact chord probes first" ); - assert!(!state(&app).fired(cmd::TOGGLE_SHEATH)); - // CTRL-ALT-Z fires nothing: the single strip drops the leftmost modifier (`ALT-CTRL-Z` + assert!(!state(&app).fired(Input::ToggleSheath)); + // CTRL-ALT-Z runs nothing: the single strip drops the leftmost modifier (`ALT-CTRL-Z` // to `CTRL-Z`, unbound) and stops, never reaching ALT-Z or Z. release_key(&mut app, KeyCode::KeyZ); press_key(&mut app, KeyCode::ControlLeft); press_key(&mut app, KeyCode::KeyZ); app.update(); assert!( - !state(&app).fired(cmd::TOGGLE_UI) && !state(&app).fired(cmd::TOGGLE_SHEATH), + lua_count(&app, "UITOGGLES") == 1 && !state(&app).fired(Input::ToggleSheath), "ALT-CTRL-Z probes CTRL-Z and stops — no second strip to ALT-Z or Z" ); // TAB and SHIFT-TAB are both bound, so each resolves exactly. let mut app = harness(); press_key(&mut app, KeyCode::Tab); app.update(); - assert!(state(&app).fired(cmd::TARGET_NEAREST_ENEMY)); + assert_eq!(lua_count(&app, "TABS"), 1); release_key(&mut app, KeyCode::Tab); press_key(&mut app, KeyCode::ShiftLeft); press_key(&mut app, KeyCode::Tab); app.update(); - assert!(state(&app).fired(cmd::TARGET_PREVIOUS_ENEMY)); - assert!(!state(&app).fired(cmd::TARGET_NEAREST_ENEMY)); + assert_eq!(lua_count(&app, "STABS"), 1); + assert_eq!(lua_count(&app, "TABS"), 1); // Super is never a binding modifier: a Super-held press builds no chord, so no fallback. let mut app = harness(); press_key(&mut app, KeyCode::SuperLeft); press_key(&mut app, KeyCode::KeyZ); app.update(); - assert!(!state(&app).fired(cmd::TOGGLE_SHEATH)); + assert!(!state(&app).fired(Input::ToggleSheath)); } - /// The dev plane spends the keyboard's fallback probe and nothing else. Asserted on - /// [`BindingDispatch::resolve`] because the colliding command is `Kind::Edge`, which the no-VM - /// harness cannot observe. + /// The dev plane spends the keyboard's fallback probe and nothing else, asserted on + /// [`BindingDispatch::resolve`] itself. #[test] fn the_dev_plane_keeps_its_letters_without_stealing_bound_chords() { - let by_name = - |n: &str| Cmd(SPECS.iter().position(|s| s.name == n).expect("registered") as u16); - let pet_paper_doll = by_name("TOGGLECHARACTER3"); // SHIFT-P - let mut dispatch = BindingDispatch::test_defaults(); + let mut app = harness(); + app.update(); + let mut dispatch = app + .world_mut() + .remove_resource::() + .expect("synced"); let plane_p = Chord::parse("CTRL-SHIFT-P").expect("parses"); // Ctrl+Shift+P is the perf HUD's; off the plane it falls back onto SHIFT-P... assert_eq!( dispatch.resolve(plane_p, false), - Some(Bound::Spec(pet_paper_doll)), + Some("TOGGLECHARACTER3"), "without the plane rule the retry does reach SHIFT-P — this is what is being blocked" ); // ...so on the plane the retry is suppressed. assert_eq!(dispatch.resolve(plane_p, true), None); // Only the fallback is suppressed: an exact CTRL-SHIFT- entry, as a player would bind, // still resolves. - dispatch.map.insert(plane_p, Bound::Spec(pet_paper_doll)); + dispatch.map.insert(plane_p, "TOGGLECHARACTER3".into()); assert_eq!( dispatch.resolve(plane_p, true), - Some(Bound::Spec(pet_paper_doll)), + Some("TOGGLECHARACTER3"), "the plane spends the retry, never the exact probe" ); - // SHIFT-P itself is still the pet paper doll. let shift_p = Chord::parse("SHIFT-P").expect("parses"); - assert_eq!( - dispatch.resolve(shift_p, false), - Some(Bound::Spec(pet_paper_doll)) - ); + assert_eq!(dispatch.resolve(shift_p, false), Some("TOGGLECHARACTER3")); } /// The pet bar routes on the CTRL digits and the number row is untouched: the two share base /// keys, kept apart only by the exact-modifier probe. CTRL-0 is slot 10. #[test] fn the_pet_lane_dispatches_on_the_ctrl_digits() { - let by_name = - |n: &str| Cmd(SPECS.iter().position(|s| s.name == n).expect("registered") as u16); let mut app = harness(); press_key(&mut app, KeyCode::ControlLeft); press_key(&mut app, KeyCode::Digit1); app.update(); - assert!(state(&app).pressed(by_name("BONUSACTIONBUTTON1"))); - assert!( - !state(&app).pressed(by_name("ACTIONBUTTON1")), + assert_eq!(lua_str(&app, "PET1"), "down"); + assert_eq!( + lua_str(&app, "AB1"), + "", "the modifier decides: CTRL-1 is not the number row's" ); - // The latch drops on the base key's release with Ctrl still held; in the VM that release - // runs `PetActionButtonUp`, which casts. + // The release runs the up half with Ctrl still held. release_key(&mut app, KeyCode::Digit1); app.update(); - assert!(!state(&app).pressed(by_name("BONUSACTIONBUTTON1"))); - // CTRL-0 is slot 10. + assert_eq!(lua_str(&app, "PET1"), "up"); press_key(&mut app, KeyCode::Digit0); app.update(); - assert!(state(&app).pressed(by_name("BONUSACTIONBUTTON10"))); - // Bare 1 is still the action bar's, with no pet command in sight. + assert_eq!(lua_str(&app, "PET10"), "down", "CTRL-0 is slot 10"); + // Bare 1 is still the action bar's. let mut app = harness(); press_key(&mut app, KeyCode::Digit1); app.update(); - assert!(state(&app).pressed(by_name("ACTIONBUTTON1"))); - assert!(!state(&app).pressed(by_name("BONUSACTIONBUTTON1"))); + assert_eq!(lua_str(&app, "AB1"), "down"); + assert_eq!(lua_str(&app, "PET1"), ""); } /// The typing gate blocks new presses and releases nothing already held: holding W and @@ -1451,25 +1245,25 @@ mod tests { let mut app = harness(); press_key(&mut app, KeyCode::KeyW); app.update(); - assert!(state(&app).pressed(cmd::MOVE_FORWARD)); + assert!(state(&app).pressed(Input::MoveForward)); // A box takes focus. You keep running, and new presses type instead of binding. app.world_mut().resource_mut::().typing = true; app.update(); assert!( - state(&app).pressed(cmd::MOVE_FORWARD), + state(&app).pressed(Input::MoveForward), "the capture edge is not a release — holding W keeps you running while you type" ); press_key(&mut app, KeyCode::KeyX); app.update(); assert!( - !state(&app).fired(cmd::SIT_OR_STAND), + !state(&app).fired(Input::SitOrStand), "typed keys are not bindings" ); // Letting go still stops you, box focused or not. release_key(&mut app, KeyCode::KeyW); app.update(); assert!( - !state(&app).pressed(cmd::MOVE_FORWARD), + !state(&app).pressed(Input::MoveForward), "the release is delivered regardless of focus" ); // Focus drops; keys work again. @@ -1478,7 +1272,7 @@ mod tests { app.update(); press_key(&mut app, KeyCode::KeyX); app.update(); - assert!(state(&app).fired(cmd::SIT_OR_STAND)); + assert!(state(&app).fired(Input::SitOrStand)); } /// A shown keyboard frame eating the key that closes it costs that key its binding and @@ -1489,15 +1283,13 @@ mod tests { let mut app = harness(); press_key(&mut app, KeyCode::KeyW); app.update(); - assert!(state(&app).pressed(cmd::MOVE_FORWARD)); + assert!(state(&app).pressed(Input::MoveForward)); - // The map eats this frame's `M`. `M` is `Kind::Edge`, invisible to a no-VM harness, so - // the suppression is asserted on `X` below. frame_ate(&mut app, &[KeyCode::KeyM]); press_key(&mut app, KeyCode::KeyM); app.update(); assert!( - state(&app).pressed(cmd::MOVE_FORWARD), + state(&app).pressed(Input::MoveForward), "the frame ate the toggle key; you are still running" ); @@ -1508,7 +1300,7 @@ mod tests { press_key(&mut app, KeyCode::KeyX); app.update(); assert!( - !state(&app).fired(cmd::SIT_OR_STAND), + !state(&app).fired(Input::SitOrStand), "the frame ate this key: its binding must not also fire" ); @@ -1519,11 +1311,11 @@ mod tests { press_key(&mut app, KeyCode::KeyX); app.update(); assert!( - state(&app).fired(cmd::SIT_OR_STAND), + state(&app).fired(Input::SitOrStand), "consumption is per key, not per frame" ); assert!( - state(&app).pressed(cmd::MOVE_FORWARD), + state(&app).pressed(Input::MoveForward), "and W has been held throughout" ); } @@ -1532,78 +1324,99 @@ mod tests { fn mouse_buttons_bind_only_over_the_world_and_the_wheel_respects_ui() { let mut app = harness(); // BUTTON4 (winit Forward) is TOGGLEAUTORUN's second default. - app.world_mut().write_message(MouseButtonInput { - button: MouseButton::Forward, - state: bevy::input::ButtonState::Pressed, - window: Entity::PLACEHOLDER, - }); + mouse(&mut app, MouseButton::Forward, ButtonState::Pressed); app.update(); - assert!(state(&app).fired(cmd::TOGGLE_AUTORUN)); + assert!(state(&app).fired(Input::ToggleAutoRun)); // Over a UI frame the press belongs to the frame. - app.world_mut().write_message(MouseButtonInput { - button: MouseButton::Forward, - state: bevy::input::ButtonState::Released, - window: Entity::PLACEHOLDER, - }); + mouse(&mut app, MouseButton::Forward, ButtonState::Released); app.update(); app.world_mut().resource_mut::().0 = Some(7); - app.world_mut().write_message(MouseButtonInput { - button: MouseButton::Forward, - state: bevy::input::ButtonState::Pressed, - window: Entity::PLACEHOLDER, - }); + mouse(&mut app, MouseButton::Forward, ButtonState::Pressed); app.update(); - assert!(!state(&app).fired(cmd::TOGGLE_AUTORUN)); - // MOVEANDSTEER (BUTTON3) is a HELD command on a mouse button: press latches, release ends. + assert!(!state(&app).fired(Input::ToggleAutoRun)); + // MOVEANDSTEER (BUTTON3): its body holds both mouse bits, the both-button run, until the + // release's Stops. let mut app = harness(); - app.world_mut().write_message(MouseButtonInput { - button: MouseButton::Middle, - state: bevy::input::ButtonState::Pressed, - window: Entity::PLACEHOLDER, - }); + mouse(&mut app, MouseButton::Middle, ButtonState::Pressed); app.update(); - assert!(state(&app).pressed(cmd::MOVE_AND_STEER)); - app.world_mut().write_message(MouseButtonInput { - button: MouseButton::Middle, - state: bevy::input::ButtonState::Released, - window: Entity::PLACEHOLDER, - }); + assert!(state(&app).steering() && state(&app).steering_began()); app.update(); - assert!(!state(&app).pressed(cmd::MOVE_AND_STEER)); - // The wheel: a notch fires CAMERAZOOMIN with its amount; over UI it belongs to the frame. + assert!(state(&app).steering() && !state(&app).steering_began()); + mouse(&mut app, MouseButton::Middle, ButtonState::Released); + app.update(); + assert!(!state(&app).steering()); + // The wheel: each notch runs CAMERAZOOMIN's `CameraZoomIn(1.0)`; over UI it belongs to + // the frame. let mut app = harness(); - app.world_mut().write_message(MouseWheel { - unit: MouseScrollUnit::Line, - x: 0.0, - y: 2.0, - window: Entity::PLACEHOLDER, - }); + wheel(&mut app, 2.0); app.update(); - assert!(state(&app).fired(cmd::CAMERA_ZOOM_IN)); - assert_eq!(state(&app).amount(cmd::CAMERA_ZOOM_IN), 2.0); + assert!(state(&app).fired(Input::CameraZoomIn)); + assert_eq!(state(&app).amount(Input::CameraZoomIn), 2.0, "two notches"); app.world_mut().resource_mut::().0 = true; - app.world_mut().write_message(MouseWheel { - unit: MouseScrollUnit::Line, - x: 0.0, - y: 2.0, - window: Entity::PLACEHOLDER, - }); + wheel(&mut app, 2.0); app.update(); assert!( - !state(&app).fired(cmd::CAMERA_ZOOM_IN), + !state(&app).fired(Input::CameraZoomIn), "a UI wheel is the frame's" ); } - /// B opens the backpack and SHIFT-B opens every bag, end to end: a real key event, the shipped - /// defaults, the binding body and the stock Lua. The windows are the stock + /// A trackpad's fractional lines carry to whole notches, each one `CameraZoomIn(1.0)`, and a + /// reversal drops the carried fraction. + #[test] + fn fractional_wheel_lines_dispatch_per_whole_notch() { + let mut app = harness(); + for want in [0.0, 0.0, 1.0] { + wheel(&mut app, 0.4); + app.update(); + assert_eq!( + state(&app).amount(Input::CameraZoomIn), + want, + "0.4 lines a frame: the third completes the notch" + ); + } + wheel(&mut app, -1.0); + app.update(); + assert_eq!(state(&app).amount(Input::CameraZoomOut), 1.0); + assert_eq!(state(&app).amount(Input::CameraZoomIn), 0.0); + } + + /// The window deactivate (`0x514490`) clears the direction bits a Lua Start set and keeps the + /// mouse pair. + #[test] + fn focus_loss_clears_the_direction_bits_and_keeps_the_mouse_pair() { + let mut app = harness(); + app.add_message::(); + let script_starts = |app: &mut App| { + app.world() + .non_send_resource::() + .execute_binding("MOVEANDSTEER", true) + .expect("runs"); + app.world() + .non_send_resource::() + .execute_binding("MOVEFORWARD", true) + .expect("runs"); + app.update(); + }; + script_starts(&mut app); + assert!(state(&app).steering() && state(&app).pressed(Input::MoveForward)); + app.world_mut() + .write_message(bevy::input::keyboard::KeyboardFocusLost); + app.update(); + assert!(!state(&app).pressed(Input::MoveForward)); + assert!(state(&app).steering(), "0x514490 masks 0xfffff00f"); + } + + /// B opens the backpack and SHIFT-B opens every bag, end to end: a real key event, the + /// install's defaults, the stock binding body and the stock Lua. The windows are the stock /// `ContainerFrame1..12`, recycled across containers, so the test asks `IsBagOpen(id)`; the /// interface loads through [`crate::ui_script::load_default_ui`], so it needs client data. #[test] fn b_opens_the_backpack_and_shift_b_opens_every_bag() { let _data = benilla_formats::wow_data_or_skip!(); let mut script = UiScript::new().expect("VM"); - script.register_bindings(®istry_commands()); + script.set_default_bindings(crate::ui_script::default_bindings()); + script.load_binding_set(1); script.set_screen_size(1024.0, 768.0); // A player exists by the time the in-game UI loads, and the stock macro window formats // `UnitName("player")` into its character tab in OnLoad. @@ -1686,4 +1499,108 @@ mod tests { "SHIFT-B again shuts them all" ); } + + /// No production source in the bindings code carries a stock body: every command's Lua is the + /// file's own, read off the chain at load. Each string literal outside the test modules, its + /// whitespace collapsed, is held against every stock body's. + #[test] + fn no_stock_binding_body_is_retyped_in_the_tree() { + benilla_formats::wow_data_or_skip!(); + let collapse = |s: &str| s.split_whitespace().collect::>().join(" "); + let stock = crate::ui_script::stock_bindings_file().expect("the install's Bindings.xml"); + let bodies: Vec = benilla_ui::bindings_xml::parse(&stock) + .expect("parses") + .iter() + .map(|b| collapse(&b.body)) + .collect(); + assert!(bodies.len() > 200); + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); + let mut files = vec![ + root.join("benilla-app/src/bindings.rs"), + root.join("benilla-ui/src/script/keybind.rs"), + root.join("benilla-ui/src/bindings_xml.rs"), + ]; + for e in std::fs::read_dir(root.join("benilla-app/src/bindings")).unwrap() { + files.push(e.unwrap().path()); + } + assert!( + !root.join("benilla-app/src/bindings/commands.rs").exists(), + "the retyped registry is gone" + ); + for path in files { + let text = std::fs::read_to_string(&path).unwrap(); + let production = text.split("#[cfg(test)]").next().unwrap_or_default(); + for literal in string_literals(production) { + let literal = collapse(&literal); + if literal.is_empty() { + continue; + } + for body in &bodies { + assert!( + !literal.contains(body.as_str()), + "{} carries the stock body {body:?}", + path.display() + ); + } + } + } + } + + /// The contents of every `"…"` and `r#"…"#` literal in Rust source, escapes left as written. + fn string_literals(src: &str) -> Vec { + let mut out = Vec::new(); + let b = src.as_bytes(); + let mut i = 0; + while i < b.len() { + if b[i] == b'/' && b.get(i + 1) == Some(&b'/') { + i = src[i..].find('\n').map_or(b.len(), |n| i + n); + continue; + } + if b[i] == b'\'' { + // A char literal or a lifetime: skip `'x'` and `'\x'` so a quote char is no string. + if b.get(i + 2) == Some(&b'\'') { + i += 3; + continue; + } + if b.get(i + 1) == Some(&b'\\') && b.get(i + 3) == Some(&b'\'') { + i += 4; + continue; + } + i += 1; + continue; + } + if b[i] == b'r' && (b.get(i + 1) == Some(&b'#') || b.get(i + 1) == Some(&b'"')) { + let hashes = src[i + 1..].bytes().take_while(|&c| c == b'#').count(); + if b.get(i + 1 + hashes) == Some(&b'"') { + let start = i + 2 + hashes; + let close = format!("\"{}", "#".repeat(hashes)); + let end = src[start..].find(&close).map_or(b.len(), |n| start + n); + out.push(src[start..end].to_string()); + i = end + close.len(); + continue; + } + } + if b[i] == b'"' { + let start = i + 1; + let mut j = start; + while j < b.len() && b[j] != b'"' { + j += if b[j] == b'\\' { 2 } else { 1 }; + } + out.push(src[start..j.min(b.len())].to_string()); + i = j + 1; + continue; + } + i += 1; + } + out + } + + /// The literal scanner finds what it must, so the check above cannot pass by seeing nothing. + #[test] + fn the_literal_scanner_reads_both_kinds_of_string() { + let found = string_literals( + "let a = \"x\\\"y\"; // \"comment\"\nlet c = '\"'; let b = r#\"ToggleBackpack();\"#;", + ); + assert_eq!(found, ["x\\\"y", "ToggleBackpack();"]); + } } diff --git a/crates/benilla-app/src/bindings/chord.rs b/crates/benilla-app/src/bindings/chord.rs index e0af1768..e5e34227 100644 --- a/crates/benilla-app/src/bindings/chord.rs +++ b/crates/benilla-app/src/bindings/chord.rs @@ -372,20 +372,30 @@ fn token_key(t: &str) -> Option { mod tests { use super::*; - /// Ties this namer to the engine's `IsValidBindingKeyString`: every default chord, and one key - /// of each token shape, since `KeyCode` cannot be enumerated. + /// Ties this namer to the engine's `IsValidBindingKeyString`: every chord the install's + /// `WTF\DefaultBindings.wtf` ships is a key string it accepts and a chord the dispatcher can + /// press, and one key of each token shape, since `KeyCode` cannot be enumerated. #[test] fn every_token_the_codec_names_is_one_setbinding_accepts() { use benilla_ui::script::keybind::normalize_binding_key; - for spec in super::super::commands::SPECS { - for default in [spec.d1, spec.d2].into_iter().flatten() { - assert_eq!( - normalize_binding_key(default).as_deref(), - Some(default), - "the default chord '{default}' ({}) is not a bindable key string", - spec.name - ); - } + let defaults = crate::ui_script::default_bindings(); + if benilla_formats::wow_data().is_some() { + assert!( + defaults.len() > 100, + "the install's defaults: {}", + defaults.len() + ); + } + for (default, command) in &defaults { + assert_eq!( + normalize_binding_key(default).as_deref(), + Some(default.as_str()), + "the default chord '{default}' ({command}) is not a bindable key string" + ); + assert!( + Chord::parse(default).is_some(), + "{command}: default '{default}' does not parse" + ); } for k in [ KeyCode::KeyW, diff --git a/crates/benilla-app/src/bindings/commands.rs b/crates/benilla-app/src/bindings/commands.rs deleted file mode 100644 index 4c7ab656..00000000 --- a/crates/benilla-app/src/bindings/commands.rs +++ /dev/null @@ -1,2313 +0,0 @@ -//! The binding command registry: every command benilla implements, in 1.12 `Bindings.xml` order, -//! with the stock default chords (`WTF\DefaultBindings.wtf`) and each command's dispatch class. -//! A command appears only over a real engine action; the rest are in [`ABSENT`], and the two -//! together are exactly the client's 228 live bindings. Labels and headers are the 1.12 -//! `BINDING_NAME_*`/`BINDING_HEADER_*` GlobalStrings. -//! -//! Dispatch classes: -//! - [`Kind::Held`]: press latches, base-key release unlatches (the `runOnUp` movement pairs); -//! engine systems read the latch ([`super::BindingsState`]). -//! - [`Kind::Edge`] / [`Kind::EdgeUpDown`]: runs the reference's binding body as Lua. -//! - [`Kind::Host`]: fires into [`super::BindingsState::fired`] for an engine system. - -/// A command's index into [`SPECS`], the engine-side handle (`cmd::JUMP`). -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] -pub(crate) struct Cmd(pub u16); - -pub(crate) enum Kind { - /// Latched while the base key is held; engine systems read the state. - Held, - /// Fires this Lua on the matching press. - Edge(&'static str), - /// The reference's `runOnUp` button pair: Lua on press, Lua on base-key release. - EdgeUpDown(&'static str, &'static str), - /// Fires into [`super::BindingsState`]; an engine system consumes it. - Host, -} - -pub(crate) struct Spec { - pub name: &'static str, - /// The category header's global-string key (`BINDING_HEADER_MOVEMENT`). - pub category: &'static str, - pub kind: Kind, - /// The 1.12 default chords; `None` is shipped unbound. - pub d1: Option<&'static str>, - pub d2: Option<&'static str>, -} - -impl Spec { - /// The reference's ``; its one reader (`0x4b7bf1`) gates the release half - /// and nothing else. - pub(crate) fn run_on_up(&self) -> bool { - matches!(self.kind, Kind::Held | Kind::EdgeUpDown(..)) - } -} - -macro_rules! spec { - ($name:literal, $cat:ident, $kind:expr, $d1:expr, $d2:expr) => { - Spec { - name: $name, - category: concat!("BINDING_HEADER_", stringify!($cat)), - kind: $kind, - d1: $d1, - d2: $d2, - } - }; -} - -/// Engine-side handles for the host-dispatched commands, as indexes into [`SPECS`]. -pub(crate) mod cmd { - use super::{Cmd, TABLE}; - - /// Resolve a command name to its [`Cmd`] at compile time; an unknown name is a build error. - const fn by_name(name: &str) -> Cmd { - let mut i = 0; - while i < TABLE.len() { - if const_str_eq(TABLE[i].name, name) { - return Cmd(i as u16); - } - i += 1; - } - panic!("no such binding command") - } - - const fn const_str_eq(a: &str, b: &str) -> bool { - let (a, b) = (a.as_bytes(), b.as_bytes()); - if a.len() != b.len() { - return false; - } - let mut i = 0; - while i < a.len() { - if a[i] != b[i] { - return false; - } - i += 1; - } - true - } - - pub(crate) const MOVE_AND_STEER: Cmd = by_name("MOVEANDSTEER"); - pub(crate) const MOVE_FORWARD: Cmd = by_name("MOVEFORWARD"); - pub(crate) const MOVE_BACKWARD: Cmd = by_name("MOVEBACKWARD"); - pub(crate) const TURN_LEFT: Cmd = by_name("TURNLEFT"); - pub(crate) const TURN_RIGHT: Cmd = by_name("TURNRIGHT"); - pub(crate) const STRAFE_LEFT: Cmd = by_name("STRAFELEFT"); - pub(crate) const STRAFE_RIGHT: Cmd = by_name("STRAFERIGHT"); - pub(crate) const JUMP: Cmd = by_name("JUMP"); - pub(crate) const SIT_OR_STAND: Cmd = by_name("SITORSTAND"); - pub(crate) const TOGGLE_SHEATH: Cmd = by_name("TOGGLESHEATH"); - pub(crate) const TOGGLE_AUTORUN: Cmd = by_name("TOGGLEAUTORUN"); - pub(crate) const TOGGLE_RUN: Cmd = by_name("TOGGLERUN"); - pub(crate) const TARGET_NEAREST_ENEMY: Cmd = by_name("TARGETNEARESTENEMY"); - pub(crate) const TARGET_PREVIOUS_ENEMY: Cmd = by_name("TARGETPREVIOUSENEMY"); - pub(crate) const ATTACK_TARGET: Cmd = by_name("ATTACKTARGET"); - pub(crate) const TOGGLE_UI: Cmd = by_name("TOGGLEUI"); - pub(crate) const CAMERA_ZOOM_IN: Cmd = by_name("CAMERAZOOMIN"); - pub(crate) const CAMERA_ZOOM_OUT: Cmd = by_name("CAMERAZOOMOUT"); -} - -/// The registry, in 1.12 `Bindings.xml` order. `TABLE` is the `const` view [`cmd::by_name`] scans, -/// since a `static` cannot be read during const evaluation. -pub(crate) static SPECS: &[Spec] = TABLE; - -const TABLE: &[Spec] = &[ - // ── Movement (BINDING_HEADER_MOVEMENT) ────────────────────────────────────────────── - spec!("MOVEANDSTEER", MOVEMENT, Kind::Held, Some("BUTTON3"), None), - spec!("MOVEFORWARD", MOVEMENT, Kind::Held, Some("W"), Some("UP")), - spec!( - "MOVEBACKWARD", - MOVEMENT, - Kind::Held, - Some("S"), - Some("DOWN") - ), - spec!("TURNLEFT", MOVEMENT, Kind::Held, Some("A"), Some("LEFT")), - spec!("TURNRIGHT", MOVEMENT, Kind::Held, Some("D"), Some("RIGHT")), - spec!("STRAFELEFT", MOVEMENT, Kind::Held, Some("Q"), None), - spec!("STRAFERIGHT", MOVEMENT, Kind::Held, Some("E"), None), - spec!("JUMP", MOVEMENT, Kind::Host, Some("SPACE"), Some("NUMPAD0")), - spec!("SITORSTAND", MOVEMENT, Kind::Host, Some("X"), None), - spec!("TOGGLESHEATH", MOVEMENT, Kind::Host, Some("Z"), None), - spec!( - "TOGGLEAUTORUN", - MOVEMENT, - Kind::Host, - Some("NUMLOCK"), - Some("BUTTON4") - ), - spec!( - "TOGGLERUN", - MOVEMENT, - Kind::Host, - Some("NUMPADDIVIDE"), - None - ), - spec!( - "FOLLOWTARGET", - MOVEMENT, - Kind::Edge(r#"FollowUnit("target")"#), - None, - None - ), - // ── Chat (BINDING_HEADER_CHAT) ────────────────────────────────────────────────────── - spec!( - "OPENCHAT", - CHAT, - Kind::Edge("ChatFrame_OpenChat(\"\")"), - Some("ENTER"), - None - ), - spec!( - "OPENCHATSLASH", - CHAT, - Kind::Edge("ChatFrame_OpenChat(\"/\")"), - Some("/"), - None - ), - spec!( - "CHATPAGEUP", - CHAT, - Kind::Edge("ChatFrame_ChatPageUp()"), - Some("PAGEUP"), - None - ), - spec!( - "CHATPAGEDOWN", - CHAT, - Kind::Edge("ChatFrame_ChatPageDown()"), - Some("PAGEDOWN"), - None - ), - spec!( - "CHATBOTTOM", - CHAT, - Kind::Edge("ChatFrame_ScrollToBottom()"), - Some("SHIFT-PAGEDOWN"), - None - ), - spec!( - "REPLY", - CHAT, - Kind::Edge("ChatFrame_ReplyTell()"), - Some("R"), - None - ), - // The last person you told, not the last who told you (`ChatFrame.lua:1650`). - spec!( - "REPLY2", - CHAT, - Kind::Edge("ChatFrame_ReplyTell2()"), - Some("SHIFT-R"), - None - ), - // The combat-log four (`Bindings.xml:108-119`): ChatFrame2's paging and `ToggleCombatLog`. - spec!( - "COMBATLOGPAGEUP", - CHAT, - Kind::Edge("ChatFrame2:PageUp()"), - Some("CTRL-PAGEUP"), - None - ), - spec!( - "COMBATLOGPAGEDOWN", - CHAT, - Kind::Edge("ChatFrame2:PageDown()"), - Some("CTRL-PAGEDOWN"), - None - ), - spec!( - "COMBATLOGBOTTOM", - CHAT, - Kind::Edge("ChatFrame2:ScrollToBottom()"), - Some("CTRL-SHIFT-PAGEDOWN"), - None - ), - spec!( - "TOGGLECOMBATLOG", - CHAT, - Kind::Edge("ToggleCombatLog()"), - Some("SHIFT-C"), - None - ), - // ── Action bar (BINDING_HEADER_ACTIONBAR) ─────────────────────────────────────────── - // The `runOnUp` pair (`Bindings.xml:121`): down shows the pushed visual, up fires. - spec!( - "ACTIONBUTTON1", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(1)", "ActionButtonUp(1)"), - Some("1"), - None - ), - spec!( - "ACTIONBUTTON2", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(2)", "ActionButtonUp(2)"), - Some("2"), - None - ), - spec!( - "ACTIONBUTTON3", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(3)", "ActionButtonUp(3)"), - Some("3"), - None - ), - spec!( - "ACTIONBUTTON4", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(4)", "ActionButtonUp(4)"), - Some("4"), - None - ), - spec!( - "ACTIONBUTTON5", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(5)", "ActionButtonUp(5)"), - Some("5"), - None - ), - spec!( - "ACTIONBUTTON6", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(6)", "ActionButtonUp(6)"), - Some("6"), - None - ), - spec!( - "ACTIONBUTTON7", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(7)", "ActionButtonUp(7)"), - Some("7"), - None - ), - spec!( - "ACTIONBUTTON8", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(8)", "ActionButtonUp(8)"), - Some("8"), - None - ), - spec!( - "ACTIONBUTTON9", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(9)", "ActionButtonUp(9)"), - Some("9"), - None - ), - spec!( - "ACTIONBUTTON10", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(10)", "ActionButtonUp(10)"), - Some("0"), - None - ), - spec!( - "ACTIONBUTTON11", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(11)", "ActionButtonUp(11)"), - Some("-"), - None - ), - spec!( - "ACTIONBUTTON12", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(12)", "ActionButtonUp(12)"), - Some("="), - None - ), - // ── Self-cast (`Bindings.xml:205-288`) ───────────────────────────────────────────── - // ACTIONBUTTON's halves with `ActionButtonUp`'s `onSelf` argument set, which stock - // `ActionButtonUp` forwards to `UseAction`. - spec!( - "SELFACTIONBUTTON1", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(1)", "ActionButtonUp(1, 1)"), - Some("ALT-1"), - None - ), - spec!( - "SELFACTIONBUTTON2", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(2)", "ActionButtonUp(2, 1)"), - Some("ALT-2"), - None - ), - spec!( - "SELFACTIONBUTTON3", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(3)", "ActionButtonUp(3, 1)"), - Some("ALT-3"), - None - ), - spec!( - "SELFACTIONBUTTON4", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(4)", "ActionButtonUp(4, 1)"), - Some("ALT-4"), - None - ), - spec!( - "SELFACTIONBUTTON5", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(5)", "ActionButtonUp(5, 1)"), - Some("ALT-5"), - None - ), - spec!( - "SELFACTIONBUTTON6", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(6)", "ActionButtonUp(6, 1)"), - Some("ALT-6"), - None - ), - spec!( - "SELFACTIONBUTTON7", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(7)", "ActionButtonUp(7, 1)"), - Some("ALT-7"), - None - ), - spec!( - "SELFACTIONBUTTON8", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(8)", "ActionButtonUp(8, 1)"), - Some("ALT-8"), - None - ), - spec!( - "SELFACTIONBUTTON9", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(9)", "ActionButtonUp(9, 1)"), - Some("ALT-9"), - None - ), - spec!( - "SELFACTIONBUTTON10", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(10)", "ActionButtonUp(10, 1)"), - Some("ALT-0"), - None - ), - spec!( - "SELFACTIONBUTTON11", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(11)", "ActionButtonUp(11, 1)"), - Some("ALT--"), - None - ), - spec!( - "SELFACTIONBUTTON12", - ACTIONBAR, - Kind::EdgeUpDown("ActionButtonDown(12)", "ActionButtonUp(12, 1)"), - Some("ALT-="), - None - ), - // The reference body is `ShapeshiftBar_ChangeForm(n)`; this clicks the visible stock - // button, whose OnClick calls it (`BonusActionBarFrame.xml:37`). - spec!( - "SHAPESHIFTBUTTON1", - ACTIONBAR, - Kind::Edge( - r#"local b = getglobal("ShapeshiftButton1"); if b and b:IsVisible() then b:Click() end"# - ), - Some("CTRL-F1"), - None - ), - spec!( - "SHAPESHIFTBUTTON2", - ACTIONBAR, - Kind::Edge( - r#"local b = getglobal("ShapeshiftButton2"); if b and b:IsVisible() then b:Click() end"# - ), - Some("CTRL-F2"), - None - ), - spec!( - "SHAPESHIFTBUTTON3", - ACTIONBAR, - Kind::Edge( - r#"local b = getglobal("ShapeshiftButton3"); if b and b:IsVisible() then b:Click() end"# - ), - Some("CTRL-F3"), - None - ), - spec!( - "SHAPESHIFTBUTTON4", - ACTIONBAR, - Kind::Edge( - r#"local b = getglobal("ShapeshiftButton4"); if b and b:IsVisible() then b:Click() end"# - ), - Some("CTRL-F4"), - None - ), - spec!( - "SHAPESHIFTBUTTON5", - ACTIONBAR, - Kind::Edge( - r#"local b = getglobal("ShapeshiftButton5"); if b and b:IsVisible() then b:Click() end"# - ), - Some("CTRL-F5"), - None - ), - spec!( - "SHAPESHIFTBUTTON6", - ACTIONBAR, - Kind::Edge( - r#"local b = getglobal("ShapeshiftButton6"); if b and b:IsVisible() then b:Click() end"# - ), - Some("CTRL-F6"), - None - ), - spec!( - "SHAPESHIFTBUTTON7", - ACTIONBAR, - Kind::Edge( - r#"local b = getglobal("ShapeshiftButton7"); if b and b:IsVisible() then b:Click() end"# - ), - Some("CTRL-F7"), - None - ), - spec!( - "SHAPESHIFTBUTTON8", - ACTIONBAR, - Kind::Edge( - r#"local b = getglobal("ShapeshiftButton8"); if b and b:IsVisible() then b:Click() end"# - ), - Some("CTRL-F8"), - None - ), - spec!( - "SHAPESHIFTBUTTON9", - ACTIONBAR, - Kind::Edge( - r#"local b = getglobal("ShapeshiftButton9"); if b and b:IsVisible() then b:Click() end"# - ), - Some("CTRL-F9"), - None - ), - spec!( - "SHAPESHIFTBUTTON10", - ACTIONBAR, - Kind::Edge( - r#"local b = getglobal("ShapeshiftButton10"); if b and b:IsVisible() then b:Click() end"# - ), - Some("CTRL-F10"), - None - ), - // The pet bar, which 1.12 names BONUSACTIONBUTTON under the ACTIONBAR header - // (`Bindings.xml:321-390`); its `BonusActionButtonDown/Up` only call - // `PetActionButtonDown/Up` (`BonusActionBarFrame.lua:106-112`). - spec!( - "BONUSACTIONBUTTON1", - ACTIONBAR, - Kind::EdgeUpDown("PetActionButtonDown(1)", "PetActionButtonUp(1)"), - Some("CTRL-1"), - None - ), - spec!( - "BONUSACTIONBUTTON2", - ACTIONBAR, - Kind::EdgeUpDown("PetActionButtonDown(2)", "PetActionButtonUp(2)"), - Some("CTRL-2"), - None - ), - spec!( - "BONUSACTIONBUTTON3", - ACTIONBAR, - Kind::EdgeUpDown("PetActionButtonDown(3)", "PetActionButtonUp(3)"), - Some("CTRL-3"), - None - ), - spec!( - "BONUSACTIONBUTTON4", - ACTIONBAR, - Kind::EdgeUpDown("PetActionButtonDown(4)", "PetActionButtonUp(4)"), - Some("CTRL-4"), - None - ), - spec!( - "BONUSACTIONBUTTON5", - ACTIONBAR, - Kind::EdgeUpDown("PetActionButtonDown(5)", "PetActionButtonUp(5)"), - Some("CTRL-5"), - None - ), - spec!( - "BONUSACTIONBUTTON6", - ACTIONBAR, - Kind::EdgeUpDown("PetActionButtonDown(6)", "PetActionButtonUp(6)"), - Some("CTRL-6"), - None - ), - spec!( - "BONUSACTIONBUTTON7", - ACTIONBAR, - Kind::EdgeUpDown("PetActionButtonDown(7)", "PetActionButtonUp(7)"), - Some("CTRL-7"), - None - ), - spec!( - "BONUSACTIONBUTTON8", - ACTIONBAR, - Kind::EdgeUpDown("PetActionButtonDown(8)", "PetActionButtonUp(8)"), - Some("CTRL-8"), - None - ), - spec!( - "BONUSACTIONBUTTON9", - ACTIONBAR, - Kind::EdgeUpDown("PetActionButtonDown(9)", "PetActionButtonUp(9)"), - Some("CTRL-9"), - None - ), - spec!( - "BONUSACTIONBUTTON10", - ACTIONBAR, - Kind::EdgeUpDown("PetActionButtonDown(10)", "PetActionButtonUp(10)"), - Some("CTRL-0"), - None - ), - // ── Action-bar pages (`Bindings.xml:391-432`) ────────────────────────────────────── - // Six pages of twelve (slots 1-72). `ChangeActionBarPage` is an engine verb that only fires - // `ACTIONBAR_PAGE_CHANGED`; the `ActionBar_Page{Up,Down}` wrap is stock ActionButton.lua's. - spec!( - "ACTIONPAGE1", - ACTIONBAR, - Kind::Edge( - "if ( CURRENT_ACTIONBAR_PAGE ~= 1 ) then CURRENT_ACTIONBAR_PAGE = 1; \ - ChangeActionBarPage(); end" - ), - Some("SHIFT-1"), - None - ), - spec!( - "ACTIONPAGE2", - ACTIONBAR, - Kind::Edge( - "if ( CURRENT_ACTIONBAR_PAGE ~= 2 ) then CURRENT_ACTIONBAR_PAGE = 2; \ - ChangeActionBarPage(); end" - ), - Some("SHIFT-2"), - None - ), - spec!( - "ACTIONPAGE3", - ACTIONBAR, - Kind::Edge( - "if ( CURRENT_ACTIONBAR_PAGE ~= 3 ) then CURRENT_ACTIONBAR_PAGE = 3; \ - ChangeActionBarPage(); end" - ), - Some("SHIFT-3"), - None - ), - spec!( - "ACTIONPAGE4", - ACTIONBAR, - Kind::Edge( - "if ( CURRENT_ACTIONBAR_PAGE ~= 4 ) then CURRENT_ACTIONBAR_PAGE = 4; \ - ChangeActionBarPage(); end" - ), - Some("SHIFT-4"), - None - ), - spec!( - "ACTIONPAGE5", - ACTIONBAR, - Kind::Edge( - "if ( CURRENT_ACTIONBAR_PAGE ~= 5 ) then CURRENT_ACTIONBAR_PAGE = 5; \ - ChangeActionBarPage(); end" - ), - Some("SHIFT-5"), - None - ), - spec!( - "ACTIONPAGE6", - ACTIONBAR, - Kind::Edge( - "if ( CURRENT_ACTIONBAR_PAGE ~= 6 ) then CURRENT_ACTIONBAR_PAGE = 6; \ - ChangeActionBarPage(); end" - ), - Some("SHIFT-6"), - None - ), - spec!( - "PREVIOUSACTIONPAGE", - ACTIONBAR, - Kind::Edge("ActionBar_PageDown()"), - Some("SHIFT-UP"), - Some("SHIFT-MOUSEWHEELUP") - ), - spec!( - "NEXTACTIONPAGE", - ACTIONBAR, - Kind::Edge("ActionBar_PageUp()"), - Some("SHIFT-DOWN"), - Some("SHIFT-MOUSEWHEELDOWN") - ), - // Flips the `LOCK_ACTIONBAR` uvar the Options window's Lock ActionBars row writes - // (`Bindings.xml:433-439`). - spec!( - "TOGGLEACTIONBARLOCK", - ACTIONBAR, - Kind::Edge( - r#"if LOCK_ACTIONBAR == "1" then LOCK_ACTIONBAR = "0" else LOCK_ACTIONBAR = "1" end"# - ), - None, - None - ), - spec!( - "TOGGLEAUTOSELFCAST", - ACTIONBAR, - Kind::Edge( - "if ( GetCVar(\"autoSelfCast\") == \"1\" ) then SetCVar(\"autoSelfCast\", \"0\"); \ - else SetCVar(\"autoSelfCast\", \"1\"); end" - ), - None, - None - ), - // ── Targeting (BINDING_HEADER_TARGETING) ──────────────────────────────────────────── - spec!( - "TARGETNEARESTENEMY", - TARGETING, - Kind::Host, - Some("TAB"), - None - ), - spec!( - "TARGETPREVIOUSENEMY", - TARGETING, - Kind::Host, - Some("SHIFT-TAB"), - None - ), - // The friendly scan; `1` is the reverse flag (`Bindings.xml:458`). - spec!( - "TARGETNEARESTFRIEND", - TARGETING, - Kind::Edge("TargetNearestFriend()"), - Some("CTRL-TAB"), - None - ), - spec!( - "TARGETPREVIOUSFRIEND", - TARGETING, - Kind::Edge("TargetNearestFriend(1)"), - Some("CTRL-SHIFT-TAB"), - None - ), - // An already-targeted unit falls through to its pet (`Bindings.xml:460-494`). - spec!( - "TARGETSELF", - TARGETING, - Kind::Edge( - r#"if UnitIsUnit("player", "target") then TargetUnit("pet") else TargetUnit("player") end"# - ), - Some("F1"), - None - ), - spec!( - "TARGETPARTYMEMBER1", - TARGETING, - Kind::Edge( - r#"if UnitIsUnit("party1", "target") then TargetUnit("partypet1") else TargetUnit("party1") end"# - ), - Some("F2"), - None - ), - spec!( - "TARGETPARTYMEMBER2", - TARGETING, - Kind::Edge( - r#"if UnitIsUnit("party2", "target") then TargetUnit("partypet2") else TargetUnit("party2") end"# - ), - Some("F3"), - None - ), - spec!( - "TARGETPARTYMEMBER3", - TARGETING, - Kind::Edge( - r#"if UnitIsUnit("party3", "target") then TargetUnit("partypet3") else TargetUnit("party3") end"# - ), - Some("F4"), - None - ), - spec!( - "TARGETPARTYMEMBER4", - TARGETING, - Kind::Edge( - r#"if UnitIsUnit("party4", "target") then TargetUnit("partypet4") else TargetUnit("party4") end"# - ), - Some("F5"), - None - ), - spec!( - "TARGETPET", - TARGETING, - Kind::Edge(r#"TargetUnit("pet")"#), - Some("SHIFT-F1"), - None - ), - spec!( - "TARGETPARTYPET1", - TARGETING, - Kind::Edge(r#"TargetUnit("partypet1")"#), - Some("SHIFT-F2"), - None - ), - spec!( - "TARGETPARTYPET2", - TARGETING, - Kind::Edge(r#"TargetUnit("partypet2")"#), - Some("SHIFT-F3"), - None - ), - spec!( - "TARGETPARTYPET3", - TARGETING, - Kind::Edge(r#"TargetUnit("partypet3")"#), - Some("SHIFT-F4"), - None - ), - spec!( - "TARGETPARTYPET4", - TARGETING, - Kind::Edge(r#"TargetUnit("partypet4")"#), - Some("SHIFT-F5"), - None - ), - spec!( - "TARGETLASTHOSTILE", - TARGETING, - Kind::Edge("TargetLastEnemy()"), - Some("G"), - None - ), - spec!( - "ASSISTTARGET", - TARGETING, - Kind::Edge(r#"AssistUnit("target")"#), - Some("F"), - None - ), - // Deviation: V and Shift-V toggle their own kind alone, where 1.12's bodies - // (`Bindings.xml:516-537`) turn the other kind off. - spec!( - "NAMEPLATES", - TARGETING, - Kind::Edge( - "if ( NAMEPLATES_ON ) then HideNameplates(); NAMEPLATES_ON = nil; \ - else ShowNameplates(); NAMEPLATES_ON = 1; end" - ), - Some("V"), - None - ), - spec!( - "FRIENDNAMEPLATES", - TARGETING, - Kind::Edge( - "if ( FRIENDNAMEPLATES_ON ) then HideFriendNameplates(); FRIENDNAMEPLATES_ON = nil; \ - else ShowFriendNameplates(); FRIENDNAMEPLATES_ON = 1; end" - ), - Some("SHIFT-V"), - None - ), - // 1.12's body (`Bindings.xml:538-550`). - spec!( - "ALLNAMEPLATES", - TARGETING, - Kind::Edge( - "if ( not (NAMEPLATES_ON and FRIENDNAMEPLATES_ON) ) then \ - ShowNameplates(); NAMEPLATES_ON = 1; ShowFriendNameplates(); FRIENDNAMEPLATES_ON = 1; \ - else HideNameplates(); NAMEPLATES_ON = nil; HideFriendNameplates(); \ - FRIENDNAMEPLATES_ON = nil; end" - ), - Some("CTRL-V"), - None - ), - spec!("ATTACKTARGET", TARGETING, Kind::Host, Some("T"), None), - spec!( - "PETATTACK", - TARGETING, - Kind::Edge("PetAttack()"), - Some("SHIFT-T"), - None - ), - // ── Interface panels (BINDING_HEADER_INTERFACE) ───────────────────────────────────── - spec!( - "TOGGLECHARACTER0", - INTERFACE, - Kind::Edge(r#"ToggleCharacter("PaperDollFrame")"#), - Some("C"), - None - ), - // The bag family: `B` opens the backpack alone, `SHIFT-B` every bag. Each body is the bare - // global, as in `Bindings.xml`, never a button's OnClick, which would add its checked state. - spec!( - "TOGGLEBACKPACK", - INTERFACE, - Kind::Edge("ToggleBackpack()"), - Some("B"), - Some("F12") - ), - // TOGGLEBAGn is bag 5-n, so F8 opens the slot farthest from the backpack - // (`Bindings.xml:564-575`). - spec!( - "TOGGLEBAG1", - INTERFACE, - Kind::Edge("ToggleBag(4)"), - Some("F8"), - None - ), - spec!( - "TOGGLEBAG2", - INTERFACE, - Kind::Edge("ToggleBag(3)"), - Some("F9"), - None - ), - spec!( - "TOGGLEBAG3", - INTERFACE, - Kind::Edge("ToggleBag(2)"), - Some("F10"), - None - ), - spec!( - "TOGGLEBAG4", - INTERFACE, - Kind::Edge("ToggleBag(1)"), - Some("F11"), - None - ), - spec!( - "OPENALLBAGS", - INTERFACE, - Kind::Edge("OpenAllBags()"), - Some("SHIFT-B"), - None - ), - spec!( - "TOGGLEKEYRING", - INTERFACE, - Kind::Edge("ToggleKeyRing()"), - None, - None - ), - spec!( - "TOGGLESPELLBOOK", - INTERFACE, - Kind::Edge("ToggleSpellBook(BOOKTYPE_SPELL)"), - Some("P"), - None - ), - // The pet book is the same window forked on `bookType`. - spec!( - "TOGGLEPETBOOK", - INTERFACE, - Kind::Edge("ToggleSpellBook(BOOKTYPE_PET)"), - Some("SHIFT-I"), - None - ), - spec!( - "TOGGLETALENTS", - INTERFACE, - Kind::Edge("ToggleTalentFrame()"), - Some("N"), - None - ), - // TOGGLECHARACTERn is the reference's page number, not a tab index: 0 PaperDoll, 1 Skill, - // 2 Reputation, 3 PetPaperDoll, 4 Honor; the file runs them 4, 3, 2, 1. - spec!( - "TOGGLECHARACTER4", - INTERFACE, - Kind::Edge(r#"ToggleCharacter("HonorFrame")"#), - Some("H"), - None - ), - spec!( - "TOGGLECHARACTER3", - INTERFACE, - Kind::Edge(r#"ToggleCharacter("PetPaperDollFrame")"#), - Some("SHIFT-P"), - None - ), - spec!( - "TOGGLECHARACTER2", - INTERFACE, - Kind::Edge(r#"ToggleCharacter("ReputationFrame")"#), - Some("U"), - None - ), - spec!( - "TOGGLECHARACTER1", - INTERFACE, - Kind::Edge(r#"ToggleCharacter("SkillFrame")"#), - Some("K"), - None - ), - spec!( - "TOGGLEQUESTLOG", - INTERFACE, - Kind::Edge("ToggleQuestLog()"), - Some("L"), - None - ), - spec!( - "TOGGLEGAMEMENU", - INTERFACE, - Kind::Edge("ToggleGameMenu()"), - Some("ESCAPE"), - None - ), - spec!( - "TOGGLEMINIMAP", - INTERFACE, - Kind::Edge( - r#"if MinimapCluster:IsVisible() then MinimapCluster:Hide() else MinimapCluster:Show() end"# - ), - None, - None - ), - spec!( - "TOGGLEWORLDMAP", - INTERFACE, - Kind::Edge("ToggleWorldMap()"), - Some("M"), - None - ), - spec!( - "TOGGLESOCIAL", - INTERFACE, - Kind::Edge("ToggleFriendsFrame()"), - Some("O"), - None - ), - spec!( - "TOGGLEFRIENDSTAB", - INTERFACE, - Kind::Edge("ToggleFriendsFrame(1)"), - None, - None - ), - spec!( - "TOGGLEWHOTAB", - INTERFACE, - Kind::Edge("ToggleFriendsFrame(2)"), - None, - None - ), - spec!( - "TOGGLEGUILDTAB", - INTERFACE, - Kind::Edge("ToggleFriendsFrame(3)"), - None, - None - ), - spec!( - "TOGGLERAIDTAB", - INTERFACE, - Kind::Edge("ToggleFriendsFrame(4)"), - None, - None - ), - // `ToggleWorldStateScoreFrame` is stock WorldStateFrame.lua's, `ToggleBattlefieldMinimap` - // stock `UIParent.lua:216` (`Bindings.xml:630-635`). - spec!( - "TOGGLEWORLDSTATESCORES", - INTERFACE, - Kind::Edge("ToggleWorldStateScoreFrame()"), - Some("SHIFT-SPACE"), - None - ), - spec!( - "TOGGLEBATTLEFIELDMINIMAP", - INTERFACE, - Kind::Edge("ToggleBattlefieldMinimap()"), - Some("SHIFT-M"), - None - ), - // ── Miscellaneous (BINDING_HEADER_MISC) ───────────────────────────────────────────── - spec!( - "MINIMAPZOOMIN", - MISC, - Kind::Edge("Minimap_ZoomInClick()"), - Some("NUMPADPLUS"), - None - ), - spec!( - "MINIMAPZOOMOUT", - MISC, - Kind::Edge("Minimap_ZoomOutClick()"), - Some("NUMPADMINUS"), - None - ), - // The CVars stock SoundOptionsFrame.lua moves: `MasterSoundEffects` is the master enable, - // `MasterVolume` steps by 0.1. - spec!( - "TOGGLEMUSIC", - MISC, - Kind::Edge( - r#"if GetCVar("EnableMusic") == "1" then SetCVar("EnableMusic", "0") else SetCVar("EnableMusic", "1") end"# - ), - Some("CTRL-M"), - None - ), - spec!( - "TOGGLESOUND", - MISC, - Kind::Edge( - r#"if GetCVar("MasterSoundEffects") == "1" then SetCVar("MasterSoundEffects", "0") else SetCVar("MasterSoundEffects", "1") end"# - ), - Some("CTRL-S"), - None - ), - spec!( - "MASTERVOLUMEUP", - MISC, - Kind::Edge( - r#"local v = tonumber(GetCVar("MasterVolume")) + 0.1; if v > 1 then v = 1 end; SetCVar("MasterVolume", v)"# - ), - Some("CTRL-="), - None - ), - spec!( - "MASTERVOLUMEDOWN", - MISC, - Kind::Edge( - r#"local v = tonumber(GetCVar("MasterVolume")) - 0.1; if v < 0 then v = 0 end; SetCVar("MasterVolume", v)"# - ), - Some("CTRL--"), - None - ), - // ALT-Z is the shipped default; a `bindings-cache.wtf` holding CTRL-Z is a player rebind. - spec!("TOGGLEUI", MISC, Kind::Host, Some("ALT-Z"), None), - // Stock WorldFrame.lua's framerate readout. - spec!( - "TOGGLEFPS", - MISC, - Kind::Edge("ToggleFramerate();"), - Some("CTRL-R"), - None - ), - // `Edge`: the `` has no `runOnUp`, and the dispatcher returns on key-up without it - // (`0x4b7bea`). - spec!( - "SCREENSHOT", - MISC, - Kind::Edge("TakeScreenshot();"), - Some("PRINTSCREEN"), - None - ), - // ── Camera (BINDING_HEADER_CAMERA) ────────────────────────────────────────────────── - // The five camera views (defaults: the table at `0x84f488`). NEXTVIEW/PREVVIEW stop at the - // ends (`0x50faa0`/`0x50fac0`); 1.12 has no SAVEVIEW1/RESETVIEW1 row though the engine - // accepts view 1. - spec!( - "NEXTVIEW", - CAMERA, - Kind::Edge("NextView()"), - Some("END"), - None - ), - spec!( - "PREVVIEW", - CAMERA, - Kind::Edge("PrevView()"), - Some("HOME"), - None - ), - spec!( - "CAMERAZOOMIN", - CAMERA, - Kind::Host, - Some("MOUSEWHEELUP"), - None - ), - spec!( - "CAMERAZOOMOUT", - CAMERA, - Kind::Host, - Some("MOUSEWHEELDOWN"), - None - ), - spec!("SETVIEW1", CAMERA, Kind::Edge("SetView(1)"), None, None), - spec!("SETVIEW2", CAMERA, Kind::Edge("SetView(2)"), None, None), - spec!("SETVIEW3", CAMERA, Kind::Edge("SetView(3)"), None, None), - spec!("SETVIEW4", CAMERA, Kind::Edge("SetView(4)"), None, None), - spec!("SETVIEW5", CAMERA, Kind::Edge("SetView(5)"), None, None), - spec!("SAVEVIEW2", CAMERA, Kind::Edge("SaveView(2)"), None, None), - spec!("SAVEVIEW3", CAMERA, Kind::Edge("SaveView(3)"), None, None), - spec!("SAVEVIEW4", CAMERA, Kind::Edge("SaveView(4)"), None, None), - spec!("SAVEVIEW5", CAMERA, Kind::Edge("SaveView(5)"), None, None), - spec!("RESETVIEW2", CAMERA, Kind::Edge("ResetView(2)"), None, None), - spec!("RESETVIEW3", CAMERA, Kind::Edge("ResetView(3)"), None, None), - spec!("RESETVIEW4", CAMERA, Kind::Edge("ResetView(4)"), None, None), - spec!("RESETVIEW5", CAMERA, Kind::Edge("ResetView(5)"), None, None), - spec!( - "FLIPCAMERAYAW", - CAMERA, - Kind::Edge("FlipCameraYaw(180)"), - None, - None - ), - // ── MultiActionBar (BINDING_HEADER_MULTIACTIONBAR) ────────────────────────────────── - // The four multibars' `runOnUp` pairs (`Bindings.xml:799-1134`), stock MultiActionBars.lua's; - // bars 2-4 under the spacer headers `BLANK`, `BLANK2` and `BLANK3`, as `Bindings.xml:883`, - // `:967` and `:1051` file them. - spec!( - "MULTIACTIONBAR1BUTTON1", - MULTIACTIONBAR, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomLeft", 1)"#, - r#"MultiActionButtonUp("MultiBarBottomLeft", 1)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR1BUTTON2", - MULTIACTIONBAR, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomLeft", 2)"#, - r#"MultiActionButtonUp("MultiBarBottomLeft", 2)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR1BUTTON3", - MULTIACTIONBAR, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomLeft", 3)"#, - r#"MultiActionButtonUp("MultiBarBottomLeft", 3)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR1BUTTON4", - MULTIACTIONBAR, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomLeft", 4)"#, - r#"MultiActionButtonUp("MultiBarBottomLeft", 4)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR1BUTTON5", - MULTIACTIONBAR, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomLeft", 5)"#, - r#"MultiActionButtonUp("MultiBarBottomLeft", 5)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR1BUTTON6", - MULTIACTIONBAR, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomLeft", 6)"#, - r#"MultiActionButtonUp("MultiBarBottomLeft", 6)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR1BUTTON7", - MULTIACTIONBAR, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomLeft", 7)"#, - r#"MultiActionButtonUp("MultiBarBottomLeft", 7)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR1BUTTON8", - MULTIACTIONBAR, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomLeft", 8)"#, - r#"MultiActionButtonUp("MultiBarBottomLeft", 8)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR1BUTTON9", - MULTIACTIONBAR, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomLeft", 9)"#, - r#"MultiActionButtonUp("MultiBarBottomLeft", 9)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR1BUTTON10", - MULTIACTIONBAR, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomLeft", 10)"#, - r#"MultiActionButtonUp("MultiBarBottomLeft", 10)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR1BUTTON11", - MULTIACTIONBAR, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomLeft", 11)"#, - r#"MultiActionButtonUp("MultiBarBottomLeft", 11)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR1BUTTON12", - MULTIACTIONBAR, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomLeft", 12)"#, - r#"MultiActionButtonUp("MultiBarBottomLeft", 12)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR2BUTTON1", - BLANK, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomRight", 1)"#, - r#"MultiActionButtonUp("MultiBarBottomRight", 1)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR2BUTTON2", - BLANK, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomRight", 2)"#, - r#"MultiActionButtonUp("MultiBarBottomRight", 2)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR2BUTTON3", - BLANK, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomRight", 3)"#, - r#"MultiActionButtonUp("MultiBarBottomRight", 3)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR2BUTTON4", - BLANK, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomRight", 4)"#, - r#"MultiActionButtonUp("MultiBarBottomRight", 4)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR2BUTTON5", - BLANK, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomRight", 5)"#, - r#"MultiActionButtonUp("MultiBarBottomRight", 5)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR2BUTTON6", - BLANK, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomRight", 6)"#, - r#"MultiActionButtonUp("MultiBarBottomRight", 6)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR2BUTTON7", - BLANK, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomRight", 7)"#, - r#"MultiActionButtonUp("MultiBarBottomRight", 7)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR2BUTTON8", - BLANK, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomRight", 8)"#, - r#"MultiActionButtonUp("MultiBarBottomRight", 8)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR2BUTTON9", - BLANK, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomRight", 9)"#, - r#"MultiActionButtonUp("MultiBarBottomRight", 9)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR2BUTTON10", - BLANK, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomRight", 10)"#, - r#"MultiActionButtonUp("MultiBarBottomRight", 10)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR2BUTTON11", - BLANK, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomRight", 11)"#, - r#"MultiActionButtonUp("MultiBarBottomRight", 11)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR2BUTTON12", - BLANK, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarBottomRight", 12)"#, - r#"MultiActionButtonUp("MultiBarBottomRight", 12)"# - ), - None, - None - ), - // The vertical bars: `MultiBarRight` is bar 3, `MultiBarLeft` bar 4. - spec!( - "MULTIACTIONBAR3BUTTON1", - BLANK2, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarRight", 1)"#, - r#"MultiActionButtonUp("MultiBarRight", 1)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR3BUTTON2", - BLANK2, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarRight", 2)"#, - r#"MultiActionButtonUp("MultiBarRight", 2)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR3BUTTON3", - BLANK2, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarRight", 3)"#, - r#"MultiActionButtonUp("MultiBarRight", 3)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR3BUTTON4", - BLANK2, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarRight", 4)"#, - r#"MultiActionButtonUp("MultiBarRight", 4)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR3BUTTON5", - BLANK2, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarRight", 5)"#, - r#"MultiActionButtonUp("MultiBarRight", 5)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR3BUTTON6", - BLANK2, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarRight", 6)"#, - r#"MultiActionButtonUp("MultiBarRight", 6)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR3BUTTON7", - BLANK2, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarRight", 7)"#, - r#"MultiActionButtonUp("MultiBarRight", 7)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR3BUTTON8", - BLANK2, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarRight", 8)"#, - r#"MultiActionButtonUp("MultiBarRight", 8)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR3BUTTON9", - BLANK2, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarRight", 9)"#, - r#"MultiActionButtonUp("MultiBarRight", 9)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR3BUTTON10", - BLANK2, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarRight", 10)"#, - r#"MultiActionButtonUp("MultiBarRight", 10)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR3BUTTON11", - BLANK2, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarRight", 11)"#, - r#"MultiActionButtonUp("MultiBarRight", 11)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR3BUTTON12", - BLANK2, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarRight", 12)"#, - r#"MultiActionButtonUp("MultiBarRight", 12)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR4BUTTON1", - BLANK3, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarLeft", 1)"#, - r#"MultiActionButtonUp("MultiBarLeft", 1)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR4BUTTON2", - BLANK3, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarLeft", 2)"#, - r#"MultiActionButtonUp("MultiBarLeft", 2)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR4BUTTON3", - BLANK3, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarLeft", 3)"#, - r#"MultiActionButtonUp("MultiBarLeft", 3)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR4BUTTON4", - BLANK3, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarLeft", 4)"#, - r#"MultiActionButtonUp("MultiBarLeft", 4)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR4BUTTON5", - BLANK3, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarLeft", 5)"#, - r#"MultiActionButtonUp("MultiBarLeft", 5)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR4BUTTON6", - BLANK3, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarLeft", 6)"#, - r#"MultiActionButtonUp("MultiBarLeft", 6)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR4BUTTON7", - BLANK3, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarLeft", 7)"#, - r#"MultiActionButtonUp("MultiBarLeft", 7)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR4BUTTON8", - BLANK3, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarLeft", 8)"#, - r#"MultiActionButtonUp("MultiBarLeft", 8)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR4BUTTON9", - BLANK3, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarLeft", 9)"#, - r#"MultiActionButtonUp("MultiBarLeft", 9)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR4BUTTON10", - BLANK3, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarLeft", 10)"#, - r#"MultiActionButtonUp("MultiBarLeft", 10)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR4BUTTON11", - BLANK3, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarLeft", 11)"#, - r#"MultiActionButtonUp("MultiBarLeft", 11)"# - ), - None, - None - ), - spec!( - "MULTIACTIONBAR4BUTTON12", - BLANK3, - Kind::EdgeUpDown( - r#"MultiActionButtonDown("MultiBarLeft", 12)"#, - r#"MultiActionButtonUp("MultiBarLeft", 12)"# - ), - None, - None - ), - // ── Raid targeting (BINDING_HEADER_RAID_TARGET) ───────────────────────────────────── - // `SetRaidTargetIcon` toggles an icon the unit already wears off; 0 clears. - spec!( - "RAIDTARGET1", - RAID_TARGET, - Kind::Edge(r#"SetRaidTargetIcon("target", 1)"#), - None, - None - ), - spec!( - "RAIDTARGET2", - RAID_TARGET, - Kind::Edge(r#"SetRaidTargetIcon("target", 2)"#), - None, - None - ), - spec!( - "RAIDTARGET3", - RAID_TARGET, - Kind::Edge(r#"SetRaidTargetIcon("target", 3)"#), - None, - None - ), - spec!( - "RAIDTARGET4", - RAID_TARGET, - Kind::Edge(r#"SetRaidTargetIcon("target", 4)"#), - None, - None - ), - spec!( - "RAIDTARGET5", - RAID_TARGET, - Kind::Edge(r#"SetRaidTargetIcon("target", 5)"#), - None, - None - ), - spec!( - "RAIDTARGET6", - RAID_TARGET, - Kind::Edge(r#"SetRaidTargetIcon("target", 6)"#), - None, - None - ), - spec!( - "RAIDTARGET7", - RAID_TARGET, - Kind::Edge(r#"SetRaidTargetIcon("target", 7)"#), - None, - None - ), - spec!( - "RAIDTARGET8", - RAID_TARGET, - Kind::Edge(r#"SetRaidTargetIcon("target", 8)"#), - None, - None - ), - spec!( - "RAIDTARGETNONE", - RAID_TARGET, - Kind::Edge(r#"SetRaidTargetIcon("target", 0)"#), - None, - None - ), -]; - -/// One 1.12 binding command this client does not register, and the mechanism it waits on; a -/// test fails once every global in [`needs`] is defined. -/// -/// [`needs`]: Absent::needs -pub(crate) struct Absent { - /// The 1.12 command name, exactly as `Bindings.xml` spells it. - pub name: &'static str, - /// Lua globals the reference's body calls that this client does not define; never empty, and - /// once all are defined the row belongs in [`SPECS`]. - #[cfg_attr( - not(test), - expect( - dead_code, - reason = "the falsifier is the gate test's to read, and the gate is the whole point \ - of the field; an `expect` rather than an `allow` so the attribute goes by \ - itself the day a runtime reader wants it" - ) - )] - pub needs: &'static [&'static str], - /// What would have to exist, in one line. - pub why: &'static str, -} - -macro_rules! absent { - ($name:literal, [$($needs:literal),* $(,)?], $why:literal) => { - Absent { - name: $name, - needs: &[$($needs),*], - why: $why, - } - }; -} - -/// The 1.12 commands benilla does not implement, in `Bindings.xml` order; with `SPECS`, exactly -/// the client's 228 live bindings. -pub(crate) static ABSENT: &[Absent] = &[ - // ── Movement ──────────────────────────────────────────────────────────────────────── - absent!( - "PITCHUP", - ["PitchUpStart", "PitchUpStop"], - "no keyboard pitch: the mover has no pitch axis at all — swim and hover attitude ride the \ - move flags the server echoes, and nothing steers them" - ), - absent!( - "PITCHDOWN", - ["PitchDownStart", "PitchDownStop"], - "no keyboard pitch — see PITCHUP" - ), - // ── Misc ──────────────────────────────────────────────────────────────────────────── - // The nine `hidden="true" debug="true"` rows: benilla's instruments for these answer to the - // dev plane's chords, not to a binding. - // Stock `StatsFrame.lua` defines `ToggleStats`; the readout it shows is `GetDebugStats`, - // which answers nothing in the release build (`0x488af0`). - absent!( - "TOGGLESTATS", - ["ToggleStats", "GetDebugStats"], - "a dev-plane instrument on a dev chord" - ), - absent!( - "TOGGLETRIS", - ["ToggleTris"], - "a dev-plane instrument on a dev chord" - ), - absent!( - "TOGGLEPORTALS", - ["TogglePortals"], - "a dev-plane instrument on a dev chord" - ), - absent!( - "TOGGLECOLLISION", - ["ToggleCollision"], - "a dev-plane instrument on a dev chord" - ), - absent!( - "TOGGLECOLLISIONDISPLAY", - ["ToggleCollisionDisplay"], - "a dev-plane instrument on a dev chord" - ), - absent!( - "TOGGLEPLAYERBOUNDS", - ["TogglePlayerBounds"], - "a dev-plane instrument on a dev chord" - ), - absent!( - "TOGGLEPERFORMANCEDISPLAY", - ["TogglePerformanceDisplay"], - "a dev-plane instrument on a dev chord" - ), - absent!( - "TOGGLEPERFORMANCEVALUES", - ["TogglePerformanceValues"], - "a dev-plane instrument on a dev chord" - ), - absent!( - "RESETPERFORMANCEVALUES", - ["ResetPerformanceValues"], - "a dev-plane instrument on a dev chord" - ), - // ── The mouse's own three (hidden) ────────────────────────────────────────────────── - // The reference's mouse-look bindings (BUTTON2, BUTTON1, CTRL-BUTTON1), hidden from the - // window; benilla's mouse-look is hardwired in `player/camera.rs`. - absent!( - "TURNORACTION", - ["TurnOrActionStart", "TurnOrActionStop"], - "right-button mouse-look is hardwired, not routed through a binding" - ), - absent!( - "CAMERAORSELECTORMOVE", - ["CameraOrSelectOrMoveStart", "CameraOrSelectOrMoveStop"], - "left-button select/move is hardwired, not routed through a binding" - ), - absent!( - "CAMERAORSELECTORMOVESTICKY", - ["CameraOrSelectOrMoveStart", "CameraOrSelectOrMoveStop"], - "left-button select/move is hardwired, not routed through a binding" - ), - // ── iTunes remote (platform="mac") ────────────────────────────────────────────────── - // 1.12's only `platform="mac"` rows; they remote the system music player, not the game's. - absent!( - "ITUNES_PLAYPAUSE", - ["MusicPlayer_PlayPause"], - "platform=\"mac\", and there is no system music player to remote" - ), - absent!( - "ITUNES_NEXTTRACK", - ["MusicPlayer_NextTrack"], - "platform=\"mac\", and there is no system music player to remote" - ), - absent!( - "ITUNES_BACKTRACK", - ["MusicPlayer_BackTrack"], - "platform=\"mac\", and there is no system music player to remote" - ), - absent!( - "ITUNES_VOLUMEUP", - ["MusicPlayer_VolumeUp"], - "platform=\"mac\", and there is no system music player to remote" - ), - absent!( - "ITUNES_VOLUMEDOWN", - ["MusicPlayer_VolumeDown"], - "platform=\"mac\", and there is no system music player to remote" - ), -]; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn every_default_chord_parses() { - for s in SPECS { - for d in [s.d1, s.d2].into_iter().flatten() { - assert!( - crate::bindings::chord::Chord::parse(d).is_some(), - "{}: default '{d}' does not parse", - s.name - ); - } - } - } - - /// One key, one command: a duplicate default would leave the loser of the steal pass unbound. - #[test] - fn no_two_commands_ship_the_same_default_chord() { - let mut seen: std::collections::HashMap<&str, &str> = std::collections::HashMap::new(); - for s in SPECS { - for d in [s.d1, s.d2].into_iter().flatten() { - if let Some(prev) = seen.insert(d, s.name) { - panic!("default '{d}' is on both {prev} and {}", s.name); - } - } - } - } - - #[test] - fn names_are_unique() { - let mut seen = std::collections::HashSet::new(); - for s in SPECS { - assert!(seen.insert(s.name), "duplicate command {}", s.name); - } - } - - /// Every row's `runOnUp` against the install's `Bindings.xml`, and its default chords against - /// `WTF\DefaultBindings.wtf`. - #[test] - fn the_registry_matches_the_installs_own_bindings() { - let data = benilla_formats::wow_data_or_skip!(); - let mut chain = benilla_formats::open_chain(&data).expect("open the 1.12 patch chain"); - - let xml = String::from_utf8_lossy( - &chain - .read_file("Interface\\FrameXML\\Bindings.xml") - .expect("the install carries Bindings.xml"), - ) - .into_owned(); - let reference = benilla_ui::bindings_xml::parse(&xml).expect("Blizzard's own file parses"); - let run_on_up: std::collections::HashMap<&str, bool> = reference - .iter() - .map(|b| (b.name.as_str(), b.run_on_up)) - .collect(); - - // `bind `; file order is the Key 1, Key 2 order. - let wtf = String::from_utf8_lossy( - &chain - .read_file("WTF\\DefaultBindings.wtf") - .expect("the install carries DefaultBindings.wtf"), - ) - .into_owned(); - let mut defaults: std::collections::HashMap<&str, Vec<&str>> = - std::collections::HashMap::new(); - for line in wtf.lines() { - let mut it = line.split_whitespace(); - if let (Some("bind"), Some(chord), Some(command), None) = - (it.next(), it.next(), it.next(), it.next()) - { - defaults.entry(command).or_default().push(chord); - } - } - - for s in SPECS { - let Some(&reference_run_on_up) = run_on_up.get(s.name) else { - panic!( - "{} is not a 1.12 binding at all — the tree is meant to be honest", - s.name - ); - }; - assert_eq!( - s.run_on_up(), - reference_run_on_up, - "{}: runOnUp disagrees with the install's Bindings.xml — that flag decides \ - whether a press of this command delivers a release half", - s.name - ); - let ours: Vec<&str> = [s.d1, s.d2].into_iter().flatten().collect(); - assert_eq!( - ours, - defaults.get(s.name).cloned().unwrap_or_default(), - "{}: default chords disagree with the install's DefaultBindings.wtf", - s.name - ); - } - } - - /// Every one of the client's 228 live bindings is in exactly one of [`SPECS`] and [`ABSENT`], - /// none of the twelve `hidden="true"` ones is in `SPECS`, and `SPECS` keeps the file's order. - #[test] - fn every_1_12_command_is_registered_or_recorded_absent() { - let Some(reference) = install_bindings() else { - return; - }; - - let registered: std::collections::HashSet<&str> = SPECS.iter().map(|s| s.name).collect(); - let recorded: std::collections::HashSet<&str> = ABSENT.iter().map(|a| a.name).collect(); - - let mut unwritten = Vec::new(); - for b in &reference { - let name = b.name.as_str(); - match (registered.contains(name), recorded.contains(name)) { - (true, true) => panic!("{name} is both registered and recorded absent"), - (false, false) => unwritten.push(name), - (true, false) => assert!( - !b.hidden, - "{name} is hidden=\"true\" in 1.12 — the Keybindings page would list a row \ - the reference never shows" - ), - (false, true) => {} - } - } - assert!( - unwritten.is_empty(), - "these 1.12 bindings are neither implemented nor recorded absent — add a `spec!` row \ - if the mechanism is here, an `absent!` row with its falsifier if it is not: {unwritten:?}" - ); - - let live: std::collections::HashSet<&str> = - reference.iter().map(|b| b.name.as_str()).collect(); - for a in ABSENT { - assert!( - live.contains(a.name), - "ABSENT names {}, which is not a live 1.12 binding (the six MOVEVIEW* rows are \ - commented out in Blizzard's own file and are not commands)", - a.name - ); - } - - // The table's order is the Keybindings page's row order. - let order: Vec<&str> = reference.iter().map(|b| b.name.as_str()).collect(); - let mut at = 0usize; - for s in SPECS { - let Some(found) = order[at..].iter().position(|n| *n == s.name) else { - panic!( - "{} is out of 1.12 Bindings.xml order — the page renders SPECS order, so this \ - is what the player sees", - s.name - ); - }; - at += found + 1; - } - } - - /// An [`ABSENT`] row whose `needs` are all defined belongs in [`SPECS`]. "Defined" is read - /// from source, not from a VM. A `hidden="true"` row can never move there (the Key Bindings - /// page would list it), so its landed needs are no signal: `TOGGLESTATS`, whose stock body and - /// readout (`ToggleStats`, `GetDebugStats`) are all defined, is one. - #[test] - fn the_absent_commands_are_still_absent() { - let defined = lua_globals_defined(); - let hidden: std::collections::HashSet = install_bindings() - .unwrap_or_default() - .into_iter() - .filter(|b| b.hidden) - .map(|b| b.name) - .collect(); - for a in ABSENT { - if a.needs.is_empty() || hidden.contains(a.name) { - continue; - } - let landed: Vec<&str> = a - .needs - .iter() - .copied() - .filter(|n| defined.contains(*n)) - .collect(); - assert_ne!( - landed.len(), - a.needs.len(), - "{}'s mechanism has landed — {:?} are all defined now, so the row belongs in \ - SPECS. (Recorded reason: {})", - a.name, - a.needs, - a.why - ); - } - } - - /// Unique names, a reason on every row, and no row without `needs`, which could go stale - /// unnoticed. - #[test] - fn the_absent_table_is_well_formed() { - let mut seen = std::collections::HashSet::new(); - for a in ABSENT { - assert!(seen.insert(a.name), "duplicate absent command {}", a.name); - assert!(!a.why.is_empty(), "{}: no reason recorded", a.name); - } - let unfalsifiable: Vec<&str> = ABSENT - .iter() - .filter(|a| a.needs.is_empty()) - .map(|a| a.name) - .collect(); - assert!( - unfalsifiable.is_empty(), - "every absent row must carry a falsifier — a row with no `needs` can go stale \ - unnoticed, which is the whole failure this table exists to end: {unfalsifiable:?}" - ); - } - - /// `lua_globals_defined` sees each store it reads and invents nothing; a blind scanner would - /// pass every [`ABSENT`] row. - #[test] - fn the_global_scanner_sees_both_kinds_of_definition() { - benilla_formats::wow_data_or_skip!(); - let defined = lua_globals_defined(); - for host in ["TargetUnit", "UseAction", "SetBinding"] { - assert!( - defined.contains(host), - "the Rust scan missed the host registration `{host}`" - ); - } - // None of these is in `assets/ui`: `ChangeActionBarPage` is a host registration and the - // other two come from stock CharacterFrame.lua and UIParent.lua off the chain, so this - // loop does not isolate the `assets/ui` scan. - for shipped in ["ToggleCharacter", "ChangeActionBarPage", "ShowUIPanel"] { - assert!( - defined.contains(shipped), - "the scan missed `{shipped}`, a host registration or a stock chain global" - ); - } - // Stock ContainerFrame.lua's, a `FrameXML.toc` row off the chain. - if benilla_formats::wow_data().is_some() { - for sourced in ["ToggleKeyRing", "ToggleBag", "OpenAllBags"] { - assert!( - defined.contains(sourced), - "the chain scan missed `{sourced}` — it is defined in the reference's own \ - ContainerFrame.lua, which the core loads off the player's install" - ); - } - } - assert!( - !defined.contains("BenillaNoSuchGlobalExists"), - "the scanner claims to define a name nobody wrote" - ); - } - - /// The install's `Bindings.xml` has the shape `benilla_ui::bindings_xml` documents; the - /// commented-out `MOVEVIEW*` family must not register. - #[test] - fn the_installs_bindings_xml_reads_as_the_parsers_header_says() { - let Some(binds) = install_bindings() else { - return; - }; - assert_eq!( - binds.len(), - 228, - "1.12.1 ships 228 live bindings — the other six are inside a comment" - ); - assert!(binds.iter().all(|b| !b.name.is_empty())); - assert!( - !binds.iter().any(|b| b.name.starts_with("MOVEVIEW")), - "the commented-out family must not register" - ); - assert_eq!(binds.iter().filter(|b| b.run_on_up).count(), 94); - assert_eq!(binds.iter().filter(|b| b.header.is_some()).count(), 13); - assert_eq!(binds.iter().filter(|b| b.hidden).count(), 12); - assert_eq!( - binds[0].header.as_deref(), - Some("MOVEMENT"), - "the file opens on the MOVEMENT section" - ); - } - - /// Each registered command sits under the header the install's `Bindings.xml` files it under: - /// the last `header=` at or before its row, so the host's `HEADER_*` rows (a category change - /// opens one) are the reference's, the spacers over multibars 2-4 included. - #[test] - fn every_command_sits_under_its_bindings_xml_header() { - let Some(binds) = install_bindings() else { - return; - }; - let mut section = String::new(); - let mut under = std::collections::HashMap::new(); - for b in &binds { - if b.platform.as_deref().is_some_and(|p| p != "windows") { - continue; - } - if let Some(h) = &b.header { - section = format!("BINDING_HEADER_{h}"); - } - under.insert(b.name.clone(), section.clone()); - } - for spec in SPECS { - assert_eq!( - under.get(spec.name).map(String::as_str), - Some(spec.category), - "{} is filed under the wrong header", - spec.name - ); - } - } - - /// The install's own `Bindings.xml`, parsed. - fn install_bindings() -> Option> { - let data = benilla_formats::wow_data_or_skip!(None); - let mut chain = benilla_formats::open_chain(&data).expect("open the 1.12 patch chain"); - let xml = String::from_utf8_lossy( - &chain - .read_file("Interface\\FrameXML\\Bindings.xml") - .expect("the install carries Bindings.xml"), - ) - .into_owned(); - Some(benilla_ui::bindings_xml::parse(&xml).expect("Blizzard's own file parses")) - } - - /// Every Lua global this client defines, read from source: host registrations - /// (`g.set("Name", …)`), function definitions in `assets/ui`'s `