From 208ab10cc3de949acb8bae145fd43c70c6ab5ac1 Mon Sep 17 00:00:00 2001 From: Sam Sung <51686767+samwhosung@users.noreply.github.com> Date: Mon, 28 Sep 2026 07:31:16 +0700 Subject: [PATCH] EnableAllAddOns and DisableAllAddOns switch the Lua index array only, so a disable-all leaves the server-hidden Blizzard_* addons loadable (#480) `EnableAllAddOns` and `DisableAllAddOns` now switch only the addons in the Lua index array, as 1.12.1's `0x48e720` and `0x48e7f0` do: bound `0x51def0`, each name from `0x51df00`, into the enable setter `0x51ea20`. That array leaves out the records `SMSG_ADDON_INFO` hid (`0x51dc4f`), which on vmangos is every `Blizzard_*` addon (`AddonHandler.cpp:129`), so an addon manager's disable-all no longer leaves the auction house, talents, trainers, trade skills, inspect, `/macro` or Key Bindings unable to load. - The per-addon verbs and the all verbs share one setter, standing for `0x51ea20`. - Before the server's addon reply the index is empty and both verbs change nothing, as the reference's count is 0 until then. Fixes #414 Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/benilla-ui/src/script/addon.rs | 18 +++- .../src/script/tests/addon_index_space.rs | 88 +++++++++++++++++++ 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/crates/benilla-ui/src/script/addon.rs b/crates/benilla-ui/src/script/addon.rs index 31185a6a..5ee3373c 100644 --- a/crates/benilla-ui/src/script/addon.rs +++ b/crates/benilla-ui/src/script/addon.rs @@ -141,6 +141,12 @@ fn row_of(model: &Model, key: &AddonKey) -> Option { } } +/// The enable setter (`0x51ea20`), the one write every enable verb makes, for the current +/// character's row. +fn set_enabled(model: &mut Model, i: usize, on: bool) { + model.addons[i].enabled = on; +} + /// The registry as [`super::addon_gate`] rows, the one input every verb's verdict reads. fn gate_rows(model: &Model) -> Vec> { model @@ -322,21 +328,25 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { let mut model = lua.app_data_mut::().expect("model"); let key = addon_key(lua, &model, &key, usage)?; if let Some(i) = row_of(&model, &key) { - model.addons[i].enabled = on; + set_enabled(&mut model, i, on); } Ok(()) })?, )?; } - // No arguments read (`0x48e720`, `0x48e7f0`). + // No arguments read (`0x48e720`, `0x48e7f0`). The loop runs over the Lua index array, not the + // registry: bound `0x51def0`, each name from `0x51df00`, into the setter `0x51ea20`. The array + // leaves out what `SMSG_ADDON_INFO` hid (`0x51dc4f`), which is every `Blizzard_*` addon on a + // server that hides them (vmangos `AddonHandler.cpp:129`), so their state is never touched. for (name, on) in [("EnableAllAddOns", true), ("DisableAllAddOns", false)] { g.set( name, lua.create_function(move |lua, _: MultiValue| { let mut model = lua.app_data_mut::().expect("model"); - for a in &mut model.addons { - a.enabled = on; + for k in 0..model.addon_index.len() { + let i = model.addon_index[k]; + set_enabled(&mut model, i, on); } Ok(()) })?, diff --git a/crates/benilla-ui/src/script/tests/addon_index_space.rs b/crates/benilla-ui/src/script/tests/addon_index_space.rs index 5e0d39f8..d2a50c82 100644 --- a/crates/benilla-ui/src/script/tests/addon_index_space.rs +++ b/crates/benilla-ui/src/script/tests/addon_index_space.rs @@ -117,3 +117,91 @@ fn a_reply_that_hides_nothing_still_builds_the_array() { s.note_addon_info_reply(&[]); assert_eq!(s.eval::("return GetNumAddOns()").ok(), Some(3)); } + +/// A player's addon `Probe` and a hidden chain addon `Blizzard_AuctionUI`, LoadOnDemand, whose one +/// file sets `AuctionLoaded`; the reply hides the Blizzard one, as a vmangos realm does. +fn with_hidden_blizzard(blizzard_enabled: bool) -> UiScript { + let mut s = UiScript::new().unwrap(); + s.set_addon_chain_reader(Box::new(|path| { + (path == "Interface/AddOns/Blizzard_AuctionUI/Blizzard_AuctionUI.lua") + .then(|| b"AuctionLoaded = 1".to_vec()) + })); + s.register_addons( + vec![ + AddOnInfo { + name: "Blizzard_AuctionUI".into(), + interface: 11200, + load_on_demand: true, + chain: true, + files: vec!["Blizzard_AuctionUI.lua".into()], + enabled: blizzard_enabled, + ..Default::default() + }, + AddOnInfo { + name: "Probe".into(), + interface: 11200, + enabled: true, + ..Default::default() + }, + ], + None, + None, + None, + ); + s.note_addon_info_reply(&["blizzard_auctionui".into()]); + s +} + +/// `GetAddOnInfo(name)`'s `enabled` slot, 1 or nil. +fn enabled(s: &UiScript, name: &str) -> bool { + s.eval::(&format!( + "local _, _, _, on = GetAddOnInfo('{name}') return on ~= nil" + )) + .unwrap() +} + +/// `DisableAllAddOns` (`0x48e7f0`) loops `0x51df00` up to `0x51def0`, the array a hidden record +/// never enters (`0x51dc4f`), so an addon manager's disable-all leaves the auction house loadable. +#[test] +fn disable_all_leaves_a_hidden_addon_enabled_and_loadable() { + let s = with_hidden_blizzard(true); + s.run("DisableAllAddOns()").unwrap(); + assert!(!enabled(&s, "Probe"), "the visible addon is disabled"); + assert!( + enabled(&s, "Blizzard_AuctionUI"), + "the hidden one is untouched" + ); + assert_eq!( + s.eval::<(Option, Option)>("return LoadAddOn('Blizzard_AuctionUI')") + .unwrap(), + (Some(1), None) + ); + assert_eq!(s.eval::("return AuctionLoaded").ok(), Some(1)); +} + +/// `EnableAllAddOns` (`0x48e720`) walks the same array, so a hidden addon keeps its own row, and +/// `AddOn_CanLoad` check 3 (`0x51e81d`) still reads that row: no check of the gate reads +/// `[rec+0x29]`, so a hidden addon disabled by name stays `DISABLED`. +#[test] +fn enable_all_touches_the_visible_addons_only() { + let s = with_hidden_blizzard(false); + s.run("DisableAddOn('Probe') EnableAllAddOns()").unwrap(); + assert!(enabled(&s, "Probe"), "the visible addon is enabled again"); + assert!( + !enabled(&s, "Blizzard_AuctionUI"), + "the hidden one is untouched" + ); + assert_eq!( + s.eval::<(Option, Option)>("return LoadAddOn('Blizzard_AuctionUI')") + .unwrap(), + (None, Some("DISABLED".into())) + ); +} + +/// Before `SMSG_ADDON_INFO` the count `[0xbe1b90]` is 0, so both loops run no iteration. +#[test] +fn the_all_verbs_do_nothing_before_the_server_answers() { + let s = seeded(); + s.run("DisableAllAddOns()").unwrap(); + assert!(enabled(&s, "Alpha") && enabled(&s, "Mike") && enabled(&s, "Zulu")); +}