ui+docs: SuperWoW's quest-log getters, and the DLL work list worked name by name

`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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-23 21:32:37 -06:00
co-authored by Claude Opus 5
parent 5555bf9c89
commit 801bc09c05
3 changed files with 176 additions and 0 deletions
@@ -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:<id>:<level>|h[<title>]|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.
@@ -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",
+60
View File
@@ -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.