Files
benilla-octo/crates/benilla-ui/src/script/compat/classic_api/state.rs
T
gromandClaude Opus 5 81d96af35d Everwood: the HD/OctoWoW line of benilla, imported onto upstream
This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.

Today's work, which is the reason a baseline exists at all:

- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
  characters were entering the sun's shadow cascades, so every skinned-rig
  shadow specialization compiled live mid-frame; the hitch detector logged
  "main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
  the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
  tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
  an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
  and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
  measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
  frame-cost reading no longer pays 2.7 ms a frame for it.

Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-15 15:01:14 -06:00

171 lines
6.5 KiB
Rust

//! 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::<Model>().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::<Model>().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::<Model>().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::<Model>().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::<Option<i64>>("return IsSwimming()").unwrap(),
None,
"nil, not 0 — the 1.12 predicate shape an addon tests with `== nil`"
);
assert_eq!(s.eval::<i64>("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::<i64>("return IsSwimming()").unwrap(), 1);
assert_eq!(s.eval::<i64>("return IsMounted()").unwrap(), 1);
assert_eq!(s.eval::<i64>("return IsIndoors()").unwrap(), 1);
assert_eq!(
s.eval::<Option<i64>>("return IsOutdoors()").unwrap(),
None,
"the exact complement of IsIndoors"
);
assert_eq!(s.eval::<Option<i64>>("return IsFalling()").unwrap(), None);
assert_eq!(s.eval::<i64>("return GetShapeshiftFormID()").unwrap(), 3);
// The addon idiom holds: under a Lua boolean neither of these would be true.
assert!(s.eval::<bool>("return IsSwimming() == 1").unwrap());
assert!(!s.eval::<bool>("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::<Option<i64>>("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::<Option<i64>>("return IsInGroup()").unwrap(), None);
assert_eq!(s.eval::<Option<i64>>("return IsInRaid()").unwrap(), None);
s.set_party(PartyState {
members: vec![PartyMemberInfo::default()],
..Default::default()
});
assert_eq!(
s.eval::<i64>("return IsInGroup()").unwrap(),
1,
"a party counts as a group"
);
assert_eq!(
s.eval::<Option<i64>>("return IsInRaid()").unwrap(),
None,
"but not as a raid"
);
}
}