From f120e8003dc8f028b0060219523eac0d01e185a3 Mon Sep 17 00:00:00 2001 From: grom Date: Fri, 23 Oct 2026 21:06:41 -0600 Subject: [PATCH] ui: six of nampower's reads, and the item-icon probe behind two of them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First tranche off the DLL work list. The method that produced it is worth recording with the code: `SuperCleveRoidMacros`'s `NampowerAPI.lua` is a capability registry — a `VERSION_REQUIREMENTS` table of every nampower name with its minimum version, reached through `HasFeature`, which tests the version **and** `_G[name] ~= nil`. So none of this was broken, and raising `GetNampowerVersion` would not have helped: the only honest way to make one of those features live is to define the global. - `PlayerIsMoving()`, `PlayerIsRooted()`, `PlayerIsSwimming()` — `1` or nil, as upstream's own doc lines state, never a boolean. All three read the per-tick posture snapshot `SelfMovement` already carries, which gains `rooted` (`MOVEFLAG_ROOT`) and `moving` (any of the six movement bits, the keyboard turns included — they are input to the same word the client tests, and a turn in place does interrupt a cast). - `IsAuraHidden(spellId)` — from a new `SpellLookup::hidden_aura`, filled with exactly `ui_aura::shown_in_aura_ui`'s predicate inverted, so it cannot contradict the aura list an addon reads beside it. An unknown spell is nil: "not hidden" is the honest answer about an aura this client knows nothing about, and the lists show a catalog miss too. - `GetItemIconTexture(itemId)` — two hops, because an item template carries a display id and not an icon: the cache row's `displayInfoID`, then `ItemDisplayInfo`'s `Icon` through a new `ItemIconProbe`. A known item with no display row is nil (a data gap); an item the cache has never seen raises and records the ask, as every `item_record` verb here does. ClassicAPI's `GetItemIcon` wants the same probe, which is why it is a probe and not a closure. - `GetTrinkets()` — slots 13 and 14 in `GetEquippedItem`'s own row shape, an empty slot absent rather than nil-valued, since the consumer walks it with `if t[i] then`. Each is in `reference_surface.rs`'s `allowed_beyond_1_12` with why 1.12 has no spelling to prefer — the client reads its own movement word and tells Lua nothing, applies the aura attribute test inside `UnitBuff`/`UnitDebuff` without exposing either, and can only reach an item icon through a bag slot it already holds. All nine gates green. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-app/src/ui_script/lifecycle.rs | 25 ++ crates/benilla-app/src/ui_unit.rs | 9 + .../src/script/compat/classic_api/spell.rs | 6 + .../src/script/compat/classic_api/state.rs | 2 + .../benilla-ui/src/script/compat/nampower.rs | 241 ++++++++++++++++++ .../benilla-ui/src/script/compat/superwow.rs | 1 + crates/benilla-ui/src/script/mod.rs | 4 +- crates/benilla-ui/src/script/model.rs | 20 ++ crates/benilla-ui/src/script/spellbook.rs | 10 + .../benilla-ui/src/script/tests/cast_info.rs | 1 + .../src/script/tests/reference_surface.rs | 22 ++ 11 files changed, 339 insertions(+), 2 deletions(-) diff --git a/crates/benilla-app/src/ui_script/lifecycle.rs b/crates/benilla-app/src/ui_script/lifecycle.rs index bd42798f..c08db01a 100644 --- a/crates/benilla-app/src/ui_script/lifecycle.rs +++ b/crates/benilla-app/src/ui_script/lifecycle.rs @@ -37,6 +37,7 @@ fn install_boot_vm(world: &mut World) { seed_vm_clock(world, &mut script); install_addon_asset_resolvers(world, &mut script); install_spell_catalog_probe(world, &mut script); + install_item_icon_probe(world, &mut script); seed_creature_info(world, &mut script); load_global_strings(world, &script); load_emote_tokens(world, &script); @@ -174,10 +175,34 @@ pub(crate) fn install_spell_probe(script: &mut UiScript, catalog: &benilla_forma .filter(|&(item, count)| item != 0 && count != 0) .collect(), dispel: d.dispel, + // The same predicate `ui_aura::shown_in_aura_ui` applies to the lists, inverted: + // a catalog hit that is hidden there must read hidden here, or `IsAuraHidden` + // would disagree with the aura list an addon reads beside it. + hidden_aura: d.hidden_from_aura_bar() || d.tracking_aura(), }) })); } +/// `ItemDisplayInfo`'s inventory icons, behind nampower's `GetItemIconTexture` and ClassicAPI's +/// `GetItemIcon`. An item template carries a display id, not an icon, so both verbs need this +/// second hop and neither can be answered from the item cache alone. +/// +/// The `(displayId, icon)` pairs are copied into the closure rather than sharing the catalog: the +/// icon is the only column either verb wants, the rest of an `ItemDisplay` is model and texture +/// paths for the renderer, and a VM build is rare (boot, world entry, `/reload`). Rows without an +/// icon are left out, so a hit is always a path. +fn install_item_icon_probe(world: &World, script: &mut UiScript) { + let Some(displays) = world.get_resource::() else { + return; + }; + let icons: std::collections::HashMap = displays + .catalog + .iter() + .filter_map(|(id, d)| d.icon.clone().map(|icon| (id, icon))) + .collect(); + script.set_item_icon_probe(Box::new(move |display_id| icons.get(&display_id).cloned())); +} + /// Install [`install_spell_probe`] from the world's catalog, if it has one. The boot VM built at /// `Startup` runs before `ui_action`'s loader has inserted [`crate::ui_action::Spells`] (its /// `insert_resource` is a command, applied at the end of that schedule), so the character-screen diff --git a/crates/benilla-app/src/ui_unit.rs b/crates/benilla-app/src/ui_unit.rs index 47b45a79..f931e9d3 100644 --- a/crates/benilla-app/src/ui_unit.rs +++ b/crates/benilla-app/src/ui_unit.rs @@ -775,6 +775,15 @@ fn feed_self_movement( mounted: store.is_some_and(|s| s.0.unit_mount_display_id() > 0), indoors: world.is_some_and(|w| w.area_interior().is_some()), falling: flags & move_flags::FALLING != 0, + rooted: flags & move_flags::ROOT != 0, + moving: flags + & (move_flags::FORWARD + | move_flags::BACKWARD + | move_flags::STRAFE_LEFT + | move_flags::STRAFE_RIGHT + | move_flags::TURN_LEFT + | move_flags::TURN_RIGHT) + != 0, shapeshift_form: store.map_or(0, |s| s.0.unit_shapeshift_form()), mouselooking: rig.is_some_and(|r| r.mouselooking()), }); diff --git a/crates/benilla-ui/src/script/compat/classic_api/spell.rs b/crates/benilla-ui/src/script/compat/classic_api/spell.rs index 9640bfec..1d22432e 100644 --- a/crates/benilla-ui/src/script/compat/classic_api/spell.rs +++ b/crates/benilla-ui/src/script/compat/classic_api/spell.rs @@ -902,6 +902,7 @@ mod tests { icon: None, reagents: Vec::new(), dispel, + hidden_aura: false, }) })); @@ -944,6 +945,7 @@ mod tests { icon: Some("Interface\\Icons\\Spell_Nature_Rejuvenation".into()), reagents: Vec::new(), dispel: 0, + hidden_aura: false, }) })); @@ -998,6 +1000,7 @@ mod tests { icon: Some("Interface\\Icons\\WRONG".into()), reagents: Vec::new(), dispel: 0, + hidden_aura: false, }) })); assert_eq!( @@ -1091,6 +1094,7 @@ mod tests { icon: None, reagents: vec![(17056, 1)], dispel: 0, + hidden_aura: false, }), // A real spell that costs nothing. 133 => Some(crate::script::SpellLookup { @@ -1099,6 +1103,7 @@ mod tests { icon: None, reagents: Vec::new(), dispel: 0, + hidden_aura: false, }), _ => None, })); @@ -1141,6 +1146,7 @@ mod tests { icon: None, reagents: vec![(17056, 1)], dispel: 0, + hidden_aura: false, }) })); s.set_spellbook(crate::script::SpellBookState { diff --git a/crates/benilla-ui/src/script/compat/classic_api/state.rs b/crates/benilla-ui/src/script/compat/classic_api/state.rs index 1a4c9117..b4e1301e 100644 --- a/crates/benilla-ui/src/script/compat/classic_api/state.rs +++ b/crates/benilla-ui/src/script/compat/classic_api/state.rs @@ -110,6 +110,8 @@ mod tests { mounted: true, indoors: true, falling: false, + rooted: false, + moving: false, shapeshift_form: 3, }); diff --git a/crates/benilla-ui/src/script/compat/nampower.rs b/crates/benilla-ui/src/script/compat/nampower.rs index 02f90467..54b809c7 100644 --- a/crates/benilla-ui/src/script/compat/nampower.rs +++ b/crates/benilla-ui/src/script/compat/nampower.rs @@ -177,6 +177,9 @@ fn equipped_table(lua: &Lua, view: &crate::script::InvSlotView) -> mlua::Result< /// expect — BetterCharacterPanel wraps exactly these three getters in `pcall` /// (`BetterCharacterPanel-lib.lua:177`, `:198`, `:252`) while calling `GetEquippedItem` bare, which /// is only worth doing for a function that can raise. +/// `INVSLOT_TRINKET1`/`INVSLOT_TRINKET2`, the two slots `GetTrinkets` reports. +const TRINKET_SLOTS: [usize; 2] = [13, 14]; + fn item_record(lua: &Lua, item_id: Option) -> mlua::Result { let found = item_id.filter(|&id| id != 0).and_then(|id| { let mut model = lua @@ -735,6 +738,108 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> { })?, )?; + // `GetItemIconTexture(itemId)` (v2.14+): the item's inventory icon as an extensionless + // `Interface\Icons\…` path, the sibling of `GetSpellIconTexture` above. Two hops, because an + // item template carries a *display* id and not an icon: the cache row's `displayInfoID`, then + // `ItemDisplayInfo`'s own `Icon` column through `Model::item_icon_probe`. + // + // An item the cache has never seen raises "Item not found in DBCache", exactly as the other + // `item_record` verbs here do — the caller's `pcall`-or-nil-check is the same either way, and + // the ask is recorded so the next call can answer. A known item whose display row is missing + // is **nil**: that is a gap in the data, not an error in the call. + lua.globals().set( + "GetItemIconTexture", + lua.create_function(|lua, item_id: Option| { + let display_id = item_record(lua, item_id)?.display_info_id; + let model = lua + .app_data_ref::() + .expect("model app_data"); + Ok(model + .item_icon_probe + .as_ref() + .filter(|_| display_id != 0) + .and_then(|probe| probe(display_id))) + })?, + )?; + + // `GetTrinkets()` (v2.14+) -> the two trinket slots as a table keyed `1` and `2`, the same + // row shape `GetEquippedItem` hands back, with an empty slot absent rather than nil-valued + // (a nil value in a Lua table *is* absence, and `SuperCleveRoidMacros` walks it with `for i = + // 1, 2 do if t[i] then`). Slots 13 and 14, the reference's `INVSLOT_TRINKET1`/`2`. + lua.globals().set( + "GetTrinkets", + lua.create_function(|lua, ()| { + let t = lua.create_table()?; + { + let model = lua + .app_data_ref::() + .expect("model app_data"); + let token = inv_token(&model, None); + for (i, slot) in [TRINKET_SLOTS[0], TRINKET_SLOTS[1]].into_iter().enumerate() { + if let Some(view) = model.inv_slot(&token, slot).filter(|v| v.item_id != 0) { + t.set(i as i64 + 1, equipped_table(lua, &view)?)?; + } + } + } + Ok(t) + })?, + )?; + + // `PlayerIsMoving()` / `PlayerIsRooted()` / `PlayerIsSwimming()` (v2.36+): `1` or **nil**, as + // upstream's own doc lines say ("Returns 1 if player is moving, nil if stationary"), not a + // boolean — `SuperCleveRoidMacros` branches on truthiness, but an addon printing the value + // would see `1` on the reference and `true` here. + // + // All three read the one per-tick posture snapshot `model.self_movement` already carries + // (`compat/classic_api/state.rs` feeds the same struct), so none of them costs a new feed. + // `PlayerIsSwimming` is `IsSwimming`'s bit under nampower's name and return shape; both exist + // because both mods publish their own, and an addon feature-detects whichever it was written + // against. + for (name, read) in [ + ("PlayerIsMoving", 0usize), + ("PlayerIsRooted", 1), + ("PlayerIsSwimming", 2), + ] { + lua.globals().set( + name, + lua.create_function(move |lua, ()| { + let model = lua + .app_data_ref::() + .expect("model app_data"); + let m = model.self_movement; + let set = match read { + 0 => m.moving, + 1 => m.rooted, + _ => m.swimming, + }; + Ok(set.then_some(1i64)) + })?, + )?; + } + + // `IsAuraHidden(spellId)` (v3.3+): `1` when that spell's aura is kept out of the Lua aura + // functions, nil otherwise — upstream's own wording, and the same wording as the `luaSlot` of + // `0` the buff/debuff events report for such an aura. + // + // Answered from `SpellLookup::hidden_aura`, which is `benilla-app`'s own + // `ui_aura::shown_in_aura_ui` predicate inverted, so this cannot disagree with the aura lists + // an addon reads beside it. An unknown spell id is **nil**, not `1`: "not hidden" is the + // honest answer about an aura this client knows nothing about, and the aura lists treat a + // catalog miss the same way — they show it. + lua.globals().set( + "IsAuraHidden", + lua.create_function(|lua, spell_id: Option| { + let model = lua + .app_data_ref::() + .expect("model app_data"); + Ok(spell_id + .filter(|&id| id != 0) + .and_then(|id| model.spell_probe.as_ref().and_then(|probe| probe(id))) + .is_some_and(|d| d.hidden_aura) + .then_some(1i64)) + })?, + )?; + lua.globals().set( "GetUnitGUID", lua.create_function(|lua, token: Option| { @@ -754,6 +859,142 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> { mod tests { use crate::script::UiScript; + /// `GetItemIconTexture` takes the two hops an item icon needs — the cache row's display id, + /// then `ItemDisplayInfo` — and tells a data gap apart from an unknown item: a known item + /// whose display row is missing answers nil, an item the cache has never seen raises, as the + /// other `item_record` verbs here do. + #[test] + fn get_item_icon_texture_resolves_through_the_display_id() { + let mut s = UiScript::new().unwrap(); + s.set_item_icon_probe(Box::new(|display_id| { + (display_id == 7777).then(|| "Interface\\Icons\\INV_Sword_04".to_string()) + })); + // Nothing cached: the same raise every item verb here makes. + assert!(s + .eval::>("return GetItemIconTexture(123)") + .is_err()); + + for (id, display) in [(123u32, 7777u32), (124, 4242), (125, 0)] { + s.set_item_template( + id, + crate::script::ItemTemplateView { + display_info_id: display, + ..Default::default() + }, + ); + } + assert_eq!( + s.eval::>("return GetItemIconTexture(123)") + .unwrap(), + Some("Interface\\Icons\\INV_Sword_04".to_string()) + ); + assert_eq!( + s.eval::>("return GetItemIconTexture(124)") + .unwrap(), + None, + "known item, missing display row: a data gap, not an error" + ); + assert_eq!( + s.eval::>("return GetItemIconTexture(125)") + .unwrap(), + None, + "display id 0 is not a row" + ); + } + + /// nampower's three posture reads and `IsAuraHidden`, in the shapes its own doc lines give: + /// `1` or **nil**, never a boolean. `SuperCleveRoidMacros`'s `NampowerAPI.lua` gates each on + /// `HasFeature`, which checks the version *and* `_G[name] ~= nil`, so a wrong return shape + /// would not stop it calling — it would just make the value print as `true`. + #[test] + fn the_posture_reads_answer_one_or_nil_and_follow_the_feed() { + let mut s = UiScript::new().unwrap(); + // Nothing fed yet: the one frame before the posture feed's first run. + for call in ["PlayerIsMoving()", "PlayerIsRooted()", "PlayerIsSwimming()"] { + assert_eq!( + s.eval::>(&format!("return {call}")).unwrap(), + None, + "{call} before the first feed" + ); + } + + s.set_self_movement(crate::script::SelfMovement { + swimming: true, + rooted: true, + moving: false, + ..Default::default() + }); + assert_eq!( + s.eval::>("return PlayerIsSwimming()").unwrap(), + Some(1) + ); + assert_eq!( + s.eval::>("return PlayerIsRooted()").unwrap(), + Some(1) + ); + assert_eq!( + s.eval::>("return PlayerIsMoving()").unwrap(), + None + ); + + // `IsSwimming` is ClassicAPI's name for the same bit; both mods publish one, and an addon + // feature-detects whichever it was written against, so they must agree. + assert!(s.eval::("return IsSwimming()").unwrap()); + + s.set_self_movement(crate::script::SelfMovement { + moving: true, + ..Default::default() + }); + assert_eq!( + s.eval::>("return PlayerIsMoving()").unwrap(), + Some(1) + ); + assert_eq!( + s.eval::>("return PlayerIsRooted()").unwrap(), + None + ); + } + + /// `IsAuraHidden` answers for the same auras the buff and debuff lists omit, and **nil** for a + /// spell this client knows nothing about — "not hidden" is the honest answer there, and it is + /// what the aura lists do with a catalog miss as well. + #[test] + fn is_aura_hidden_follows_the_aura_lists_own_predicate() { + let mut s = UiScript::new().unwrap(); + assert_eq!( + s.eval::>("return IsAuraHidden(123)").unwrap(), + None, + "no probe at all" + ); + s.set_spell_probe(Box::new(|id| { + Some(crate::script::SpellLookup { + name: format!("Spell {id}"), + rank: None, + icon: None, + reagents: Vec::new(), + dispel: 0, + hidden_aura: id == 999, + }) + })); + assert_eq!( + s.eval::>("return IsAuraHidden(999)").unwrap(), + Some(1) + ); + assert_eq!( + s.eval::>("return IsAuraHidden(123)").unwrap(), + None + ); + assert_eq!( + s.eval::>("return IsAuraHidden(0)").unwrap(), + None, + "id 0 is not a spell" + ); + assert_eq!( + s.eval::>("return IsAuraHidden()").unwrap(), + None + ); + } + /// BetterCharacterPanel's four reads, in its own shapes /// (`BetterCharacterPanel-lib.lua:93`, `:177`, `:193`, `:252`): the enchant check, the item /// level, the quality colour and the two-hand test. All four were dead — `GetEquippedItem` was diff --git a/crates/benilla-ui/src/script/compat/superwow.rs b/crates/benilla-ui/src/script/compat/superwow.rs index 8d13d38f..877e4ec1 100644 --- a/crates/benilla-ui/src/script/compat/superwow.rs +++ b/crates/benilla-ui/src/script/compat/superwow.rs @@ -291,6 +291,7 @@ mod option_verbs { icon: Some("Interface\\Icons\\Spell_Nature_Rejuvenation".into()), reagents: Vec::new(), dispel: 0, + hidden_aura: false, }) })); diff --git a/crates/benilla-ui/src/script/mod.rs b/crates/benilla-ui/src/script/mod.rs index 0d389295..28b8a030 100644 --- a/crates/benilla-ui/src/script/mod.rs +++ b/crates/benilla-ui/src/script/mod.rs @@ -241,8 +241,8 @@ pub use model::{ RaceInfo, }; pub use model::{ - FontProbe, SpellDbcExtras, SpellIconProbe, SpellLookup, SpellMechanicProbe, SpellProbe, - SpellUsable, TextureProbe, TextureSizeProbe, + FontProbe, ItemIconProbe, SpellDbcExtras, SpellIconProbe, SpellLookup, SpellMechanicProbe, + SpellProbe, SpellUsable, TextureProbe, TextureSizeProbe, }; pub(crate) use model::{Model, PendingTimer}; pub use model::{PlayerCacheEntry, SelfMovement, ServerClock}; diff --git a/crates/benilla-ui/src/script/model.rs b/crates/benilla-ui/src/script/model.rs index 30ef5230..bf6209bf 100644 --- a/crates/benilla-ui/src/script/model.rs +++ b/crates/benilla-ui/src/script/model.rs @@ -51,6 +51,11 @@ pub struct SpellLookup { /// (pfUI keys a `dispelTypeMap` on the number, `libs/libdebuff.lua:379`), and a name would /// make a localized realm answer something its table cannot match. pub dispel: u32, + /// Whether this spell's aura is kept out of the aura UI: `ATTR_DO_NOT_DISPLAY`, + /// `ATTR_EX_NO_AURA_ICON`, or a tracking aura. Exactly the predicate `benilla-app`'s + /// `ui_aura::shown_in_aura_ui` applies to the buff and debuff lists, so this answers for the + /// same auras those omit. Backs nampower's `IsAuraHidden`. + pub hidden_aura: bool, } /// The host's answer for *any* spell id, learned or not: `Spell.dbc`'s whole display table, which @@ -60,6 +65,12 @@ pub struct SpellLookup { /// back to the spellbook. pub type SpellProbe = Box Option>; +/// The host's answer for an `ItemDisplayInfo` row id: its inventory icon as an extensionless +/// `Interface\Icons\…` path. Keyed on the **display** id, not the item id, because that is the +/// column an item template carries and the one the reference's own vendor-cursor path resolves +/// through (`benilla-app`'s `cursor.rs`). `None` where the row or its icon is absent. +pub type ItemIconProbe = Box Option>; + /// A spell id to its `Spell.dbc` `Mechanic` column and that mechanic's enUS name from /// `SpellMechanic.dbc`, for ClassicAPI's `C_Spell.GetSpellMechanicByID`. /// @@ -258,6 +269,13 @@ pub struct SelfMovement { pub indoors: bool, /// `MOVEFLAG_FALLING`, set for the whole airborne arc. pub falling: bool, + /// `MOVEFLAG_ROOT`, for nampower's `PlayerIsRooted()`. Not 1.12 data either: the client reads + /// its own movement word, and no stock binding exposes it. + pub rooted: bool, + /// Any of the six movement bits set — forward, backward, the two strafes, the two keyboard + /// turns — for nampower's `PlayerIsMoving()`. The turn keys count because they are movement + /// input to the same word the client tests, and a turn-in-place does interrupt a cast. + pub moving: bool, /// `UNIT_FIELD_BYTES_2` byte 3, `0` not shapeshifted (`ShapeshiftForm.dbc` row otherwise). pub shapeshift_form: u8, /// `IsMouselooking()`: the camera rig's own mouse-look flag, the reference's `[cam+0x90] & 1` @@ -699,6 +717,7 @@ pub(crate) struct Model { pub(crate) spell_usable: std::collections::HashMap, /// `SpellIcon.dbc` lookup ([`SpellIconProbe`]). pub(crate) spell_icon_probe: Option, + pub(crate) item_icon_probe: Option, /// Creature display id to model path ([`CreatureModelProbe`]). pub(crate) creature_model_probe: Option, /// The pet's spellbook, a second array as in the reference (`0xb700f0`, `0xb6f098`). @@ -1597,6 +1616,7 @@ impl Model { measurer: None, texture_probe: None, spell_probe: None, + item_icon_probe: None, spell_mechanic_probe: None, player_cache: HashMap::new(), player_cache_enabled: [true, true], diff --git a/crates/benilla-ui/src/script/spellbook.rs b/crates/benilla-ui/src/script/spellbook.rs index 3b1b14b5..535d0320 100644 --- a/crates/benilla-ui/src/script/spellbook.rs +++ b/crates/benilla-ui/src/script/spellbook.rs @@ -89,6 +89,12 @@ impl super::UiScript { } /// Install the `SpellIcon.dbc` lookup behind `GetSpellIconTexture`. + /// Install [`super::ItemIconProbe`], the `ItemDisplayInfo` icon lookup behind nampower's + /// `GetItemIconTexture` and ClassicAPI's `GetItemIcon`. + pub fn set_item_icon_probe(&mut self, probe: super::ItemIconProbe) { + self.model_mut().item_icon_probe = Some(probe); + } + pub fn set_spell_icon_probe(&mut self, probe: super::SpellIconProbe) { self.model_mut().spell_icon_probe = Some(probe); } @@ -278,6 +284,10 @@ pub(super) fn spell_display(model: &Model, spell_id: u32) -> Option UiScript { icon: Some("Interface\\Icons\\Spell_Shadow_SiphonMana".into()), reagents: Vec::new(), dispel: 0, + hidden_aura: false, }) })); for (token, guid, name) in [ diff --git a/crates/benilla-ui/src/script/tests/reference_surface.rs b/crates/benilla-ui/src/script/tests/reference_surface.rs index 5757ae7b..26c0b0a7 100644 --- a/crates/benilla-ui/src/script/tests/reference_surface.rs +++ b/crates/benilla-ui/src/script/tests/reference_surface.rs @@ -295,6 +295,28 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> { // addon branches on to decide which of its rows to show. Not a 1.12 verb, and not // namreeb/nampower's either — that fork has no Lua surface at all. "GetNampowerVersion", + // nampower v2's three posture reads (script/compat/nampower.rs). 1.12 publishes no + // movement getter of any kind — the client reads its own movement word and tells Lua + // nothing — so there is no 1.12 spelling to prefer. `1`-or-nil, as upstream's own doc + // lines state. `SuperCleveRoidMacros`'s `NampowerAPI.lua` registers each in its + // `VERSION_REQUIREMENTS` table and reaches them through `HasFeature`, which tests the + // version *and* `_G[name] ~= nil`, so the honest way to make that feature live is to + // define the global rather than to raise the version claim. + // nampower v2's `GetItemIconTexture(itemId)` and `GetTrinkets()` (same file). 1.12 has + // neither: its only icon path for an item is `GetContainerItemInfo` on a bag slot you + // already hold, and it has no way at all to ask about an arbitrary item id or to read the + // two trinket slots as a pair. Both are in `SuperCleveRoidMacros`'s `HasFeature` table. + "GetItemIconTexture", + "GetTrinkets", + "PlayerIsMoving", + "PlayerIsRooted", + "PlayerIsSwimming", + // nampower v2's `IsAuraHidden(spellId)` (same file): whether an aura is kept out of the + // Lua aura functions. 1.12 has no way to ask — the client applies the attribute test + // inside `UnitBuff`/`UnitDebuff` and exposes neither the test nor the attributes. Answers + // from the same predicate `benilla-app`'s `ui_aura::shown_in_aura_ui` applies to the + // lists, so it cannot contradict the aura list an addon reads beside it. + "IsAuraHidden", // ClassicAPI's Enum (script/compat/classic_api/enums.rs): the modern client's named // constants, so an addon can write `Enum.PowerType.Mana` for 0. Pure data, no 1.12 // counterpart — 1.12 spells the same numbers as bare literals or `LE_*`/`ITEM_QUALITY_*`