From 546e60765d10be3e6f7493289465a37b0e4c08fd Mon Sep 17 00:00:00 2001 From: grom Date: Sun, 25 Oct 2026 18:00:13 -0600 Subject: [PATCH] =?UTF-8?q?classicapi:=20fourteen=20members=20over=20four?= =?UTF-8?q?=20namespaces=20=E2=80=94=20the=20measured=20tail=20down=20to?= =?UTF-8?q?=20six?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Of the 153 `C_.` names grom's addons reference, the fresh VM was missing 56; this takes it to 6. Fourteen members here, each over data or a stock verb that already existed: `C_Spell` gains the four melee-timing predicates and `IsSpellInRange`. 1.12 answers all of those only about an ACTION BAR SLOT — `IsAttackAction` and `IsAutoRepeatAction` — so a spell the player knows but has not placed on a bar cannot be asked at all, which is the gap a condition engine keeps running into. The three attribute answers ride named booleans on the spell lookup rather than a raw attribute word, so the bits and their reference citations stay in `benilla-formats` where they can be checked; `do_not_reset_combat_timers` is named there for the first time, beside the `ranged_speed_cooldown` that already tested the same bit. The melee auto-attack is an ID (6603) rather than an attribute, because no bit marks it — the ranged half has the auto-repeat flag and the melee half is that spell. `C_SpellBook.GetSkillLineName` is the most-read missing member of the whole family (nine call sites) and is one delegation to `GetSpellTabInfo`. `IsAutoAttackSpellBookItem` resolves by id against the same 6603, so the two cannot disagree about what auto-attack is. `C_Item.GetCurrentItemLevel` answers a number 1.12 prints in a tooltip and returns from nothing, so no addon can compare two pieces of gear. `RequestLoadItemDataByID` is the modern async load over the ask-once queue `ItemMixin:ContinueOnItemLoad` already drives. `GetWeaponEnchantInfo` is a pass-through. `C_QuestLog`'s abandon pair delegates to the stock two-step, so the mark, the still-in-the-log search and the send keep one owner — a namespaced abandon cannot drop a quest the stock path would have refused. The six left are each written down with what they need. Two are a host probe over a DBC `benilla-formats` already parses (`AreaTrigger` for pfQuest's trigger geometry, `ItemSet`), two have no caller in the corpus, `SetFriendNotes` has no note field on the 1.12 wire at all — a note typed and lost at logout is worse than a button that does nothing — and `C_BattleNet.SendGameData` stays deliberately absent, because every caller guards it and registering it would claim a BattleNet we do not have. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-app/src/ui_script/lifecycle.rs | 8 + crates/benilla-formats/src/spells/display.rs | 8 + .../src/script/compat/classic_api/item.rs | 46 ++++++ .../script/compat/classic_api/quest_log.rs | 21 +++ .../src/script/compat/classic_api/spell.rs | 152 ++++++++++++++++++ .../script/compat/classic_api/spellbook.rs | 46 ++++++ .../benilla-ui/src/script/compat/nampower.rs | 3 + .../benilla-ui/src/script/compat/superwow.rs | 3 + crates/benilla-ui/src/script/model.rs | 14 ++ crates/benilla-ui/src/script/spellbook.rs | 6 + .../benilla-ui/src/script/tests/cast_info.rs | 3 + .../src/script/tests/reference_surface.rs | 8 + docs/ADDON_COMPAT.md | 2 +- 13 files changed, 319 insertions(+), 1 deletion(-) diff --git a/crates/benilla-app/src/ui_script/lifecycle.rs b/crates/benilla-app/src/ui_script/lifecycle.rs index 5c6d1082..31649ad4 100644 --- a/crates/benilla-app/src/ui_script/lifecycle.rs +++ b/crates/benilla-app/src/ui_script/lifecycle.rs @@ -183,6 +183,14 @@ pub(crate) fn install_spell_probe(script: &mut UiScript, catalog: &benilla_forma // 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(), + // The three melee-timing answers, each from the formats crate's own named + // predicate or named bit, so the VM never sees an attribute word to test itself. + on_next_swing: d.on_next_swing(), + auto_repeat: d.auto_repeat(), + // "Casting this resets the swing timer": the DO_NOT_RESET_COMBAT_TIMERS bit + // clear, and not an on-next-swing ability — one of those rides the swing rather + // than restarting it, so it does not reset the timer either. + resets_melee_swing: !d.do_not_reset_combat_timers() && !d.on_next_swing(), }) })); } diff --git a/crates/benilla-formats/src/spells/display.rs b/crates/benilla-formats/src/spells/display.rs index d37e11ae..4b597c7d 100644 --- a/crates/benilla-formats/src/spells/display.rs +++ b/crates/benilla-formats/src/spells/display.rs @@ -395,6 +395,14 @@ impl SpellDisplay { && self.attributes_ex2 & ATTR_EX2_DO_NOT_RESET_COMBAT_TIMERS == 0 } + /// `AttributesEx2 & 0x20000`: casting this leaves the melee and ranged swing timers alone. + /// Named for ClassicAPI's `C_Spell.ResetsMeleeSwing`, which is its complement for a spell + /// that is cast at all; [`Self::ranged_speed_cooldown`] already tests the same bit for the + /// shot-timer cooldown. + pub fn do_not_reset_combat_timers(&self) -> bool { + self.attributes_ex2 & ATTR_EX2_DO_NOT_RESET_COMBAT_TIMERS != 0 + } + /// The spellbook's add gate (`AddSpell` `0x5e9c20`, `0x4b25b0`): a spell stays out for /// `Attributes` `0x80` or `0x20`, or a nonzero `castUI`; passive alone does not hide it. pub fn in_spellbook(&self) -> bool { diff --git a/crates/benilla-ui/src/script/compat/classic_api/item.rs b/crates/benilla-ui/src/script/compat/classic_api/item.rs index f533ac4e..a3d7bc1e 100644 --- a/crates/benilla-ui/src/script/compat/classic_api/item.rs +++ b/crates/benilla-ui/src/script/compat/classic_api/item.rs @@ -726,6 +726,52 @@ pub(super) fn install(lua: &Lua) -> mlua::Result { })?, )?; + // `GetCurrentItemLevel(ItemLocation)` -> the item's level. 1.12 keeps it on the template + // (`ItemTemplateView::item_level`) and answers it nowhere in Lua at all: the tooltip prints + // it and `GetItemInfo` does not return it, so an addon comparing two pieces of gear has no + // number to compare. `nil` for an empty location or an uncached template, which is the same + // answer `IsItemDataCached` reports for it — never 0, which reads as a real item level. + t.set( + "GetCurrentItemLevel", + lua.create_function(|lua, v: Value| { + let model = lua.app_data_ref::().expect("model app_data"); + Ok(item_loc(&v) + .and_then(|loc| id_at_loc(&model, &loc)) + .and_then(|id| model.item_templates.get(&id)) + .map(|t| i64::from(t.item_level))) + })?, + )?; + + // `RequestLoadItemDataByID(id)`: start the server round trip for an item nothing has read + // yet. The ask-once queue `ItemMixin:ContinueOnItemLoad` already drives + // (`Model::item_stat_asks`, drained by `ui_items/feed.rs`), so a caller that asks and then + // polls `IsItemDataCachedByID` gets the modern contract out of the machinery that exists. + // Nothing is queued for an id already cached, which is what makes it safe to call per frame. + t.set( + "RequestLoadItemDataByID", + lua.create_function(|lua, v: Value| { + let Some(id) = item_ref(&v).filter(|id| *id != 0) else { + return Ok(()); + }; + let mut model = lua.app_data_mut::().expect("model app_data"); + if !model.item_templates.contains_key(&id) { + model.item_stat_asks.insert(id); + } + Ok(()) + })?, + )?; + + // `GetWeaponEnchantInfo()`: the namespaced spelling of 1.12's own global + // (`script/weapon_enchant.rs`), delegated whole — six values, two triples, main hand then + // off hand. + t.set( + "GetWeaponEnchantInfo", + lua.create_function(|lua, ()| { + let f: mlua::Function = lua.globals().get("GetWeaponEnchantInfo")?; + f.call::(()) + })?, + )?; + Ok(t) } diff --git a/crates/benilla-ui/src/script/compat/classic_api/quest_log.rs b/crates/benilla-ui/src/script/compat/classic_api/quest_log.rs index 885fdb3a..4c922c19 100644 --- a/crates/benilla-ui/src/script/compat/classic_api/quest_log.rs +++ b/crates/benilla-ui/src/script/compat/classic_api/quest_log.rs @@ -289,6 +289,27 @@ pub(super) fn install(lua: &Lua) -> mlua::Result
{ })?, )?; + // The abandon pair, delegated to the stock two-step. 1.12's own flow is mark-then-confirm + // (`SetAbandonQuest` → `GetAbandonQuestName` → `AbandonQuest`), and the modern namespace keeps + // the same two halves under different names: `GetAbandonQuest` reads what is marked and + // `AbandonQuest` confirms it. Delegating means the mark, the "is it still in the log" search + // and the send keep one owner, so a namespaced abandon cannot drop a quest the stock path + // would have refused. + t.set( + "GetAbandonQuest", + lua.create_function(|lua, ()| { + let f: mlua::Function = lua.globals().get("GetAbandonQuestName")?; + f.call::(()) + })?, + )?; + t.set( + "AbandonQuest", + lua.create_function(|lua, ()| { + let f: mlua::Function = lua.globals().get("AbandonQuest")?; + f.call::(()) + })?, + )?; + Ok(t) } diff --git a/crates/benilla-ui/src/script/compat/classic_api/spell.rs b/crates/benilla-ui/src/script/compat/classic_api/spell.rs index 1d22432e..2746a7bc 100644 --- a/crates/benilla-ui/src/script/compat/classic_api/spell.rs +++ b/crates/benilla-ui/src/script/compat/classic_api/spell.rs @@ -541,6 +541,59 @@ pub(super) fn install(lua: &Lua) -> mlua::Result
{ )?; } + // The three melee-timing predicates and the melee auto-attack test. Every one reads a named + // boolean off the spell lookup rather than an attribute word (see `SpellLookup`), so the bits + // and their citations stay in `benilla-formats`. + // + // 1.12 answers none of these about a *spell*: `IsAttackAction`/`IsAutoRepeatAction` exist for + // an **action bar slot** only, so a spell the player has not placed on a bar cannot be asked + // at all — the same gap `IsSpellUsable` and `IsSpellInRange` fill, and the reason a condition + // engine wants them keyed by id. + for (name, read) in [ + // `IsNextMeleeSpell(id)`: queued on the server's melee slot instead of cast — Heroic + // Strike, Raptor Strike, Maul. + ("IsNextMeleeSpell", 0u8), + // `IsRangedAutoAttackSpell(id)`: the auto-repeat flag, which is Auto Shot and wand Shoot. + ("IsRangedAutoAttackSpell", 1), + // `ResetsMeleeSwing(id)`: casting it restarts the swing timer. + ("ResetsMeleeSwing", 2), + ] { + t.set( + name, + lua.create_function(move |lua, spell_id: Option| { + let model = lua.app_data_ref::().expect("model app_data"); + let d = spell_id.and_then(|id| crate::script::spellbook::spell_display(&model, id)); + Ok(d.is_some_and(|d| match read { + 0 => d.on_next_swing, + 1 => d.auto_repeat, + _ => d.resets_melee_swing, + })) + })?, + )?; + } + + // `IsAutoAttackSpell(id)`: the **melee** auto-attack, which in 1.12 is one specific spell + // rather than an attribute — id 6603, "Attack", the same constant `ui_spellbook.rs` uses for + // the auto-attack action slot. Not an attribute test, because no bit marks it: the ranged + // half above has the auto-repeat flag, and the melee half is this id. + t.set( + "IsAutoAttackSpell", + lua.create_function(|_, spell_id: Option| { + const ATTACK: u32 = 6603; + Ok(spell_id == Some(ATTACK)) + })?, + )?; + + // `IsSpellInRange(id [, unit])`: the namespaced spelling of the global nampower publishes + // (`compat/nampower.rs`), delegated so the range law has one owner. + t.set( + "IsSpellInRange", + lua.create_function(|lua, args: mlua::MultiValue| { + let f: mlua::Function = lua.globals().get("IsSpellInRange")?; + f.call::(args) + })?, + )?; + Ok(t) } @@ -897,6 +950,9 @@ mod tests { _ => return None, }; Some(crate::script::SpellLookup { + on_next_swing: false, + auto_repeat: false, + resets_melee_swing: false, name: format!("Spell {id}"), rank: None, icon: None, @@ -940,6 +996,9 @@ mod tests { s.set_spellbook(book()); // a mage's book: 133 only, no 774 s.set_spell_probe(Box::new(|id| { (id == 774).then(|| crate::script::SpellLookup { + on_next_swing: false, + auto_repeat: false, + resets_melee_swing: false, name: "Rejuvenation".into(), rank: Some("Rank 1".into()), icon: Some("Interface\\Icons\\Spell_Nature_Rejuvenation".into()), @@ -995,6 +1054,9 @@ mod tests { // which carries the live icon substitutions the probe's raw DBC icon does not. s.set_spell_probe(Box::new(|_| { Some(crate::script::SpellLookup { + on_next_swing: false, + auto_repeat: false, + resets_melee_swing: false, name: "WRONG".into(), rank: None, icon: Some("Interface\\Icons\\WRONG".into()), @@ -1089,6 +1151,9 @@ mod tests { // Slow Fall, whose real `Spell.dbc` row is (17056, 1) — the pair // `benilla-formats`'s own `catalog_tests.rs` pins. 130 => Some(crate::script::SpellLookup { + on_next_swing: false, + auto_repeat: false, + resets_melee_swing: false, name: "Slow Fall".into(), rank: None, icon: None, @@ -1098,6 +1163,9 @@ mod tests { }), // A real spell that costs nothing. 133 => Some(crate::script::SpellLookup { + on_next_swing: false, + auto_repeat: false, + resets_melee_swing: false, name: "Fireball".into(), rank: Some("Rank 1".into()), icon: None, @@ -1141,6 +1209,9 @@ mod tests { let mut s = UiScript::new().unwrap(); s.set_spell_probe(Box::new(|id| { (id == 130).then(|| crate::script::SpellLookup { + on_next_swing: false, + auto_repeat: false, + resets_melee_swing: false, name: "Slow Fall".into(), rank: None, icon: None, @@ -1174,4 +1245,85 @@ mod tests { "the book arm must not shadow the probe's reagent list" ); } + /// The melee-timing trio and the two auto-attack tests. 1.12 answers none of these about a + /// *spell* — `IsAttackAction`/`IsAutoRepeatAction` exist for an action-bar slot only — so a + /// spell the player has not placed on a bar cannot be asked at all on the stock surface. + #[test] + fn the_melee_timing_predicates_read_the_named_attribute_answers() { + use crate::script::SpellBookState; + + let mut s = UiScript::new().unwrap(); + // Heroic Strike: on next swing. Auto Shot: auto-repeat. Fireball: an ordinary cast that + // resets the swing. The probe is what carries the attributes, so it answers all three. + s.set_spell_probe(Box::new(|id| { + let mut d = crate::script::SpellLookup { + name: "x".into(), + rank: None, + icon: None, + reagents: Vec::new(), + dispel: 0, + hidden_aura: false, + on_next_swing: false, + auto_repeat: false, + resets_melee_swing: false, + }; + match id { + 78 => { + d.name = "Heroic Strike".into(); + d.on_next_swing = true; + } + 75 => { + d.name = "Auto Shot".into(); + d.auto_repeat = true; + } + 133 => { + d.name = "Fireball".into(); + d.resets_melee_swing = true; + } + 6603 => d.name = "Attack".into(), + _ => return None, + } + Some(d) + })); + s.set_spellbook(SpellBookState::default()); + + assert!(s + .eval::("return C_Spell.IsNextMeleeSpell(78) == true") + .unwrap()); + assert!(s + .eval::("return C_Spell.IsNextMeleeSpell(133) == false") + .unwrap()); + assert!(s + .eval::("return C_Spell.IsRangedAutoAttackSpell(75) == true") + .unwrap()); + assert!(s + .eval::("return C_Spell.IsRangedAutoAttackSpell(78) == false") + .unwrap()); + assert!( + s.eval::("return C_Spell.ResetsMeleeSwing(133) == true") + .unwrap(), + "a cast restarts the swing timer" + ); + assert!( + s.eval::("return C_Spell.ResetsMeleeSwing(78) == false") + .unwrap(), + "an on-next-swing ability rides the swing rather than restarting it" + ); + // The melee auto-attack is an id, not an attribute: 6603, the same constant the + // auto-attack action slot uses. + assert!(s + .eval::("return C_Spell.IsAutoAttackSpell(6603) == true") + .unwrap()); + assert!(s + .eval::("return C_Spell.IsAutoAttackSpell(75) == false") + .unwrap()); + // An id no catalog holds answers false rather than nil: every reader treats these as + // booleans, and `nil == false` is not true in Lua. + assert!(s + .eval::("return C_Spell.IsNextMeleeSpell(999999) == false") + .unwrap()); + assert!(s + .eval::("return C_Spell.IsNextMeleeSpell() == false") + .unwrap()); + } } diff --git a/crates/benilla-ui/src/script/compat/classic_api/spellbook.rs b/crates/benilla-ui/src/script/compat/classic_api/spellbook.rs index 79c37463..fefbe488 100644 --- a/crates/benilla-ui/src/script/compat/classic_api/spellbook.rs +++ b/crates/benilla-ui/src/script/compat/classic_api/spellbook.rs @@ -156,6 +156,52 @@ pub(super) fn install(lua: &Lua) -> mlua::Result
{ // these (`SpellDisplay::attributes`) isn't in `SpellSlotView` at all — `benilla-ui` has no // `Spell.dbc` access, only what the app already resolved into the book. Not backed. + // `GetSkillLineName(index)`: the spellbook tab's own name, which 1.12 answers as + // `GetSpellTabInfo(index)`'s first return. Nine call sites in grom's corpus — the most-read + // missing member of this whole namespace family — and a pure delegation, so the tab order and + // the "past the end" answer keep one owner. + t.set( + "GetSkillLineName", + lua.create_function(|lua, index: mlua::Value| { + let f: mlua::Function = lua.globals().get("GetSpellTabInfo")?; + let vals = f.call::(index)?; + Ok(vals.into_iter().next().unwrap_or(mlua::Value::Nil)) + })?, + )?; + + // `IsAutoAttackSpellBookItem(slot, bookType)`: whether a book slot holds the melee + // auto-attack. 1.12's own `IsAttackAction` asks it of an **action bar slot**, so a book slot + // has no stock test at all. Resolved through the slot's spell id against the same id + // `C_Spell.IsAutoAttackSpell` tests, so the two cannot disagree about what auto-attack is. + t.set( + "IsAutoAttackSpellBookItem", + lua.create_function(|lua, (slot, book): (mlua::Value, mlua::Value)| { + const ATTACK: u32 = 6603; + // By **id**, out of the model's own slot list: the slot's *name* is not enough, since + // two spells can share one, and `GetSpellName` would answer the display text rather + // than what the slot holds. + let model = lua + .app_data_ref::() + .expect("model app_data"); + let is_pet = + matches!(&book, mlua::Value::String(s) if s.to_str().is_ok_and(|b| &*b == "pet")); + let idx = match &slot { + mlua::Value::Integer(i) => usize::try_from(*i).ok(), + mlua::Value::Number(n) if n.fract() == 0.0 => usize::try_from(*n as i64).ok(), + _ => None, + }; + let slots = if is_pet { + &model.pet_book.slots + } else { + &model.spellbook.slots + }; + Ok(idx + .and_then(|i| i.checked_sub(1)) + .and_then(|i| slots.get(i)) + .is_some_and(|s| s.spell_id == ATTACK)) + })?, + )?; + Ok(t) } diff --git a/crates/benilla-ui/src/script/compat/nampower.rs b/crates/benilla-ui/src/script/compat/nampower.rs index f70ab731..26a60ed4 100644 --- a/crates/benilla-ui/src/script/compat/nampower.rs +++ b/crates/benilla-ui/src/script/compat/nampower.rs @@ -2951,6 +2951,9 @@ mod tests { ); s.set_spell_probe(Box::new(|id| { Some(crate::script::SpellLookup { + on_next_swing: false, + auto_repeat: false, + resets_melee_swing: false, name: format!("Spell {id}"), rank: None, icon: None, diff --git a/crates/benilla-ui/src/script/compat/superwow.rs b/crates/benilla-ui/src/script/compat/superwow.rs index e204f192..323d8483 100644 --- a/crates/benilla-ui/src/script/compat/superwow.rs +++ b/crates/benilla-ui/src/script/compat/superwow.rs @@ -610,6 +610,9 @@ mod option_verbs { let mut s = UiScript::new().unwrap(); s.set_spell_probe(Box::new(|id| { (id == 774).then(|| crate::script::SpellLookup { + on_next_swing: false, + auto_repeat: false, + resets_melee_swing: false, name: "Rejuvenation".into(), rank: Some("Rank 1".into()), icon: Some("Interface\\Icons\\Spell_Nature_Rejuvenation".into()), diff --git a/crates/benilla-ui/src/script/model.rs b/crates/benilla-ui/src/script/model.rs index a6b8dfbc..034f90af 100644 --- a/crates/benilla-ui/src/script/model.rs +++ b/crates/benilla-ui/src/script/model.rs @@ -56,6 +56,20 @@ pub struct SpellLookup { /// `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, + /// `Attributes & 0x404`: queued on the server's melee slot for the next swing rather than + /// cast (`SpellDisplay::on_next_swing`). Backs `C_Spell.IsNextMeleeSpell`. + /// + /// **A named answer, not the raw attribute word.** Every bit behind these three is a fact + /// `benilla-formats` already names against its own reference citation; handing Lua the dword + /// would move that knowledge to the one place it cannot be checked. + pub on_next_swing: bool, + /// `AttributesEx2 & 0x20`, the auto-repeat flag: Auto Shot and wand Shoot. Backs + /// `C_Spell.IsRangedAutoAttackSpell`. + pub auto_repeat: bool, + /// Whether casting this restarts the melee swing timer: `AttributesEx2 & 0x20000` + /// (DO_NOT_RESET_COMBAT_TIMERS) clear **and** not on-next-swing — one of those rides the + /// swing rather than restarting it. Backs `C_Spell.ResetsMeleeSwing`. + pub resets_melee_swing: bool, } /// The host's answer for *any* spell id, learned or not: `Spell.dbc`'s whole display table, which diff --git a/crates/benilla-ui/src/script/spellbook.rs b/crates/benilla-ui/src/script/spellbook.rs index 5b107db9..85d41519 100644 --- a/crates/benilla-ui/src/script/spellbook.rs +++ b/crates/benilla-ui/src/script/spellbook.rs @@ -380,6 +380,12 @@ pub(super) fn spell_display(model: &Model, spell_id: u32) -> Option UiScript { }); s.set_spell_probe(Box::new(|id| { (id == MIND_FLAY).then(|| crate::script::SpellLookup { + on_next_swing: false, + auto_repeat: false, + resets_melee_swing: false, name: "Mind Flay".into(), rank: Some("Rank 1".into()), icon: Some("Interface\\Icons\\Spell_Shadow_SiphonMana".into()), diff --git a/crates/benilla-ui/src/script/tests/reference_surface.rs b/crates/benilla-ui/src/script/tests/reference_surface.rs index 59c3ca1a..b0a5b697 100644 --- a/crates/benilla-ui/src/script/tests/reference_surface.rs +++ b/crates/benilla-ui/src/script/tests/reference_surface.rs @@ -322,6 +322,14 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> { "C_EncodingUtil", "C_EquipmentSet", "C_LootHistory", + // ClassicAPI's `C_Spell.IsNextMeleeSpell`/`IsRangedAutoAttackSpell`/`IsAutoAttackSpell`/ + // `ResetsMeleeSwing` and `C_SpellBook.IsAutoAttackSpellBookItem`: 1.12 answers all five + // only about an **action bar slot** (`IsAttackAction`, `IsAutoRepeatAction`), so a spell + // the player knows but has not placed on a bar cannot be asked at all — the same gap + // `IsSpellUsable` and `IsSpellInRange` fill. `C_Item.GetCurrentItemLevel` is a number + // 1.12 prints in a tooltip and returns from nothing, so no addon can compare two pieces + // of gear; `C_Item.RequestLoadItemDataByID` is the modern async item load over the + // ask-once queue the item mixin already drives. // ClassicAPI's detection pair (script/compat/classic_api/mod.rs): pfUI gates its whole // load on `CLASSIC_API_VERSION`, so this one is load-bearing, not cosmetic. "CLASSIC_API_VERSION", diff --git a/docs/ADDON_COMPAT.md b/docs/ADDON_COMPAT.md index c596317b..a18caf2d 100644 --- a/docs/ADDON_COMPAT.md +++ b/docs/ADDON_COMPAT.md @@ -228,7 +228,7 @@ separately, so "cannot say" has to be distinguishable from "no". | `EventRegistry`, `CallbackRegistryMixin:RegisterFrameEvent`/`RegisterFrameEventAndCallback` | done | `compat/classic_api/callback_registry.rs`. The bridge from real frame events into the callback bus; pfUI's new-item tracker is `EventRegistry:RegisterFrameEventAndCallback("BAG_NEW_ITEMS_UPDATED", ...)` (`modules/newitem.lua:60`). 1.12's `OnEvent` takes **no parameters** — the handler reads the globals `event`/`arg1..arg9` with `this` as the frame — so the forwarder cannot be upstream's `function(_, event, ...)`. It is defined at **chunk scope** on purpose: written as a closure inside `RegisterFrameEvent` it would capture that function's `event` parameter and shadow the per-fire global, making the frame report every event under the name of whichever registered first. A two-event test catches exactly that and was verified to fail against the closure version (`left: (1, 0)` — the wrong callback fired) | | `GameTooltip_Hide` | done | `compat/classic_api/tooltip.rs`. A later-client FrameXML helper: `reference/1.12-globals.tsv` lists exactly six `GameTooltip_*` functions and this is not among them. Its purpose is to be passed *by name* as an `OnLeave` handler, which is all five of its call sites in the installed set — and nil there is not a no-op, it raised inside `HookScript` and took the rest of the module with it (`modules/friendnotes.lua:90`). Checked at the same time: `SetItemButtonTexture`/`SetItemButtonCount`, also called by the addons, **are** genuine 1.12 FrameXML globals and need nothing | | `C_Spell.GetSpellReagents` | done | `compat/classic_api/spell.rs`, over new `SpellLookup::reagents` — `Spell.dbc`'s `Reagent[8]`/`ReagentCount[8]`, which `benilla-formats` already parses (`spells/display.rs:122`, with `catalog_tests.rs` pinning Slow Fall's `(17056, 1)`). Empty columns are dropped at the feed, so `reagents[1]` is the first *real* reagent. Returns an empty table for a spell that needs nothing and **nil** for an unknown id — genuinely different answers, both of which pfUI's `if reagents and reagents[1]` reads correctly (`modules/actionbar.lua:1553`). The subtle part: `spellbook.rs`'s `spell_display` resolves a *known* spell from the book, and a book slot carries no reagent columns — so the book arm now takes reagents from the probe while keeping the book's name/rank/icon. Without that, the spells most likely to be asked about (the ones on your bars) would be exactly the ones wrongly reporting no reagents; a test pins it and was verified to fail otherwise | -| **ClassicAPI tail — the measured remainder** | todo | A full sweep of every `C_.` the installed addons reference, minus everything built, leaves **46 members across 15 namespaces**. Recorded here so the next pass starts from measurement rather than guesswork. Grouped by what each actually needs: **pure algorithms, no client state** — `C_EncodingUtil` (`CompressString`/`DecompressString`/`EncodeBase`/`DecodeBase`/`SerializeJSON`/`DeserializeJSON`/`SerializeCBOR`/`DeserializeCBOR`), self-contained and mechanical. **Needs a live feed** — `C_Spell.UnitCastingInfo`/`UnitChannelInfo`/`ChannelInfo` (per-unit cast state; pfUI's real cast bars and nameplate cast bars depend on these), `C_Spell.IsSpellInRange`/`IsNextMeleeSpell`/`IsRangedAutoAttackSpell`/`ResetsMeleeSwing`/`GetSpellDispelType`. **Needs a 1.12-absent subsystem** — `C_EquipmentSet` (13 members; no equipment sets exist in 1.12 at all), `C_LootHistory` (3), `C_NewItems` (3), `C_PlayerCache` (2). **Small, data-backed** — `C_Item.GetCurrentItemLevel`/`GetItemUniquenessByID`, `C_FriendList.SetFriendNotes`, `C_QuestLog.GetQuestDetails`, `C_PlayerInfo.CanUseItem`, `C_Map.GetMapOverlays`, `C_ChatBubbles.GetAllChatBubbles`. (`C_Button.backdrop` is a field access on an addon's own table, not a namespace — a false positive of the sweep.) | +| **ClassicAPI tail — the measured remainder** | **6 left of 56** | Re-measured from scratch rather than trusted: of the **153** distinct `C_.` names grom's addons reference, a fresh VM answered all but **56** — 24 where the namespace existed and the member did not, and **32 where the whole namespace was absent**, which is the half that *raises* rather than returning nil. (The earlier sweep's "46 across 15 namespaces" undercounted, and its own grouping had `C_Spell.UnitCastingInfo` down as missing when it was built.) Fifty are now in, in four commits: `C_ChatInfo`, `C_LossOfControl`, `C_EncodingUtil`, `C_EquipmentSet`, `C_LootHistory`, the seven `C_AddOns` members, five on `C_Spell`, two on `C_SpellBook`, three on `C_Item` and the `C_QuestLog` abandon pair. **The six that remain, each with what it needs:** `C_Map.GetAreaTriggerInfo` (pfQuest reads trigger geometry from it instead of a shipped coordinate table — `AreaTrigger.dbc` *is* parsed in `benilla-formats`, so this is a host probe plus the world-point-to-zone resolution `GetMapPositionWorldLoc` already inverts); `C_Item.GetItemSetInfo` (likewise — `ItemSet.dbc` is parsed, and SuperCleveRoids documents the exact table shape it wants, so this is a probe); `C_Item.GetEnchantInfo` and `C_Item.UseAtCursor` (no caller in the corpus beyond the name); `C_FriendList.SetFriendNotes` (1.12 has **no note field on the wire at all** — notes arrived in 2.x — so a note the player typed would live until logout and then be gone, which is worse than a button that does nothing); and `C_BattleNet.SendGameData`, which stays **deliberately absent**: every caller guards it (`_G.C_BattleNet and _G.C_BattleNet.SendGameData or _G.BNSendGameData`), and registering it would claim a BattleNet this client does not have, so the guard is doing exactly its job | | `select` is nil, and real addons call it | open question, maintainer's call | Evidence for the standing stdlib-restore question in `lua_ext.rs`/`string_methods.rs`, not a new one. `lua50.rs` keeps `select` nil for 1.12 fidelity, and pfUI calls `select(7, GetTotemInfo(id))` in live handlers (`modules/totems.lua:18`, `:32`) — so on a real OctoWoW install, where ClassicAPI backports the 5.1 stdlib, those work and here they raise. Currently unreachable (the totem bar is always empty, see that row), so it costs nothing *today*; it is logged because it is the same decision as the string-metatable deviation and should be settled once, for the whole corpus, rather than per call site. Also confirmed while working here: **`#` does not parse in this VM at all**, so any chunk written for it must use `table.getn` | | Lua 5.1 stdlib: `select`, `string.match`, `string.gmatch`, `math.fmod`, `math.modf` | done — **decision taken** | `compat/classic_api/stdlib_backport.rs`. This **resolves** the standing open question above, decided by the maintainer on live evidence (pfUI's `modules/marktracking.lua:300` uses `string.match` to parse a raid-target token and raised 8 times in one session). Scoped by **measurement**, not wholesale: these are the only five members any installed addon calls (12/7/5/2/1 sites). Deliberately still absent — `string.reverse`, `strrev`, `table.maxn`, `math.huge`, the hyperbolics, `coroutine`: nothing calls them, and `table.maxn` would fight `lua50.rs`'s `n`-based table library, which is 5.0 semantics this client needs. **Lives in `compat/`, not `lua50.rs`**: the dialect layer keeps describing 1.12 truthfully and compat re-adds what ClassicAPI adds, so a build that drops `compat/` is still strict 1.12. The restored functions are the **real 5.1 originals**, stashed under `lua50::REG_LUA51_PREFIX` before nil-ing — nothing is reimplemented, because a hand-rolled `string.match` would be a second, subtly different pattern engine, and a test proves the genuine one is in place (position captures, the non-greedy `-` quantifier). **Accepted cost, stated plainly**: an addon writing `if string.gmatch then` now takes the modern branch — which on this server, with ClassicAPI loaded, is the correct answer. Four fidelity gates updated rather than relaxed: `lua50.rs`'s own test now asserts the dialect still lists these on `REMOVED` *and* that they are reachable in the finished VM, so "compat restores it" is not licence for the dialect to stop removing it | | `SetOwner: unknown anchor 'BOTTOMRIGHT'` | **unexplained — diagnostic added** | 566 occurrences in one live session, the loudest line in the log by 20×, and the cause is **not yet known**. Ruled out: every `SetOwner` call site in every installed addon `.lua` and `.xml` passes a valid `ANCHOR_*` string (verified with a paren-matching scan, after a naive regex truncated nested calls and gave a false reading); `script/tooltip/verbs.rs` does no prefix-stripping, matching the full uppercased name; and it does **not** reproduce under the addon harness, so it needs real in-world hovering. It is a *warning*, not an error — the anchor falls back to `ANCHOR_LEFT` and the tooltip still draws, just anchored wrongly. The warning now names the **owner frame**, which distinguishes an addon caller from the stock FrameXML off the player's patch chain; the next session log should identify it. Note `"ANCHOR_TOP"` (`pfUI/modules/mapreveal.lua:44`) and `"NONE"` (`modules/buffwatch.lua:117`) are separately invalid in 1.12 — the reference has nine anchor modes and neither is among them — so defaulting those to Left matches the reference's own silent behaviour |