ui+app+formats: the Spell.dbc row, and seven columns that answered nil
`GetSpellRecField` knew three names. Counting what the corpus actually asks it for: `name` (4 sites), `school` (3), `stackAmount` (2), `spellIconID` (2), then one each of `rank`, `rangeIndex`, `mechanic`, `manaCost`, `effectMechanic`, `effectApplyAuraName`. Seven of those ten answered nil, and each was a feature quietly off rather than an error — DoiteAuras caps its stack display on `stackAmount` (DoitePlayerAuras.lua:539) and SuperCleveRoidMacros classifies a crowd-control effect off the two arrays (Utility.lua:7776-7794). The eighty field names are the DLL's own, read off its pool in Spell.dbc column order. The mapping to `SpellDisplay` lives in `ui_script/spell_rec.rs`, beside the struct it reads — a copy on the VM side would be free to drift — and `GetSpellRec(spellId)` is that same field list walked through that same per-field reader, so the two verbs cannot answer differently for one name. A test asserts exactly that, key by key, over one row. The array columns are a 1-based table of three, which is measured, not guessed: SuperCleveRoidMacros walks `effectMechanic` as `for i = 1, 3 do local em = effectMechanics[i]`. `totem`, `reagent` and `reagentCount` take the same shape, two and eight long. One new DBC column — `StackAmount`, 39, bracketed by two constants this tree already had (38 ModalNextSpell, 40 Totem[0]). It is the only one added: fifteen further names upstream answers are left nil rather than putting fifteen unused u32s on a struct the spell catalog holds one of per row. They are listed at `FIELDS` with that reason, so the gap is a decision rather than an omission. `GetSpellRecField`'s three string columns keep the spellbook fallback the verb had before the probe, so a VM with a book and no catalog — a capture, or the boot VM before the catalog loads — still answers a learnt spell's name and rank. Every other column is a DBC row and has nowhere else to come from, which is why the fallback is exactly three names wide and says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -333,6 +333,27 @@ pub(crate) fn install_spell_extras_probes(
|
||||
})
|
||||
}));
|
||||
}
|
||||
// nampower's `Spell.dbc` row readers, both off the one mapping in `ui_script/spell_rec.rs`.
|
||||
// Static DBC data, so a copy at VM build is right — unlike the modifier probe next door,
|
||||
// nothing moves these.
|
||||
{
|
||||
let for_field = catalog.displays_shared();
|
||||
let for_record = catalog.displays_shared();
|
||||
script.set_spell_rec_probes(
|
||||
Box::new(move |spell_id, name| {
|
||||
super::spell_rec::field(for_field.get(&spell_id)?, name)
|
||||
}),
|
||||
Box::new(move |spell_id| {
|
||||
let d = for_record.get(&spell_id)?;
|
||||
Some(
|
||||
super::spell_rec::FIELDS
|
||||
.iter()
|
||||
.filter_map(|&name| super::spell_rec::field(d, name).map(|v| (name, v)))
|
||||
.collect(),
|
||||
)
|
||||
}),
|
||||
);
|
||||
}
|
||||
// `GetSpellIconTexture`: `SpellIcon.dbc` by row id, the other half of
|
||||
// `GetSpellRecField(id, "spellIconID")`. Shared by refcount like the display table.
|
||||
let icons = catalog.icons_shared();
|
||||
|
||||
@@ -441,6 +441,7 @@ fn arbitrate_pointer_over_ui(
|
||||
|
||||
/// The session lifecycle: the VM's birth, identity, death and reload.
|
||||
mod lifecycle;
|
||||
mod spell_rec;
|
||||
pub(crate) use lifecycle::{
|
||||
end_ui_session, ingame_ui_up, install_spell_extras_probes, install_spell_probe,
|
||||
run_pending_reload, setup_script, AddOnIdentity, LeavingWorldArmed, PendingEntryUiLoad,
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
//! nampower's `Spell.dbc` row readers: the name-to-column mapping behind `GetSpellRec(spellId)`
|
||||
//! and `GetSpellRecField(spellId, fieldName)`.
|
||||
//!
|
||||
//! **The names are the DLL's own**, read off its string pool in `Spell.dbc` column order
|
||||
//! (`0x91d68`–`0x92460`, bracketed on one side by the item-row names and on the other by format
|
||||
//! strings — `docs/ADDON_COMPAT.md` has the technique). Eighty of them; this answers the sixty-odd
|
||||
//! that `benilla_formats::SpellDisplay` parses and `None` for the rest, which [`FIELDS`] lists
|
||||
//! with the reason.
|
||||
//!
|
||||
//! **Why this lives here and not in `benilla-ui`.** It is a mapping from upstream's spelling to
|
||||
//! `SpellDisplay`'s fields, so it belongs beside that struct's reader; a copy on the VM side would
|
||||
//! be free to drift from it. Both verbs go through one [`field`] call, so they cannot answer
|
||||
//! differently for one name — `GetSpellRec` is [`FIELDS`] walked through [`field`], nothing more.
|
||||
|
||||
use benilla_formats::SpellDisplay;
|
||||
use benilla_ui::script::SpellRecValue;
|
||||
|
||||
/// A three-element array as the 1-based Lua table every consumer walks:
|
||||
/// `for i = 1, 3 do local em = effectMechanics[i]` (`SuperCleveRoidMacros/Utility.lua:7778`).
|
||||
fn ints<T: Copy + Into<i64>>(a: &[T; 3]) -> SpellRecValue {
|
||||
SpellRecValue::Ints(a.iter().map(|&v| v.into()).collect())
|
||||
}
|
||||
|
||||
fn nums(a: &[f32; 3]) -> SpellRecValue {
|
||||
SpellRecValue::Nums(a.iter().map(|&v| f64::from(v)).collect())
|
||||
}
|
||||
|
||||
fn int(v: impl Into<i64>) -> Option<SpellRecValue> {
|
||||
Some(SpellRecValue::Int(v.into()))
|
||||
}
|
||||
|
||||
/// Every name this mapping answers, in `Spell.dbc` column order — which is also the key order
|
||||
/// `GetSpellRec`'s table is built in.
|
||||
///
|
||||
/// **What is deliberately absent**, each a column `SpellDisplay` does not parse and no consumer
|
||||
/// asks for: `attributesEx4`, `targetCreatureType`, `procFlags`, `procCharges`,
|
||||
/// `manaPerSecondPerLevel`, `stackAmount`'s neighbours `maxTargetLevel` and `maxAffectedTargets`,
|
||||
/// `dmgClass`, `spellPriority`, `spellVisual2`, `minFactionId`, `minReputation`,
|
||||
/// `requiredAuraVision`, `effectPointsPerComboPoint` and `dmgMultiplier`. Adding them would mean
|
||||
/// fifteen more `u32`s on a struct the spell catalog holds one of per row, for a name nothing
|
||||
/// reads — `GetSpellRecField` answering nil for a name upstream answers is the honest cost, and
|
||||
/// it is written here rather than left to be discovered.
|
||||
pub(crate) const FIELDS: &[&str] = &[
|
||||
"name",
|
||||
"nameSubtext",
|
||||
"rank",
|
||||
"school",
|
||||
"category",
|
||||
"castUI",
|
||||
"dispel",
|
||||
"mechanic",
|
||||
"attributes",
|
||||
"attributesEx",
|
||||
"attributesEx2",
|
||||
"attributesEx3",
|
||||
"stances",
|
||||
"stancesNot",
|
||||
"targets",
|
||||
"requiresSpellFocus",
|
||||
"casterAuraState",
|
||||
"targetAuraState",
|
||||
"castingTimeIndex",
|
||||
"recoveryTime",
|
||||
"categoryRecoveryTime",
|
||||
"interruptFlags",
|
||||
"auraInterruptFlags",
|
||||
"channelInterruptFlags",
|
||||
"procChance",
|
||||
"maxLevel",
|
||||
"baseLevel",
|
||||
"spellLevel",
|
||||
"durationIndex",
|
||||
"powerType",
|
||||
"manaCost",
|
||||
"manaCostPerlevel",
|
||||
"manaPerSecond",
|
||||
"rangeIndex",
|
||||
"speed",
|
||||
"modalNextSpell",
|
||||
"stackAmount",
|
||||
"totem",
|
||||
"reagent",
|
||||
"reagentCount",
|
||||
"equippedItemClass",
|
||||
"equippedItemSubClassMask",
|
||||
"equippedItemInventoryTypeMask",
|
||||
"spellVisual",
|
||||
"spellIconID",
|
||||
"activeIconID",
|
||||
"manaCostPercentage",
|
||||
"startRecoveryCategory",
|
||||
"startRecoveryTime",
|
||||
"spellFamilyName",
|
||||
"spellFamilyFlags",
|
||||
"preventionType",
|
||||
"stanceBarOrder",
|
||||
"effect",
|
||||
"effectDieSides",
|
||||
"effectBaseDice",
|
||||
"effectDicePerLevel",
|
||||
"effectRealPointsPerLevel",
|
||||
"effectBasePoints",
|
||||
"effectMechanic",
|
||||
"effectImplicitTargetA",
|
||||
"effectImplicitTargetB",
|
||||
"effectRadiusIndex",
|
||||
"effectApplyAuraName",
|
||||
"effectAmplitude",
|
||||
"effectMultipleValue",
|
||||
"effectChainTarget",
|
||||
"effectItemType",
|
||||
"effectMiscValue",
|
||||
"effectTriggerSpell",
|
||||
];
|
||||
|
||||
/// One field of `d` by nampower's name for it.
|
||||
///
|
||||
/// `rank` is upstream's `nameSubtext` under the name this client's own earlier binding used, kept
|
||||
/// because `SuperCleveRoidMacros` asks by it (`GetSpellRecField(spellId, "rank")`); both answer
|
||||
/// the same string. A spell with no rank line answers the empty string rather than nothing, as
|
||||
/// the reference's own `GetSpellName` second return does.
|
||||
pub(crate) fn field(d: &SpellDisplay, name: &str) -> Option<SpellRecValue> {
|
||||
Some(match name {
|
||||
"name" => SpellRecValue::Str(d.name.clone()),
|
||||
"nameSubtext" | "rank" => SpellRecValue::Str(d.rank.clone().unwrap_or_default()),
|
||||
"school" => return int(d.school),
|
||||
"category" => return int(d.category),
|
||||
"castUI" => return int(d.cast_ui),
|
||||
"dispel" => return int(d.dispel),
|
||||
"mechanic" => return int(d.mechanic),
|
||||
"attributes" => return int(d.attributes),
|
||||
"attributesEx" => return int(d.attributes_ex),
|
||||
"attributesEx2" => return int(d.attributes_ex2),
|
||||
"attributesEx3" => return int(d.attributes_ex3),
|
||||
"stances" => return int(d.stances),
|
||||
"stancesNot" => return int(d.stances_not),
|
||||
"targets" => return int(d.targets),
|
||||
"requiresSpellFocus" => return int(d.requires_spell_focus),
|
||||
"casterAuraState" => return int(d.caster_aura_state),
|
||||
"targetAuraState" => return int(d.target_aura_state),
|
||||
"castingTimeIndex" => return int(d.casting_time_index),
|
||||
"recoveryTime" => return int(d.recovery_ms),
|
||||
"categoryRecoveryTime" => return int(d.category_recovery_ms),
|
||||
"interruptFlags" => return int(d.interrupt_flags),
|
||||
"auraInterruptFlags" => return int(d.aura_interrupt_flags),
|
||||
"channelInterruptFlags" => return int(d.channel_interrupt_flags),
|
||||
"procChance" => return int(d.proc_chance),
|
||||
"maxLevel" => return int(d.max_level),
|
||||
"baseLevel" => return int(d.base_level),
|
||||
"spellLevel" => return int(d.spell_level),
|
||||
"durationIndex" => return int(d.duration_index),
|
||||
"powerType" => return int(d.power_type),
|
||||
"manaCost" => return int(d.mana_cost),
|
||||
"manaCostPerlevel" => return int(d.mana_cost_per_level),
|
||||
"manaPerSecond" => return int(d.mana_per_second),
|
||||
"rangeIndex" => return int(d.range_index),
|
||||
"speed" => SpellRecValue::Num(f64::from(d.speed)),
|
||||
"modalNextSpell" => return int(d.modal_next_spell),
|
||||
"stackAmount" => return int(d.stack_amount),
|
||||
// The two totem slots and the eight reagent pairs, as their own 1-based tables — the
|
||||
// same shape the effect arrays take, since upstream's record holds all of them as C
|
||||
// arrays and Lua has one way to spell an array.
|
||||
"totem" => SpellRecValue::Ints(d.totems.iter().map(|&v| i64::from(v)).collect()),
|
||||
"reagent" => SpellRecValue::Ints(d.reagents.iter().map(|&(id, _)| i64::from(id)).collect()),
|
||||
"reagentCount" => {
|
||||
SpellRecValue::Ints(d.reagents.iter().map(|&(_, n)| i64::from(n)).collect())
|
||||
}
|
||||
"equippedItemClass" => return int(d.equipped_item_class),
|
||||
"equippedItemSubClassMask" => return int(d.equipped_item_subclass_mask),
|
||||
"equippedItemInventoryTypeMask" => return int(d.equipped_item_inventory_type_mask),
|
||||
"spellVisual" => return int(d.visual),
|
||||
"spellIconID" => return int(d.icon_id),
|
||||
"activeIconID" => return int(d.active_icon_id),
|
||||
"manaCostPercentage" => return int(d.mana_cost_pct),
|
||||
"startRecoveryCategory" => return int(d.start_recovery_category),
|
||||
"startRecoveryTime" => return int(d.start_recovery_ms),
|
||||
"spellFamilyName" => return int(d.spell_family),
|
||||
// The 64-bit mask, which Lua 5.1 numbers hold exactly only to 2^53. Every real 1.12 row
|
||||
// fits: the highest bit any `Spell.dbc` family mask sets is 42. Cast rather than
|
||||
// truncated so a hypothetical high bit would be visible as a wrong number rather than
|
||||
// silently dropped.
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
"spellFamilyFlags" => SpellRecValue::Int(d.spell_family_flags as i64),
|
||||
"preventionType" => return int(d.prevention_type),
|
||||
"stanceBarOrder" => return int(d.stance_bar_order),
|
||||
"effect" => ints(&d.effects.map(|v| v as i64)),
|
||||
"effectDieSides" => ints(&d.effect_die_sides),
|
||||
"effectBaseDice" => ints(&d.effect_base_dice),
|
||||
"effectDicePerLevel" => ints(&d.effect_dice_per_level),
|
||||
"effectRealPointsPerLevel" => nums(&d.effect_real_points_per_level),
|
||||
"effectBasePoints" => ints(&d.effect_base_points),
|
||||
"effectMechanic" => ints(&d.effect_mechanic),
|
||||
"effectImplicitTargetA" => ints(&d.effect_implicit_target_a),
|
||||
"effectImplicitTargetB" => ints(&d.effect_implicit_target_b),
|
||||
"effectRadiusIndex" => ints(&d.effect_radius_index),
|
||||
"effectApplyAuraName" => ints(&d.effect_apply_aura),
|
||||
"effectAmplitude" => ints(&d.effect_amplitude),
|
||||
"effectMultipleValue" => nums(&d.effect_multiple_value),
|
||||
"effectChainTarget" => ints(&d.effect_chain_targets),
|
||||
"effectItemType" => ints(&d.effect_item_type),
|
||||
"effectMiscValue" => ints(&d.effect_misc_value),
|
||||
"effectTriggerSpell" => ints(&d.effect_trigger_spell),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Every name [`FIELDS`] lists is one [`field`] answers, and vice versa for the sample row —
|
||||
/// the gate that keeps `GetSpellRec`'s key list and the per-field reader in step. A name in
|
||||
/// the list the reader does not know would be a nil-valued key, which in Lua is an absent one.
|
||||
#[test]
|
||||
fn every_listed_field_is_one_the_reader_answers() {
|
||||
let d = SpellDisplay {
|
||||
name: "Fireball".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let missing: Vec<&str> = FIELDS
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|n| field(&d, n).is_none())
|
||||
.collect();
|
||||
assert!(
|
||||
missing.is_empty(),
|
||||
"FIELDS names the reader does not answer: {missing:?}"
|
||||
);
|
||||
assert!(
|
||||
field(&d, "notAColumn").is_none(),
|
||||
"and an unknown name is still None"
|
||||
);
|
||||
// No duplicates: a repeated key would be written twice into one table.
|
||||
let mut sorted = FIELDS.to_vec();
|
||||
sorted.sort_unstable();
|
||||
let before = sorted.len();
|
||||
sorted.dedup();
|
||||
assert_eq!(sorted.len(), before, "FIELDS has a duplicate");
|
||||
}
|
||||
|
||||
/// The array fields are three long and in effect order, which is the shape every consumer
|
||||
/// walks (`for i = 1, 3`). Asserted on real values so a transposed or truncated array shows.
|
||||
#[test]
|
||||
fn the_array_fields_are_three_long_in_effect_order() {
|
||||
let d = SpellDisplay {
|
||||
effect_mechanic: [0, 12, 0],
|
||||
effect_apply_aura: [3, 0, 0],
|
||||
effect_misc_value: [-1, 7, 0],
|
||||
effect_real_points_per_level: [0.0, 1.5, 0.0],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
field(&d, "effectMechanic"),
|
||||
Some(SpellRecValue::Ints(vec![0, 12, 0]))
|
||||
);
|
||||
assert_eq!(
|
||||
field(&d, "effectApplyAuraName"),
|
||||
Some(SpellRecValue::Ints(vec![3, 0, 0]))
|
||||
);
|
||||
assert_eq!(
|
||||
field(&d, "effectMiscValue"),
|
||||
Some(SpellRecValue::Ints(vec![-1, 7, 0])),
|
||||
"signed, and the sign survives"
|
||||
);
|
||||
assert_eq!(
|
||||
field(&d, "effectRealPointsPerLevel"),
|
||||
Some(SpellRecValue::Nums(vec![0.0, 1.5, 0.0]))
|
||||
);
|
||||
// The reagent pair splits into two arrays of eight, ids and counts in slot order.
|
||||
let d = SpellDisplay {
|
||||
reagents: [
|
||||
(17_030, 1),
|
||||
(0, 0),
|
||||
(0, 0),
|
||||
(0, 0),
|
||||
(0, 0),
|
||||
(0, 0),
|
||||
(0, 0),
|
||||
(0, 0),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
field(&d, "reagent"),
|
||||
Some(SpellRecValue::Ints(vec![17_030, 0, 0, 0, 0, 0, 0, 0]))
|
||||
);
|
||||
assert_eq!(
|
||||
field(&d, "reagentCount"),
|
||||
Some(SpellRecValue::Ints(vec![1, 0, 0, 0, 0, 0, 0, 0]))
|
||||
);
|
||||
}
|
||||
|
||||
/// A rank-less spell answers the empty string, not nothing: the caller concatenates it.
|
||||
#[test]
|
||||
fn a_rankless_spell_answers_an_empty_rank() {
|
||||
let d = SpellDisplay::default();
|
||||
assert_eq!(field(&d, "rank"), Some(SpellRecValue::Str(String::new())));
|
||||
assert_eq!(
|
||||
field(&d, "nameSubtext"),
|
||||
field(&d, "rank"),
|
||||
"upstream's name and this client's own earlier one answer the same string"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,10 @@ pub struct SpellDisplay {
|
||||
/// it is the running auto-repeat, that is only re-armed (`0x6e745d`). Every hunter shot
|
||||
/// names 75, Auto Shot: that is how shooting starts.
|
||||
pub modal_next_spell: u32,
|
||||
/// `StackAmount`: an aura's maximum stacks, `0` for one that does not stack. Nothing in the
|
||||
/// reference's own Lua reaches it — the client applies the cap internally — which is why
|
||||
/// nampower publishes it as `GetSpellRecField(id, "stackAmount")`.
|
||||
pub stack_amount: u32,
|
||||
/// `Attributes & 0x40`, `SPELL_ATTR_PASSIVE`: the spellbook grays it.
|
||||
pub passive: bool,
|
||||
/// `castUI` (column 3): nonzero keeps the spell out of the spellbook; 0 on ordinary spells.
|
||||
@@ -202,6 +206,7 @@ impl Default for SpellDisplay {
|
||||
attributes_ex: 0,
|
||||
attributes_ex2: 0,
|
||||
modal_next_spell: 0,
|
||||
stack_amount: 0,
|
||||
attributes_ex3: 0,
|
||||
spell_family: 0,
|
||||
spell_family_flags: 0,
|
||||
|
||||
@@ -69,6 +69,12 @@ const COL_MANA_PER_SECOND: usize = 34;
|
||||
const COL_RANGE_INDEX: usize = 36;
|
||||
/// `modalNextSpell` (`+0x98`): 52 of its 57 nonzero rows are hunter shots naming Auto Shot.
|
||||
const COL_MODAL_NEXT_SPELL: usize = 38;
|
||||
/// `StackAmount` (`+0x9c`): an aura's maximum stacks, 0 for one that does not stack. Read by
|
||||
/// nampower's `GetSpellRecField(id, "stackAmount")` — DoiteAuras caps its stack display on it
|
||||
/// (`Modules/DoitePlayerAuras.lua:539`, where `0` is normalised to 1) and
|
||||
/// `SuperCleveRoidMacros` predicts a stack increment against it (`Conditionals.lua:1087`).
|
||||
/// Bracketed by two constants here: 38 is `ModalNextSpell` and 40 is `Totem[0]`.
|
||||
const COL_STACK_AMOUNT: usize = 39;
|
||||
/// `StartRecoveryCategory` (`+0x274`) and `StartRecoveryTime` (`+0x278`): the global cooldown
|
||||
/// `StartGlobalCooldown` (`0x6e2de0`) starts at the cast send (`0x6e58fb`).
|
||||
const COL_START_RECOVERY_CATEGORY: usize = 157;
|
||||
@@ -545,6 +551,7 @@ pub fn load_spell_catalog(chain: &mut Chain) -> Result<SpellCatalog> {
|
||||
mana_per_second: u32_at(r, COL_MANA_PER_SECOND).unwrap_or(0),
|
||||
range_index: u32_at(r, COL_RANGE_INDEX).unwrap_or(0),
|
||||
modal_next_spell: u32_at(r, COL_MODAL_NEXT_SPELL).unwrap_or(0),
|
||||
stack_amount: u32_at(r, COL_STACK_AMOUNT).unwrap_or(0),
|
||||
targets: u32_at(r, COL_TARGETS).unwrap_or(0),
|
||||
implicit_target_a1: u32_at(r, COL_IMPLICIT_TARGET_A1).unwrap_or(0),
|
||||
effect_implicit_target_a: std::array::from_fn(|i| {
|
||||
|
||||
@@ -169,6 +169,31 @@ fn equipped_table(lua: &Lua, view: &crate::script::InvSlotView) -> mlua::Result<
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
/// One [`crate::script::SpellRecValue`] as Lua sees it: a number or a string, or a 1-based table
|
||||
/// for an array column.
|
||||
fn spell_rec_value(lua: &Lua, value: crate::script::SpellRecValue) -> mlua::Result<Value> {
|
||||
use crate::script::SpellRecValue as V;
|
||||
Ok(match value {
|
||||
V::Int(n) => Value::Integer(n),
|
||||
V::Num(n) => Value::Number(n),
|
||||
V::Str(s) => Value::String(lua.create_string(&s)?),
|
||||
V::Ints(v) => {
|
||||
let t = lua.create_table()?;
|
||||
for (i, n) in v.into_iter().enumerate() {
|
||||
t.set(i as i64 + 1, n)?;
|
||||
}
|
||||
Value::Table(t)
|
||||
}
|
||||
V::Nums(v) => {
|
||||
let t = lua.create_table()?;
|
||||
for (i, n) in v.into_iter().enumerate() {
|
||||
t.set(i as i64 + 1, n)?;
|
||||
}
|
||||
Value::Table(t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// nampower's cooldown table: twenty keys, read off the DLL's own string pool in table order
|
||||
/// (`0xa88d4`), shared by `GetSpellIdCooldown` and `GetItemIdCooldown` because upstream's addon
|
||||
/// layer says so in as many words — `API.GetItemCooldownInfo` "returns same table structure as
|
||||
@@ -518,40 +543,103 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// 0 reads as "free", and its Clearcasting rule turns on exactly that test.
|
||||
//
|
||||
// `spellIconID` is answered: `SpellDisplay::icon_id` carries the raw column beside the path
|
||||
// it resolves to, and `GetSpellIconTexture` below resolves it back.
|
||||
// `GetSpellRec(spellId, [copy])` -> the whole `Spell.dbc` row as a table, and
|
||||
// `GetSpellRecField(spellId, fieldName, [copy])` -> one column of it.
|
||||
//
|
||||
// The key names are the DLL's own, read off its string pool in column order and mapped to
|
||||
// `SpellDisplay`'s fields in `benilla-app`'s `ui_script/spell_rec.rs` — which is also where
|
||||
// the fifteen columns this client does not parse are listed with the reason. `GetSpellRec` is
|
||||
// that same field list walked through that same per-field reader, so the two verbs cannot
|
||||
// answer differently for one name.
|
||||
//
|
||||
// **The array columns are a 1-based table of three**, which is measured rather than guessed:
|
||||
// `SuperCleveRoidMacros` walks `GetSpellRecField(spellID, "effectMechanic")` as
|
||||
// `for i = 1, 3 do local em = effectMechanics[i]` (`Utility.lua:7776-7794`) and does the same
|
||||
// with `effectApplyAuraName`. `totem`, `reagent` and `reagentCount` take the same shape, two
|
||||
// and eight long.
|
||||
//
|
||||
// What the corpus actually asks `GetSpellRecField` for, counted: `name` (4 sites), `school`
|
||||
// (3), `stackAmount` (2), `spellIconID` (2), then one each of `rank`, `rangeIndex`,
|
||||
// `mechanic`, `manaCost`, `effectMechanic`, `effectApplyAuraName`. Seven of those ten
|
||||
// answered **nil** before this — DoiteAuras caps its stack display on `stackAmount`
|
||||
// (`Modules/DoitePlayerAuras.lua:539`) and `SuperCleveRoidMacros` classifies a crowd-control
|
||||
// effect off the two arrays, so each nil was a feature quietly off rather than an error.
|
||||
//
|
||||
// `copy` is accepted and ignored: upstream's flag asks for a copy rather than a reference
|
||||
// into its own cache, and every table built here is already a fresh one.
|
||||
lua.globals().set(
|
||||
"GetSpellRec",
|
||||
lua.create_function(|lua, (spell_id, _copy): (Option<u32>, Option<Value>)| {
|
||||
let rows = {
|
||||
let model = lua
|
||||
.app_data_ref::<crate::script::Model>()
|
||||
.expect("model app_data");
|
||||
spell_id.filter(|&id| id != 0).and_then(|id| {
|
||||
model
|
||||
.spell_record_probe
|
||||
.as_ref()
|
||||
.and_then(|probe| probe(id))
|
||||
})
|
||||
};
|
||||
let Some(rows) = rows else {
|
||||
return Ok(Value::Nil);
|
||||
};
|
||||
let t = lua.create_table()?;
|
||||
for (name, value) in rows {
|
||||
t.set(name, spell_rec_value(lua, value)?)?;
|
||||
}
|
||||
Ok(Value::Table(t))
|
||||
})?,
|
||||
)?;
|
||||
lua.globals().set(
|
||||
"GetSpellRecField",
|
||||
lua.create_function(|lua, (spell_id, field): (Option<u32>, Option<String>)| {
|
||||
let (Some(spell_id), Some(field)) = (spell_id, field) else {
|
||||
return Ok(mlua::Value::Nil);
|
||||
};
|
||||
let model = lua
|
||||
.app_data_ref::<crate::script::Model>()
|
||||
.expect("model app_data");
|
||||
let Some(d) = crate::script::spellbook::spell_display(&model, spell_id) else {
|
||||
return Ok(mlua::Value::Nil);
|
||||
};
|
||||
let text = |v: Option<String>| -> mlua::Result<mlua::Value> {
|
||||
Ok(v.and_then(|t| lua.create_string(&t).ok())
|
||||
.map_or(mlua::Value::Nil, mlua::Value::String))
|
||||
};
|
||||
match field.as_str() {
|
||||
"name" => text(Some(d.name)),
|
||||
// The rank subtext, under both spellings upstream uses for it.
|
||||
"rank" | "nameSubtext" => text(Some(d.rank.unwrap_or_default())),
|
||||
// The `SpellIcon.dbc` row id, which the caller hands straight to
|
||||
// `GetSpellIconTexture`. 0 means the spell has no icon row, and the caller's own
|
||||
// `iconId > 0` gate reads that correctly, so it is answered rather than withheld.
|
||||
"spellIconID" => Ok(model
|
||||
.spell_mechanic_probe
|
||||
.as_ref()
|
||||
.and_then(|probe| probe(spell_id))
|
||||
.map_or(mlua::Value::Nil, |e| {
|
||||
mlua::Value::Integer(i64::from(e.icon_id))
|
||||
})),
|
||||
_ => Ok(mlua::Value::Nil),
|
||||
}
|
||||
})?,
|
||||
lua.create_function(
|
||||
|lua, (spell_id, field, _copy): (Option<u32>, Option<String>, Option<Value>)| {
|
||||
let asked = field;
|
||||
let found = {
|
||||
let model = lua
|
||||
.app_data_ref::<crate::script::Model>()
|
||||
.expect("model app_data");
|
||||
spell_id
|
||||
.filter(|&id| id != 0)
|
||||
.zip(asked.as_deref())
|
||||
.and_then(|(id, field)| {
|
||||
model
|
||||
.spell_rec_field_probe
|
||||
.as_ref()
|
||||
.and_then(|probe| probe(id, field))
|
||||
})
|
||||
};
|
||||
if let Some(value) = found {
|
||||
return spell_rec_value(lua, value);
|
||||
}
|
||||
// The three string columns keep the book fallback this verb had before the
|
||||
// `Spell.dbc` probe existed: a VM with a spellbook and no catalog (a capture, an
|
||||
// early boot) still answers a learnt spell's name and rank, which is what
|
||||
// `GetSpellNameAndRankForId` beside it does from the same `spell_display`. Every
|
||||
// other column is a DBC row and has nowhere else to come from.
|
||||
let Some(field) =
|
||||
asked.filter(|f| matches!(f.as_str(), "name" | "nameSubtext" | "rank"))
|
||||
else {
|
||||
return Ok(Value::Nil);
|
||||
};
|
||||
let model = lua
|
||||
.app_data_ref::<crate::script::Model>()
|
||||
.expect("model app_data");
|
||||
let Some(d) = spell_id
|
||||
.filter(|&id| id != 0)
|
||||
.and_then(|id| crate::script::spellbook::spell_display(&model, id))
|
||||
else {
|
||||
return Ok(Value::Nil);
|
||||
};
|
||||
let text = if field == "name" {
|
||||
d.name
|
||||
} else {
|
||||
d.rank.unwrap_or_default()
|
||||
};
|
||||
Ok(Value::String(lua.create_string(&text)?))
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
|
||||
// `GetSpellNameAndRankForId(spellID)` -> name, rank. DoiteAuras' hardest-leaned-on nampower
|
||||
@@ -2646,9 +2734,142 @@ mod tests {
|
||||
|
||||
/// `GetSpellRecField`: the columns this client has, and **nil for the ones it does not** —
|
||||
/// asserted, because a plausible zero is worse than nothing here (DoiteAuras' Clearcasting
|
||||
/// rule turns on `manaCost > 0`, so a fabricated 0 would silently disable it).
|
||||
/// `GetSpellRec` and `GetSpellRecField` over the `Spell.dbc` probe, including the seven
|
||||
/// names the corpus asks for that answered nil before it existed.
|
||||
///
|
||||
/// Both verbs are asserted on the **same row** so they cannot be seen to disagree, and the
|
||||
/// array columns are read the way their one consumer reads them: `for i = 1, 3`.
|
||||
#[test]
|
||||
fn get_spell_rec_field_answers_what_it_has_and_nil_for_what_it_does_not() {
|
||||
fn the_spell_record_verbs_answer_every_column_the_corpus_asks_for() {
|
||||
use crate::script::SpellRecValue as V;
|
||||
let mut s = UiScript::new().unwrap();
|
||||
let row: Vec<(&'static str, V)> = vec![
|
||||
("name", V::Str("Fireball".into())),
|
||||
("rank", V::Str("Rank 1".into())),
|
||||
("nameSubtext", V::Str("Rank 1".into())),
|
||||
("school", V::Int(2)),
|
||||
("mechanic", V::Int(0)),
|
||||
("manaCost", V::Int(30)),
|
||||
("rangeIndex", V::Int(4)),
|
||||
("spellIconID", V::Int(135_812)),
|
||||
("stackAmount", V::Int(0)),
|
||||
("speed", V::Num(24.0)),
|
||||
("effectMechanic", V::Ints(vec![0, 12, 0])),
|
||||
("effectApplyAuraName", V::Ints(vec![3, 0, 0])),
|
||||
("effectRealPointsPerLevel", V::Nums(vec![0.0, 1.5, 0.0])),
|
||||
];
|
||||
let field_rows = row.clone();
|
||||
s.set_spell_rec_probes(
|
||||
Box::new(move |id, name| {
|
||||
(id == 133)
|
||||
.then(|| {
|
||||
field_rows
|
||||
.iter()
|
||||
.find(|(n, _)| *n == name)
|
||||
.map(|(_, v)| v.clone())
|
||||
})
|
||||
.flatten()
|
||||
}),
|
||||
Box::new(move |id| (id == 133).then(|| row.clone())),
|
||||
);
|
||||
|
||||
// The three names this verb already answered.
|
||||
assert_eq!(
|
||||
s.eval::<(String, String, i64)>(
|
||||
r#"return GetSpellRecField(133, "name"), GetSpellRecField(133, "rank"),
|
||||
GetSpellRecField(133, "spellIconID")"#
|
||||
)
|
||||
.unwrap(),
|
||||
("Fireball".to_string(), "Rank 1".to_string(), 135_812)
|
||||
);
|
||||
// And the seven that did not. `stackAmount` is 0 for a non-stacking spell, which is a
|
||||
// real answer the consumer normalises to 1 itself — nil would make it skip that step.
|
||||
assert_eq!(
|
||||
s.eval::<(i64, i64, i64, i64, f64)>(
|
||||
r#"return GetSpellRecField(133, "school"), GetSpellRecField(133, "manaCost"),
|
||||
GetSpellRecField(133, "rangeIndex"), GetSpellRecField(133, "stackAmount"),
|
||||
GetSpellRecField(133, "speed")"#
|
||||
)
|
||||
.unwrap(),
|
||||
(2, 30, 4, 0, 24.0)
|
||||
);
|
||||
// `SuperCleveRoidMacros/Utility.lua:7776-7794`, transcribed: the crowd-control classifier
|
||||
// that walks both arrays. It is the only reader of either, and it reads them 1-based.
|
||||
assert_eq!(
|
||||
s.eval::<i64>(
|
||||
r#"
|
||||
local effectMechanics = GetSpellRecField(133, "effectMechanic")
|
||||
if effectMechanics then
|
||||
for i = 1, 3 do
|
||||
local em = effectMechanics[i]
|
||||
if em and em > 0 then return em end
|
||||
end
|
||||
end
|
||||
return -1
|
||||
"#
|
||||
)
|
||||
.unwrap(),
|
||||
12,
|
||||
"effect 2's mechanic, found by walking the 1-based table"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>(r#"return GetSpellRecField(133, "effectApplyAuraName")[1]"#)
|
||||
.unwrap(),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<f64>(r#"return GetSpellRecField(133, "effectRealPointsPerLevel")[2]"#)
|
||||
.unwrap(),
|
||||
1.5,
|
||||
"a float column stays a float"
|
||||
);
|
||||
|
||||
// `GetSpellRec` is the same row in one table, and the two verbs agree key by key.
|
||||
assert_eq!(
|
||||
s.eval::<(String, i64, i64, f64)>(
|
||||
r#"local r = GetSpellRec(133)
|
||||
return r.name, r.school, r.effectMechanic[2], r.speed"#
|
||||
)
|
||||
.unwrap(),
|
||||
("Fireball".to_string(), 2, 12, 24.0)
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>(
|
||||
r#"local r = GetSpellRec(133)
|
||||
for k, v in pairs(r) do
|
||||
if type(v) ~= "table" and v ~= GetSpellRecField(133, k) then return false end
|
||||
end
|
||||
return true"#
|
||||
)
|
||||
.unwrap(),
|
||||
"every scalar key of GetSpellRec is what GetSpellRecField answers for that name"
|
||||
);
|
||||
|
||||
// A name the mapping has no column for, and an id no spell owns, are both nil — never a
|
||||
// fabricated zero, which is what the consumer's `if m and m > 0` guard is written for.
|
||||
for call in [
|
||||
r#"GetSpellRecField(133, "notAColumn")"#,
|
||||
r#"GetSpellRecField(999999, "name")"#,
|
||||
r#"GetSpellRecField(133)"#,
|
||||
r#"GetSpellRecField()"#,
|
||||
"GetSpellRec(999999)",
|
||||
"GetSpellRec()",
|
||||
] {
|
||||
assert!(
|
||||
s.eval::<Option<mlua::Value>>(&format!("return {call}"))
|
||||
.unwrap()
|
||||
.is_none_or(|v| v.is_nil()),
|
||||
"{call}"
|
||||
);
|
||||
}
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// With no `Spell.dbc` probe at all — a capture, or the boot VM before the catalog loads —
|
||||
/// the three string columns still answer off the spellbook, which is where this verb read
|
||||
/// before the probe existed. Every other column has nowhere else to come from.
|
||||
#[test]
|
||||
fn the_string_columns_keep_their_spellbook_fallback() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_spellbook(crate::script::SpellBookState {
|
||||
slots: vec![crate::script::SpellSlotView {
|
||||
@@ -2660,14 +2881,16 @@ mod tests {
|
||||
..Default::default()
|
||||
});
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetSpellRecField(133, "name")"#)
|
||||
.unwrap(),
|
||||
"Fireball"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return GetSpellRecField(133, "rank")"#)
|
||||
.unwrap(),
|
||||
"Rank 1"
|
||||
s.eval::<(String, String, String)>(
|
||||
r#"return GetSpellRecField(133, "name"), GetSpellRecField(133, "rank"),
|
||||
GetSpellRecField(133, "nameSubtext")"#
|
||||
)
|
||||
.unwrap(),
|
||||
(
|
||||
"Fireball".to_string(),
|
||||
"Rank 1".to_string(),
|
||||
"Rank 1".to_string()
|
||||
)
|
||||
);
|
||||
for absent in ["stackAmount", "manaCost", "spellIconID", "whatever"] {
|
||||
assert!(
|
||||
@@ -2677,7 +2900,7 @@ mod tests {
|
||||
"{absent} must be nil, not a fabricated number"
|
||||
);
|
||||
}
|
||||
// DoiteAuras' own guards, run against the verb: both must survive the nil.
|
||||
// DoiteAuras' own guard, run against the verb: it must survive the nil.
|
||||
assert_eq!(
|
||||
s.eval::<String>(
|
||||
r#"local m = GetSpellRecField(133, "manaCost")
|
||||
@@ -2686,10 +2909,6 @@ mod tests {
|
||||
.unwrap(),
|
||||
"guarded"
|
||||
);
|
||||
assert!(s
|
||||
.eval::<Option<i64>>(r#"return GetSpellRecField(999999, "name")"#)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
|
||||
@@ -246,7 +246,10 @@ pub use model::{
|
||||
SpellProbe, SpellUsable, TextureProbe, TextureSizeProbe,
|
||||
};
|
||||
pub(crate) use model::{Model, PendingTimer};
|
||||
pub use model::{PlayerCacheEntry, SelfCast, SelfMovement, ServerClock, SpellModifierProbe};
|
||||
pub use model::{
|
||||
PlayerCacheEntry, SelfCast, SelfMovement, ServerClock, SpellModifierProbe, SpellRecFieldProbe,
|
||||
SpellRecValue, SpellRecordProbe,
|
||||
};
|
||||
pub use party::{PartyMemberInfo, PartyRequest, PartyState, RaidMemberInfo, SavedInstanceInfo};
|
||||
pub use pet::{PetActionView, PetStats};
|
||||
pub use pvp::{HonorState, InspectHonorData};
|
||||
|
||||
@@ -71,6 +71,34 @@ pub type SpellProbe = Box<dyn Fn(u32) -> Option<SpellLookup>>;
|
||||
/// through (`benilla-app`'s `cursor.rs`). `None` where the row or its icon is absent.
|
||||
pub type ItemIconProbe = Box<dyn Fn(u32) -> Option<String>>;
|
||||
|
||||
/// One `Spell.dbc` column as nampower's `GetSpellRec`/`GetSpellRecField` hands it to Lua.
|
||||
///
|
||||
/// The array variants are a **1-based Lua table of three**, which is not a guess:
|
||||
/// `SuperCleveRoidMacros` walks `GetSpellRecField(spellID, "effectMechanic")` as
|
||||
/// `for i = 1, 3 do local em = effectMechanics[i]` (`Utility.lua:7776-7794`), and does the same
|
||||
/// with `effectApplyAuraName`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SpellRecValue {
|
||||
Int(i64),
|
||||
Num(f64),
|
||||
Str(String),
|
||||
Ints(Vec<i64>),
|
||||
Nums(Vec<f64>),
|
||||
}
|
||||
|
||||
/// One field of a spell's `Spell.dbc` row by nampower's own name for it, or `None` for a name this
|
||||
/// client does not parse (and for an id no spell owns).
|
||||
///
|
||||
/// **The name-to-column mapping lives in `benilla-app`,** beside the `SpellDisplay` it reads: this
|
||||
/// is a mapping from upstream's spelling to that struct's fields, so it belongs where that struct
|
||||
/// is, and a copy here would be free to drift from it.
|
||||
pub type SpellRecFieldProbe = Box<dyn Fn(u32, &str) -> Option<SpellRecValue>>;
|
||||
|
||||
/// The whole row as `(name, value)` pairs in `Spell.dbc` column order, for `GetSpellRec`. Built
|
||||
/// by walking the same field list through the same per-field function the probe above uses, so
|
||||
/// the two verbs cannot answer differently for one name.
|
||||
pub type SpellRecordProbe = Box<dyn Fn(u32) -> Option<Vec<(&'static str, SpellRecValue)>>>;
|
||||
|
||||
/// nampower's `GetSpellModifiers(spellId, modifierType)`: the `(flat, percent)` pair the client's
|
||||
/// own `GetSpellModifiers 0x6e6b30` folds out of its two talent-modifier tables, or `None` for
|
||||
/// any of that reader's four false exits (the spell has no family, it is not the player's class
|
||||
@@ -799,6 +827,8 @@ pub(crate) struct Model {
|
||||
pub(crate) spell_icon_probe: Option<SpellIconProbe>,
|
||||
pub(crate) item_icon_probe: Option<ItemIconProbe>,
|
||||
pub(crate) spell_modifier_probe: Option<SpellModifierProbe>,
|
||||
pub(crate) spell_rec_field_probe: Option<SpellRecFieldProbe>,
|
||||
pub(crate) spell_record_probe: Option<SpellRecordProbe>,
|
||||
/// 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`).
|
||||
@@ -1701,6 +1731,8 @@ impl Model {
|
||||
spell_probe: None,
|
||||
item_icon_probe: None,
|
||||
spell_modifier_probe: None,
|
||||
spell_rec_field_probe: None,
|
||||
spell_record_probe: None,
|
||||
spell_mechanic_probe: None,
|
||||
player_cache: HashMap::new(),
|
||||
player_cache_enabled: [true, true],
|
||||
|
||||
@@ -131,6 +131,18 @@ impl super::UiScript {
|
||||
self.model_mut().spell_modifier_probe = Some(probe);
|
||||
}
|
||||
|
||||
/// Install nampower's `Spell.dbc` row readers, both built from the one name-to-column mapping
|
||||
/// in `benilla-app` (see [`super::SpellRecFieldProbe`]).
|
||||
pub fn set_spell_rec_probes(
|
||||
&mut self,
|
||||
field: super::SpellRecFieldProbe,
|
||||
record: super::SpellRecordProbe,
|
||||
) {
|
||||
let mut m = self.model_mut();
|
||||
m.spell_rec_field_probe = Some(field);
|
||||
m.spell_record_probe = Some(record);
|
||||
}
|
||||
|
||||
/// Replace the live per-spell usability map ([`super::SpellUsable`]), nampower's
|
||||
/// `IsSpellUsable`/`IsSpellInRange` source. Pushed by the action feed, which already holds
|
||||
/// every input the reference's own usable walk reads.
|
||||
|
||||
@@ -109,6 +109,7 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> {
|
||||
// client reaches a spell's name, rank and icon path and no other column of that row, so
|
||||
// the rest answer nil rather than a plausible zero. DoiteAuras guards each non-name read.
|
||||
"GetSpellRecField",
|
||||
"GetSpellRec",
|
||||
// nampower v4's spellbook index helpers (script/compat/nampower.rs), all four of them the
|
||||
// fast paths DoiteAuras takes before falling back to a 200-slot `GetSpellName` scan per
|
||||
// lookup — it does one of those per tracked aura per condition evaluation.
|
||||
|
||||
+39
-2
@@ -726,6 +726,43 @@ in the corpus calls the global at all — the only mention is a comment in
|
||||
`SuperCleveRoidMacros/ClassicAPI.lua:272` saying it reads the same client field. A bare name match
|
||||
is not a call site.
|
||||
|
||||
### The `Spell.dbc` row — `GetSpellRec` and seven columns `GetSpellRecField` answered nil for
|
||||
|
||||
**Built**, and this one is a measured gap rather than a completeness exercise. Counting what the
|
||||
corpus actually asks `GetSpellRecField` for: `name` (4 sites), `school` (3), `stackAmount` (2),
|
||||
`spellIconID` (2), then one each of `rank`, `rangeIndex`, `mechanic`, `manaCost`,
|
||||
`effectMechanic`, `effectApplyAuraName`. **Seven of those ten answered nil** — the binding knew
|
||||
three names. DoiteAuras caps its stack display on `stackAmount` (`DoitePlayerAuras.lua:539`) and
|
||||
`SuperCleveRoidMacros` classifies a crowd-control effect off the two arrays
|
||||
(`Utility.lua:7776-7794`), so each nil was a feature quietly off.
|
||||
|
||||
The names are the DLL's own, read off its pool in `Spell.dbc` column order (`0x91d68`–`0x92460`):
|
||||
eighty of them. The mapping to `benilla_formats::SpellDisplay` lives in
|
||||
`benilla-app`'s `ui_script/spell_rec.rs` — beside the struct it reads, because a copy on the VM side
|
||||
would be free to drift — and `GetSpellRec(spellId)` is that same field list walked through that
|
||||
same per-field reader, so the two verbs cannot answer differently for one name. A test asserts
|
||||
exactly that, key by key, over one row.
|
||||
|
||||
**The array columns are a 1-based table of three, which is measured and not guessed.**
|
||||
`SuperCleveRoidMacros` walks `GetSpellRecField(spellID, "effectMechanic")` as
|
||||
`for i = 1, 3 do local em = effectMechanics[i]`. `totem`, `reagent` and `reagentCount` take the
|
||||
same shape, two and eight long.
|
||||
|
||||
One new DBC column: `StackAmount`, column **39**, bracketed by two constants this tree already had
|
||||
(38 `ModalNextSpell`, 40 `Totem[0]`). It is the only one added — fifteen further names upstream
|
||||
answers are left nil rather than putting fifteen unused `u32`s on a struct the spell catalog holds
|
||||
one of per row. They are listed at `FIELDS` with that reason, so the gap is a decision and not an
|
||||
omission: `attributesEx4`, `targetCreatureType`, `procFlags`, `procCharges`,
|
||||
`manaPerSecondPerLevel`, `maxTargetLevel`, `maxAffectedTargets`, `dmgClass`, `spellPriority`,
|
||||
`spellVisual2`, `minFactionId`, `minReputation`, `requiredAuraVision`,
|
||||
`effectPointsPerComboPoint`, `dmgMultiplier`.
|
||||
|
||||
`GetSpellRecField`'s three string columns keep the spellbook fallback the verb had before the
|
||||
probe: a VM with a book and no catalog — a capture, or the boot VM before `ui_action`'s loader
|
||||
runs — still answers a learnt spell's name and rank, which is what `GetSpellNameAndRankForId`
|
||||
beside it does from the same `spell_display`. Every other column is a DBC row and has nowhere else
|
||||
to come from.
|
||||
|
||||
### The whole registered list, accounted for
|
||||
|
||||
With the registration table read (the section above), the surface is no longer a count of what a
|
||||
@@ -742,10 +779,10 @@ comm -23 /tmp/np /tmp/ours
|
||||
|
||||
| Bucket | Count | Names |
|
||||
|---|---|---|
|
||||
| **Built** | 37 | `CancelPlayerAuraSlot`, `CancelPlayerAuraSpellId`, `CastSpellByName`, `CastSpellByNameNoQueue`, `CastSpellNoQueue`, `FindPlayerItemSlot`, `GetBagItem`, `GetBagItems`, `GetEquippedItem`, `GetEquippedItems`, `GetItemIconTexture`, `GetItemIdCooldown`, `GetItemLevel`, `GetItemStats`, `GetItemStatsField`, `GetNampowerVersion`, `GetPlayerAuraDuration`, `GetSpellDuration`, `GetSpellIconTexture`, `GetSpellIdCooldown`, `GetSpellIdForName`, `GetSpellModifiers`, `GetSpellNameAndRankForId`, `GetSpellRecField`, `GetSpellSlotTypeIdForName`, `GetTrinkets`, `GetUnitField`, `GetUnitGUID`, `IsAuraHidden`, `IsSpellInRange`, `IsSpellUsable`, `PlayerIsMoving`, `PlayerIsRooted`, `PlayerIsSwimming`, `SetMouseoverUnit`, `UseItemIdOrName` |
|
||||
| **Built** | 38 | `CancelPlayerAuraSlot`, `CancelPlayerAuraSpellId`, `CastSpellByName`, `CastSpellByNameNoQueue`, `CastSpellNoQueue`, `FindPlayerItemSlot`, `GetBagItem`, `GetBagItems`, `GetEquippedItem`, `GetEquippedItems`, `GetItemIconTexture`, `GetItemIdCooldown`, `GetItemLevel`, `GetItemStats`, `GetItemStatsField`, `GetNampowerVersion`, `GetPlayerAuraDuration`, `GetSpellDuration`, `GetSpellIconTexture`, `GetSpellIdCooldown`, `GetSpellIdForName`, `GetSpellModifiers`, `GetSpellNameAndRankForId`, `GetSpellRec`, `GetSpellRecField`, `GetSpellSlotTypeIdForName`, `GetTrinkets`, `GetUnitField`, `GetUnitGUID`, `IsAuraHidden`, `IsSpellInRange`, `IsSpellUsable`, `PlayerIsMoving`, `PlayerIsRooted`, `PlayerIsSwimming`, `SetMouseoverUnit`, `UseItemIdOrName` |
|
||||
| **Withheld pending grom's say-so** | 8 | `WriteCustomFile`, `ReadCustomFile`, `CustomFileExists`, `ImportFile`, `ExportFile`, `ExecuteCustomLuaFile`, `StartItemExport` (writes a file of its own), `EncryptPassword` + `EncryptedServerLogin` |
|
||||
| **Not ours to add** | 4 | `QueueSpellByName`, `QueueScript`, `ChannelStopCastingNextTick`, `LearnTalentRank` |
|
||||
| **To build, in corpus-call order** | 13 | `DisenchantAll` (5 call sites outside the API layer), then `GetSpellPower`, `GetSpellRec`, `GetTrinketCooldown`, `UseTrinket`, `GetUnitData`, `GetSpellRangeData`, `CombatLogFlush`, `GetRaidTargets`, `SetLocalRaidTargetIndex`, `GetQuestLogQuestIds`, `GetQuestDialogQuestId`, `StartItemExport` (0 each — present for completeness, not need) |
|
||||
| **To build, in corpus-call order** | 12 | `DisenchantAll` (5 call sites outside the API layer), then `GetSpellPower`, `GetTrinketCooldown`, `UseTrinket`, `GetUnitData`, `GetSpellRangeData`, `CombatLogFlush`, `GetRaidTargets`, `SetLocalRaidTargetIndex`, `GetQuestLogQuestIds`, `GetQuestDialogQuestId`, `StartItemExport` (0 each — present for completeness, not need) |
|
||||
|
||||
The two that were at the head of that last bucket are now built — see "The cast feed" above for
|
||||
why they came first and what each decision cost.
|
||||
|
||||
Reference in New Issue
Block a user