compat: the cast-info pair as bare globals, and IsMouseButtonDown over a real feed
Cinematic's last two session errors. With these it loads clean, survives a session start, survives a UI probe and **draws** — all four of the harness's runtime columns. **`UnitCastingInfo`/`UnitChannelInfo` as bare globals** is an export, not a feature: the namespaced pair was already built with the whole contract in `cast_info` (the two return shapes, the millisecond unit, every position this client leaves nil and why), and these hand out the **same function values** the `C_Spell` table holds, so the two spellings cannot drift. Six calls of each in `Cinematic/Camera.lua` and its siblings, unguarded. `CastingInfo`/ `ChannelInfo` stay namespace-only: nothing on this install calls them bare, and pfUI reaches the one it wants through the namespace (`libdebuff.lua:887`). **`IsMouseButtonDown` needed a feed before it could be built honestly, and the reason it was declined is why.** `compat/classic_api/input.rs` had said no on the grounds that the only press state here is `Model::mouse_down_on`, which the pointer layer fills *only* for a press that landed on a frame — and `Cinematic/Camera.lua:612` asks in order to spot a right-drag **on the world**, which is exactly what that map cannot see. Answering from it would have been wrong for the one caller. So `Model::mouse_buttons` is new: `[Left, Right, Middle, Button4, Button5]`, pushed each frame from `ButtonInput<MouseButton>` beside the modifiers. Placed there deliberately rather than with the pointer feed below it, because that feed is skipped for a cursor off the window or a hidden UI, and "is the right button still held" has to stay true through both — which is the camera's question. Both spellings (`RightButton` and `Button2`), no argument means any button as upstream's does, and a button no device has answers not-down rather than raising. The reference capture of the real client's `_G` has no mouse-button getter at all, and `ClassicAPI.dll` carries the name, so it is compat's rather than a 1.12 gap. Corpus: 100 addons, 85 ok / 1 fail, roster identical. benilla-ui 2082 passed. Chain green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -143,6 +143,16 @@ pub(super) fn feed_ui_input(
|
||||
let ctrl = keys.pressed(KeyCode::ControlLeft) || keys.pressed(KeyCode::ControlRight);
|
||||
let alt = keys.pressed(KeyCode::AltLeft) || keys.pressed(KeyCode::AltRight);
|
||||
script.set_modifiers(shift, ctrl, alt);
|
||||
// The device's own button state, for `IsMouseButtonDown`. Pushed here rather than from the
|
||||
// pointer feed below because that one is skipped for a cursor off the window or a hidden UI,
|
||||
// and "is the right button held" stays true through both.
|
||||
script.set_mouse_buttons([
|
||||
buttons.pressed(MouseButton::Left),
|
||||
buttons.pressed(MouseButton::Right),
|
||||
buttons.pressed(MouseButton::Middle),
|
||||
buttons.pressed(MouseButton::Other(4)),
|
||||
buttons.pressed(MouseButton::Other(5)),
|
||||
]);
|
||||
// ── Mouse ── A cursor off the window, or a UI hidden by TOGGLEUI, skips only the mouse feed.
|
||||
// The headless hover probe's aim stands in for a missing cursor, so `PointerOverUi` rises and
|
||||
// falls over a panel in an automated run as it does for a person; a real pointer always wins.
|
||||
|
||||
@@ -8,11 +8,16 @@
|
||||
//! caller asking "is left shift down" gets "is shift down", never a fabricated `false` for the
|
||||
//! half the engine does not model.
|
||||
//!
|
||||
//! **`IsMouseButtonDown([button])` is now built, and the old reason for leaving it out is why it
|
||||
//! needed a feed first.** That reason stands: `Model::mouse_down_on` is a button-to-frame map the
|
||||
//! pointer layer fills *only* for a press that landed on a frame, so a press on the world reads as
|
||||
//! not-down there and answering from it would be wrong more often than right — and wrong for the
|
||||
//! very caller that wanted it, since `Cinematic/Camera.lua:612` asks in order to spot a right-drag
|
||||
//! **on the world**. So the verb reads `Model::mouse_buttons` instead, the host's own button state
|
||||
//! pushed per frame beside the modifiers (`ui_script/input.rs`), which is the device rather than
|
||||
//! the UI.
|
||||
//!
|
||||
//! Not built, for want of state rather than by choice:
|
||||
//! - `IsMouseButtonDown([button])`: the only mouse-press state here is `Model::mouse_down_on`, a
|
||||
//! button-to-frame map the pointer layer fills *only* for a press that landed on a frame and
|
||||
//! clears otherwise (`script/pointer.rs`). A press on the world would read as not-down, so
|
||||
//! answering from it would be wrong more often than right.
|
||||
//! - `GetMouseButtonClicked()`: wants the button that triggered the handler currently running.
|
||||
//! Handler dispatch (`script/event.rs`) passes the button to `OnClick` as an argument and keeps
|
||||
//! no ambient "current event's button", which this getter would read.
|
||||
@@ -22,6 +27,20 @@ use mlua::Lua;
|
||||
use crate::script::binding_abi::flag;
|
||||
use crate::script::Model;
|
||||
|
||||
/// The index into [`Model::mouse_buttons`] a button name reads, or `None` for a name no device
|
||||
/// has. Both spellings: `LeftButton` is what this client's own click path passes a handler, and
|
||||
/// `Button1`..`Button5` is the numbered form later clients accept.
|
||||
fn button_index(name: &str) -> Option<usize> {
|
||||
match name {
|
||||
"LeftButton" | "Button1" => Some(0),
|
||||
"RightButton" | "Button2" => Some(1),
|
||||
"MiddleButton" | "Button3" => Some(2),
|
||||
"Button4" => Some(3),
|
||||
"Button5" => Some(4),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Which of the `(shift, ctrl, alt)` triple a name reads.
|
||||
fn held(lua: &Lua, pick: usize) -> mlua::Value {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
@@ -57,6 +76,23 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// `IsMouseButtonDown([button])`: the device's own state, `Model::mouse_buttons`. With no
|
||||
// argument, any button, as upstream's does. The `1`/nil shape its siblings use.
|
||||
//
|
||||
// A name no device has answers nil rather than raising: an addon probing `"Button6"` on a
|
||||
// three-button mouse is asking a fair question and the answer is "not down".
|
||||
g.set(
|
||||
"IsMouseButtonDown",
|
||||
lua.create_function(|lua, name: Option<String>| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let down = match name.as_deref() {
|
||||
None => model.mouse_buttons.iter().any(|b| *b),
|
||||
Some(n) => button_index(n).is_some_and(|i| model.mouse_buttons[i]),
|
||||
};
|
||||
Ok(flag(down))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -64,6 +100,53 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
mod tests {
|
||||
use crate::script::UiScript;
|
||||
|
||||
/// The device's buttons, not the UI's presses: a right-drag begun on the world is what
|
||||
/// `Cinematic/Camera.lua:612` asks about, and `Model::mouse_down_on` — the map the pointer
|
||||
/// layer fills only for a press that landed on a frame — cannot see it. Nothing here touches
|
||||
/// a frame at all.
|
||||
#[test]
|
||||
fn is_mouse_button_down_reads_the_device_and_takes_both_spellings() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>(r#"return IsMouseButtonDown("RightButton") == nil"#)
|
||||
.unwrap());
|
||||
assert!(
|
||||
s.eval::<bool>("return IsMouseButtonDown() == nil").unwrap(),
|
||||
"no argument is any button, and none is down"
|
||||
);
|
||||
|
||||
// Right and Button5 held, nothing else.
|
||||
s.set_mouse_buttons([false, true, false, false, true]);
|
||||
assert_eq!(
|
||||
s.eval::<u32>(r#"return IsMouseButtonDown("RightButton")"#)
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<u32>(r#"return IsMouseButtonDown("Button2")"#)
|
||||
.unwrap(),
|
||||
1,
|
||||
"the numbered spelling reads the same button"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<u32>(r#"return IsMouseButtonDown("Button5")"#)
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert!(s
|
||||
.eval::<bool>(r#"return IsMouseButtonDown("LeftButton") == nil"#)
|
||||
.unwrap());
|
||||
assert_eq!(
|
||||
s.eval::<u32>("return IsMouseButtonDown()").unwrap(),
|
||||
1,
|
||||
"no argument is any button"
|
||||
);
|
||||
// A button no device has is not down, rather than a raise.
|
||||
assert!(s
|
||||
.eval::<bool>(r#"return IsMouseButtonDown("Button9") == nil"#)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_side_reads_the_combined_bit_the_engine_actually_tracks() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -626,6 +626,19 @@ pub(super) fn install_globals(lua: &Lua, t: &Table) -> mlua::Result<()> {
|
||||
// pfUI's `modules/bags.lua:703` calls it bare, keyed by spell id, to decide which extract /
|
||||
// disenchant button its bag frame may show.
|
||||
g.set("IsSpellKnown", t.get::<Value>("IsSpellKnown")?)?;
|
||||
// `UnitCastingInfo`/`UnitChannelInfo` as bare globals, which is how an addon written for a
|
||||
// later client calls them — `Cinematic/Camera.lua:632` and five siblings, six calls of each,
|
||||
// and the nil global was its last session error. The **same function values** the table
|
||||
// holds, as every row here does, so the namespaced and bare spellings cannot drift: the whole
|
||||
// contract (the two return shapes, milliseconds, every position this client leaves nil and
|
||||
// why) stays in `cast_info` alone.
|
||||
//
|
||||
// Not exported bare: `CastingInfo`/`ChannelInfo`, the no-argument pair. They are `C_Spell`'s
|
||||
// legacy spelling here and nothing on this install calls them bare — pfUI reaches the one it
|
||||
// wants through the namespace (`libdebuff.lua:887`).
|
||||
for name in ["UnitCastingInfo", "UnitChannelInfo"] {
|
||||
g.set(name, t.get::<Value>(name)?)?;
|
||||
}
|
||||
|
||||
// `GetSpellInfo` as a **bare global**, in both of the forms upstream lists
|
||||
// (`docs/classic-api-functions.md`'s Spell section says so outright: "GetSpellInfo (both
|
||||
|
||||
@@ -26,10 +26,12 @@ mod bind_confirm;
|
||||
mod binder;
|
||||
mod binding_abi;
|
||||
mod dialog_verbs;
|
||||
/// Numeric texture ids, which later clients address art by and 1.12 does not.
|
||||
mod file_ids;
|
||||
mod tutorial;
|
||||
mod worldmap_arrow;
|
||||
// `camera_view`: the five camera views and `FlipCameraYaw`, the reference's `UIUtil\Camera.cpp`.
|
||||
mod button;
|
||||
pub(in crate::script) mod button;
|
||||
mod camera_view;
|
||||
mod cast_events;
|
||||
mod channel;
|
||||
@@ -823,6 +825,17 @@ impl UiScript {
|
||||
model.modifiers = (shift, ctrl, alt);
|
||||
}
|
||||
|
||||
/// Push which mouse buttons are physically down, `[Left, Right, Middle, Button4, Button5]`,
|
||||
/// for `IsMouseButtonDown`. Call it beside [`Self::set_modifiers`], before the mouse feed, so
|
||||
/// a handler asking during a click sees that click's own state.
|
||||
///
|
||||
/// The host's button state, not the UI's: a press on the world counts here and never reaches
|
||||
/// `Model::mouse_down_on`, which is the distinction the verb exists for.
|
||||
pub fn set_mouse_buttons(&mut self, down: [bool; 5]) {
|
||||
let mut model = self.model_mut();
|
||||
model.mouse_buttons = down;
|
||||
}
|
||||
|
||||
/// Push the player's WMO-containment state (the client's `0xceaa60`) onto every Minimap: it
|
||||
/// picks which persisted zoom index `GetZoom`/`SetZoom` act on. Call it before the script
|
||||
/// tick, on each inside/outside edge and whenever [`Self::minimap_widgets_created`] moves.
|
||||
|
||||
@@ -1673,6 +1673,15 @@ pub(crate) struct Model {
|
||||
pub(crate) framerate: f64,
|
||||
/// `(shift, ctrl, alt)` for `Is*KeyDown`, pushed before the frame's mouse events.
|
||||
pub(crate) modifiers: (bool, bool, bool),
|
||||
/// Which mouse buttons are physically down, in the order
|
||||
/// `[Left, Right, Middle, Button4, Button5]`, fed per frame by
|
||||
/// [`crate::script::UiScript::set_mouse_buttons`] from the host's own button state.
|
||||
///
|
||||
/// Distinct from [`Self::mouse_down_on`], which is a *press that landed on a frame* and is
|
||||
/// what the pointer layer routes clicks with. A press on the world is in this array and not
|
||||
/// in that map, which is the whole reason this exists: `IsMouseButtonDown` is asked about the
|
||||
/// device, not about the UI.
|
||||
pub(crate) mouse_buttons: [bool; 5],
|
||||
}
|
||||
|
||||
impl Model {
|
||||
@@ -2349,6 +2358,7 @@ impl Model {
|
||||
summon_confirms: 0,
|
||||
framerate: 0.0,
|
||||
modifiers: (false, false, false),
|
||||
mouse_buttons: [false; 5],
|
||||
money: 0,
|
||||
net_latency_ms: 0,
|
||||
player_xp: 0,
|
||||
|
||||
@@ -202,6 +202,25 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> {
|
||||
// "spellIconID")`. 1.12 reaches an icon only through a spellbook slot, so a spell the
|
||||
// player does not know has no texture an addon can draw.
|
||||
"GetSpellIconTexture",
|
||||
// ClassicAPI's `UnitCastingInfo`/`UnitChannelInfo` as bare globals
|
||||
// (script/compat/classic_api/spell.rs's `install_globals`). 1.12 has no casting-info API
|
||||
// at all, which is why SuperWoW and nampower each added their own; the namespaced pair was
|
||||
// already built and these hand out the same function values, so the two spellings cannot
|
||||
// drift. Six calls of each in `Cinematic/Camera.lua` and its siblings, unguarded.
|
||||
"UnitCastingInfo",
|
||||
"UnitChannelInfo",
|
||||
// ClassicAPI's `IsMouseButtonDown` (script/compat/classic_api/input.rs): absent from the
|
||||
// reference capture of the client's own `_G` — it has no mouse-button getter at all — and
|
||||
// in `ClassicAPI.dll`'s string pool. Reads `Model::mouse_buttons`, the host's own button
|
||||
// state pushed per frame, which is the *device* rather than the UI: the press state the
|
||||
// pointer layer keeps only covers presses that landed on a frame, and the caller that
|
||||
// wanted this (`Cinematic/Camera.lua:612`) is asking about a right-drag on the world.
|
||||
"IsMouseButtonDown",
|
||||
// `GetFileIDFromPath` (script/compat/classic_api/cvar.rs's globals installer, over
|
||||
// `script/file_ids.rs`): later clients address art by numeric id, which 1.12 has no
|
||||
// notion of, and an addon that holds one wants the path back. Answers only the ids the
|
||||
// installed addons annotate at their own call sites, nil otherwise. OpenMapClassic asks.
|
||||
"GetFileIDFromPath",
|
||||
// ClassicAPI's `GetCVarBool` (script/compat/classic_api/cvar.rs). Absent from 1.12 on this
|
||||
// file's own evidence — `reference/1.12-globals.tsv` is a capture of the real client's
|
||||
// `_G` and carries `GetCVar` and `GetCVarDefault` and nothing else of the family — and
|
||||
|
||||
Reference in New Issue
Block a user