ui+app: nampower's bag group, aura cancels, no-queue casts, unit-event CVars

Second tranche. Eight more verbs and six CVar rows, all from state this
client already owns.

The bag group — `FindPlayerItemSlot(idOrName)`, `GetBagItem(bag, slot)`,
`GetItemIdCooldown(itemId)`, `UseItemIdOrName(idOrName)` — shares one
lookup, deliberately: the three that search must agree about *which* of
two identical stacks they mean, and they answer the first, reading bags
and slots ascending, as the reference's own auto-store search walks
(`0x4f8030`) and as a player reads their bags. A name matches through the
item cache, case-insensitively, since a bag slot carries no name of its
own. `GetItemIdCooldown` answers the stock `0, 0, 1` for a cold or
uncarried item rather than nil, because the consumer does arithmetic on
the triple. `UseItemIdOrName` calls `UseContainerItem` itself rather than
reaching past it, so the cursor-clear and repair-mode rules the stock
verb applies still happen.

`CancelPlayerAuraSpellId(spellId, [ignoreMissing])` and
`CancelPlayerAuraSlot(auraSlot)`: the two keys into the `CMSG_CANCEL_AURA`
queue `CancelPlayerBuff` already fills, so the three cannot disagree. The
slot is the **raw 0-based** descriptor slot upstream documents and the
buff events report, not the Lua index. `ignoreMissing` is accepted and
changes nothing — it asks upstream not to complain, and nothing here
would have complained.

`CastSpellNoQueue(spellId, spellBook [, unit])` and
`CastSpellByNameNoQueue(name [, unit])`: the plain cast, which is the
honest mapping and not a stub — benilla has no spell queue to bypass, so
"cast this now, do not queue it" is exactly what happens. The point of
them is the first argument: a **spell id** where stock `CastSpell` takes
a book slot. `"mouseover"` routes to the queue SuperWoW's
`CastSpellByName(name, "CLICK")` already uses.

The six `NP_EnableUnitEventsX` switches are registered stored-only, one
step further back than the four `NP_Enable*` rows beside them: benilla
raises no GUID-keyed unit event at all, so there is no stream for a token
filter to narrow. Registering them stops the unknown-CVar warning and
lets `SuperCleveRoidMacros`'s writes persist; it claims nothing about the
events arriving, and `cvars.rs`'s own reader gate carries that reason.

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:
2026-10-23 21:23:41 -06:00
co-authored by Claude Opus 5
parent f120e8003d
commit 5555bf9c89
3 changed files with 501 additions and 0 deletions
+71
View File
@@ -1281,6 +1281,49 @@ pub(crate) const REGISTERED: &[Registered] = &[
"nampower v2 addon-compat, STORED ONLY: benilla fires no `SPELL_START_SELF`/`_OTHER` — \
their shape wants itemId/spellType/corpseOwnerGuid columns this client does not carry",
),
// The six `NP_EnableUnitEventsX` switches (nampower v2.33+): which unit tokens fire the
// GUID-keyed unit events, so an addon can cut the stream to the units it watches.
// `SuperCleveRoidMacros`'s `NampowerAPI.lua` writes all six at setup.
//
// STORED ONLY, and the reason is one step further back than the other `NP_Enable*` rows:
// benilla fires no GUID-keyed unit event at all — no `UNIT_HEALTH_GUID`, `UNIT_MANA_GUID`,
// `UNIT_RAGE_GUID`, `UNIT_ENERGY_GUID` or `UNIT_COMBAT_GUID` — so there is no stream for a
// filter to narrow. Registering them is what stops the engine's unknown-CVar warning and lets
// the addon's writes persist; it is not a claim that the events arrive. Defaults are
// upstream's own documented `0`.
ours(
"NP_EnableUnitEventsGuid",
"0",
"nampower v2 addon-compat, STORED ONLY: benilla fires no GUID-keyed unit event, so there \
is no stream for this switch to open",
),
ours(
"NP_EnableUnitEventsGuidFiltering",
"0",
"nampower v2 addon-compat, STORED ONLY: the filter half of `NP_EnableUnitEventsGuid`, and \
there is nothing to filter",
),
ours(
"NP_EnableUnitEventsMouseover",
"0",
"nampower v2 addon-compat, STORED ONLY: which tokens fire the GUID-keyed unit events, \
none of which this client raises",
),
ours(
"NP_EnableUnitEventsParty",
"0",
"nampower v2 addon-compat, STORED ONLY: see `NP_EnableUnitEventsMouseover`",
),
ours(
"NP_EnableUnitEventsPet",
"0",
"nampower v2 addon-compat, STORED ONLY: see `NP_EnableUnitEventsMouseover`",
),
ours(
"NP_EnableUnitEventsRaid",
"0",
"nampower v2 addon-compat, STORED ONLY: see `NP_EnableUnitEventsMouseover`",
),
ours(
"NP_EnableSpellGoEvents",
"0",
@@ -4898,6 +4941,34 @@ mod tests {
"NP_EnableSpellHealEvents",
"pfUI libs/libdebuff.lua writes it at PLAYER_LOGIN; no SPELL_HEAL_* here",
),
// The six unit-event switches: `SuperCleveRoidMacros`'s `NampowerAPI.lua` writes all
// six at setup. One step further back than the four above — there is no GUID-keyed
// unit event here for a token filter to narrow, so nothing could read them even in
// principle until that family exists.
(
"NP_EnableUnitEventsGuid",
"SuperCleveRoidMacros NampowerAPI.lua writes it; no GUID-keyed unit events here",
),
(
"NP_EnableUnitEventsGuidFiltering",
"SuperCleveRoidMacros NampowerAPI.lua writes it; no GUID-keyed unit events here",
),
(
"NP_EnableUnitEventsMouseover",
"SuperCleveRoidMacros NampowerAPI.lua writes it; no GUID-keyed unit events here",
),
(
"NP_EnableUnitEventsParty",
"SuperCleveRoidMacros NampowerAPI.lua writes it; no GUID-keyed unit events here",
),
(
"NP_EnableUnitEventsPet",
"SuperCleveRoidMacros NampowerAPI.lua writes it; no GUID-keyed unit events here",
),
(
"NP_EnableUnitEventsRaid",
"SuperCleveRoidMacros NampowerAPI.lua writes it; no GUID-keyed unit events here",
),
// SuperWoW-era plate cast bar: pfUI's nameplates module writes 0 at load. benilla's
// V-key plates draw no cast bar, so nothing here reads it — see the row's own note.
(
@@ -177,6 +177,52 @@ 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.
/// The first bag slot holding `ident` — a number is an item id, a string a case-insensitive name
/// match — ascending bag then ascending slot, which is the order the reference's own auto-store
/// search walks (`0x4f8030`) and the order a player reads their bags in. Backs
/// `FindPlayerItemSlot`, `GetItemIdCooldown` and `UseItemIdOrName`, which must all agree about
/// *which* of two identical stacks they mean.
fn find_bag_slot(model: &crate::script::Model, ident: Option<&Value>) -> Option<(i64, u32)> {
let wanted_id = match ident {
Some(Value::Integer(n)) => u32::try_from(*n).ok(),
Some(Value::Number(n)) => (*n >= 0.0).then_some(*n as u32),
_ => None,
};
let wanted_name = match ident {
Some(Value::String(s)) => Some(s.to_str().ok()?.to_lowercase()),
_ => None,
};
if wanted_id.is_none_or(|id| id == 0) && wanted_name.is_none() {
return None;
}
// 0 is the backpack, 1..=4 the equipped bags: `GetContainerItemInfo`'s own numbering.
for bag in 0i64..=4 {
let container = model.containers.get(&bag)?;
let mut slots: Vec<u32> = container.slots.keys().copied().collect();
slots.sort_unstable();
for slot in slots {
let Some(s) = container.slots.get(&slot).filter(|s| s.item_id != 0) else {
continue;
};
let hit = match (&wanted_id, &wanted_name) {
(Some(id), _) => s.item_id == *id,
// The slot carries no name of its own; the cache row it points at does. An
// uncached item cannot be matched by name, which is the same limit every
// name-taking verb here has.
(_, Some(name)) => model
.item_templates
.get(&s.item_id)
.is_some_and(|t| t.name.to_lowercase() == *name),
_ => false,
};
if hit {
return Some((bag, slot));
}
}
}
None
}
/// `INVSLOT_TRINKET1`/`INVSLOT_TRINKET2`, the two slots `GetTrinkets` reports.
const TRINKET_SLOTS: [usize; 2] = [13, 14];
@@ -738,6 +784,206 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// `CancelPlayerAuraSpellId(spellId, [ignoreMissing])` and `CancelPlayerAuraSlot(auraSlot)`
// (v2.35+): the two ways to drop one of the player's own auras without first finding its
// 1-based buff index, which is what stock `CancelPlayerBuff` demands and what makes it awkward
// from a macro. Both queue the spell id onto the same `CMSG_CANCEL_AURA` queue
// `CancelPlayerBuff` and `CancelUnitBuff` use, so the three cannot disagree.
//
// `auraSlot` is the **raw 0-based** descriptor slot, as upstream states and as the buff and
// debuff events report it — not the Lua index. `AuraState::slot` is that number.
//
// No return value on any path, as `CancelPlayerBuff` has none, and a missing aura is a silent
// no-op whatever `ignoreMissing` says: the flag asks upstream not to complain, and there is
// nothing here that would have complained. Accepted and ignored rather than refused, so a
// caller passing it is not surprised.
lua.globals().set(
"CancelPlayerAuraSpellId",
lua.create_function(
|lua, (spell_id, _ignore_missing): (Option<u32>, Option<Value>)| {
let Some(spell_id) = spell_id.filter(|&id| id != 0) else {
return Ok(());
};
let mut model = lua
.app_data_mut::<crate::script::Model>()
.expect("model app_data");
if model
.auras
.get("player")
.is_some_and(|l| l.iter().any(|a| a.spell_id == spell_id && a.cancelable))
{
model.cancel_aura_requests.push(spell_id);
}
Ok(())
},
)?,
)?;
lua.globals().set(
"CancelPlayerAuraSlot",
lua.create_function(|lua, aura_slot: Option<u32>| {
let Some(aura_slot) = aura_slot else {
return Ok(());
};
let mut model = lua
.app_data_mut::<crate::script::Model>()
.expect("model app_data");
let found = model.auras.get("player").and_then(|l| {
l.iter()
.find(|a| u32::from(a.slot) == aura_slot && a.cancelable)
.map(|a| a.spell_id)
});
if let Some(spell_id) = found {
model.cancel_aura_requests.push(spell_id);
}
Ok(())
})?,
)?;
// `CastSpellNoQueue(spellId, spellBook [, unit])` (v3.1+) and `CastSpellByNameNoQueue(name
// [, unit])` (v2.0+): cast without entering nampower's spell queue.
//
// **benilla has no spell queue to bypass**, so these are the plain cast — and that is the
// honest mapping rather than a stub, because what the caller asked for ("cast this now, do not
// queue it") is exactly what happens here. `spell/inflight.rs` carries namreeb's always-on
// cast-ready rule, not pepo's queue windows; `docs/ADDON_COMPAT.md` records why the queue is
// unbuilt.
//
// Unlike stock `CastSpell`, the first argument is a **spell id**, not a book slot — upstream's
// whole reason for the function. The id goes on the same queue `CastSpell` fills, so a cast
// from here and one from a bar are the same send. `"mouseover"` routes to the mouseover queue
// SuperWoW's `CastSpellByName(name, "CLICK")` already uses; any other token takes the
// current-selection path, which is what our `CastSpellByName` does with a token today — no
// new deviation, and `docs/ADDON_COMPAT.md` has the row.
for name in ["CastSpellNoQueue", "CastSpellByNameNoQueue"] {
let by_name = name == "CastSpellByNameNoQueue";
lua.globals().set(
name,
lua.create_function(move |lua, (first, second, third): (Value, Value, Value)| {
let mut model = lua
.app_data_mut::<crate::script::Model>()
.expect("model app_data");
// `CastSpellByNameNoQueue(name, unit)` puts the unit second;
// `CastSpellNoQueue(id, book, unit)` third.
let unit = if by_name { &second } else { &third };
let mouseover = matches!(unit, Value::String(s)
if s.to_str().ok().as_deref() == Some("mouseover"));
// `spell_ident` is the number-or-name resolver every other verb in this file
// uses, so a name here resolves exactly as `IsSpellUsable`'s does: through the
// player's own book, highest rank first.
let spell_id = spell_ident(&model, Some(&first));
if let Some(spell_id) = spell_id.filter(|&id| id != 0) {
if mouseover {
model.mouseover_spell_casts.push(spell_id);
} else {
model.spell_casts.push(spell_id);
}
}
Ok(())
})?,
)?;
}
// `UseItemIdOrName(itemIdOrName)` (v2.14+): use the first bag slot holding that item, through
// the same queue `UseContainerItem` fills, so the cursor and repair-mode rules the stock verb
// applies are not bypassed. Nothing carried is a silent no-op — the consumer's macro is
// allowed to name an item the player does not have.
lua.globals().set(
"UseItemIdOrName",
lua.create_function(|lua, ident: Option<Value>| {
let found = {
let model = lua
.app_data_ref::<crate::script::Model>()
.expect("model app_data");
find_bag_slot(&model, ident.as_ref())
};
if let Some((bag, slot)) = found {
let use_item: mlua::Function = lua.globals().get("UseContainerItem")?;
use_item.call::<()>((bag, slot))?;
}
Ok(())
})?,
)?;
// `FindPlayerItemSlot(itemIdOrName)` (v2.14+) -> `bag, slot`, or nil. Bag `0` is the backpack
// and `1..4` the equipped bags, which is `GetContainerItemInfo`'s own numbering.
//
// Ascending bag then ascending slot, which is the order the reference's own auto-store search
// walks (`0x4f8030`) and so the order a player reads their bags in: two stacks of the same
// item answer the first, not an arbitrary one. A number argument is an item id; a string is
// matched against the slot's name, case-insensitively, because the consumer passes whatever
// the macro said and 1.12 item names are display strings.
lua.globals().set(
"FindPlayerItemSlot",
lua.create_function(|lua, ident: Option<Value>| {
let model = lua
.app_data_ref::<crate::script::Model>()
.expect("model app_data");
Ok(match find_bag_slot(&model, ident.as_ref()) {
Some((bag, slot)) => (Some(bag), Some(slot)),
None => (None, None),
})
})?,
)?;
// `GetBagItem(bag, slot)` (v2.14+) -> that slot as a table, nil for an empty or unknown one.
// The columns are the ones a bag slot actually carries here; `GetItemStats(itemId)` beside it
// is the whole cache row, so this deliberately does not re-derive template fields it would
// have to guess at.
lua.globals().set(
"GetBagItem",
lua.create_function(|lua, (bag, slot): (Option<i64>, Option<u32>)| {
let (Some(bag), Some(slot)) = (bag, slot) else {
return Ok(Value::Nil);
};
let found = {
let model = lua
.app_data_ref::<crate::script::Model>()
.expect("model app_data");
model
.containers
.get(&bag)
.and_then(|c| c.slots.get(&slot))
.filter(|s| s.item_id != 0)
.cloned()
};
let Some(s) = found else {
return Ok(Value::Nil);
};
let t = lua.create_table()?;
t.set("bag", bag)?;
t.set("slot", slot)?;
t.set("itemId", s.item_id)?;
t.set("count", s.count)?;
t.set("texture", s.texture.clone())?;
t.set("quality", s.quality)?;
t.set("link", s.link.clone())?;
t.set("locked", s.locked)?;
Ok(Value::Table(t))
})?,
)?;
// `GetItemIdCooldown(itemId)` (v2.14+) -> `start, duration, enable` on the `GetTime` clock,
// `GetContainerItemCooldown`'s triple for whichever bag slot holds that item. A cold or
// absent item answers `0, 0, 1`, which is what the stock verb answers for a slot with no
// cooldown — the consumer does arithmetic on these, so nil would be the wrong shape.
lua.globals().set(
"GetItemIdCooldown",
lua.create_function(|lua, item_id: Option<Value>| {
let now: f64 = lua.globals().get("__benilla_now").unwrap_or(0.0);
let model = lua
.app_data_ref::<crate::script::Model>()
.expect("model app_data");
let cooldown = find_bag_slot(&model, item_id.as_ref())
.and_then(|(bag, slot)| model.container_cooldowns.get(&(bag, slot)).copied())
.filter(|&(start, duration, enabled)| start + duration > now || !enabled);
Ok(match cooldown {
Some((start, duration, enabled)) => (start, duration, i32::from(enabled)),
None => (0.0, 0.0, 1),
})
})?,
)?;
// `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
@@ -859,6 +1105,173 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
mod tests {
use crate::script::UiScript;
/// The two aura cancels reach the same `CMSG_CANCEL_AURA` queue `CancelPlayerBuff` does — one
/// by spell id, one by the **raw 0-based** descriptor slot the buff events report — and both
/// refuse an aura the client would not let the player cancel.
#[test]
fn the_aura_cancels_queue_the_same_send_as_cancelplayerbuff() {
let mut s = UiScript::new().unwrap();
let aura = |spell_id: u32, slot: u8, cancelable: bool| crate::script::AuraState {
spell_id,
slot,
cancelable,
helpful: true,
count: 1,
..Default::default()
};
s.set_auras(
"player",
Some(vec![aura(100, 3, true), aura(200, 7, false)]),
);
s.run("CancelPlayerAuraSpellId(100)").unwrap();
assert_eq!(s.take_cancel_aura_requests(), vec![100]);
// Not cancelable, not carried, and nonsense ids are all silent no-ops.
s.run("CancelPlayerAuraSpellId(200)").unwrap();
s.run("CancelPlayerAuraSpellId(999)").unwrap();
s.run("CancelPlayerAuraSpellId(0)").unwrap();
s.run("CancelPlayerAuraSpellId()").unwrap();
assert!(s.take_cancel_aura_requests().is_empty());
// The slot is raw, not the Lua index: aura 100 sits at slot 3, index 1.
s.run("CancelPlayerAuraSlot(3)").unwrap();
assert_eq!(s.take_cancel_aura_requests(), vec![100]);
s.run("CancelPlayerAuraSlot(1)").unwrap();
s.run("CancelPlayerAuraSlot(7)").unwrap();
assert!(
s.take_cancel_aura_requests().is_empty(),
"slot 1 holds nothing and slot 7's aura is not cancelable"
);
// `ignoreMissing` is accepted and changes nothing: there is nothing here to complain.
s.run("CancelPlayerAuraSpellId(100, 1)").unwrap();
assert_eq!(s.take_cancel_aura_requests(), vec![100]);
}
/// The `*NoQueue` pair takes a **spell id** where stock `CastSpell` takes a book slot — the
/// whole reason upstream has them — and reaches the same queue a bar press does, since there
/// is no spell queue here to bypass. `"mouseover"` routes to the mouseover queue.
#[test]
fn the_no_queue_casts_push_a_spell_id_onto_the_ordinary_queue() {
let mut s = UiScript::new().unwrap();
s.set_spellbook(crate::script::SpellBookState {
tabs: Vec::new(),
slots: vec![crate::script::SpellSlotView {
spell_id: 133,
name: "Fireball".into(),
rank: Some("Rank 1".into()),
..Default::default()
}],
});
s.run("CastSpellNoQueue(133, 1)").unwrap();
assert_eq!(s.take_spell_casts(), vec![133]);
// An id the book does not carry: `spell_ident` answers for ids directly, as upstream's
// "cast by spell ID" says, so this is a send and not a refusal.
s.run("CastSpellNoQueue(4242, 1)").unwrap();
assert_eq!(s.take_spell_casts(), vec![4242]);
s.run(r#"CastSpellByNameNoQueue("Fireball")"#).unwrap();
assert_eq!(s.take_spell_casts(), vec![133]);
// The unit is third for the id form and second for the name form.
s.run(r#"CastSpellNoQueue(133, 1, "mouseover")"#).unwrap();
assert!(s.take_spell_casts().is_empty());
assert_eq!(s.take_mouseover_spell_casts(), vec![133]);
s.run(r#"CastSpellByNameNoQueue("Fireball", "mouseover")"#)
.unwrap();
assert!(s.take_spell_casts().is_empty());
assert_eq!(s.take_mouseover_spell_casts(), vec![133]);
// A name nothing answers for, and no argument at all, are silent.
s.run(r#"CastSpellByNameNoQueue("Nothing At All")"#)
.unwrap();
s.run("CastSpellNoQueue()").unwrap();
assert!(s.take_spell_casts().is_empty());
}
/// The bag group — `FindPlayerItemSlot`, `GetBagItem`, `GetItemIdCooldown` — shares one
/// lookup, so the three cannot disagree about *which* of two identical stacks they mean: the
/// first, reading bags and slots ascending, as the reference's own auto-store search does.
#[test]
fn the_bag_group_finds_the_first_stack_and_agrees_about_which() {
let mut s = UiScript::new().unwrap();
assert_eq!(
s.eval::<(Option<i64>, Option<u32>)>("return FindPlayerItemSlot(1234)")
.unwrap(),
(None, None),
"no containers fed yet"
);
// Two stacks of 1234: backpack slot 5 and bag 2 slot 1. The backpack one must win.
let slot = |item_id: u32, count: u32| crate::script::ContainerSlot {
item_id,
count,
..Default::default()
};
for (bag, slots) in [
(0i64, vec![(5u32, slot(1234, 3)), (9, slot(555, 1))]),
(1, vec![]),
(2, vec![(1, slot(1234, 20))]),
(3, vec![]),
(4, vec![]),
] {
s.set_container(
bag,
Some(crate::script::ContainerState {
name: None,
num_slots: 16,
slots: slots.into_iter().collect(),
}),
);
}
assert_eq!(
s.eval::<(Option<i64>, Option<u32>)>("return FindPlayerItemSlot(1234)")
.unwrap(),
(Some(0), Some(5))
);
assert_eq!(
s.eval::<(Option<i64>, Option<u32>)>("return FindPlayerItemSlot(555)")
.unwrap(),
(Some(0), Some(9))
);
assert_eq!(
s.eval::<(Option<i64>, Option<u32>)>("return FindPlayerItemSlot(4242)")
.unwrap(),
(None, None),
"not carried"
);
// `GetBagItem` reports the slot it was asked about, not the found one.
assert_eq!(
s.eval::<u32>(r#"return GetBagItem(2, 1).count"#).unwrap(),
20
);
assert_eq!(
s.eval::<u32>(r#"return GetBagItem(0, 5).itemId"#).unwrap(),
1234
);
assert!(s
.eval::<Option<mlua::Value>>("return GetBagItem(0, 7)")
.unwrap()
.is_none_or(|v| v.is_nil()));
// A cold item answers the stock triple, not nil: the consumer does arithmetic on these.
assert_eq!(
s.eval::<(f64, f64, i64)>("return GetItemIdCooldown(1234)")
.unwrap(),
(0.0, 0.0, 1)
);
assert_eq!(
s.eval::<(f64, f64, i64)>("return GetItemIdCooldown(4242)")
.unwrap(),
(0.0, 0.0, 1),
"not carried reads cold, as an absent slot does"
);
}
/// `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
@@ -306,6 +306,23 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> {
// 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.
// nampower v2's bag group (same file), all three sharing one lookup so they cannot
// disagree about which of two identical stacks they mean. 1.12 can ask about a bag slot
// it names (`GetContainerItemInfo`) but has no search by item id or name, and no
// cooldown read that is not already slot-addressed.
// nampower v2's outbound group (same file). 1.12 cancels an aura only by its 1-based
// buff *index* (`CancelPlayerBuff`), casts only by book slot (`CastSpell`), and uses an
// item only by a bag slot it already names — so each of these is a different key into a
// send that exists, not a new send. The `*NoQueue` pair is the plain cast here: there is
// no spell queue to bypass (`docs/ADDON_COMPAT.md`).
"CancelPlayerAuraSlot",
"CancelPlayerAuraSpellId",
"CastSpellByNameNoQueue",
"CastSpellNoQueue",
"UseItemIdOrName",
"FindPlayerItemSlot",
"GetBagItem",
"GetItemIdCooldown",
"GetItemIconTexture",
"GetTrinkets",
"PlayerIsMoving",