`MouselookStart()` and `MouselookStop()` now start and end mouse look with no button held, as in 1.12.1, so an addon such as CustomNameplates can steer with the mouse; `IsMouselooking()` reads the same state. Before, both did nothing. - `MouselookStart` (`0x514210`, ungated) disarms a pending world click (`0x514810(0)`) and sets the turn channel the held right button drives (`0x515090(1, 1, …)`), entering the right button's look session: the same cursor lock and hide, the camera and the character turning with the mouse. `MouselookStop` (`0x514240`) clears it; `IsMouselooking` (`0x514270`) tests it. A right-button release ends a script-started look, as its `TurnOrActionStop` clears the same bit; the loading cover clears it (`0x5144c0`); a window deactivate does not (`0x5144a3 and eax,0xfffff00f` keeps the bit). - A look session, right-drag or script-started, outlives losing window focus, as 1.12.1's does, and the OS cursor is handed back while the window is unfocused and taken again on focus, with no turn from the pointer's travel away. On macOS winit's cursor lock is the global `CGAssociateMouseAndMouseCursorPosition(false)`, which a deactivate leaves in place, so a held lock would freeze the pointer in other apps; 1.12.1's Windows client gives the cursor back while inactive. - While focused, the plain right- and left-button paths are unchanged. Fixes #487 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
536 lines
20 KiB
Rust
536 lines
20 KiB
Rust
//! The functions the stock `Bindings.xml` bodies call, from the reference's three registrar
|
|
//! tables: the movement table `0x8500b8` (loop `0x514349`), the camera table `0x84f7a0` (loop
|
|
//! `0x50c089`) and the targeting and posture verbs of the main table `0x83de68` (loop
|
|
//! `0x49027c`). A key and its Lua function share one path: a held command's Start and Stop are the
|
|
//! key's press and release ([`HeldInput`]), a one-shot command fires as its key does
|
|
//! ([`FiredInput`]), and the targeting and attack calls go through the call queue
|
|
//! ([`ScriptCall`]), so a macro's `TargetNearestEnemy()` then `/cast` casts at the new target.
|
|
//!
|
|
//! Every movement function but the mouselook three opens on the hardware-event gate `0x494a50`
|
|
//! with tag 0. Untainted, it passes only inside a real input event, which for a binding body is
|
|
//! `CBindings::ExecuteBinding 0x4b7990` on the stack (`[[0xb71290]+0xd8]`, raised at `0x4b7b02`);
|
|
//! tainted code (a macro, `RunScript`, an addon's own) is refused. So a key's body moves and a
|
|
//! `/script MoveForwardStart()` does not. benilla has no taint model, so here the gate is "a
|
|
//! binding body is running" ([`UiScript::run_binding`]), which refuses the same callers, silently
|
|
//! where the reference fires `MACRO_ACTION_FORBIDDEN` (431) or `ADDON_ACTION_FORBIDDEN` (432).
|
|
//! The camera table and the main-table verbs have no gate.
|
|
|
|
use mlua::{Lua, MultiValue, Value};
|
|
|
|
use super::binding_abi::{bool_or_default, flag};
|
|
use super::calls::{NearestMode, ScriptCall};
|
|
use super::{Model, UiScript};
|
|
|
|
/// A held movement command: one bit of the reference's `[InputControl+4]` word, set by its Start
|
|
/// and cleared by its Stop through `0x515090`.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum HeldInput {
|
|
/// `0x10`, `MoveForwardStart`/`Stop` (`0x513e20`/`0x513e50`).
|
|
MoveForward,
|
|
/// `0x20`, `MoveBackwardStart`/`Stop` (`0x513e80`/`0x513eb0`).
|
|
MoveBackward,
|
|
/// `0x100`, `TurnLeftStart`/`Stop` (`0x513ee0`/`0x513f10`).
|
|
TurnLeft,
|
|
/// `0x200`, `TurnRightStart`/`Stop` (`0x513f40`/`0x513f70`).
|
|
TurnRight,
|
|
/// `0x40`, `StrafeLeftStart`/`Stop` (`0x513fa0`/`0x513fd0`).
|
|
StrafeLeft,
|
|
/// `0x80`, `StrafeRightStart`/`Stop` (`0x514000`/`0x514030`).
|
|
StrafeRight,
|
|
}
|
|
|
|
/// A one-shot command, fired as its key fires it.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum FiredInput {
|
|
/// `Jump()` (`0x513bd0`).
|
|
Jump,
|
|
/// `ToggleRun()` (`0x513d50` → `0x60e080`), the walk toggle.
|
|
ToggleRun,
|
|
/// `ToggleAutoRun()` (`0x513de0`), which toggles `0x1000`.
|
|
ToggleAutoRun,
|
|
/// `SitOrStand()` (`0x48b920`): sit when standing, else stand (`0x5ed430(1)` or `(0)`).
|
|
SitOrStand,
|
|
/// `ToggleSheath()` (`0x48a070` → `0x5eb480`).
|
|
ToggleSheath,
|
|
/// `CameraZoomIn([yards])` (`0x50b400` → `0x50fc60`), 1.0 when absent.
|
|
CameraZoomIn(f32),
|
|
/// `CameraZoomOut([yards])` (`0x50b450` → `0x50fcd0`), 1.0 when absent.
|
|
CameraZoomOut(f32),
|
|
}
|
|
|
|
/// One call for the app's binding state, in call order.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum BindingInput {
|
|
Start(HeldInput),
|
|
Stop(HeldInput),
|
|
Fire(FiredInput),
|
|
}
|
|
|
|
/// The VM's half of the binding functions.
|
|
#[derive(Default)]
|
|
pub(crate) struct InputState {
|
|
/// The calls made since the app last drained them.
|
|
queue: Vec<BindingInput>,
|
|
/// How many binding bodies are running: the hardware-event gate's `[[0xb71290]+0xd8]`.
|
|
binding_depth: u32,
|
|
/// `[InputControl+4] & 1`, the TurnOrAction channel `IsMouselooking` tests (`0x514278`), as
|
|
/// the app pushed it or a `MouselookStart`/`Stop` since moved it.
|
|
turn_or_action: bool,
|
|
/// The `MouselookStart` (`true`) and `MouselookStop` calls since the app last drained them.
|
|
mouselook: Vec<bool>,
|
|
}
|
|
|
|
impl UiScript {
|
|
/// Take the binding calls queued since the last call, in call order.
|
|
pub fn take_binding_input(&mut self) -> Vec<BindingInput> {
|
|
std::mem::take(&mut self.model_mut().input.queue)
|
|
}
|
|
|
|
/// Run a binding body as `CBindings::RunCommand` (`0x4b7b50`) does from inside
|
|
/// `ExecuteBinding`: the movement functions' hardware-event gate passes while it runs.
|
|
pub fn run_binding(&self, chunk: &str) -> mlua::Result<()> {
|
|
self.model_mut().input.binding_depth += 1;
|
|
let result = self.run(chunk);
|
|
self.model_mut().input.binding_depth -= 1;
|
|
result
|
|
}
|
|
|
|
/// Push whether the TurnOrAction channel is held: the world holds the right mouse button, or
|
|
/// a `MouselookStart` holds it with none.
|
|
pub fn set_turn_or_action_held(&mut self, held: bool) {
|
|
self.model_mut().input.turn_or_action = held;
|
|
}
|
|
|
|
/// Take the `MouselookStart` (`true`) and `MouselookStop` calls made since the last call, in
|
|
/// call order, for the app's look session.
|
|
pub fn take_mouselook_calls(&mut self) -> Vec<bool> {
|
|
std::mem::take(&mut self.model_mut().input.mouselook)
|
|
}
|
|
}
|
|
|
|
/// The hardware-event gate `0x494a50(0)`: inside a binding body.
|
|
fn in_binding(lua: &Lua) -> bool {
|
|
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
|
model.input.binding_depth > 0
|
|
}
|
|
|
|
fn push(lua: &Lua, input: BindingInput) {
|
|
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
|
model.input.queue.push(input);
|
|
}
|
|
|
|
fn push_call(lua: &Lua, call: ScriptCall) {
|
|
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
|
model.script_calls.push(call);
|
|
}
|
|
|
|
/// A zoom's optional amount: `0x6f34d0` (is-number, a numeric string passes) then `0x6f3620`,
|
|
/// else the stored `1.0f` (`0x50b42d`). A NaN or infinite amount is dropped: what `0x50fc60`'s
|
|
/// tick conversion makes of it is not established.
|
|
fn zoom_amount(lua: &Lua, v: Option<Value>) -> Option<f32> {
|
|
let n = match v {
|
|
Some(v) => lua.coerce_number(v).ok().flatten().unwrap_or(1.0),
|
|
None => 1.0,
|
|
} as f32;
|
|
n.is_finite().then_some(n)
|
|
}
|
|
|
|
/// Register a function that takes any arguments and does nothing: the mechanism behind it is not
|
|
/// built, so the call is answered with the reference's zero results and no effect.
|
|
fn inert(lua: &Lua, names: &[&str]) -> mlua::Result<()> {
|
|
let g = lua.globals();
|
|
for name in names {
|
|
g.set(*name, lua.create_function(|_, _: MultiValue| Ok(()))?)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Register the 49 functions: the three tables' 48 and `TargetNearestFriend`.
|
|
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
|
let g = lua.globals();
|
|
|
|
// ── The movement table `0x8500b8` ──────────────────────────────────────────────────────
|
|
// Each Start/Stop is `0x494a50(0)`, then `0x515090(bit, set, time, 0)`; no argument is read,
|
|
// nothing is returned.
|
|
let held: [(&str, &str, HeldInput); 6] = [
|
|
(
|
|
"MoveForwardStart",
|
|
"MoveForwardStop",
|
|
HeldInput::MoveForward,
|
|
),
|
|
(
|
|
"MoveBackwardStart",
|
|
"MoveBackwardStop",
|
|
HeldInput::MoveBackward,
|
|
),
|
|
("TurnLeftStart", "TurnLeftStop", HeldInput::TurnLeft),
|
|
("TurnRightStart", "TurnRightStop", HeldInput::TurnRight),
|
|
("StrafeLeftStart", "StrafeLeftStop", HeldInput::StrafeLeft),
|
|
(
|
|
"StrafeRightStart",
|
|
"StrafeRightStop",
|
|
HeldInput::StrafeRight,
|
|
),
|
|
];
|
|
for (start, stop, input) in held {
|
|
g.set(
|
|
start,
|
|
lua.create_function(move |lua, _: MultiValue| {
|
|
if in_binding(lua) {
|
|
push(lua, BindingInput::Start(input));
|
|
}
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
g.set(
|
|
stop,
|
|
lua.create_function(move |lua, _: MultiValue| {
|
|
if in_binding(lua) {
|
|
push(lua, BindingInput::Stop(input));
|
|
}
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
}
|
|
// `Jump` (`0x513bd0`), `ToggleRun` (`0x513d50`) and `ToggleAutoRun` (`0x513de0`) open on the
|
|
// same gate; their further refusals (a dead or rooted player) are the engine's, as for the key.
|
|
let fired: [(&str, FiredInput); 3] = [
|
|
("Jump", FiredInput::Jump),
|
|
("ToggleRun", FiredInput::ToggleRun),
|
|
("ToggleAutoRun", FiredInput::ToggleAutoRun),
|
|
];
|
|
for (name, input) in fired {
|
|
g.set(
|
|
name,
|
|
lua.create_function(move |lua, _: MultiValue| {
|
|
if in_binding(lua) {
|
|
push(lua, BindingInput::Fire(input));
|
|
}
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
}
|
|
// `IsMouselooking()` (`0x514270`): `[InputControl+4] & 1`, the TurnOrAction channel, not the
|
|
// camera's freelook bit; 1 or nil (`0x514280`/`0x514293`).
|
|
g.set(
|
|
"IsMouselooking",
|
|
lua.create_function(|lua, _: MultiValue| {
|
|
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
|
Ok(flag(model.input.turn_or_action))
|
|
})?,
|
|
)?;
|
|
// `MouselookStart`/`MouselookStop` (`0x514210`/`0x514240`) pass no gate: each disarms the
|
|
// pending world click (`0x514810(0)`) and sets or clears the TurnOrAction channel
|
|
// (`0x515090(1, set, now, 0)`), the bit the held right button drives. The bit moves at the
|
|
// call, so `IsMouselooking` answers it at once; the app's look session applies the call.
|
|
for (name, on) in [("MouselookStart", true), ("MouselookStop", false)] {
|
|
g.set(
|
|
name,
|
|
lua.create_function(move |lua, _: MultiValue| {
|
|
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
|
model.input.turn_or_action = on;
|
|
model.input.mouselook.push(on);
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
}
|
|
// Not built, so these answer and do nothing:
|
|
// - the pitch pair (`0x400`/`0x800`): the mover has no keyboard pitch axis (`ABSENT` PITCHUP);
|
|
// - TurnOrAction and CameraOrSelectOrMove (`0x514120`-`0x5141d0`), which arm the world click
|
|
// (`0x514810`) and set the mouse bits: the look session reads the physical buttons for
|
|
// these (`ABSENT` TURNORACTION, CAMERAORSELECTORMOVE).
|
|
inert(
|
|
lua,
|
|
&[
|
|
"PitchUpStart",
|
|
"PitchUpStop",
|
|
"PitchDownStart",
|
|
"PitchDownStop",
|
|
"TurnOrActionStart",
|
|
"TurnOrActionStop",
|
|
"CameraOrSelectOrMoveStart",
|
|
"CameraOrSelectOrMoveStop",
|
|
],
|
|
)?;
|
|
|
|
// ── The camera table `0x84f7a0` ────────────────────────────────────────────────────────
|
|
g.set(
|
|
"CameraZoomIn",
|
|
lua.create_function(|lua, v: Option<Value>| {
|
|
if let Some(yards) = zoom_amount(lua, v) {
|
|
push(lua, BindingInput::Fire(FiredInput::CameraZoomIn(yards)));
|
|
}
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
g.set(
|
|
"CameraZoomOut",
|
|
lua.create_function(|lua, v: Option<Value>| {
|
|
if let Some(yards) = zoom_amount(lua, v) {
|
|
push(lua, BindingInput::Fire(FiredInput::CameraZoomOut(yards)));
|
|
}
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
// Not built: the `MoveView*` pairs (`0x50b4a0`-`0x50b590`, `0x50fd40(direction, time, 0)` and
|
|
// `0x50fd90(direction, time)` for In, Out, Right, Left, Up, Down = 0..5), whose effect on the
|
|
// camera is not established; 1.12's own `MOVEVIEW*` bindings are commented out. And
|
|
// `ToggleMouseMove` (`0x50b5a0` → `0x50f910`), which toggles freelook with no button held.
|
|
inert(
|
|
lua,
|
|
&[
|
|
"MoveViewInStart",
|
|
"MoveViewInStop",
|
|
"MoveViewOutStart",
|
|
"MoveViewOutStop",
|
|
"MoveViewRightStart",
|
|
"MoveViewRightStop",
|
|
"MoveViewLeftStart",
|
|
"MoveViewLeftStop",
|
|
"MoveViewUpStart",
|
|
"MoveViewUpStop",
|
|
"MoveViewDownStart",
|
|
"MoveViewDownStop",
|
|
"ToggleMouseMove",
|
|
],
|
|
)?;
|
|
|
|
// ── The main table `0x83de68` ──────────────────────────────────────────────────────────
|
|
// The four `TargetNearest*` shims are byte-identical but for the mode: argument 1 through
|
|
// `0x6f1c10` with default 0 is the reverse flag, then `0x493f60(reverse, mode)`.
|
|
let nearest: [(&str, NearestMode); 4] = [
|
|
("TargetNearestEnemy", NearestMode::Enemy),
|
|
("TargetNearestFriend", NearestMode::Friend),
|
|
("TargetNearestPartyMember", NearestMode::PartyMember),
|
|
("TargetNearestRaidMember", NearestMode::RaidMember),
|
|
];
|
|
for (name, mode) in nearest {
|
|
g.set(
|
|
name,
|
|
lua.create_function(move |lua, reverse: Option<Value>| {
|
|
let reverse = bool_or_default(reverse.as_ref(), false);
|
|
push_call(lua, ScriptCall::TargetNearest { mode, reverse });
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
}
|
|
// `TargetLastTarget()` (`0x489b00`) and `AttackTarget()` (`0x489b50`) read no argument.
|
|
g.set(
|
|
"TargetLastTarget",
|
|
lua.create_function(|lua, _: MultiValue| {
|
|
push_call(lua, ScriptCall::TargetLastTarget);
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
g.set(
|
|
"AttackTarget",
|
|
lua.create_function(|lua, _: MultiValue| {
|
|
push_call(lua, ScriptCall::AttackTarget);
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
// `ToggleSheath()` and `SitOrStand()` resolve the active player and act on it, ungated.
|
|
let posture: [(&str, FiredInput); 2] = [
|
|
("ToggleSheath", FiredInput::ToggleSheath),
|
|
("SitOrStand", FiredInput::SitOrStand),
|
|
];
|
|
for (name, input) in posture {
|
|
g.set(
|
|
name,
|
|
lua.create_function(move |lua, _: MultiValue| {
|
|
push(lua, BindingInput::Fire(input));
|
|
Ok(())
|
|
})?,
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// The three reference tables' 48 names (`0x8500b8`, `0x84f7a0`, and the main table's seven
|
|
/// at `0x83de68`), each a callable global that answers without raising.
|
|
const NAMES: [&str; 48] = [
|
|
"Jump",
|
|
"ToggleRun",
|
|
"ToggleAutoRun",
|
|
"MoveForwardStart",
|
|
"MoveForwardStop",
|
|
"MoveBackwardStart",
|
|
"MoveBackwardStop",
|
|
"TurnLeftStart",
|
|
"TurnLeftStop",
|
|
"TurnRightStart",
|
|
"TurnRightStop",
|
|
"StrafeLeftStart",
|
|
"StrafeLeftStop",
|
|
"StrafeRightStart",
|
|
"StrafeRightStop",
|
|
"PitchUpStart",
|
|
"PitchUpStop",
|
|
"PitchDownStart",
|
|
"PitchDownStop",
|
|
"TurnOrActionStart",
|
|
"TurnOrActionStop",
|
|
"CameraOrSelectOrMoveStart",
|
|
"CameraOrSelectOrMoveStop",
|
|
"MouselookStart",
|
|
"MouselookStop",
|
|
"IsMouselooking",
|
|
"CameraZoomIn",
|
|
"CameraZoomOut",
|
|
"MoveViewInStart",
|
|
"MoveViewInStop",
|
|
"MoveViewOutStart",
|
|
"MoveViewOutStop",
|
|
"MoveViewRightStart",
|
|
"MoveViewRightStop",
|
|
"MoveViewLeftStart",
|
|
"MoveViewLeftStop",
|
|
"MoveViewUpStart",
|
|
"MoveViewUpStop",
|
|
"MoveViewDownStart",
|
|
"MoveViewDownStop",
|
|
"ToggleMouseMove",
|
|
"TargetNearestEnemy",
|
|
"TargetNearestPartyMember",
|
|
"TargetNearestRaidMember",
|
|
"TargetLastTarget",
|
|
"AttackTarget",
|
|
"ToggleSheath",
|
|
"SitOrStand",
|
|
];
|
|
|
|
#[test]
|
|
fn every_binding_function_is_a_global_that_answers() {
|
|
let s = UiScript::new().unwrap();
|
|
for name in NAMES {
|
|
let kind: String = s.eval(&format!("return type({name})")).unwrap();
|
|
assert_eq!(kind, "function", "{name}");
|
|
s.run(&format!("{name}()"))
|
|
.unwrap_or_else(|e| panic!("{name}() raised: {e}"));
|
|
s.run_binding(&format!("{name}(1)"))
|
|
.unwrap_or_else(|e| panic!("{name}(1) in a binding raised: {e}"));
|
|
}
|
|
}
|
|
|
|
/// The gate `0x494a50(0)`: a movement call queues only from inside a binding body, while the
|
|
/// camera and posture verbs, which have no gate, queue from anywhere.
|
|
#[test]
|
|
fn the_movement_calls_act_only_inside_a_binding_body() {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.run("MoveForwardStart() StrafeLeftStop() Jump() ToggleRun()")
|
|
.unwrap();
|
|
assert!(s.take_binding_input().is_empty());
|
|
s.run_binding("MoveForwardStart() StrafeLeftStop() Jump() ToggleRun()")
|
|
.unwrap();
|
|
assert_eq!(
|
|
s.take_binding_input(),
|
|
vec![
|
|
BindingInput::Start(HeldInput::MoveForward),
|
|
BindingInput::Stop(HeldInput::StrafeLeft),
|
|
BindingInput::Fire(FiredInput::Jump),
|
|
BindingInput::Fire(FiredInput::ToggleRun),
|
|
]
|
|
);
|
|
// The gate closes again after the body, and after a body that raised.
|
|
assert!(s.run_binding("error('boom')").is_err());
|
|
s.run("TurnLeftStart()").unwrap();
|
|
assert!(s.take_binding_input().is_empty());
|
|
s.run("SitOrStand() ToggleSheath() CameraZoomIn()").unwrap();
|
|
assert_eq!(
|
|
s.take_binding_input(),
|
|
vec![
|
|
BindingInput::Fire(FiredInput::SitOrStand),
|
|
BindingInput::Fire(FiredInput::ToggleSheath),
|
|
BindingInput::Fire(FiredInput::CameraZoomIn(1.0)),
|
|
]
|
|
);
|
|
}
|
|
|
|
/// The zoom's amount: a number or numeric string (`0x6f34d0`), else 1.0 (`0x50b42d`); a
|
|
/// non-finite amount is dropped.
|
|
#[test]
|
|
fn a_zoom_takes_its_yards_or_one() {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.run(r#"CameraZoomIn(2.5) CameraZoomOut("4") CameraZoomIn("far") CameraZoomOut(0/0)"#)
|
|
.unwrap();
|
|
assert_eq!(
|
|
s.take_binding_input(),
|
|
vec![
|
|
BindingInput::Fire(FiredInput::CameraZoomIn(2.5)),
|
|
BindingInput::Fire(FiredInput::CameraZoomOut(4.0)),
|
|
BindingInput::Fire(FiredInput::CameraZoomIn(1.0)),
|
|
]
|
|
);
|
|
}
|
|
|
|
/// `IsMouselooking` answers the pushed TurnOrAction channel as 1 or nil.
|
|
#[test]
|
|
fn is_mouselooking_answers_one_or_nil() {
|
|
let mut s = UiScript::new().unwrap();
|
|
assert_eq!(s.arity("IsMouselooking()").unwrap(), 1);
|
|
assert!(s
|
|
.eval::<Option<i64>>("return IsMouselooking()")
|
|
.unwrap()
|
|
.is_none());
|
|
s.set_turn_or_action_held(true);
|
|
assert_eq!(
|
|
s.eval::<Option<i64>>("return IsMouselooking()").unwrap(),
|
|
Some(1)
|
|
);
|
|
}
|
|
|
|
/// `MouselookStart`/`Stop` pass no gate (`0x514210`/`0x514240`), queue for the look session in
|
|
/// call order, and move the channel at the call, so `IsMouselooking` answers them at once.
|
|
#[test]
|
|
fn mouselook_start_and_stop_move_the_channel_from_any_caller() {
|
|
let mut s = UiScript::new().unwrap();
|
|
assert_eq!(s.arity("MouselookStart()").unwrap(), 0);
|
|
s.run("MouselookStop()").unwrap();
|
|
s.take_mouselook_calls();
|
|
let looking = |s: &UiScript| s.eval::<Option<i64>>("return IsMouselooking()").unwrap();
|
|
s.run("MouselookStart()").unwrap();
|
|
assert_eq!(
|
|
looking(&s),
|
|
Some(1),
|
|
"an addon's own call, outside any binding"
|
|
);
|
|
s.run("MouselookStop() MouselookStart() MouselookStop()")
|
|
.unwrap();
|
|
assert_eq!(looking(&s), None);
|
|
assert_eq!(s.take_mouselook_calls(), vec![true, false, true, false]);
|
|
assert!(s.take_mouselook_calls().is_empty(), "drained");
|
|
assert!(
|
|
s.take_binding_input().is_empty(),
|
|
"not a movement-binding call"
|
|
);
|
|
// The app's push is the channel's truth from then on: a right release ended it.
|
|
s.run("MouselookStart()").unwrap();
|
|
s.set_turn_or_action_held(false);
|
|
assert_eq!(looking(&s), None);
|
|
}
|
|
|
|
/// `TargetLastTarget` and `AttackTarget` join the call queue, in call order with the rest.
|
|
#[test]
|
|
fn the_targeting_calls_join_the_call_queue_in_order() {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.run(r#"TargetNearestEnemy(1) TargetLastTarget() AttackTarget() ClearTarget()"#)
|
|
.unwrap();
|
|
assert_eq!(
|
|
s.take_script_calls(),
|
|
vec![
|
|
ScriptCall::TargetNearest {
|
|
mode: NearestMode::Enemy,
|
|
reverse: true
|
|
},
|
|
ScriptCall::TargetLastTarget,
|
|
ScriptCall::AttackTarget,
|
|
ScriptCall::ClearTarget,
|
|
]
|
|
);
|
|
}
|
|
}
|