From 801bc09c050ab46294ddaf6468a72727fb1442a4 Mon Sep 17 00:00:00 2001 From: grom Date: Fri, 23 Oct 2026 21:32:37 -0600 Subject: [PATCH] ui+docs: SuperWoW's quest-log getters, and the DLL work list worked name by name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `GetQuestIDForLogIndex(index)` and `GetQuestLinkForLogIndex(index)`: 1.12's `GetQuestLogTitle` answers the title and never the id, so neither the id nor a chat link for a log row is derivable from the stock API. The id reads the same `quest_log.entries` ClassicAPI's `C_QuestLog.GetQuestIDForLogIndex` does, so an addon that feature-detects one and falls back to the other gets one answer; a header row is id `0` and **no** link, rather than a link to quest 0. `docs/ADDON_COMPAT.md` now carries the whole work list with a status per name, and the most useful finding in it is not a missing function: `SuperCleveRoidMacros/NampowerAPI.lua` is a capability registry whose `HasFeature` checks the version **and** `_G[name] ~= nil`, so nothing on the list was broken and raising `GetNampowerVersion` would not have helped. Defining the global is the only honest way to make one live. Counts, so the list cannot be read as more alarming than it is: 16 done across this tranche and the two before it; 12 that were already present and only looked absent because the script compares against `dump_globals` on a bare VM (and because an *event* is never a global); 6 that need a feed, each with the specific thing missing; 3 that are the spell queue this client deliberately does not have; and 7 withheld. The seven withheld are `ExportFile`, `ImportFile`, `WriteCustomFile`, `ReadCustomFile`, `CustomFileExists`, `ExecuteCustomLuaFile` and `EncryptPassword`. Six of those give addon Lua read, write and **execute** on files under the client folder and the seventh takes a password. They are real in nampower and used for config export, but they are a different kind of surface from a getter and the right default for a client that did not have them yesterday is not to grow them quietly. `GetCastInfo` and `GetCurrentCastingInfo` are left out for a reason worth keeping: their consumers divide by the cast and GCD timings, and this client has a spell id, a deadline and a guard bit — no start instant, no channel flag, no GCD end. A table of three real fields and seven zeros is worse than the nil the addon's own fallback already handles correctly. 269 data-gated failures, identical to the baseline. All nine gates green. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- .../benilla-ui/src/script/compat/superwow.rs | 110 ++++++++++++++++++ .../src/script/tests/reference_surface.rs | 6 + docs/ADDON_COMPAT.md | 60 ++++++++++ 3 files changed, 176 insertions(+) diff --git a/crates/benilla-ui/src/script/compat/superwow.rs b/crates/benilla-ui/src/script/compat/superwow.rs index 877e4ec1..bebd5761 100644 --- a/crates/benilla-ui/src/script/compat/superwow.rs +++ b/crates/benilla-ui/src/script/compat/superwow.rs @@ -34,6 +34,41 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> { g.set("SUPERWOW_VERSION", SUPERWOW_VERSION)?; g.set("SUPERWOW_STRING", SUPERWOW_STRING)?; + // `GetQuestIDForLogIndex(index)` and `GetQuestLinkForLogIndex(index)`: the quest log's row + // `index` as its quest id and as a chat link. 1.12's own `GetQuestLogTitle` answers the title + // and never the id, so neither can be derived from the stock API — which is why SuperWoW adds + // both and why `SuperAPI` reaches for the link one. + // + // ClassicAPI publishes the id getter too, as `C_QuestLog.GetQuestIDForLogIndex` + // (`compat/classic_api/quest_log.rs`); both read `model.quest_log.entries`, so the namespaced + // and the bare name cannot disagree. A header row's id is `0`, as that one documents, and an + // index past the end is nil. + // + // The link is `|Hquest::|h[]|h`, the shape the stock quest-log shift-click + // builds and `item_mixin`'s own fixture pins. A header row has no link — nil, not a link to + // quest 0 — and the level is the row's, which is what a receiver's tooltip shows. + g.set( + "GetQuestIDForLogIndex", + lua.create_function(|lua, index: Option<usize>| { + let model = lua.app_data_ref::<Model>().expect("model app_data"); + Ok(index + .and_then(|i| i.checked_sub(1)) + .and_then(|i| model.quest_log.entries.get(i)) + .map(|e| e.quest_id)) + })?, + )?; + g.set( + "GetQuestLinkForLogIndex", + lua.create_function(|lua, index: Option<usize>| { + let model = lua.app_data_ref::<Model>().expect("model app_data"); + Ok(index + .and_then(|i| i.checked_sub(1)) + .and_then(|i| model.quest_log.entries.get(i)) + .filter(|e| !e.is_header && e.quest_id != 0) + .map(|e| format!("|Hquest:{}:{}|h[{}]|h", e.quest_id, e.level, e.title))) + })?, + )?; + // UnitPosition(unitid): world XYZ, fed only for tokens a `ui_unit.rs`/`ui_tooltip.rs` feed // site has a `Transform` beside (player/target/targettarget/npc/mouseover so far, per // `docs/ADDON_COMPAT.md` — party/raid/pet aren't wired yet, so read as absent, not an error). @@ -342,6 +377,81 @@ impl crate::script::UiScript { mod tests { use crate::script::{SpellBookState, SpellSlotView, UiScript, UnitState}; + /// SuperWoW's two quest-log getters: the id and the chat link for a log row. 1.12 answers the + /// title and never the id, so neither is derivable from the stock API. + /// + /// The id must agree with `C_QuestLog.GetQuestIDForLogIndex`, which ClassicAPI publishes over + /// the same rows — an addon that feature-detects one and falls back to the other has to get + /// the same answer. + #[test] + fn the_quest_log_getters_answer_the_id_and_the_link_for_a_row() { + let mut s = UiScript::new().unwrap(); + assert_eq!( + s.eval::<Option<u32>>("return GetQuestIDForLogIndex(1)") + .unwrap(), + None, + "an empty log" + ); + s.set_quest_log(crate::script::QuestLogState { + entries: vec![ + crate::script::QuestLogEntryView { + title: "Elwynn Forest".into(), + is_header: true, + ..Default::default() + }, + crate::script::QuestLogEntryView { + quest_id: 1234, + title: "Kobold Camp Cleanup".into(), + level: 5, + ..Default::default() + }, + ], + ..Default::default() + }); + + // A header row: id 0 and no link — not a link to quest 0. + assert_eq!( + s.eval::<Option<u32>>("return GetQuestIDForLogIndex(1)") + .unwrap(), + Some(0) + ); + assert_eq!( + s.eval::<Option<String>>("return GetQuestLinkForLogIndex(1)") + .unwrap(), + None + ); + + assert_eq!( + s.eval::<Option<u32>>("return GetQuestIDForLogIndex(2)") + .unwrap(), + Some(1234) + ); + assert_eq!( + s.eval::<Option<String>>("return GetQuestLinkForLogIndex(2)") + .unwrap(), + Some("|Hquest:1234:5|h[Kobold Camp Cleanup]|h".to_string()) + ); + assert_eq!( + s.eval::<Option<u32>>("return C_QuestLog.GetQuestIDForLogIndex(2)") + .unwrap(), + Some(1234), + "the namespaced getter reads the same rows" + ); + + // Out of range, and index 0, are nil rather than a wrapped read. + for call in [ + "GetQuestIDForLogIndex(3)", + "GetQuestIDForLogIndex(0)", + "GetQuestIDForLogIndex()", + ] { + assert_eq!( + s.eval::<Option<u32>>(&format!("return {call}")).unwrap(), + None, + "{call}" + ); + } + } + /// The override is a WRITE to the mouseover, not a permanent shadow over it. It must win while /// it stands — that is the feature, a `mouseover` macro driven off a unit frame — and it must /// be takeable back by the engine's own hover, which is what `clear_mouseover_override` is. diff --git a/crates/benilla-ui/src/script/tests/reference_surface.rs b/crates/benilla-ui/src/script/tests/reference_surface.rs index d0e02ff9..cc340c2e 100644 --- a/crates/benilla-ui/src/script/tests/reference_surface.rs +++ b/crates/benilla-ui/src/script/tests/reference_surface.rs @@ -315,6 +315,12 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> { // 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`). + // SuperWoW's two quest-log getters (script/compat/superwow.rs). 1.12's + // `GetQuestLogTitle` answers the title and never the id, so neither the id nor a chat + // link for a log row is derivable from the stock API. The id agrees with + // `C_QuestLog.GetQuestIDForLogIndex`, which ClassicAPI publishes over the same rows. + "GetQuestIDForLogIndex", + "GetQuestLinkForLogIndex", "CancelPlayerAuraSlot", "CancelPlayerAuraSpellId", "CastSpellByNameNoQueue", diff --git a/docs/ADDON_COMPAT.md b/docs/ADDON_COMPAT.md index b5b7875d..4c0872c3 100644 --- a/docs/ADDON_COMPAT.md +++ b/docs/ADDON_COMPAT.md @@ -486,3 +486,63 @@ rather than guessed at — `CancelPlayerAuraSlot`, `CancelPlayerAuraSpellId`, the events `SPELL_FAILED_SELF`, `SPELL_QUEUE_EVENT`, `UNIT_DIED`, `UNIT_HEALTH_GUID`. A name being called is not proof an addon needs it — most sit behind an `if GetTrinkets then` — so the next step is reading each call site, not implementing the list. + +## The DLL work list, name by name — `scripts/dll-surface.py`'s output, worked + +The script above enumerates what the DLLs publish; this is what came of reading each name's call +sites in grom's own corpus. **The single most useful thing found was not a missing function.** +`SuperCleveRoidMacros/NampowerAPI.lua` is a capability registry: a `VERSION_REQUIREMENTS` table +naming every nampower function with its minimum version, reached through `API.HasFeature`, which +checks the version **and** `_G[name] ~= nil` (`:448-465`). So nothing on this list was *broken*, and +raising `GetNampowerVersion` would not have helped — the only honest way to make one of these live +is to define the global. Read a call site before implementing: the first pass of the script flagged +25 names as "unguarded" that were doc comments and table keys inside that registry, and pfUI's +`GetCastInfo` is its own file-local function (`modules/nameplates.lua:105`), not nampower's. + +**Done (16).** `PlayerIsMoving`, `PlayerIsRooted`, `PlayerIsSwimming`, `IsAuraHidden`, +`GetItemIconTexture`, `GetTrinkets`, `FindPlayerItemSlot`, `GetBagItem`, `GetItemIdCooldown`, +`UseItemIdOrName`, `CancelPlayerAuraSpellId`, `CancelPlayerAuraSlot`, `CastSpellNoQueue`, +`CastSpellByNameNoQueue` (nampower), `GetQuestIDForLogIndex`, `GetQuestLinkForLogIndex` (SuperWoW). +Plus the six `NP_EnableUnitEventsX` CVars, registered stored-only. Each has its row in +`reference_surface.rs`'s `allowed_beyond_1_12` saying why 1.12 has no spelling to prefer. + +**Already present, counted as missing by the script.** `IsSwimming`, `IsMounted`, `IsIndoors`, +`SpellInfo`, `SetAutoloot`, `GetPlayerBuffID`, `SetMouseoverUnit`, `UnitExists`'s GUID return, +`UnitBuff`/`UnitDebuff`'s aura ids, `GetLootMethod`'s master looter, `UNIT_CASTEVENT`, +`RAW_COMBATLOG`. The script compares against `dump_globals` on a **bare** VM, so a verb installed +later and an *event* (never a global at all) both read as absent. Subtract those before counting. + +**Withheld, pending a decision that is not mine (7).** `ExportFile`, `ImportFile`, +`WriteCustomFile`, `ReadCustomFile`, `CustomFileExists`, `ExecuteCustomLuaFile`, `EncryptPassword` +(with `EncryptedServerLogin`). The first six give addon Lua read, write and **execute** on files +under the client folder; the last takes a password. Every one is real in nampower and used for +config export, but they are a different kind of surface from a getter, and the right default for a +client that did not have them yesterday is not to grow them silently. `CombatLogFlush` is here only +because it is in the same family — it would flush our own `WoWCombatLog.txt`, which is benign. + +**Needs a feed this client does not have, with what it would take (6).** + +| Name | What is missing | +|---|---| +| `GetCastInfo()` | Its table is `castId, spellId, guid, castType, castStartS, castEndS, castRemainingMs, castDurationMs, gcdEndS, gcdRemainingMs` (`NampowerAPI.lua:1859`). `PendingCast` (`spell/inflight.rs`) carries a spell id, a deadline and a guard bit — no start instant, no channel state, no GCD end. The consumer **divides by these** (`API.GetGCDRemainingMs`), so a table of three real fields and seven zeros is worse than the nil its own fallback already handles correctly. Needs `SMSG_SPELL_START`'s cast time kept as a start/end pair, a channel flag, and the GCD end the action bar already computes but does not publish | +| `GetCurrentCastingInfo()` | `castId, visId, autoId, casting, channeling, onswing, autoattack`. Same gap: `model.casting` is one bool that explicitly excludes channels, and `visId`/`autoId`/`onswing` are queue state this client has none of | +| `GetSpellRangeData(spellId)` | `SpellRange.dbc` is not on the spell probe — `IsSpellInRange` answers the *verdict* from the live usable map, not the yards. One more probe beside `install_spell_extras_probes` | +| `GetSpellModifiers(spellId)` | `spell/mods.rs` stores the server's `SPELLMOD_*` cells but publishes no reader | +| `GetUnitData(unit)` | The whole descriptor as a table, where `GetUnitField` answers one field. Needs a decision about which fields: the real one returns GUID-typed columns (charm, summon, charmedBy) this client's field reader does not type | +| `GetTrinketCooldown()` | Equipped-slot cooldowns. `container_cooldowns` is keyed `(bag, slot)`; the equipped slots are not in it | + +**Not ours to add (3).** `QueueSpellByName`, `QueueScript` and `ChannelStopCastingNextTick` are the +spell queue itself, which this client deliberately does not have (see the nampower rows above: +namreeb's always-on cast rule is ported, pepo's queue machinery is not). `LearnTalentRank(page, +index, rank)` is left out for a narrower reason: `LearnTalent` here learns the *next* rank, so +reaching a named rank means queueing the pair once per missing rank, and whether the server accepts +that burst is not something this client can check before sending. + +**Events (7), all already tracked in the nampower section above**: `SPELL_CAST_EVENT`, +`SPELL_QUEUE_EVENT`, `SPELL_START_SELF`, `SPELL_GO_SELF`, `SPELL_FAILED_SELF`, `UNIT_DIED`, +`UNIT_HEALTH_GUID`. The first two are queue events; the rest want columns the start/go rows above +already name as the blocker. + +**ClassicAPI's 70** are the remaining bulk and are tracked in `docs/classic-api-functions.md` +rather than here. The same caution applies twice over: its 999 pointed-at names include a great deal +that is not Lua surface at all, and the corpus intersection is the only part worth reading.