Files
benilla-octo/crates/benilla-app/src/bindings/commands.rs
T
SamandClaude Fable 5.1 2f7cfcabf5 the action bars are the reference's own, and a gift can be wrapped
- the action-bar cluster is the reference's own: `MainMenuBar.xml`, `ActionBarFrame.xml`, `MultiActionBars.xml` and `BonusActionBarFrame.xml` run off the player's own chain, and our `ActionBar.xml` (2047 lines), `MultiBars.xml` and `StanceBar.xml` are deleted — three, since the stock bonus-bar file declares the shapeshift bar too; one engine verb, `ChangeActionBarPage`, fires `ACTIONBAR_PAGE_CHANGED` and nothing else; one load blocker: stock `MultiActionBars.lua` writes the options window's table from its OnLoad, the reference's own load-order hack, so our options window moves to the reference's toc slot; and ten things the reference does that ours did not: an empty button is HIDDEN while showgrid is 0, the bonus overlay sits one pixel right of the main buttons, every slide takes one frame more than its time, and `ActionButton_ShowGrid` ghosts EVERY ring, occupied included (1938)
- a gift can be wrapped: right-clicking wrapping paper fell through to plain use and sent `CMSG_USE_ITEM`, so nothing happened — you could receive a gift and never give one; the wrap is a displayed-cursor MODE plus a stashed slot, never a payload, so `CursorHasItem()` stays false and every stock gate on it stays shut: right-click arms it, a left-click on a bag slot sends `CMSG_WRAP_ITEM`, and the client applies no eligibility test — the server's `ERR_CANT_WRAP_*` reasons are the error line (1934); a duration-limited item counts down: `SMSG_ITEM_TIME_UPDATE` parks a deadline and the tooltip's `ITEM_DURATION` line runs the enchant countdown's bucket ladder, so "Duration: 1 days" is what the reference renders; an update for an item we do not hold is DROPPED, no queue (1933); and a renamed crowd-control refusal names its mechanic — "Can't do that while stunned" — off `SpellMechanic.dbc` (1943)
- a creature paths on a boat: `SMSG_MONSTER_MOVE_TRANSPORT` carries the transport's guid and deck-local coordinates, and is the attach/detach edge; a deck sample lands on the rider or nowhere, never as a world write a few yards from the map origin; and "there is no terrain under a boat" turned out wrong: the ground probe admits GameObject meshes — a boat's `.wmo` is a GameObject, so the probe hits the deck (1936); `MSG_MOVE_TIME_SKIPPED` is read, not dropped — the reference adds the lag to the observed unit's last-wire stamp, so dropping it scheduled their next packet `lag` ms late, a hitch for every player who stalled zoning in near us; and our own settle hold reports once (1935); and the tradeskill tool order was backwards — focus FIRST, so every two-tool recipe had read "Blacksmith Hammer, Anvil" for the reference's "Anvil, Blacksmith Hammer"; the focus's `hasTool` is 1.0 in the reference too (1942)
- the crowd's second sitting, instruments first: the probe line carries the tail — cpu p95/p99 and the six worst frames — because a mean under budget hides the one frame in twenty over it (raid legs railed at 16.67 with p95 at 18–20 ms) and whether that frame was work or a wait is the first question about a stutter, and the visible meshes are counted by lane; then two cuts — a terrain tile furnishes as 4×4 cells of 4×4 chunks, one mesh, one entity, one draw per 133-yd cell instead of per 33-yd chunk (a city horizon is over a thousand chunks a frame, and a cell changes nothing a pixel can see; Stormwind 10.55 → 10.32 `cpu_ms`), and an authored point light is our own `WorldPointLight`, not Bevy's `PointLight`, which was only a data carrier while Bevy ran its whole light lane over every lamp, ~2 % of the alone frame; the sitting's record is 1945, and the code says so (1945)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 20:54:27 +07:00

2395 lines
84 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The command registry (decision 0997) — every binding command benilla actually implements, in
//! 1.12 `Bindings.xml` order, with the 1.12 default chords (byte-real: the client's own
//! `bindings-cache.wtf`, account ONE) and each command's dispatch class.
//!
//! **Honest tree**: a command appears here only over a real engine action — the same law as the
//! options rows (0954). The 1.12 commands with no benilla mechanism yet are absent, not stubbed;
//! the page shows only what's here, and only non-empty categories (era law). Labels/headers are
//! the 1.12 GlobalStrings (`BINDING_NAME_*`/`BINDING_HEADER_*`), defined in the window's XML.
//!
//! **…and the absence is written down** ([`ABSENT`], decision 1745). The honest tree's one hole
//! was that nothing noticed when a mechanism ARRIVED: 0997 promised "each returns the day its
//! mechanism lands, one registry row", and then the keyring (0765), the pet book (1050), the
//! reputation and honor pages, the six action-bar pages and the two vertical multibars (1500)
//! all shipped their mechanism with no row — some for five hundred commits, none of them a
//! mistake anyone could see. `SPECS` ∪ `ABSENT` is now exactly the client's 228 live bindings,
//! and each absent row names the Lua globals whose arrival falsifies it.
//!
//! Three dispatch classes:
//! - [`Kind::Held`] — press latches, base-key release unlatches (the reference's `runOnUp`
//! movement pairs); engine systems read the latch ([`super::BindingsState`]).
//! - [`Kind::Edge`] / [`Kind::EdgeUpDown`] — fires Lua in the VM (the reference's binding body,
//! quoted 1:1 where our FrameXML port has the same functions).
//! - [`Kind::Host`] — fires into [`super::BindingsState::fired`]; an engine system consumes it
//! (chat open, TAB targeting, nameplates, autorun, …).
//!
//! Recorded default divergences (0997): `TOGGLEUI` ships `ALT-Z` (0870: the cache's `CTRL-Z` is
//! a player rebind all three accounts inherited, not the shipped default — the one row that does
//! not trust the cache). That is now the ONLY one — the bag row's divergence (`OPENALLBAGS` wearing
//! both `B` and `SHIFT-B`, because benilla had a single all-bags knob) is gone as of 1494: the
//! reference's split ships whole, keys and bodies alike.
/// 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`) — the window's
/// sidebar groups by it, era-style.
pub category: &'static str,
pub kind: Kind,
/// The 1.12 default chords (canonical strings; `None` = shipped unbound).
pub d1: Option<&'static str>,
pub d2: Option<&'static str>,
}
impl Spec {
/// Press+release semantics — the reference's `<Binding runOnUp=…>`, whose ONE reader is
/// `RunCommand 0x4b7bf1`: it gates the release half and nothing else. (It used to gate a
/// mousewheel refusal at `SetBinding` time here; that refusal is not in the client — 1295.)
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 (indexes into [`SPECS`], asserted by
/// test). Lua-bodied commands need no handle — nothing in Rust names them.
pub(crate) mod cmd {
use super::{Cmd, TABLE};
/// Resolve a command NAME to its [`Cmd`] handle at compile time — a linear scan of [`TABLE`]
/// in a `const fn`, so a name that is not in the registry is a BUILD error, not a runtime
/// surprise.
///
/// These handles used to be hand-written row numbers, and every row that landed ahead of one
/// silently re-pointed it at its neighbour: 1057's `TOGGLECHARACTER3` moved three of them
/// 86/87/88 → 87/88/89, 1136's `TOGGLEACTIONBARLOCK` moved the same three again, and 1494's
/// bag family would have moved them a third time (`TOGGLE_UI` landing on `MINIMAPZOOMOUT`). A
/// guard test named the drift but the numbers still had to be re-typed by hand each time; the
/// name is the thing we actually mean, so the name is what the table is asked for now.
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 OPEN_CHAT: Cmd = by_name("OPENCHAT");
pub(crate) const OPEN_CHAT_SLASH: Cmd = by_name("OPENCHATSLASH");
pub(crate) const REPLY: Cmd = by_name("REPLY");
pub(crate) const REPLY2: Cmd = by_name("REPLY2");
pub(crate) const TARGET_NEAREST_ENEMY: Cmd = by_name("TARGETNEARESTENEMY");
pub(crate) const TARGET_PREVIOUS_ENEMY: Cmd = by_name("TARGETPREVIOUSENEMY");
pub(crate) const NAMEPLATES: Cmd = by_name("NAMEPLATES");
pub(crate) const FRIEND_NAMEPLATES: Cmd = by_name("FRIENDNAMEPLATES");
pub(crate) const ALL_NAMEPLATES: Cmd = by_name("ALLNAMEPLATES");
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, 1.12 `Bindings.xml` order. Sub-tables (action buttons, shapeshift, raid
/// targets) are written out because each row carries its own Lua body string.
///
/// `TABLE` is the `const` view [`cmd::by_name`] scans at compile time (a `static` cannot be read
/// during const evaluation); `SPECS` is the one everything else uses.
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::Host, Some("ENTER"), None),
spec!("OPENCHATSLASH", CHAT, Kind::Host, Some("/"), None),
spec!(
"CHATPAGEUP",
CHAT,
Kind::Edge(r#"getglobal("ChatFrame" .. BenillaFCF.selected):PageUp()"#),
Some("PAGEUP"),
None
),
spec!(
"CHATPAGEDOWN",
CHAT,
Kind::Edge(r#"getglobal("ChatFrame" .. BenillaFCF.selected):PageDown()"#),
Some("PAGEDOWN"),
None
),
spec!(
"CHATBOTTOM",
CHAT,
Kind::Edge(r#"getglobal("ChatFrame" .. BenillaFCF.selected):ScrollToBottom()"#),
Some("SHIFT-PAGEDOWN"),
None
),
spec!("REPLY", CHAT, Kind::Host, Some("R"), None),
// The other reply: the last person YOU told, not the last who told you
// (`ChatEdit_GetLastToldTarget`, ChatFrame.lua l.1650). The memory was already being kept by
// the send path and read by nothing — 1745.
spec!("REPLY2", CHAT, Kind::Host, Some("SHIFT-R"), None),
// ── Action bar (BINDING_HEADER_ACTIONBAR) ───────────────────────────────────────────
// The ref's runOnUp pair (Bindings.xml:121: DOWN shows the pushed visual, UP fires) —
// exactly what the old hardcoded number-row table sent.
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
),
// The stance/shapeshift row (ref ShapeshiftBar_ChangeForm(n)) — ours clicks the bar's own
// buttons, which carry the full form-switch law (stock BonusActionBarFrame.xml, 1938).
// ── The self-cast dozen (1.12 `Bindings.xml`:257-293) ───────────────────────────────
// The same two halves as ACTIONBUTTON, with `ActionButtonUp`'s second argument set: the
// reference's own `onSelf`, which stock `ActionButtonUp` forwards to `UseAction`'s third
// and the host used to drop (1745). `ALT-1`…`ALT-=` are byte-real from DefaultBindings.wtf,
// and they sit one modifier off the plain bar exactly as the reference lays them out.
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
),
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's row — 1.12 calls it BONUSACTIONBUTTON and files it under this same ACTIONBAR
// header (Bindings.xml:321-390 carries no `header` attribute, so it inherits l.121's), which
// is why "Secondary Action Button 1-10" reads under Action Bar in the reference's own window.
// The ref's body is BonusActionButtonDown/Up(id), and those two are one-liners onto
// PetActionButtonDown/Up (BonusActionBarFrame.lua:106-112) — the PET bar's buttons, not the
// bonus bar's. benilla has no bonus bar at all, so the pet bar is the whole lane, exactly as
// the reference wires it. Defaults are byte-real and unanimous: CTRL-1..CTRL-0 in all three
// of the install's `bindings-cache.wtf` files (unlike TOGGLEUI's rebind, 0870).
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
),
// ── The action-bar PAGES (1.12 `Bindings.xml`:395-431) ──────────────────────────────
// The bar is six pages of twelve (action slots 1..72) and it has been since 1500 shipped the
// multibars; `ChangeActionBarPage` is the engine's (it fires ACTIONBAR_PAGE_CHANGED and nothing
// else — 1938) and the `ActionBar_Page{Up,Down}` wrap is stock ActionButton.lua's. `SHIFT-1..6`
// and the SHIFT-arrow / SHIFT-wheel steps are
// byte-real from `WTF\\DefaultBindings.wtf`.
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")
),
// The action-bar lock (decision 1136), the ref's own binding body verbatim (Bindings.xml:433-
// 439) — it flips the `LOCK_ACTIONBAR` uvar OptionsFrame.xml declares (1938), the same global the
// Options window's Action Bars row writes. It sits here because the reference files it under
// this header (l.433 carries no `header=`, so it inherits l.121's ACTIONBAR), and it ships
// **unbound**: no `TOGGLEACTIONBARLOCK` line in any of the install's three
// `bindings-cache.wtf` files, which is also what the option's own tooltip implies ("can be
// bound to a function key in the keybindings interface").
spec!(
"TOGGLEACTIONBARLOCK",
ACTIONBAR,
Kind::Edge(
r#"if LOCK_ACTIONBAR == "1" then LOCK_ACTIONBAR = "0" else LOCK_ACTIONBAR = "1" end"#
),
None,
None
),
// The `autoSelfCast` CVar's own toggle, the reference's body verbatim — real since 1745
// wired that CVar to the cast arm's `AutoSelfCast` knob, which had been welded to a Resource
// default with nothing able to move it. Ships unbound, like the reference.
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 cone — the same scan with the reaction test flipped (1745). `CTRL-TAB` /
// `CTRL-SHIFT-TAB`, byte-real; the reverse flag is the reference's own
// `TargetNearestFriend(1)`, and its `Bindings.xml` comment says so out loud.
spec!(
"TARGETNEARESTFRIEND",
TARGETING,
Kind::Edge("TargetNearestFriend()"),
Some("CTRL-TAB"),
None
),
spec!(
"TARGETPREVIOUSFRIEND",
TARGETING,
Kind::Edge("TargetNearestFriend(1)"),
Some("CTRL-SHIFT-TAB"),
None
),
// The self/party bodies are 1.12's own, 1:1 (Bindings.xml:460-509 — already-targeted
// falls through to the pet).
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
),
spec!("NAMEPLATES", TARGETING, Kind::Host, Some("V"), None),
spec!(
"FRIENDNAMEPLATES",
TARGETING,
Kind::Host,
Some("SHIFT-V"),
None
),
spec!("ALLNAMEPLATES", TARGETING, Kind::Host, 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, in the reference's own `Bindings.xml` order and with its own bodies and
// defaults (1494). 0997 collapsed all of this onto ONE command — `OPENALLBAGS` wearing both
// `B` and `SHIFT-B` over a `ToggleBackpack()` that opened every bag — because benilla had a
// single all-bags knob to hang keys off. The reference has three knobs and the director
// reported the difference: `B` opens the backpack ALONE, `SHIFT-B` opens the lot.
//
// Every default here is byte-real from the client's own `bindings-cache.wtf`, identical across
// all three independent accounts (ONE, TWO, WINUSER) — which is what rules out a player rebind
// (the `TOGGLEUI` trap, 0870).
//
// Each body is the bare GLOBAL, exactly the ref's own `Bindings.xml` — NOT a button's OnClick
// handler: a handler carries the button's checked bookkeeping, and routing a key through it
// drags that onto a path the reference keeps clean (with a bag addon holding `ToggleBackpack`,
// the key press is the path that lights the backpack button on the real client — the addon's
// OnShow write is the last word).
spec!(
"TOGGLEBACKPACK",
INTERFACE,
Kind::Edge("ToggleBackpack()"),
Some("B"),
Some("F12")
),
// TOGGLEBAG**N** is bag **5-N**: the reference's numbering runs the bar right-to-left, so F8
// opens the slot FARTHEST from the backpack. Quoted from ref Bindings.xml l.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
),
// The keyring's own toggle (1.12 `Bindings.xml`; ships UNBOUND, and the reference's page
// shows it as such). 0765 landed the keyring plate and its `HasKey` gate; the row it was
// owed has been missing since.
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` — 1050's law, and the reason this is
// a second command rather than a second key on the first.
spec!(
"TOGGLEPETBOOK",
INTERFACE,
Kind::Edge("ToggleSpellBook(BOOKTYPE_PET)"),
Some("SHIFT-I"),
None
),
spec!(
"TOGGLETALENTS",
INTERFACE,
Kind::Edge("ToggleTalentFrame()"),
Some("N"),
None
),
// TOGGLECHARACTER**N** is the reference's PAGE number, not our tab index: 0 = PaperDoll,
// 1 = Skill, 2 = Reputation, 3 = PetPaperDoll, 4 = Honor. The rows run 4, 3, 2, 1 in the
// file, and the file's order is this table's order.
spec!(
"TOGGLECHARACTER4",
INTERFACE,
Kind::Edge(r#"ToggleCharacter("HonorFrame")"#),
Some("H"),
None
),
// `SHIFT-P` is byte-real from the client's own `bindings-cache.wtf`, and identical in two
// independent accounts (ONE and WINUSER) — which is what rules out a player rebind (the
// `TOGGLEUI` trap, 0870). Decision 1057.
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
),
// ── 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 sound toggles/steps flip the same CVars the ref's SoundOptionsFrame_* bodies do
// (1.12 SoundOptionsFrame.lua: master enable = MasterSoundEffects, step 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, not the cache's CTRL-Z — settled by 0870: all three of the install's accounts
// descend from one profile whose TOGGLEUI had been rebound; ALT-Z is the shipped default
// (the one command whose default does NOT trust the cache file).
spec!("TOGGLEUI", MISC, Kind::Host, Some("ALT-Z"), None),
// Print screen (decision 1487). The body is the reference's own one-liner because our
// `TakeScreenshot` has the same contract its does (ScreenshotStatus.xml) — hide the last
// shot's confirmation, then ask the engine.
//
// `PRINTSCREEN` comes from the SHIPPED default, not a player's cache: `DefaultBindings.wtf`
// lives inside `patch.MPQ` and its line 128 is `bind PRINTSCREEN SCREENSHOT` (wow-re's
// dispatch on this feature; the install's account-ONE `bindings-cache.wtf` agrees). `Edge`,
// not `EdgeUpDown`, is also byte-real: the `<Binding>` carries no `runOnUp`, and the
// reference's dispatcher returns on key-up unless that flag is set (`0x4b7bea`).
//
// On a Mac keyboard the token arrives as F13, which is the reference's own Mac mapping rather
// than an accommodation (`KEY_PRINTSCREEN_MAC = "F13"`); `super::chord` does the translation.
spec!(
"SCREENSHOT",
MISC,
Kind::Edge("TakeScreenshot();"),
Some("PRINTSCREEN"),
None
),
// ── Camera (BINDING_HEADER_CAMERA) ──────────────────────────────────────────────────
// The five named camera views (decision 1745) — `player::camera_view`, whose defaults are the
// reference's own `0x84f488` table. NEXTVIEW/PREVVIEW ship on END/HOME and **do not wrap**
// (`0x50faa0`/`0x50fac0` are hard stops); Set/Save/Reset ship unbound, and 1.12 files no
// SAVEVIEW1/RESETVIEW1 row even though its engine accepts view 1 — that is a `Bindings.xml`
// decision, so the table follows the file.
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 two bottom bars' buttons (stock MultiActionBars.xml renders exactly these; 1.12's right bars
// and their MULTIACTIONBAR3/4 commands stay out — honest tree). Ref bodies are the
// MultiActionButtonDown/Up runOnUp pair (Bindings.xml:799-966), defined in stock
// MultiActionBars.lua; shipped UNBOUND like the ref (no MULTIACTIONBAR* line in any of the
// install's bindings-cache.wtf files). 1.12 files bar 2 under a BLANK spacer-header;
// both bars sit under the one MULTIACTIONBAR header here (1008, recorded).
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",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarBottomRight", 1)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 1)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR2BUTTON2",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarBottomRight", 2)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 2)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR2BUTTON3",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarBottomRight", 3)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 3)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR2BUTTON4",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarBottomRight", 4)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 4)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR2BUTTON5",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarBottomRight", 5)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 5)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR2BUTTON6",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarBottomRight", 6)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 6)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR2BUTTON7",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarBottomRight", 7)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 7)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR2BUTTON8",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarBottomRight", 8)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 8)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR2BUTTON9",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarBottomRight", 9)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 9)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR2BUTTON10",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarBottomRight", 10)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 10)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR2BUTTON11",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarBottomRight", 11)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 11)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR2BUTTON12",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarBottomRight", 12)"#,
r#"MultiActionButtonUp("MultiBarBottomRight", 12)"#
),
None,
None
),
// ── The two VERTICAL bars (1.12 files them under its own `BLANK2`/`BLANK3` spacer
// headers; they join bar 2 under MULTIACTIONBAR here for the same reason 1008 folded that
// one — a spacer is a display device in the reference's flat list, and this page's sections
// are named). Both bars are real since 1500; `MultiBarRight` is bar 3, `MultiBarLeft` bar 4,
// and both ship UNBOUND exactly as the reference does.
spec!(
"MULTIACTIONBAR3BUTTON1",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarRight", 1)"#,
r#"MultiActionButtonUp("MultiBarRight", 1)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR3BUTTON2",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarRight", 2)"#,
r#"MultiActionButtonUp("MultiBarRight", 2)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR3BUTTON3",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarRight", 3)"#,
r#"MultiActionButtonUp("MultiBarRight", 3)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR3BUTTON4",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarRight", 4)"#,
r#"MultiActionButtonUp("MultiBarRight", 4)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR3BUTTON5",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarRight", 5)"#,
r#"MultiActionButtonUp("MultiBarRight", 5)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR3BUTTON6",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarRight", 6)"#,
r#"MultiActionButtonUp("MultiBarRight", 6)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR3BUTTON7",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarRight", 7)"#,
r#"MultiActionButtonUp("MultiBarRight", 7)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR3BUTTON8",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarRight", 8)"#,
r#"MultiActionButtonUp("MultiBarRight", 8)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR3BUTTON9",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarRight", 9)"#,
r#"MultiActionButtonUp("MultiBarRight", 9)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR3BUTTON10",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarRight", 10)"#,
r#"MultiActionButtonUp("MultiBarRight", 10)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR3BUTTON11",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarRight", 11)"#,
r#"MultiActionButtonUp("MultiBarRight", 11)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR3BUTTON12",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarRight", 12)"#,
r#"MultiActionButtonUp("MultiBarRight", 12)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR4BUTTON1",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarLeft", 1)"#,
r#"MultiActionButtonUp("MultiBarLeft", 1)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR4BUTTON2",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarLeft", 2)"#,
r#"MultiActionButtonUp("MultiBarLeft", 2)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR4BUTTON3",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarLeft", 3)"#,
r#"MultiActionButtonUp("MultiBarLeft", 3)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR4BUTTON4",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarLeft", 4)"#,
r#"MultiActionButtonUp("MultiBarLeft", 4)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR4BUTTON5",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarLeft", 5)"#,
r#"MultiActionButtonUp("MultiBarLeft", 5)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR4BUTTON6",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarLeft", 6)"#,
r#"MultiActionButtonUp("MultiBarLeft", 6)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR4BUTTON7",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarLeft", 7)"#,
r#"MultiActionButtonUp("MultiBarLeft", 7)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR4BUTTON8",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarLeft", 8)"#,
r#"MultiActionButtonUp("MultiBarLeft", 8)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR4BUTTON9",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarLeft", 9)"#,
r#"MultiActionButtonUp("MultiBarLeft", 9)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR4BUTTON10",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarLeft", 10)"#,
r#"MultiActionButtonUp("MultiBarLeft", 10)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR4BUTTON11",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarLeft", 11)"#,
r#"MultiActionButtonUp("MultiBarLeft", 11)"#
),
None,
None
),
spec!(
"MULTIACTIONBAR4BUTTON12",
MULTIACTIONBAR,
Kind::EdgeUpDown(
r#"MultiActionButtonDown("MultiBarLeft", 12)"#,
r#"MultiActionButtonUp("MultiBarLeft", 12)"#
),
None,
None
),
// ── Raid targeting (BINDING_HEADER_RAID_TARGET) ─────────────────────────────────────
// 1.12 bodies 1:1 (SetRaidTargetIcon toggles when the unit already wears the icon; 0
// clears — party.rs's registered semantics). All unbound by default, like the client.
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.
///
/// The honest tree (0997) says a command appears in [`SPECS`] only over a real engine action. That
/// rule is right and it stays; what it never had was a way to notice when the action *arrived*.
/// 0997's own residue promised "each returns the day its mechanism lands, one registry row" and
/// then five mechanisms landed without their rows: the keyring (0765), the pet book (1050), the
/// reputation and honor pages (1057-era), the six action-bar pages and the two vertical multibars
/// (1500). Nothing was wrong with any of those commits — nothing was *watching*.
///
/// So the absence is written down rather than merely true, and it is written down in a form that
/// goes stale loudly: [`needs`] names the Lua globals the reference's own binding body calls that
/// this client does not define, and
/// `the_absent_commands_are_still_absent` fails the day the last one lands.
///
/// [`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. **This is the
/// falsifier**: while at least one is missing the absence is real; when they are all defined
/// the mechanism has landed and the row belongs in [`SPECS`].
///
/// Empty is allowed and means *there is no Lua-global signal* — the missing mechanism is
/// host-side behind a global that already exists (self-cast lives behind `UseAction`'s third
/// argument, which the Lua passes and the host drops). Those rows carry the whole weight in
/// [`why`], and they are the ones to be suspicious of.
///
/// [`why`]: Absent::why
#[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 here — one line, mechanism-first.
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 — the other half of the
/// registry, and the half that used to be invisible. `SPECS` ∪ `ABSENT` is exactly the client's
/// 228 live bindings, asserted by [`tests::every_1_12_command_is_registered_or_recorded_absent`].
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"
),
// ── Chat ────────────────────────────────────────────────────────────────────────────
absent!(
"COMBATLOGPAGEUP",
["ToggleCombatLog"],
"ChatFrame2 is a real frame that nothing writes into — the combat-log line pipeline does \
not exist, which is why 1.12's own ToggleCombatLog has no home here"
),
absent!(
"COMBATLOGPAGEDOWN",
["ToggleCombatLog"],
"nothing writes the combat log — see COMBATLOGPAGEUP"
),
absent!(
"COMBATLOGBOTTOM",
["ToggleCombatLog"],
"nothing writes the combat log — see COMBATLOGPAGEUP"
),
absent!(
"TOGGLECOMBATLOG",
["ToggleCombatLog"],
"nothing writes the combat log — see COMBATLOGPAGEUP"
),
// ── Action bar ──────────────────────────────────────────────────────────────────────
// ── Interface ───────────────────────────────────────────────────────────────────────
absent!(
"TOGGLEWORLDSTATESCORES",
["ToggleWorldStateScoreFrame"],
"no battlegrounds"
),
absent!(
"TOGGLEBATTLEFIELDMINIMAP",
["ToggleBattlefieldMinimap"],
"no battlegrounds"
),
// ── Misc ────────────────────────────────────────────────────────────────────────────
absent!(
"TOGGLEFPS",
["ToggleFramerate"],
"the only framerate readout here is the dev HUD's cost pill, which is behind \
`#[cfg(feature = \"dev\")]` and is an instrument, not a player display (perf/hud.rs) — \
there is no player-facing framerate to toggle. (0997 recorded this as \"the perf pill is \
always-on by design\"; the pill is not the thing TOGGLEFPS toggles.)"
),
// The nine `hidden="true" debug="true"` rows. Every one of them names an instrument benilla
// really has (the tri counter, the collision display, the portal draw, the perf pill) — they
// are absent because those instruments answer to the dev plane's chords (0702/1043), not to a
// binding. Whether that is right is a real question and 1745 takes it; until then the rows
// stay recorded rather than quietly missing.
absent!(
"TOGGLESTATS",
["ToggleStats"],
"a dev-plane instrument (1043)"
),
absent!(
"TOGGLETRIS",
["ToggleTris"],
"a dev-plane instrument (1043)"
),
absent!(
"TOGGLEPORTALS",
["TogglePortals"],
"a dev-plane instrument (1043)"
),
absent!(
"TOGGLECOLLISION",
["ToggleCollision"],
"a dev-plane instrument (1043)"
),
absent!(
"TOGGLECOLLISIONDISPLAY",
["ToggleCollisionDisplay"],
"a dev-plane instrument (1043)"
),
absent!(
"TOGGLEPLAYERBOUNDS",
["TogglePlayerBounds"],
"a dev-plane instrument (1043)"
),
absent!(
"TOGGLEPERFORMANCEDISPLAY",
["TogglePerformanceDisplay"],
"a dev-plane instrument (1043)"
),
absent!(
"TOGGLEPERFORMANCEVALUES",
["TogglePerformanceValues"],
"a dev-plane instrument (1043)"
),
absent!(
"RESETPERFORMANCEVALUES",
["ResetPerformanceValues"],
"a dev-plane instrument (1043)"
),
// ── The mouse's own three (hidden) ──────────────────────────────────────────────────
// These are the reference's mouse-look bindings — BUTTON2 turn-or-action, BUTTON1
// select-or-move, CTRL-BUTTON1 the sticky variant — and they are `hidden` because the window
// never lists them, not because they are debug. benilla's mouse-look is hardwired in
// `player/camera.rs` instead, which is exactly the shape 1043 spent a decision undoing for
// the keyboard.
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") ──────────────────────────────────────────────────
// The one place 1.12's own binding file is OS-specific: five rows carrying `platform="mac"`,
// the whole use of that attribute in the client. They remote the *system* music player, not
// the game's, and benilla has nothing to remote.
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::*;
/// Every default chord in the table parses — a typo'd token would otherwise silently ship
/// an unpressable default.
#[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
);
}
}
}
/// No two commands ship the SAME default chord. One key, one command is the binding table's
/// own law (the steal pass in [`super::store::resolve`] enforces it at load), so a duplicate
/// here would ship a table that eats itself: registration order would decide the winner and
/// the loser would come up silently unbound. Cheap tripwire for the one mistake a new block
/// of rows can make — 1052's CTRL-1..CTRL-0 went in with nothing to catch a collision.
#[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);
}
}
}
}
/// Names are unique (the table is keyed by them everywhere: files, Lua, the window).
#[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);
}
}
/// **The registry against the real client's own two files.** Two columns of this table are
/// pure transcription from the 1.12 install, and both are load-bearing in a way nothing else
/// here can see: `runOnUp` — which decides whether a wheel notch delivers a release half at
/// all (1295), and which B265 turned entirely on — and every command's default chords. A
/// hand-typed table drifts; this reads the source of truth instead of trusting the typing.
///
/// `Interface\FrameXML\Bindings.xml` carries the flag, `WTF\DefaultBindings.wtf` the chords.
/// Both are install content, read at runtime, never committed; the test skips (passes)
/// without a client, the house pattern for a real-data test. Running Blizzard's own 234-row
/// file through our [`benilla_ui::bindings_xml`] is a second gate riding along — that is the
/// parser every addon's `Bindings.xml` lands in.
#[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 <CHORD> <COMMAND>`, in the file's own order — which is the order the command's
// Key 1 and Key 2 slots take.
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 (1295)",
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
);
}
}
/// **The coverage gate** (decision 1745): every one of the client's 228 live bindings is
/// either in [`SPECS`] or in [`ABSENT`], and never both.
///
/// This is the half [`the_registry_matches_the_installs_own_bindings`] could not see. That
/// test walks OUR rows and checks them against the install, so a command we never wrote down
/// is invisible to it — which is how the keyring, the pet book, the reputation and honor
/// pages, the six action-bar pages and the two vertical multibars all shipped their mechanism
/// and kept their key unbound, some of them for five hundred commits. Walking the INSTALL's
/// rows instead makes an unwritten command a build failure the day it becomes reachable.
///
/// `hidden` rides along: 1.12 marks twelve bindings `hidden="true"` (the nine debug toggles
/// and the three mouse-look ones) and the window never lists them. This client registers none
/// of them, and this asserts that stays true — a hidden row in `SPECS` would be a row the
/// Keybindings page shows and the reference does not.
#[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, and the module claims it is the
// file's. It was not: TOGGLECHARACTER1/3 sat ahead of TOGGLESPELLBOOK until 1745.
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;
}
}
/// **The staleness half.** Each [`ABSENT`] row names the Lua globals the reference's own body
/// calls that this client does not define; while at least one is still missing the absence is
/// real. When they are all defined the mechanism has landed and the row is owed to [`SPECS`] —
/// which is precisely the event nothing was watching for.
///
/// "Defined" is read from source, not from a VM: a harness would have to load the whole of
/// FrameXML to answer, and the question here is "has anyone written it yet", which the sources
/// say directly. Same posture as `tests/world_api_wall.rs`.
#[test]
fn the_absent_commands_are_still_absent() {
let defined = lua_globals_defined();
for a in ABSENT {
if a.needs.is_empty() {
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
);
}
}
/// The absent table is well formed: unique names, and every row either carries a falsifier or
/// is one of the host-side ones the header calls out. The second half is not a formality — a
/// row with no `needs` can rot silently, so the count of them is pinned and moving it is a
/// deliberate act.
#[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:?}"
);
}
/// **The scanner's own falsifier.** [`the_absent_commands_are_still_absent`] is only a gate
/// while `lua_globals_defined` really sees every kind of definition; a scanner that quietly
/// returned nothing would pass every row forever, which is the exact failure it exists to
/// end. So: one global of each kind that is certainly defined, and one that certainly is not.
///
/// **There are THREE kinds since decision 1751, and this test found the third the hard way.**
/// `ToggleKeyRing` sat in the FrameXML row below and started failing the day the bag windows
/// migrated: it is defined in `ContainerFrame.lua`, which this client no longer ships — it
/// EXECUTES it off the player's own patch chain. The honest repair was not to swap the name
/// for one still in `assets/ui`; it was that the scan had gone blind to a whole store, and
/// blind to it in exactly the direction that matters (1751 §4 makes "ship the stock files off
/// the chain" the default for a NEW window, so the global that retires an ABSENT row —
/// `ToggleBattlefieldMinimap`, say — will appear in neither Rust nor `assets/ui`). So
/// `lua_globals_defined` reads the chain half of the manifest too, and `ToggleKeyRing` stays
/// here as the witness for it.
#[test]
fn the_global_scanner_sees_both_kinds_of_definition() {
let defined = lua_globals_defined();
for host in ["TargetUnit", "UseAction", "SetBinding"] {
assert!(
defined.contains(host),
"the Rust scan missed the host registration `{host}`"
);
}
// Each is defined ONLY in `assets/ui` — the reference's own homes for them
// (CharacterFrame.lua, ActionBarFrame.lua, UIParent.lua) are not among the files
// `benilla.toc` sources, so the chain scan below cannot cover for a broken one.
for shipped in ["ToggleCharacter", "ChangeActionBarPage", "ShowUIPanel"] {
assert!(
defined.contains(shipped),
"the assets/ui scan missed `{shipped}`"
);
}
// The chain half needs a client, like every other reader of install content — and the
// two rows above still run without one, so the test keeps its teeth either way.
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 benilla.toc sources off the player's install"
);
}
}
assert!(
!defined.contains("BenillaNoSuchGlobalExists"),
"the scanner claims to define a name nobody wrote"
);
}
/// The install's own `Bindings.xml`, parsed — `None` (and a skipped test) without a client.
fn install_bindings() -> Option<Vec<benilla_ui::bindings_xml::AddonBinding>> {
let data = benilla_formats::wow_data()?;
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 out of its own sources — **all three stores**:
/// the host registrations (`g.set("Name", …)` anywhere in the workspace), our own FrameXML
/// port's function definitions (`function Name(` / `Name = function` inside the `<Script>`
/// blocks of `assets/ui`), and the reference's own FrameXML that decision 1751 executes off
/// the player's installed patch chain.
///
/// The third store is not decoration. Until the bag windows migrated, "the FrameXML this
/// client runs" and "the files in `assets/ui`" were the same set; they are not any more, and
/// 1751 §4 makes the divergence grow — a NEW window ships as the stock files off the chain
/// plus the engine verbs they call, so the global that would retire an [`ABSENT`] row can land
/// without a line changing in either half this scan used to read. `benilla.toc` is the one
/// ordered list of both stores (a manifest entry carrying a path is the reference's file), so
/// it is what the chain half walks. No install ⇒ no chain half, the standard degradation.
///
/// Deliberately syntactic and deliberately generous — a false "defined" fails the gate loudly
/// (it asks for a row that turns out not to work), while a false "missing" would let a landed
/// mechanism stay silent, which is the failure this whole gate exists to end. A reference
/// file's `local function name()` is therefore counted as well; that is the generous direction
/// on purpose.
fn lua_globals_defined() -> std::collections::HashSet<String> {
fn walk(dir: &std::path::Path, ext: &str, out: &mut Vec<std::path::PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if path.file_name().is_some_and(|n| n == "target") {
continue;
}
walk(&path, ext, out);
} else if path.extension().is_some_and(|e| e == ext) {
out.push(path);
}
}
}
fn push(names: &mut std::collections::HashSet<String>, s: &str) {
if s.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
{
names.insert(s.to_string());
}
}
/// One Lua-carrying text's own definitions — a `<Script>` block of ours or a reference
/// `.lua` off the chain, the same two shapes either way.
fn scan_lua(text: &str, names: &mut std::collections::HashSet<String>) {
let mut rest = text;
while let Some(i) = rest.find("function ") {
rest = &rest[i + "function ".len()..];
let end = rest
.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
.unwrap_or(rest.len());
if rest[end..].trim_start().starts_with('(') {
push(names, &rest[..end]);
}
}
for line in text.lines() {
let line = line.trim_start();
if let Some((lhs, rhs)) = line.split_once('=') {
let lhs = lhs.trim();
if rhs.trim_start().starts_with("function")
&& lhs.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
{
push(names, lhs);
}
}
}
}
/// The `.lua` files an XML document pulls in through `<Script file="…"/>`. Only a script
/// reference ends in `.lua` — a `<Texture file=>` names art — so the extension is the
/// whole test, and the paths come back relative to the document's own directory, which is
/// how the loader resolves them (1186).
fn script_files(text: &str) -> Vec<String> {
let mut out = Vec::new();
let mut rest = text;
while let Some(i) = rest.find("file=\"") {
rest = &rest[i + "file=\"".len()..];
let Some(end) = rest.find('"') else { break };
if rest[..end].to_ascii_lowercase().ends_with(".lua") {
out.push(rest[..end].to_string());
}
rest = &rest[end..];
}
out
}
// `crates/benilla-app/` → the workspace root.
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("the workspace root is two above the crate")
.to_path_buf();
let mut names = std::collections::HashSet::new();
let mut rust = Vec::new();
walk(&root.join("crates"), "rs", &mut rust);
for path in rust {
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
// `.set("Name"` — the host-registration idiom, over any amount of intervening
// whitespace (rustfmt breaks the long ones onto their own line).
let mut rest = text.as_str();
while let Some(i) = rest.find(".set(") {
rest = &rest[i + ".set(".len()..];
let head = rest.trim_start();
if let Some(body) = head.strip_prefix('"') {
if let Some(end) = body.find('"') {
push(&mut names, &body[..end]);
}
}
}
}
let ui = root.join("crates/benilla-app/assets/ui");
let mut xml = Vec::new();
walk(&ui, "xml", &mut xml);
for path in xml {
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
scan_lua(&text, &mut names);
}
// The chain half of the manifest — the reference's own files, read the way the client
// reads them.
let Some(data) = benilla_formats::wow_data() else {
return names;
};
let Ok(mut chain) = benilla_formats::open_chain(&data) else {
return names;
};
let manifest =
std::fs::read_to_string(ui.join("benilla.toc")).expect("the manifest is committed");
for entry in manifest
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
// The manifest's own rule for which store an entry names
// (`ui_script::reference_ui::is_chain_entry`): our tree is flat, so a separator is a
// path and a path is the reference's. The bare names were walked above.
.filter(|l| l.contains('\\') || l.contains('/'))
{
let Ok(bytes) = chain.read_file(entry) else {
continue;
};
let text = String::from_utf8_lossy(&bytes).into_owned();
scan_lua(&text, &mut names);
if !entry.to_ascii_lowercase().ends_with(".xml") {
continue;
}
let dir = entry.rfind('\\').map_or("", |i| &entry[..=i]);
for script in script_files(&text) {
if let Ok(b) = chain.read_file(&format!("{dir}{script}")) {
scan_lua(&String::from_utf8_lossy(&b), &mut names);
}
}
}
names
}
}