fourteen settings go live, /leave holds, and /assist can swing

- mouse-look speed is cameraYawMoveSpeed and cameraPitchMoveSpeed, so pfUI's afkcam does what it asks and an addon driving the stock Interface Options window runs its load and Defaults walks to the end; the shipped feel does not move (2147)
- the eight combat-log range sliders are real settings, so BigWigs' Range plugin and DPSMate read and write them, and the Show Pet Melee Damage and periodic-spell settings genuinely silence what they gate (2147)
- the stock Sound Options window loads hidden beside ours for addons to find, and its three check-box settings are real: the listener sits at your character, emote voices can be muted, zone music can loop without its silence gap (2147)
- /assist with assistAttack set selects and starts swinging; /leave General survives a relog, a /join puts the channel back in its chat window's list, and SetGuildRecruitmentMode joins and leaves GuildRecruitment - City (2144)
- wipe, strsplit, strjoin, strtrim, strconcat and tostringall are gone: no 1.12 client has them, and the addons that call them are multi-client builds whose vanilla path raises on the real client too (2146)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-11 20:53:36 +07:00
co-authored by Claude Fable 5.1
parent 31371641eb
commit 26fb54b402
40 changed files with 2912 additions and 631 deletions
@@ -4380,39 +4380,87 @@ fn without_a_seated_measurer_the_same_fit_reads_zero() {
// and the reason is the row's whole point — "no key without a reader" (1134 §4) is why the CVar is
// absent, and this is where that decision is written down per setting.
const UNBACKED_REFERENCE_CVARS: &[(&str, &str)] = &[
("autointeract", "click-to-move"),
("assistAttack", "assist-attack"),
("UnitNamePlayerGuild", "guild names over player nameplates"),
// **A blocker is now a SPEC, not a label.** Every row below has had its mechanism derived at
// the bytes since 2115 wrote this list, so what is missing is the *feature*, never the
// knowledge — and saying which is the difference between a row somebody can pick up and a row
// that just reads "not done".
(
"autointeract",
"click-to-move — the one row here that is a whole movement mode rather than a knob. \
`CanAutoInteract 0x60f900` gates the world-click pick mask's bit 0 and inverts \
`0x5ec110`'s interaction-distance refusal, so an out-of-range corpse/GO/NPC/cast click \
queues an approach (`0x60fed0`, move kinds 6/7/9/0xa) instead of refusing. The commit \
itself puts NOTHING on the wire — it stamps a local goal at `0x611130` — and the packets \
are its consequences (a stand, then the original verb on arrival). benilla has only the \
keyboard controller and /follow (mode 3). Registered default is \"0\" on every locale but \
koKR (`0x603374` selects on the locale index), so stock West ships it OFF",
),
(
"UnitNamePlayerGuild",
"the `\\n<%s>` guild line in the OVERHEAD name stack — slot a5 of `0x608f50`, gated by bit \
`0x10` of the render mask `[0xce8720]`, player branch only, text from `0x5e09f0` through \
the WGLD guild-identity cache. NOT the V-key nameplate (`ShouldShowName 0x6070a0` \
returns 0 whenever a plate frame is live) and NOT the tooltip (FrameXML's own \
`GetGuildInfo`). The data resolves here now — `ui_guild::GuildState` serves \
`GetGuildInfo` — so what is missing is the third line in `nameplates::drive_nameplates` \
plus its `lines_current` comparator arm and that comparator's differential test",
),
(
"UnitNamePlayerPVPTitle",
"PvP titles over player nameplates",
"the PvP rank prefix on the overhead name line — slot a4 of `0x608f50`, bit `0x20` of the \
same mask, resolved by `0x609370` through the `PVP_RANK_%d_%d` GlobalStrings key. Blocked \
one step further back than its guild twin: the rank byte streams, but the key's second \
index is a FACTION SIDE that `ui_unit` does not resolve for an arbitrary player yet",
),
("cameraTerrainTilt", "camera terrain tilt"),
("cameraBobbing", "camera head bob"),
("cameraWaterCollision", "camera water collision"),
("cameraPivot", "the smart-pivot camera"),
(
"CombatDamage",
"the floating-combat-text master gate is `combat_text::law::COMBAT_DAMAGE`, a const bool \
— the same shape ChatBubbles had before 1139: a faithful gate with no way to reach it",
"cameraTerrainTilt",
"the camera's ground-pitch channel. VERIFIED end to end by the §5 round this work \
dispatched (wow-re `ui/scratch/camera-cvar-gates.md`): the CVar gates predicate \
`0x5105a0`, whose false leg zeroes `[cam+0xa0]` at `0x50d922` and skips both `0x672170` \
terrain raycasts; `0x50d900` forms `slope = (Δz + 5/3)/√(Δx²+Δy²)`, walks the 10-entry \
slope→pitch table at `0x808a40`, clamps ±20°, throttled to 100 ms. Registered \"0\", so \
building it changes nothing until a player asks",
),
("CombatLogPeriodicSpells", "a combat log"),
(
"PetMeleeDamage",
"`combat_text::law::PET_MELEE_DAMAGE`, a const bool — see CombatDamage",
"cameraBobbing",
"head bob. VERIFIED by the same §5 round, including the threshold nobody had read: \
`[0x8089ac] = 1/6` is a camera DISTANCE and the compare is inclusive, so head bob is \
FIRST-PERSON ONLY. `0x511920` computes `ampH/ampV` from the two amplitude CVars × 1/36, \
`speed = clamp(moveSpeed/7.2, [0.5,1.5])`, horizontal at `freq` and vertical at `2×freq`, \
into the same eye-translation accumulator as the shake; `0x5106f0` is the DECAY \
predicate and fades the residual rather than snapping it. Registered \"0\"",
),
// …and the SLIDERS' half, which this census was blind to until 2115 extended it from
// `UIOptionsFrameCheckButtons` to `UIOptionsFrameSliders` as well. One row, and it is not
// cosmetic: `UIOptionsFrame_Load` does `slider:SetValue(GetCVar(value.cvar))`, and
// `Slider:SetValue` is a shape-A binding (`0x790980`, wow-re `numeric-arg-coercion-law.md`)
// that RAISES on a nil in the reference too — so this is what stops the stock window's
// `_Load()` running clean, and pfUI's GVAR path with it.
(
"cameraYawMoveSpeed",
"the Mouse Look Speed slider — benilla's mouse-look yaw rate is not a CVar-driven knob \
yet; its sibling `cameraYawSmoothSpeed` (auto-follow) is registered and drives \
`follow.yaw_speed`, and this one has no counterpart in `player::camera`",
"cameraWaterCollision",
"two consumers, both VERIFIED by the §5 round. (1) the collision sweep's class mask \
becomes `0x1f0171` instead of `0x100171` — the `0xf0000` nibble is the ADT liquid layers \
— and `0x511ad0`'s bands re-base the pivot floor/cap. (2) a band CROSSING (not a level \
test: `0x50eb14` snapshots the previous frame's bits before `0x511ad0` rewrites them) \
arms a 5° pitch through the `+0xf4` channel. Registered \"1\", so this one is ON in the \
reference and benilla is the divergence — but the eye/arm verdict of \
`camera-arm-liquid-blind.md` still stands: it is the PIVOT that is liquid-aware",
),
(
"cameraPivot",
"smart pivot. VERIFIED by the §5 round, which also corrected the ±89° claim: predicate \
`0x510690` is `enabled ∧ TYPEMASK_UNIT ∧ not moving/strafing (`&0xf`, so turning in place \
passes) ∧ pitched down ∧ the collision solver's own clip flags `0x30000` nonzero` — i.e. \
it only engages in a frame the camera was ACTUALLY clipped. A mostly-vertical drag then \
routes into the pitch-BIAS channel `cam+0x104` instead of the pitch integrator, with a \
one-sided floor, and `0x5107f0` eases it back at `cameraTargetSmoothSpeed` when the \
predicate drops. Registered \"1\"",
),
// **The SLIDERS' half is empty, and that is the point.** 2115 extended this census from
// `UIOptionsFrameCheckButtons` to `UIOptionsFrameSliders` and it caught exactly one row —
// `cameraYawMoveSpeed`, the MOUSE_LOOK_SPEED slider — which was not cosmetic:
// `UIOptionsFrame_Load` does `slider:SetValue(GetCVar(value.cvar))`, `Slider:SetValue` is a
// shape-A binding (`0x790980`) that RAISES on a nil in the reference too, and that raise is
// what stopped the stock window's `_Load()` (and `_SetDefaults`, through `GetCVarDefault`).
// It is registered now, so all four slider CVars are backed and the window's `_Load()` runs to
// completion. Every row left below is a check button, and a check button's
// `GetCVar(x) == "1"` is nil-safe — so nothing on this list can raise any more. A new SLIDER
// row arriving here is therefore a live raise, not a missing feature, and should be read that
// way by whoever adds it.
];
/// **Every CVar the reference's own options table names is registered here, or listed above with
@@ -1651,6 +1651,128 @@ fn the_stock_interface_options_window_loads_hidden_and_ours_is_still_the_players
);
}
/// **The reference's own Sound Options window is on the manifest, HIDDEN, and the alias is gone**
/// — 2115's argument applied to the window its §9 handed over.
///
/// Four claims, each with its own way to fail:
///
/// 1. **`SoundOptionsFrame` is the reference's frame, not an alias onto ours.** This one is not
/// the interface window's story repeated: our `OptionsFrame.xml` loads *after* the stock file,
/// so `SoundOptionsFrame = OptionsFrame` would not have sat beside the real frame — it would
/// have **clobbered** it, and pfUI's `options-sound.lua` would then have stripped textures off
/// and re-anchored the window the player opens. The check is the reference's own CHILDREN,
/// which an alias could never grow, plus the identity itself.
/// 2. **It is never shown** — `hidden="true"` at the stock file's own xml l.18.
/// 3. **`SoundOptionsFrame_Load()` runs clean**, which is the Sound window's counterpart to the
/// interface window's `cameraYawMoveSpeed` hole: its `_Load` reaches
/// `slider:SetValue(value.initialValue)` for four sliders, and all four of their CVars are
/// registered. The three check-button CVars that were missing are built now too, so the walk
/// has nothing to trip on.
/// 4. **Ours is still the player's** — the ESC menu's Options button opens `OptionsFrame`, and
/// neither stock window.
#[test]
fn the_stock_sound_options_window_loads_hidden_and_the_alias_is_gone() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = benilla_ui::script::UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.set_unit(
"player",
Some(benilla_ui::script::UnitState {
exists: true,
name: Some("Probefive".into()),
level: 60,
class: Some("Warrior".into()),
class_file: Some("WARRIOR".into()),
..Default::default()
}),
);
let failures = super::load_default_ui(&s);
assert!(failures.is_empty(), "manifest load errors: {failures:#?}");
// 1 · the reference's own frame, by the children only it declares — and this list IS the set
// pfUI's `skins/blizzard/options-sound.lua` walks (there is deliberately no CheckButton3 in
// the stock file; pfUI's own `if btn then` guard handles the hole).
for name in [
"SoundOptionsFrame",
"SoundOptionsFrameHeader",
"SoundOptionsFrameOkay",
"SoundOptionsFrameCancel",
"SoundOptionsFrameDefaults",
"SoundOptionsFrameCheckButton1",
"SoundOptionsFrameCheckButton2",
"SoundOptionsFrameCheckButton4",
"SoundOptionsFrameCheckButton8",
"SoundOptionsFrameSlider1",
"SoundOptionsFrameSlider4",
] {
assert!(
s.eval::<bool>(&format!("return getglobal({name:?}) ~= nil"))
.unwrap(),
"{name} — pfUI's options-sound skin walks every one of these"
);
}
assert!(
s.eval::<bool>("return SoundOptionsFrame ~= OptionsFrame")
.unwrap(),
"the alias is gone — and because OUR file loads later, an alias would have CLOBBERED the \
real frame rather than merely shadowed it"
);
assert!(
s.eval::<bool>("return SoundOptionsFrameCheckButton3 == nil")
.unwrap(),
"the stock file declares 1,2,4..8 — a CheckButton3 here means this is not that file"
);
// 2 · …and hidden, by its own file's attribute.
assert!(
!s.eval::<bool>("return SoundOptionsFrame:IsShown()")
.unwrap(),
"the stock Sound window must never be on screen"
);
// 3 · `_Load` runs clean, and every CVar its two tables name answers.
s.run("this = SoundOptionsFrameOkay SoundOptionsFrame_Load()")
.expect("_Load runs to completion");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
let unbacked: Vec<String> = s
.eval(
"local out = {} \
for _, v in ipairs(SoundOptionsFrameSliders) do \
if GetCVar(v.cvar) == nil then table.insert(out, v.cvar) end \
end \
for _, v in ipairs(SoundOptionsFrameCheckButtons) do \
if v.cvar and GetCVar(v.cvar) == nil then table.insert(out, v.cvar) end \
end \
return out",
)
.expect("read the Sound window's own two tables");
assert!(
unbacked.is_empty(),
"these Sound-window CVars answer nil: {unbacked:?} — a SLIDER among them is a live raise"
);
// 4 · ours is still the window the ESC menu opens.
s.run("GameMenuButtonOptions:Click()").unwrap();
assert!(
s.eval::<bool>("return OptionsFrame:IsShown()").unwrap(),
"the player's Options button opens OUR window"
);
assert!(
!s.eval::<bool>("return SoundOptionsFrame:IsShown()")
.unwrap(),
"…and never the stock Sound one"
);
// The alias's two stock consumers still answer correctly WITHOUT it: both read all three
// window names unguarded in one `or` chain, and that chain already contains
// `OptionsFrame:IsVisible()` — this frame's own name. Ours is open here, so both say yes.
assert!(
s.eval::<bool>("return IsOptionFrameOpen() and true or false")
.unwrap(),
"UIParent.lua:997 must still see an open options window with the alias retired"
);
}
/// **pfUI's `UIOptionsFrame_Save()` path runs clean, and `_Load()` stops at exactly one thing**
/// (decision 2115).
///
@@ -1658,14 +1780,13 @@ fn the_stock_interface_options_window_loads_hidden_and_ours_is_still_the_players
/// `UIOptionsFrame_Load()` … `UIOptionsFrame_Save()`, so both are reached at runtime by a real
/// addon and both had to be more than nil.
///
/// `_Save` runs clean. `_Load` does not, and the failure is **pinned rather than hidden** because
/// it names a real gap this record surfaced: `UIOptionsFrameSliders` row 3 is
/// `cameraYawMoveSpeed`, which `cvars::REGISTERED` does not carry, and `_Load` does
/// `slider:SetValue(GetCVar(value.cvar))` — `Slider:SetValue` is a shape-A binding (`0x790980`,
/// wow-re `numeric-arg-coercion-law.md`) that raises on a nil in the reference too. The row is
/// listed in [`super::options_tests`]'s unbacked census with its blocker; the day something
/// registers it, THIS assertion goes red and gets deleted, which is the point of writing it as an
/// expectation rather than an ignore.
/// **Both run clean now.** 2115 shipped this test asserting that `_Load` *raised* — pinning the
/// gap rather than hiding it, and saying in as many words that "the day something registers it,
/// THIS assertion goes red and gets deleted". That day is this change: `UIOptionsFrameSliders`
/// row 3 is `cameraYawMoveSpeed`, `_Load` does `slider:SetValue(GetCVar(value.cvar))`, and
/// `Slider:SetValue` is a shape-A binding (`0x790980`, wow-re `numeric-arg-coercion-law.md`) that
/// raises on a nil in the reference too. All four slider CVars are registered, so the walk reaches
/// its end — and `_SetDefaults`, which does the same through `GetCVarDefault`, with it.
#[test]
fn the_stock_options_windows_load_and_save_are_reachable_for_addons() {
let _data = benilla_formats::wow_data_or_skip!();
@@ -1677,6 +1798,17 @@ fn the_stock_options_windows_load_and_save_are_reachable_for_addons() {
exists: true,
name: Some("Probefive".into()),
level: 60,
// **The player has a CLASS, because a real one always does** — and `_Load`'s tail
// needs it: `UIOptionsFrame_UpdateDependencies` (lua l.759-763) does
// `local temp, class = UnitClass("player"); class = strupper(class)` to decide whether
// the combo-point combat-text box applies, and `strupper(nil)` raises. It could not be
// reached before, because `_Load` died at slider 3 forty lines earlier — so this is
// 2115 §10's lesson a second time, in the same window: a probe VM that runs the stock
// interface has to be a VM the stock interface's own assumptions hold in. Warrior
// rather than Rogue/Druid so the DISABLE branch runs, which is the one with a call in
// it (`OptionsFrame_DisableCheckBox`).
class: Some("Warrior".into()),
class_file: Some("WARRIOR".into()),
..Default::default()
}),
);
@@ -1688,6 +1820,52 @@ fn the_stock_options_windows_load_and_save_are_reachable_for_addons() {
// `SHOW_PARTY_PETS` arm reaches `RefreshBuffs`, whose first act is `this.hasDispellable = nil`
// (`BuffFrame.lua:266`), so the reference's ambient handler global has to be set. It is, at
// every real call site: the Okay button here, and pfUI's own checkbox on its GVAR path.
// **`_Load` first, because that is the window's own order** — the reference loads on show and
// saves on Okay, and driving Okay over a window nothing had loaded is what made an earlier cut
// of this test read a slider floor back out of the CVar. It also makes the Okay run below
// exercise a *populated* window, which is strictly the better coverage.
s.run("this = UIOptionsFrameOkay UIOptionsFrame_Load()")
.expect("_Load runs to completion — every slider CVar it reads is registered");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
// The four slider CVars, by the file's own table rather than by a list retyped here: every one
// must answer, because a single nil among them puts the raise back.
let unbacked: Vec<String> = s
.eval(
"local out = {} \
for _, v in ipairs(UIOptionsFrameSliders) do \
if GetCVar(v.cvar) == nil then table.insert(out, v.cvar) end \
end \
return out",
)
.expect("read UIOptionsFrameSliders");
assert!(
unbacked.is_empty(),
"these slider CVars answer nil, and `_Load` raises on the first of them: {unbacked:?}"
);
// …and the walk really did reach slider 3 rather than stopping short of it: the Mouse Look
// Speed slider is sitting on `cameraYawMoveSpeed`'s registered 180. Slider 3 is the one that
// matters — it is the row whose nil raised, forty lines into a walk of 69 check buttons.
let slider3: f64 = s
.eval("return UIOptionsFrameSlider3:GetValue()")
.expect("the Mouse Look Speed slider's value");
assert!(
(slider3 - 180.0).abs() < 0.001,
"slider 3 should carry cameraYawMoveSpeed's registered 180, got {slider3}"
);
// `_SetDefaults` is the same walk through `GetCVarDefault`, and it had the same raise.
s.run("this = UIOptionsFrameDefaults UIOptionsFrame_SetDefaults()")
.expect("_SetDefaults runs to completion too");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
// The Okay path, over the window `_Load` just populated. Driven through the reference's OWN
// caller — `UIOptionsFrameOkay`'s `<OnClick>` (xml l.1205-1209) — rather than as a bare call,
// and that is not ceremony: the `SHOW_PARTY_PETS` arm reaches `RefreshBuffs`, whose first act
// is `this.hasDispellable = nil` (`BuffFrame.lua:266`), so the reference's ambient handler
// global has to be set. It is, at every real call site: the Okay button here, and pfUI's own
// checkbox on its GVAR path.
s.run("UIOptionsFrameOkay:Click()")
.expect("the stock Okay button's own handler");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
@@ -1696,17 +1874,18 @@ fn the_stock_options_windows_load_and_save_are_reachable_for_addons() {
"…and the window it hides on the way out was hidden to begin with"
);
let err = s
.run("this = UIOptionsFrameOkay UIOptionsFrame_Load()")
.expect_err("_Load stops at the one unregistered slider CVar — see the doc comment");
let err = err.to_string();
assert!(
err.contains("SetValue"),
"the ONLY thing that may stop _Load is slider 3's nil CVar; got: {err}"
// **`_Save` writes the two move-speed CVars for real now**, which is the other half of the
// wiring and was a silent no-op before they were registered: it writes the yaw slider's value
// and, beside it, `cameraPitchMoveSpeed = value / 2` (lua l.355-356) — the relation the
// reference's own registered defaults confirm, 180 and 90.
let (yaw, pitch): (f64, f64) = (
s.eval(r#"return tonumber(GetCVar("cameraYawMoveSpeed"))"#)
.expect("yaw move speed"),
s.eval(r#"return tonumber(GetCVar("cameraPitchMoveSpeed"))"#)
.expect("pitch move speed"),
);
assert!(
s.eval::<bool>(r#"return GetCVar("cameraYawMoveSpeed") == nil"#)
.unwrap(),
"…and that is the cause: cameraYawMoveSpeed is the unregistered one"
(yaw - 180.0).abs() < 0.001 && (pitch - 90.0).abs() < 0.001,
"_Save should write the slider's 180 and its half; got yaw={yaw} pitch={pitch}"
);
}
+4 -2
View File
@@ -281,8 +281,10 @@ pub(super) const UIPARENT_STAND_INS: &str = r#"
end
-- The four options/menu windows `IsOptionFrameOpen` (l.997) and `ToggleGameMenu` (l.1467)
-- index unguarded. `IsOptionFrameOpen` is on the path of every window close, so a kit that
-- loads no options window raised on the first bag click; ours answers all three of the
-- reference's options windows (1987) and a kit may load none of them.
-- loads no options window raised on the first bag click. In the shipped manifest all four
-- names are real — `OptionsFrame` is our window's own, `UIOptionsFrame` and
-- `SoundOptionsFrame` are the reference's own files loaded hidden — but a KIT is a prefix of
-- the manifest and may load none of them, which is what these stand-ins are for.
local function benilla_seat_options()
benilla_seat({ "GameMenuFrame", "OptionsFrame", "UIOptionsFrame", "SoundOptionsFrame" })
if not OptionsFrameCancel then