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:
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user