//! 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 { 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 { 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 { 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::(r#"return C_AddOns.GetAddOnName(1)"#) .unwrap(), "Alpha" ); assert_eq!( s.eval::(r#"return C_AddOns.GetAddOnTitle("Alpha")"#) .unwrap(), "Alpha Title", "by name, through the same stock argument parsing" ); assert_eq!( s.eval::(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::(r#"local n = GetAddOnInfo(1) return n"#) .unwrap(), s.eval::(r#"return C_AddOns.GetAddOnName(1)"#) .unwrap() ); } #[test] fn loaded_and_existence_track_the_registry() { let s = two_addons(); assert_eq!( s.eval::(r#"return C_AddOns.IsAddOnLoaded("Alpha")"#) .unwrap(), 1 ); assert_eq!( s.eval::>(r#"return C_AddOns.IsAddOnLoaded("Beta")"#) .unwrap(), None, "nil, the 1.12 predicate shape the stock global answers with" ); assert!(s .eval::(r#"return C_AddOns.DoesAddOnExist("Alpha")"#) .unwrap()); assert!( !s.eval::(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::(r#"return C_AddOns.GetAddOnSecurity("Beta")"#) .unwrap(), 0, "## Secure: 1 reads as Secure (0)" ); assert_eq!( s.eval::(r#"return C_AddOns.GetAddOnSecurity("Alpha")"#) .unwrap(), 1, "an ordinary addon is Insecure (1)" ); assert_eq!( s.eval::(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" ); } }