//! ClassicAPI's `State` namespace: bare globals, no `C_*` table. 1.12's own Lua has no path to //! any of the posture facts here — no mounted/swim/indoor/falling getter exists on the reference //! client at all — so these are fed from the engine through [`SelfMovement`] //! (`script/self_movement.rs`, filled once per tick by `benilla-app`'s //! `ui_unit::feed_self_movement`), not read off a descriptor field some stock verb already //! exposes. //! //! Not built, each for a stated reason rather than a guess (`docs/ADDON_COMPAT.md` carries the //! same list): //! - `IsStealthed`, `GetSheathState`, `IsAssistingRitual`: no engine state tracks any of the three //! today; each needs its own new feed, not a binding. //! - `GetMirrorTimerInfo`, `GetMirrorTimerProgress`: benilla's mirror timers (breath, fatigue) are //! event-driven only — `benilla-app`'s `ui_mirror.rs` fires `MIRROR_TIMER_START`/`_PAUSE`/`_STOP` //! and keeps no queryable snapshot, which these two getters would need. //! - `Dismount`, `CancelShapeshiftForm`: actions, not getters. Vanilla cancels a form by re-casting //! its own spell, so an honest `CancelShapeshiftForm` needs the active form's spell id (the //! engine knows the form *index*, [`SelfMovement::shapeshift_form`], not the spell behind it); //! `Dismount` has no stock verb here to reuse at all. use mlua::Lua; use crate::script::binding_abi::flag; use crate::script::Model; /// Read one posture bool out of the per-tick snapshot, as the number 1 or nil — the shape every /// 1.12 predicate uses, which an addon tests with `== 1` or `== nil`, never `== true`. fn posture(lua: &Lua, f: impl FnOnce(&crate::script::SelfMovement) -> bool) -> mlua::Value { let model = lua.app_data_ref::().expect("model app_data"); flag(f(&model.self_movement)) } pub(super) fn install(lua: &Lua) -> mlua::Result<()> { let g = lua.globals(); g.set( "IsSwimming", lua.create_function(|lua, ()| Ok(posture(lua, |m| m.swimming)))?, )?; g.set( "IsMounted", lua.create_function(|lua, ()| Ok(posture(lua, |m| m.mounted)))?, )?; g.set( "IsIndoors", lua.create_function(|lua, ()| Ok(posture(lua, |m| m.indoors)))?, )?; // The exact complement of `IsIndoors`: the area pass's bit is one or the other, never neither. g.set( "IsOutdoors", lua.create_function(|lua, ()| Ok(posture(lua, |m| !m.indoors)))?, )?; g.set( "IsFalling", lua.create_function(|lua, ()| Ok(posture(lua, |m| m.falling)))?, )?; // `GetShapeshiftFormID()`: the form index, nil when not shapeshifted — the modern spelling of // a fact 1.12 only exposes per-form through `GetShapeshiftForm*`. g.set( "GetShapeshiftFormID", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model app_data"); let form = model.self_movement.shapeshift_form; Ok((form != 0).then_some(i64::from(form))) })?, )?; // `IsInGroup`/`IsInRaid` read the party/raid roster directly, the one authority the stock // `GetNumPartyMembers`/`GetNumRaidMembers` (`script/party.rs`) already answer from — not the // posture snapshot, so there is only ever one source of truth for group membership. g.set( "IsInGroup", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model app_data"); Ok(flag( !model.party.members.is_empty() || !model.party.raid.is_empty(), )) })?, )?; g.set( "IsInRaid", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model app_data"); Ok(flag(!model.party.raid.is_empty())) })?, )?; Ok(()) } #[cfg(test)] mod tests { use crate::script::{SelfMovement, UiScript}; #[test] fn posture_getters_read_the_fed_snapshot_as_one_or_nil() { let mut s = UiScript::new().unwrap(); // The pre-feed default: everything false, so every getter is nil, never 0. assert_eq!( s.eval::>("return IsSwimming()").unwrap(), None, "nil, not 0 — the 1.12 predicate shape an addon tests with `== nil`" ); assert_eq!(s.eval::("return IsOutdoors()").unwrap(), 1); s.set_self_movement(SelfMovement { mouselooking: false, swimming: true, mounted: true, indoors: true, falling: false, shapeshift_form: 3, }); assert_eq!(s.eval::("return IsSwimming()").unwrap(), 1); assert_eq!(s.eval::("return IsMounted()").unwrap(), 1); assert_eq!(s.eval::("return IsIndoors()").unwrap(), 1); assert_eq!( s.eval::>("return IsOutdoors()").unwrap(), None, "the exact complement of IsIndoors" ); assert_eq!(s.eval::>("return IsFalling()").unwrap(), None); assert_eq!(s.eval::("return GetShapeshiftFormID()").unwrap(), 3); // The addon idiom holds: under a Lua boolean neither of these would be true. assert!(s.eval::("return IsSwimming() == 1").unwrap()); assert!(!s.eval::("return IsSwimming() == true").unwrap()); } #[test] fn shapeshift_form_zero_reads_nil_not_zero() { let mut s = UiScript::new().unwrap(); s.set_self_movement(SelfMovement { shapeshift_form: 0, ..Default::default() }); assert_eq!( s.eval::>("return GetShapeshiftFormID()") .unwrap(), None, "not shapeshifted is nil, which is what a caller branches on" ); } #[test] fn group_predicates_read_the_roster_not_the_posture_snapshot() { use crate::script::{PartyMemberInfo, PartyState}; let mut s = UiScript::new().unwrap(); assert_eq!(s.eval::>("return IsInGroup()").unwrap(), None); assert_eq!(s.eval::>("return IsInRaid()").unwrap(), None); s.set_party(PartyState { members: vec![PartyMemberInfo::default()], ..Default::default() }); assert_eq!( s.eval::("return IsInGroup()").unwrap(), 1, "a party counts as a group" ); assert_eq!( s.eval::>("return IsInRaid()").unwrap(), None, "but not as a raid" ); } }