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
243 lines
9.3 KiB
Rust
243 lines
9.3 KiB
Rust
//! ClassicAPI's `AddOns` namespace (`C_AddOns`): the modern per-field spelling of facts 1.12
|
|
//! already answers as one positional tuple from `GetAddOnInfo`. Signatures are from the upstream
|
|
//! repo's own `docs/API.md` (fetched 2026-10-05).
|
|
//!
|
|
//! Almost pure delegation, and deliberately so: every entry here calls the *stock* global
|
|
//! (`script/addon.rs`) rather than reading `Model::addons` a second time, so the index-or-name
|
|
//! argument handling, the unknown-name placeholder answers, and the load-gate verdicts stay owned
|
|
//! by one implementation. A caller cannot get a different answer from `C_AddOns.IsAddOnLoaded` than
|
|
//! from `IsAddOnLoaded`.
|
|
//!
|
|
//! Not built, with the reason: **`GetAddOnLocalTable`** returns the addon's own private `...`
|
|
//! second-vararg table, which modern WoW gives every addon file as a load-time argument. 1.12
|
|
//! passes addon files no arguments at all (`script/addon.rs`'s loader runs each as a bare chunk),
|
|
//! so there is no such table in existence to hand back — providing one would mean inventing a
|
|
//! per-addon table that nothing populates, which an addon would then read as "my saved locals are
|
|
//! empty" rather than "this client has no such feature".
|
|
|
|
use mlua::{Function, Lua, MultiValue, Table, Value};
|
|
|
|
/// Call a stock addon global with the caller's `indexOrName`, so argument parsing and every
|
|
/// unknown-key answer come from the one owner.
|
|
fn stock(lua: &Lua, name: &str, key: Value) -> mlua::Result<MultiValue> {
|
|
let f: Function = lua.globals().get(name)?;
|
|
f.call(key)
|
|
}
|
|
|
|
/// `GetAddOnInfo`'s nth return for this key, the modern per-field getters' shared body.
|
|
fn info_field(lua: &Lua, key: Value, n: usize) -> mlua::Result<Value> {
|
|
let vals = stock(lua, "GetAddOnInfo", key)?;
|
|
Ok(vals.into_iter().nth(n).unwrap_or(Value::Nil))
|
|
}
|
|
|
|
/// `Enum.AddOnSecurityStatus`, the upstream's own table. 2 (`Banned`) is unreachable here: it is
|
|
/// set from a server addon-banlist response, which this client does not model.
|
|
const SECURITY_SECURE: i64 = 0;
|
|
const SECURITY_INSECURE: i64 = 1;
|
|
const SECURITY_NOT_AVAILABLE: i64 = 3;
|
|
|
|
pub(super) fn install(lua: &Lua) -> mlua::Result<Table> {
|
|
let t = lua.create_table()?;
|
|
|
|
// The three name/title/notes getters are `GetAddOnInfo` returns 1, 2 and 3.
|
|
for (fn_name, idx) in [
|
|
("GetAddOnName", 0usize),
|
|
("GetAddOnTitle", 1),
|
|
("GetAddOnNotes", 2),
|
|
] {
|
|
t.set(
|
|
fn_name,
|
|
lua.create_function(move |lua, key: Value| info_field(lua, key, idx))?,
|
|
)?;
|
|
}
|
|
|
|
// `IsAddOnLoadable`: `GetAddOnInfo`'s 5th return (`loadable`), which is already "enabled and
|
|
// the load gate passes" — the same verdict `LoadAddOn` acts on.
|
|
t.set(
|
|
"IsAddOnLoadable",
|
|
lua.create_function(|lua, key: Value| info_field(lua, key, 4))?,
|
|
)?;
|
|
|
|
// Straight pass-throughs: same name, same contract, same owner.
|
|
for name in ["IsAddOnLoaded", "LoadAddOn"] {
|
|
t.set(
|
|
name,
|
|
lua.create_function(move |lua, key: Value| stock(lua, name, key))?,
|
|
)?;
|
|
}
|
|
|
|
// `DoesAddOnExist`: an index or name the registry knows. `GetAddOnInfo` answers an unknown
|
|
// *name* with the caller's own string back and `"MISSING"` as its reason (`script/addon.rs`),
|
|
// so existence is "the reason is not MISSING" rather than "something was returned".
|
|
t.set(
|
|
"DoesAddOnExist",
|
|
lua.create_function(|lua, key: Value| {
|
|
let vals = stock(lua, "GetAddOnInfo", key)?;
|
|
let reason = vals.into_iter().nth(5).unwrap_or(Value::Nil);
|
|
Ok(match reason {
|
|
Value::String(s) => s.to_str().map(|r| &*r != "MISSING").unwrap_or(true),
|
|
// A known addon's reason is nil when it is loadable.
|
|
_ => true,
|
|
})
|
|
})?,
|
|
)?;
|
|
|
|
// `GetAddOnSecurity`: the upstream's enum over `GetAddOnInfo`'s 7th return, the
|
|
// `"SECURE"`/`"INSECURE"` string 1.12 reports. An addon that does not exist is
|
|
// `NotAvailable` (3) rather than defaulting to insecure.
|
|
t.set(
|
|
"GetAddOnSecurity",
|
|
lua.create_function(|lua, key: Value| {
|
|
let vals = stock(lua, "GetAddOnInfo", key.clone())?;
|
|
let mut it = vals.into_iter().skip(5);
|
|
let reason = it.next().unwrap_or(Value::Nil);
|
|
let security = it.next().unwrap_or(Value::Nil);
|
|
let missing = matches!(&reason, Value::String(s) if s.to_str().map(|r| &*r == "MISSING").unwrap_or(false));
|
|
if missing {
|
|
return Ok(SECURITY_NOT_AVAILABLE);
|
|
}
|
|
Ok(match security {
|
|
Value::String(s) if s.to_str().map(|v| &*v == "SECURE").unwrap_or(false) => {
|
|
SECURITY_SECURE
|
|
}
|
|
_ => SECURITY_INSECURE,
|
|
})
|
|
})?,
|
|
)?;
|
|
|
|
// `GetAddOnOptionalDependencies`: 1.12's `.toc` has only `## Dependencies`/`## RequiredDeps`,
|
|
// both *required*, and `script/addon.rs` folds them into the one `dependencies` list that the
|
|
// stock `GetAddOnDependencies` answers. There is no optional-dependency directive in the
|
|
// format, so the honest answer is "none", not the required list under an optional name —
|
|
// returning the required ones would make a caller treat a hard dependency as skippable.
|
|
t.set(
|
|
"GetAddOnOptionalDependencies",
|
|
lua.create_function(|_, _key: Value| Ok(MultiValue::new()))?,
|
|
)?;
|
|
|
|
Ok(t)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::script::{AddOnInfo, UiScript};
|
|
|
|
fn two_addons() -> UiScript {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.register_addons(
|
|
vec![
|
|
AddOnInfo {
|
|
name: "Alpha".into(),
|
|
title: Some("Alpha Title".into()),
|
|
notes: Some("Alpha notes".into()),
|
|
enabled: true,
|
|
loaded: true,
|
|
..Default::default()
|
|
},
|
|
AddOnInfo {
|
|
name: "Beta".into(),
|
|
title: Some("Beta Title".into()),
|
|
secure: true,
|
|
enabled: false,
|
|
..Default::default()
|
|
},
|
|
],
|
|
None,
|
|
None,
|
|
None,
|
|
);
|
|
// The *index* array stays empty until the server's `SMSG_ADDON_INFO` reply lands
|
|
// (`addon.rs`'s `rebuild_index`), faithfully to 1.12 — so a by-index read raises
|
|
// "AddOn index must be in the range of 1 to 0" before it. An empty hidden list is a reply
|
|
// that hid nothing, which is what a server with no banlist sends.
|
|
s.note_addon_info_reply(&[]);
|
|
s
|
|
}
|
|
|
|
#[test]
|
|
fn the_field_getters_mirror_get_addon_info_by_index_and_name() {
|
|
let s = two_addons();
|
|
assert_eq!(
|
|
s.eval::<String>(r#"return C_AddOns.GetAddOnName(1)"#)
|
|
.unwrap(),
|
|
"Alpha"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<String>(r#"return C_AddOns.GetAddOnTitle("Alpha")"#)
|
|
.unwrap(),
|
|
"Alpha Title",
|
|
"by name, through the same stock argument parsing"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<String>(r#"return C_AddOns.GetAddOnNotes(1)"#)
|
|
.unwrap(),
|
|
"Alpha notes"
|
|
);
|
|
// And it really is the stock global's own answer, not a second reading of the registry.
|
|
assert_eq!(
|
|
s.eval::<String>(r#"local n = GetAddOnInfo(1) return n"#)
|
|
.unwrap(),
|
|
s.eval::<String>(r#"return C_AddOns.GetAddOnName(1)"#)
|
|
.unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn loaded_and_existence_track_the_registry() {
|
|
let s = two_addons();
|
|
assert_eq!(
|
|
s.eval::<i64>(r#"return C_AddOns.IsAddOnLoaded("Alpha")"#)
|
|
.unwrap(),
|
|
1
|
|
);
|
|
assert_eq!(
|
|
s.eval::<Option<i64>>(r#"return C_AddOns.IsAddOnLoaded("Beta")"#)
|
|
.unwrap(),
|
|
None,
|
|
"nil, the 1.12 predicate shape the stock global answers with"
|
|
);
|
|
assert!(s
|
|
.eval::<bool>(r#"return C_AddOns.DoesAddOnExist("Alpha")"#)
|
|
.unwrap());
|
|
assert!(
|
|
!s.eval::<bool>(r#"return C_AddOns.DoesAddOnExist("NotInstalled")"#)
|
|
.unwrap(),
|
|
"an unknown name is MISSING, so it does not exist"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn security_maps_the_stock_string_onto_the_upstream_enum() {
|
|
let s = two_addons();
|
|
assert_eq!(
|
|
s.eval::<i64>(r#"return C_AddOns.GetAddOnSecurity("Beta")"#)
|
|
.unwrap(),
|
|
0,
|
|
"## Secure: 1 reads as Secure (0)"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<i64>(r#"return C_AddOns.GetAddOnSecurity("Alpha")"#)
|
|
.unwrap(),
|
|
1,
|
|
"an ordinary addon is Insecure (1)"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<i64>(r#"return C_AddOns.GetAddOnSecurity("NotInstalled")"#)
|
|
.unwrap(),
|
|
3,
|
|
"absent is NotAvailable (3), not a default of insecure"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn optional_dependencies_answer_none_because_the_toc_format_has_no_such_directive() {
|
|
let s = two_addons();
|
|
assert_eq!(
|
|
s.arity(r#"C_AddOns.GetAddOnOptionalDependencies("Alpha")"#)
|
|
.unwrap(),
|
|
0,
|
|
"no returns at all — not an empty table, which `getn` would read as a real answer"
|
|
);
|
|
}
|
|
}
|