ui: six of nampower's reads, and the item-icon probe behind two of them
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -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::<crate::entities::ItemDisplays>() else {
|
||||
return;
|
||||
};
|
||||
let icons: std::collections::HashMap<u32, String> = 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
|
||||
|
||||
@@ -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()),
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -110,6 +110,8 @@ mod tests {
|
||||
mounted: true,
|
||||
indoors: true,
|
||||
falling: false,
|
||||
rooted: false,
|
||||
moving: false,
|
||||
shapeshift_form: 3,
|
||||
});
|
||||
|
||||
|
||||
@@ -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<u32>) -> mlua::Result<crate::script::ItemTemplateView> {
|
||||
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<u32>| {
|
||||
let display_id = item_record(lua, item_id)?.display_info_id;
|
||||
let model = lua
|
||||
.app_data_ref::<crate::script::Model>()
|
||||
.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::<crate::script::Model>()
|
||||
.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::<crate::script::Model>()
|
||||
.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<u32>| {
|
||||
let model = lua
|
||||
.app_data_ref::<crate::script::Model>()
|
||||
.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<String>| {
|
||||
@@ -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::<Option<String>>("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::<Option<String>>("return GetItemIconTexture(123)")
|
||||
.unwrap(),
|
||||
Some("Interface\\Icons\\INV_Sword_04".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<Option<String>>("return GetItemIconTexture(124)")
|
||||
.unwrap(),
|
||||
None,
|
||||
"known item, missing display row: a data gap, not an error"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<Option<String>>("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::<Option<i64>>(&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::<Option<i64>>("return PlayerIsSwimming()").unwrap(),
|
||||
Some(1)
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<Option<i64>>("return PlayerIsRooted()").unwrap(),
|
||||
Some(1)
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<Option<i64>>("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::<bool>("return IsSwimming()").unwrap());
|
||||
|
||||
s.set_self_movement(crate::script::SelfMovement {
|
||||
moving: true,
|
||||
..Default::default()
|
||||
});
|
||||
assert_eq!(
|
||||
s.eval::<Option<i64>>("return PlayerIsMoving()").unwrap(),
|
||||
Some(1)
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<Option<i64>>("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::<Option<i64>>("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::<Option<i64>>("return IsAuraHidden(999)").unwrap(),
|
||||
Some(1)
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<Option<i64>>("return IsAuraHidden(123)").unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<Option<i64>>("return IsAuraHidden(0)").unwrap(),
|
||||
None,
|
||||
"id 0 is not a spell"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<Option<i64>>("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
|
||||
|
||||
@@ -291,6 +291,7 @@ mod option_verbs {
|
||||
icon: Some("Interface\\Icons\\Spell_Nature_Rejuvenation".into()),
|
||||
reagents: Vec::new(),
|
||||
dispel: 0,
|
||||
hidden_aura: false,
|
||||
})
|
||||
}));
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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<dyn Fn(u32) -> Option<SpellLookup>>;
|
||||
|
||||
/// 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<dyn Fn(u32) -> Option<String>>;
|
||||
|
||||
/// 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<u32, SpellUsable>,
|
||||
/// `SpellIcon.dbc` lookup ([`SpellIconProbe`]).
|
||||
pub(crate) spell_icon_probe: Option<SpellIconProbe>,
|
||||
pub(crate) item_icon_probe: Option<ItemIconProbe>,
|
||||
/// Creature display id to model path ([`CreatureModelProbe`]).
|
||||
pub(crate) creature_model_probe: Option<CreatureModelProbe>,
|
||||
/// 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],
|
||||
|
||||
@@ -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<super::Spell
|
||||
.map(|d| d.reagents.clone())
|
||||
.unwrap_or_default(),
|
||||
dispel: probed.as_ref().map_or(0, |d| d.dispel),
|
||||
// Also the probe's: a spellbook slot carries no attribute columns, and `false` here
|
||||
// would claim every known spell's aura is shown. A probe miss keeps `false`, which is
|
||||
// what the aura lists do with a catalog miss too.
|
||||
hidden_aura: probed.as_ref().is_some_and(|d| d.hidden_aura),
|
||||
});
|
||||
}
|
||||
model.spell_probe.as_ref().and_then(|probe| probe(spell_id))
|
||||
|
||||
@@ -54,6 +54,7 @@ fn harness() -> UiScript {
|
||||
icon: Some("Interface\\Icons\\Spell_Shadow_SiphonMana".into()),
|
||||
reagents: Vec::new(),
|
||||
dispel: 0,
|
||||
hidden_aura: false,
|
||||
})
|
||||
}));
|
||||
for (token, guid, name) in [
|
||||
|
||||
@@ -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_*`
|
||||
|
||||
Reference in New Issue
Block a user