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
171 lines
6.5 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|