# Addon compat — SuperWoW / nampower / UnitXP_SP3 / ClassicAPI / VanillaUtils / VanillaHelpers This fork has no `WoW.exe` and no DLL injection, so the six client-extension DLLs an OctoWoW install ships beside the real client (`SuperWoWhook.dll`, `nampower.dll`, `UnitXP_SP3.dll`, `ClassicAPI.dll`, `VanillaUtils.dll`, `VanillaHelpers.dll`) cannot be loaded. This tracks porting their features — both the Lua API addons feature-detect and the direct client behaviour changes that aren't addon-mediated — natively into benilla's own engine and Lua VM, always on, so addons written against them behave the same here. Every row below is therefore intentionally "beyond 1.12.1" in `scripts/api-coverage.sh`'s report; that script's "beyond" bucket is expected to be large because of this file, not a regression. Status: `todo` not started · `partial` some functions/behaviours land, not all · `done` complete for what we could confirm. Confidence: `confirmed` from the tool's own public source/docs · `inferred` from exported-symbol names and/or related addon call sites, not a confirmed source — these need checking against real OctoWoW addon behaviour in play, and ideally the OctoWoW team's own source if this bundle is theirs. Detection globals/flags flip to "present" only once that tool's functions are actually implemented — never ahead of the functions themselves, so a feature-detecting addon doesn't find a flag with nothing behind it. ## SuperWoW — confirmed (public wiki, `balakethelock/SuperWoW`) | Area | Status | Notes | |---|---|---| | Detection (`SUPERWOW_STRING`, `SUPERWOW_VERSION`) | done | `script/compat/superwow.rs`; registered as an intentional beyond-1.12 exception in `script/tests/reference_surface.rs` | | Unit-id grammar (`owner`, `mark1..8`, raw GUID as unit id) | done | `Model::unit` (`script/model.rs`) plus `superwow_token_recognised` (`script/unit/mod.rs`) gating it past the reference's own token-shape check. GUID scope is currently whatever's reachable by a named token this tick (party/raid/target/mouseover/pet/npc), not every loaded object — SuperWoW's own wider reach needs a per-tick GUID-indexed push from `benilla-app`, not yet built | | `UnitExists` + GUID | done | second return only when a GUID resolves (`script/unit/bindings.rs`); 16-hex-digit opaque token, round-trips through the grammar above. Exact byte format SuperWoW itself uses is undocumented publicly — ours only needs to agree with itself | | `UnitBuff`/`UnitDebuff` + id, `GetPlayerBuffID` | done | `script/aura/mod.rs`'s `returns_1121` (+ spell id), `script/aura/player_buff.rs` | | `CastSpellByName(name [, "unit"])` | **done, both forms** | `"CLICK"` queues a separate `mouseover_spell_casts`, drained in `ui_spellbook.rs`'s `drain_spell_casts` straight at `"mouseover"`, bypassing the player's own selection. **And `arg2` as a unit, which this row used to call unbuildable** — "no name-to-unit index exists here, only tokens". The DLL's own usage string says `"unit"`, i.e. a unit **id**, which is what every real call site passes and what `Model::unit` already resolves (a token, a GUID, `markN`, an `owner`). It used to fall through to the ordinary queue, which casts at the player's **own selection**: a silent wrong target at six sites in grom's corpus — pfUI's `/castfocus` (`modules/focus.lua:78`, a GUID) and `/castmouseover` (`modules/mouseover.lua:22`, a token), Decursive's cure-at-guid (`Decursive.lua:2735`), BigWigs' spell requests, two BigWigs encounter scripts. Each asked for a unit and got whatever was targeted. The stock meaning of `arg2` (`onSelf`, truthy) keeps it: only a **string that resolves to a live unit** takes the new path, so `CastSpellByName(spell, 1)` is unchanged and a string naming nothing live falls through rather than dropping the cast | | `LootSlot` force-loot arg | done | `script/loot.rs`; truthy (Lua truthiness, e.g. `1`) routes to the take queue instead of the confirm queue | | `SetRaidTarget` `"local"` arg | done | `PartyRequest::SetRaidTarget.local` (`script/party.rs`); the live drain (`ui_party/feed.rs`) applies to `GroupState` directly instead of sending `CMSG`/`MSG_RAID_TARGET_UPDATE` | | Nameplate `frame:GetName(1)` → GUID | done | `script/object/frame_state.rs`'s shared `GetName`, `NamePlates::guid_for_frame` (`script/nameplate.rs`) | | `GetWeaponEnchantID` | **done** | the one thing `GetWeaponEnchantInfo` cannot say: the reference reads that very dword as its gate (`0x4c97f8`, nonzero, no DBC lookup) and then reports only that there *was* an enchant, so on the stock surface nothing can tell Windfury from Rockbiter or a shaman's imbue from a rogue's poison. The id was already in hand app-side and discarded — `ui_char.rs`'s `weapon_enchant` used it as a `?` gate — so this was one field on the snapshot. Two returns, main hand then off hand, which is the order its six-value sibling already answers in; the shape is a reading, since upstream publishes no usage string for it and nothing in the corpus calls it. A test asserts the two functions cannot disagree about *which* hands are enchanted | | `GetWeaponEnchantInfo([unit])`'s unit argument | todo, blocked | the extension that takes a **unit** still needs per-unit weapon-enchant tracking; today only `"player"`'s is fed at all | | `GetContainerItemInfo` charges (negative stack count) | todo, blocked | the raw signed field exists (`ObjectFields::item_spell_charges`, benilla-protocol), but nothing in `benilla-formats`' item template says *which* of an item's five spell-trigger slots (if any) is a consumable charge rather than an equip/proc effect — exposing it without that would be a guess, not a port | | New functions (`SpellInfo`, `TrackUnit`/`UntrackUnit`, `SetMouseoverUnit`, `Clickthrough`, `SetAutoloot`, `UnitNameplate`, `CanLootUnit`, `CombatLogAdd`, Import/Export/AppendText, map/world coord conversions, `IsSwimming`/`IsMounted`/`IsIndoors`/`GetSpeed`, mail and quest-link getters) | **superseded: every one of them is built** | this row used to read "todo, blocked — each needs new app-side state, not a binding tweak", and three of its four stated blockers turned out to be wrong. The self-movement snapshot it asked for **already existed** (`SelfMovement`, built for ClassicAPI's `State` namespace), and the three posture getters are SuperWoW's own spelling — `IsIndoors`, capital-I, not the lowercase `isIndoors` this row guessed at — so they were live before the row was written. `CanLootUnit` needed no feed at all: `UnitState` already carried `dead` and raw `dynamic_flags`. `UnitNameplate` was one line over a lookup ClassicAPI's `C_NamePlate` had already built. Only `GetSpeed` really did need a new field, and `net::current_speed` already computed the number. The row is kept rather than deleted because that pattern — a "needs a feed" verdict answered by data the tree already had — is the expensive mistake this file exists to record. Each name's own row, with what it is measured from, is in the DLL-manifest section below | | `Clickthrough([0|1])` | **done** | the world pick skips a dead unit holding no loot for us while this is set, so a click lands on whatever is behind it — SuperAPI's own description is "click through corpses to loot corpses underneath them", the pile after an AoE pull. The test is `CanLootUnit`'s own, negated (`hover.rs`'s `clickthrough_skips`), so "lootless" means one thing to that verb, to this pick and to the loot sparkle; and because the bit is per-viewer on the server, a corpse somebody else may loot is lootless *to us*, which is exactly the pile the feature is for. A **living** unit is never skipped whatever its flags — that would make mobs unclickable. The flag is mirrored out of the VM once a frame (`target::ClickthroughCorpses`) rather than read in the pick, which is a `SystemParam` with no reach into the script; a one-frame lag on a preference set once is immaterial | | `UnitPosition(unitid)`, `CursorPosition()` | done | `UnitState.position` (`script/unit/mod.rs`), fed by `Transform` from `ui_unit.rs`'s `feed_units` (player/target/targettarget/npc) and `ui_tooltip.rs`'s `drive_mouseover_tooltip` (mouseover); functions in `compat/superwow.rs`. Party/raid/pet aren't wired (their own `snapshot()` call sites in `ui_party/feed.rs`/`ui_pet/unit.rs` pass `None` — each needs its own `Entity`→`Transform` lookup threaded through, not attempted this pass). Reading `Transform` broadly past the usual one-frame-stale tolerance raised `game_plugins.rs`'s `UPDATE_ACTIONABLE_CEILING` by 30 (cited there) — `Transform` is written by enough movement/animation/physics systems that `With` alone doesn't shrink the ambiguity count | | `UNIT_CASTEVENT` | done | `crates/benilla-app/src/superwow_events.rs`: a `CastEventFeed` queue the cast handlers push into, drained once per tick by one VM-holding system that fires the event — the pattern `ui_mirror.rs` already uses, and the only way across, since `spell/net.rs`'s `Lifecycle`/`Scene` bundles deliberately hold no `UiScript` (`game_plugins.rs`'s `every_vm_holder_in_update_declares_its_side_of_the_tick` gates that). Edges wired: `START` from `SMSG_SPELL_START` with its wire cast time as `arg5` (an instant raises none — its only edge is `CAST`, which is what the wiki's own START/CAST split means), `CAST` from `SMSG_SPELL_GO`, `FAIL` from `SMSG_SPELL_FAILED_OTHER`, `CHANNEL` from `MSG_CHANNEL_START` with its duration. GUIDs go out in the same 16-hex-digit spelling every other `compat/` surface uses, so `arg1`/`arg2` compare directly against a `UnitExists`/`UnitGUID` return. Not wired: `MAINHAND`/`OFFHAND` — benilla's melee-swing path raises no cast edge at all, so there is nothing to forward. `MSG_CHANNEL_START` names no caster (1.12 sends it for the local player's own channel only), so `CHANNEL`'s `arg1` is self and its `arg2` is 0 | | `RAW_COMBATLOG` | **done** | `ui_chat/frames.rs`'s `route` — the one funnel every combat line already passes on its way to a window and to `WoWCombatLog.txt`. Two arguments, as its one consumer in grom's corpus destructures them: the printed sentence, and the same sentence with each unit name spelled as its GUID (`PendingCombat::guid_fills`, composed a second time through the same `compose_line`, so the two can only ever differ in the name slots). Fired **before** the `CHAT_MSG_*`, which is what "raw" means upstream — it hooks the combat-log parse, ahead of the client's own formatting. Gated on `has_listener` — the gate nampower's combat-log family already uses — **or on SuperWoW's raw log being open**, since that file is the same `arg2` form written to disk; the composition is skipped under the same one test (`frames::wants_raw_form`), because a raid's log is tens of lines a second. A variant whose template says "you" keeps saying it, because that word is in the *template* and not in a fill — and its consumer depends on exactly that, reading the word and then asking `UnitExists("player")` for its own guid. **Why this row came first:** of SuperWoW's whole surface, it and `CombatLogAdd` are the only two names grom's addons call that this client lacked (`scripts/dll-surface.py --wanted`), and `SuperCleveRoidMacros/Utility.lua:10206` funnels **five** subsystems through this single event — immunity and resist learning, hidden-CC ("afflicted by") detection, debuff-duration learning off the GUID half, and the reactive-proc window that lights **Overpower** and **Revenge**. Its own comment there is why the last of those can come from nowhere else: "SPELL_GO only provides hit/miss COUNTS, not miss TYPES, so it cannot distinguish dodge (Overpower proc) from parry/block/resist". This does **not** by itself fix the Overpower complaint: that needs the dodged-*ability* sentence to be emitted at all, which is its own open item (`benilla-overpower-aura-spelldodged`) | | `CombatLogAdd("text" [, addToRawLog])` | **done** | `compat/superwow.rs` queues the line; `ui_chat/logging.rs`'s `sync_chat_logging` appends it to `WoWCombatLog.txt` through the same `record` every routed line uses, so it is stamped identically and one writer owns the file. **The second argument names a FILE**, not the event: `Logs\WoWRawCombatLog.txt` is its own literal in the DLL's string pool, a few strings on from `Logs\WoWCombatLog.txt`, and it is the GUID-form log — the same lines `RAW_COMBATLOG` carries as `arg2`. That corrects a first reading of the name ("also fire the event"), which was an inference from the name alone. Dropped while a log is closed, which is upstream's behaviour too. A non-string argument is ignored rather than raising: its one real caller is a debug-trace helper (`BigWigs/Plugins/MageTools.lua:761`) and a raise there would take the addon down for something the player cannot act on. **It is also a detection name**: `SpellRequests.lua:6` is `if CombatLogAdd then hasSuperWow = true`, which gates that whole module | | `CREATE_CHATBUBBLE` | **done** | the last of SuperWoW's three events, and it needed the bubbles to become **widgets** first: its payload can only be the bubble, and this client drew them straight into the overlay quad lane. `benilla-ui`'s `script/chat_bubble.rs` is now a pool of real frames — anonymous `WorldFrame` children with a backdrop, a tail texture and a `FontString`, which is what the reference's own `CGChatBubbleFrame` is — driven by the app's existing feed, the same split `vplates.rs` already makes for the plates. `arg1` is the frame, which is the only sensible payload and a reading rather than a measurement (the DLL publishes no usage string for an event). Fired **per appearance**, not per pool slot ever built, which is the opposite of `NAME_PLATE_CREATED`'s rule and deliberate: what an addon wants from this is "here is a bubble to skin or move, now", and a recycled frame has been re-anchored, re-sized and re-texted since it was last seen | | `NameplateRange` | done | one CVar, two readers: `nameplates.rs`'s `NameConfig::range_sq` (the overhead name — introduces a cull where the reference has none at all) and `vplates.rs`'s `VPlateMode::range_sq` (replaces the reference's own hardcoded 20 yd V-key-plate range). Default `80`, the top of the mod's documented 10-80 range — no shipped default is published, so this is the least-restrictive honest choice, not a confirmed number | | `ChatBubbleRange` | done | `chat_bubble.rs`'s `BubbleConfig::range_sq`, replacing the reference's hardcoded 20 yd. Default `20` (today's exact value) — no shipped default published for this one either, within the mod's documented 10-200 yd range | | `NameplateMotion` | **done, two of its four values** | the investigation this row asked for had an answer already in the tree: benilla *does* spread plates, with the reference's own SmartScreenRect solver (`crate::smart_rect`, bucket 0). So `0` ("Overlap") is that solver bypassed — the plate sits on its own projected point and nothing is claimed either, since a claim with no solve is half a spread — and `1` ("Default spread") is it, which is today's behaviour and the registered default. `2` ("Smart spread") and `3` ("Compact spread") are the mod's own alternative algorithms, unknowable from here, and **fall back to 1**: a documented silent-equal rather than a refused row, because the benign direction is plates that still do not overlap, and refusing would leave a player no way to turn the spread off at all. The four meanings come from SuperAPI's own panel (`SuperAPIOptions.lua:20-25`), the DLL publishing only the name. `NameplateVerticalSpacing`, which this row used to carry, is **not in the DLL's string pool at all** — another ghost | | `FoV` | **done, and the hold-back is resolved** | the row was held because the mod's documented default `1.57` looked irreconcilable with `benilla-world`'s `CAM_FOVY` of 0.785 — double, with no obvious reason. **It is the same number.** `1.57` is π/2, which is the reference's own `[camera+0x40]` default of 90° in radians, and that field is not a vertical field of view at all: `player/scoped_view.rs` already divides by it as a *denominator*, because what the reference's camera field expresses is a ratio (its own comment: the spyglass is "a 6× zoom off whatever our normal FOV is, not a literal 15 degrees"). So `FoV` is that ratio, upstream's own default is this client's existing lens **by construction**, and shipping it changes nothing — which is the test's assertion. Clamped to SuperAPI's own slider bounds (0.1–3.14 radians, `SuperAPIOptions.lua:103-105`), and it composes with the spyglass by multiplication. One deviation, in our favour: upstream's panel says "Requires UI Reload"; this one is live | | `LootSparkle` | done, CVar `LootSparkle`, default `1` | `creature_anim/spell_visual.rs`'s `arm_loot_fx` grew a **GameObject** arm beside its unit and corpse ones, hanging the same `"HARDCODED Loot Art"` row (`0x13`) at the same tag. 1.12 sparkles a lootable corpse and nothing else — a chest, a herb, a vein or a quest object is found by eye — and sparkling those arrived in **2.3.0** ("interactable game objects sparkle and display their names"), which is what the mod backports: upstream describes it as "the sparkling effect on lootable treasure". The test takes **two** arms, because "lootable treasure" is neither one alone: the server's own per-viewer `GO_DYNFLAG_LO_ACTIVATE` in `GAMEOBJECT_DYN_FLAGS` (`GameObject::ActivateToQuest`), documented on its field reader as "the sparkle" — but vmangos raises it through `ActivateToQuest`, so on a chest it means *quest* loot and keying on it alone would light up nothing else — **or** `GAMEOBJECT_TYPE_CHEST`, which is exactly chests, herbs and mining nodes, treasure by construction and a type the client reads without the server's help, excluding `GO_FLAG_IN_USE`/`NO_INTERACT` so an emptied or disabled chest stops. Both arms are a judgement about what the mod does, not a trace of it: if only quest objects light up the first is doing all the work, if doors and signs do the second is too wide. Defaulted **on**: upstream publishes no default, and the sparkle is present on this install with SuperWoW loaded, which is what "as if its default config loaded" means here; `SetCVar("LootSparkle", "0")` is stock 1.12. `FxStage` is immaterial for this effect and the row no longer claims otherwise — `Particles\LootFX.m2` (the `"HARDCODED Loot Art"` row's own model, extracted from this install) holds **one** 2400 ms sequence with `flags = 0`, and bit 0 clear is a looping sequence, so `FxAnimLife::arm` repeats it whatever the stage says | | Healing text, uncap sounds, selection-circle style, interface volume | **registered, stored only** | all four are in SuperAPI's own options panel, which reads and writes them (`SuperAPIOptions.lua`), so registering is what makes that panel round-trip and stops the engine's `unknown CVar` warning — it is not a claim that the behaviour arrived, the bargain `ShowVKeyCastbar`'s row states at length. `SelectionCircleStyle` ships `1`, the art this client draws; its own labels say 2 and 3 need a texture the player downloads and we do not ship, and 4 needs the ring rotated to the unit's facing. `UncapSounds` ships `1` because **it is already this client's behaviour**: the reference caps simultaneous sounds and benilla never did. `HealingText` ships `0`, the reference's behaviour — 1.12 floats no heals at all (`combat_text`'s own header says so) — and the path to wiring it is short and written down in `cvars.rs`: `SMSG_SPELLHEALLOG` is already parsed for the chat line and `combat_text` already takes a spawn message, so what is missing is only the *look*, which is authored content rather than a binding. `InterfaceVolume` is the one row **no addon here writes at all**, and benilla mixes UI sounds on the master bus with no second fader. **Outline/shadow text, force-show-auras and XP text are ghosts**: none of those names is in the DLL's string pool, and nor is `NameplateVerticalSpacing` — they came from the early wiki reading, not from the binary | | Macro limit 255→510 | done | `script/macros.rs`'s `MAX_MACRO_BODY`; `ui_script/manifest.rs`'s `install_macro_letters_patch` re-patches the stock FrameXML's `SetMaxLetters`/char-count string at boot, since both come from the player's own install | | Targeting-circle spells (Blizzard, Flamestrike) no longer refuse movement | done | `cast_target::is_ground_targeted` (`spell/cast_target.rs`) exempts them in `validator::cast_moving_refusal` (`spell/validator.rs`); the real cast-time interrupt-on-move rule (`inflight.rs`) is untouched | | Ranged abilities (Aimed Shot) firing `SPELLCAST_START` | done | already satisfied — `spell/net.rs`'s `spell_start` already opens the bar on any real `SpellCastTimes.dbc` duration rather than the reference's narrower ranged-spell suppression (see the `has_cast_time` comment there); confirmed by the existing test `a_timed_ranged_cast_opens_the_shot_bar_but_an_instant_shot_does_not` | | Friendly-player item-visibility range limit removed | todo | not investigated this session — need to find where benilla currently gates item-row/tooltip visibility by range for a friendly player, if it does at all | | `ShowVKeyCastbar` | **registered, stored only** | pfUI writes `0` at `modules/nameplates.lua:3` ("disable original castbars") under `pcall`, and that write was one of the session log's six `unknown CVar '…' (not host-registered) — ignored` warnings. **Provenance, measured not assumed**: the name appears *nowhere* in this OctoWoW install except that one pfUI line — `grep -rli ShowVKeyCastbar` over the whole tree hits one file, and neither `WoW.exe` nor any of its eighteen DLLs (SuperWoW's included) carries it. So no mod here registers it and **there is no published default to match**; pfUI writes it defensively for whichever client does. Registered in `cvars.rs` as `ours("ShowVKeyCastbar", "0", …)` and **stored, not wired**: benilla's V-key plates have no cast bar to suppress (`benilla-ui`'s `script/nameplate.rs` header says so outright, and `vplates.rs` draws name, level, health bar and raid mark and nothing else), so `0` *is* this client's behaviour rather than a chosen value, shipping `1` would advertise a bar that does not exist, and an observer would give a knob two positions with one picture. If a plate cast bar is ever built this row becomes its switch | ### SuperWoW's own manifest, read off the DLL — the remaining work is now a closed list `scripts/dll-surface.py` finds the names a DLL points at; `SuperWoWhook.dll` goes further and **publishes its own usage strings**, in one contiguous block beside the names. So the arity and the argument *meaning* of every remaining function are measured here, not inferred from the wiki — the same trick that read `VanillaHelpers`' two blips. Three corrections came straight out of it: - **`SUPERWOW_COMBAT_LOG_EVENT` does not exist.** The DLL registers exactly three events: `UNIT_CASTEVENT`, `RAW_COMBATLOG`, `CREATE_CHATBUBBLE`. That name appears nowhere in the pool, so the row above that called its parameter schema "its own sub-project" was a sub-project for a ghost. Deleted rather than carried. - **`IsIndoors` is capital-I**, beside `IsSwimming` and `IsMounted` — not the lowercase `isIndoors` this file and `self_movement.rs` both guessed at. All three are already live off one feed (`SelfMovement`, built for ClassicAPI's `State` namespace), so that whole clause of the "needs a self-movement snapshot" row was answered before it was written. `GetSpeed` is the only posture fact still missing, and `current_speed` (`net.rs`, the reference's own `CMovement::GetCurrentSpeed 0x7c4c90`) already computes it for every unit. - **The detection pair is `SUPERWOW_STRING="SuperWoW 2.2 by Balake"` and `SUPERWOW_VERSION="2.2"`**, both quoted, assigned by a `superwowInfo.lua` the DLL carries. Ours had the string without its author clause and the version as a *number*. Fixed, and welded by a test. The usage strings, verbatim, for what is left: | Usage string, as the DLL prints it | Status | |---|---| | `CombatLogAdd("text" [, addToRawLog])` | **done** — the second argument is why this row is here: nothing in the corpus passes it, and it would have been missed | | `TrackUnit("unit")` / `UntrackUnit("unit")` | **done** — a set of **guids** in the VM (`compat/superwow.rs`), read by the minimap's blip pass as a **fourth always-show clause** beside the reference's own three (`shows_tracking_dot` in `minimap/blips/dots.rs`; `tracked_creature` stays exactly `0x5ed210`). So a tracked unit gets the ordinary tracked-unit cell, hover name and distance cull, and nothing new is drawn. Guids rather than tokens, so a tracked raid member keeps its dot while the roster shuffles its token. **Why it is a real feature and not a cosmetic one:** 1.12 draws party members on the minimap and raid members **nowhere** — `emit_party_dots` walks `party_slots()`, which is what the reference's own placement does — and pfUI puts the whole raid on it with these two, for every party and raid token on `PARTY_MEMBERS_CHANGED`/`RAID_ROSTER_UPDATE`/`PLAYER_ENTERING_WORLD` under `C.unitframes.track_group`, each inside a `pcall`. So on a client without the function the feature is simply absent, which is what it has been here. The set is read off the VM handle the blip system already holds rather than behind a resource of its own, because `BlipInputs` is at the 16-param ceiling | | `UnitPosition("unit")` | done | | `GetPlayerBuffID(buffIndex)` | done | | `UnitNameplate("unit")` | **done** — `compat/superwow.rs`, over `NamePlates::frame_for_guid`, through the **same** `plate_frame` wrapper ClassicAPI's `C_NamePlate.GetNamePlateForUnit` uses, so the two names cannot hand back different objects for one plate. Welded by a test that asserts exactly that, because an addon may feature-detect either | | `CanlootUnit("unit")` | **done** — needed no new feed: `UnitState` already carries `dead` and raw `dynamic_flags`, so it is the loot sparkle's own two-part test (`shows_loot_sparkle`: `unit_is_dead() && unit_lootable()`) read through the token map instead of the ECS, and a corpse that sparkles answers 1. Bit `0x1` is **per viewer** on the server — vmangos raises it only for players who may loot — so "can" really means can. **Note the lowercase `l`**: the *usage* string says `CanlootUnit` where the registered name is `CanLootUnit`; the registered name is what an addon calls, and the typo is in the error message only | | `GetMapPositionWorldLoc(continentIndex, zoneIndex, mapX, mapY)` | **done** — the **inverse of `GetPlayerMapPosition`**, which is the assertion the test makes rather than checking three numbers: `benilla-world`'s `map_proj::zone_world` is the reference's `0x4a7100`, whose own note is that used one axis per call it inverts the `zone_uv` that verb answers from. The law is transcribed into `benilla-ui` (which does not depend on `benilla-world`) and the round trip is welded. `mapX` is the horizontal fraction and so the world **Y** axis, `mapY` the vertical and so world **X** — `WorldMapArea`'s own convention, and why the returns are a world *loc* | | `GetMapBoundaries(continentIndex, zoneIndex)` | **done** — both indices are the pair `SetMapZoom` takes, 1-based with zone `0` the whole continent, resolved through one new `worldmap::loc_rect_at` that the hover grid's own windowing now shares, so no two readers of a selection can disagree about which rect it means. **Returns in `WorldMapArea.dbc`'s own field order** (LocLeft, LocRight, LocTop, LocBottom): upstream publishes the argument list and not the returns, and nothing in the corpus calls it, so the row's layout is the only order with evidence behind it | | `GetSpeed()` (no usage string: it takes no argument) | **done** — one new field on the posture snapshot, filled by `ui_unit.rs`'s `feed_self_movement` from `net.rs`'s `current_speed`, the reference's own `CMovement::GetCurrentSpeed 0x7c4c90` walk over our speed block and the same move flags every other field of that snapshot reads. **What could not be settled**: upstream publishes no prose and no addon in the corpus calls it, so whether its number is the *current* speed or the unit's *run* speed is unknowable from here. Current is the reading taken — it is the one client function named after speed that a hook would return, and the strictly more informative of the two (a snare or a mount shows in it). The consequence to know: standing still it is 0, exactly as that getter is | | `GetInboxItemLink(messageIndex)` | done (built for ClassicAPI's `Mail`) | | `GetTradeSkillItemLink(index[, spellLink])` | **done** — truthy, the second argument answers the **recipe's** spell link instead of the product's item link, which is the only way to link a recipe that makes no item: an **enchant**, whose `product_item` is 0 and which therefore answers nothing at all on the stock surface. pfUI knows that corner from the other side, hooking `GetCraftItemLink` to turn `spell:` links back into `enchant:` ones for players on an *old* SuperWoW (`modules/superwow.lua:48-73`, skipped once `SUPERWOW_VERSION` is set) | | `LootSlot(slot [, force])` | done — the usage string confirms the shape this file already carried | | `CastSpellByName(name [, "unit"])` | **done, both forms** — see its own row in the table above; the usage string is what named the second argument a unit **id** rather than a name | | `GetSoundDriverName(driverIndex)` | **deliberately not built** — with `GetSoundDevices`, `GetNumSoundDrivers`, `GetLoadedSoundDriver`: four getters over an audio-driver enumeration. Each one's *return shape* is unknowable from here (is a loaded driver an index or a name? is a device list a table or multiple returns?), nothing in grom's corpus calls any of them, and benilla has one backend and no enumeration to report. So all four would be guessed shapes over invented data, which is the same bargain `KEY_DOWN`/`KEY_UP` lost in the nampower section: a guessed shape an addon switches on is worse than an absent name it can feature-detect | | `GetQuestIDForLogIndex(questIndex)` / `GetQuestLinkForLogIndex(questIndex)` | done | | `GetQuestLinkForID(questID)` | **done for a quest in the log**, nil otherwise — which is also what upstream answers on the first call for one it has to fetch (`Requesting quest ID %d from server` is the DLL's own next string). The gap is one feed, not new protocol: `ui_quest_log.rs` already runs an ask-once `CMSG_QUEST_QUERY` template cache, so finishing it means a request queue out of the VM and an (id → title, level) feed back. **This row also corrected the link format for both getters**: the DLL's own formatter is `\|cffffff00\|Hquest:%d:%d\|h[%s]\|h\|r`, and ours had been built without the colour wrapper on the reasoning that it matched the stock shift-click — but 1.12 has no quest links at all, so there was no stock shape to match and the measured one is the only authority | | `ImportFile("filename")` / `ExportFile("filename","text")` | done, under nampower's fence. **Upstream's own folder is `Imports\.txt`** (two more of its literals), where ours is `benilla-config/CustomFiles/` — the deviation is written up in the file-and-credential section below. **The raw combat log is a third file literal in the same pool** (`Logs\WoWRawCombatLog.txt`), which is what `CombatLogAdd`'s second argument names and which `ui_chat/logging.rs` now keeps beside the ordinary one | **The four chat-bubble rows are now live.** `ChatBubblesWhisper`, `ChatBubblesRaid` and `ChatBubblesBattleground` are nampower's `NP_ChatBubbles*` under SuperWoW's own names, writing the same `BubbleConfig` fields — two mods, one knob, last writer wins, as the nameplate reach already is — and each keeps `0`, because the reference bubbles none of those three kinds. `ChatBubblesCreatures` is SuperWoW's alone and the only *behaviour* of the four: it splits the monster say and yell kinds out of `ChatBubbles`, which 1.12 gates them under. It ships `1`, so an untouched client bubbles them exactly as 1.12 does, and it **narrows** `ChatBubbles` rather than standing beside it — the mod hooks the reference's own gate (`0x608b0d`), and a switch added at that site can only subtract; the other reading would bubble creatures with the master switch off, which is a surprise no setting advertises. All four are welded to their struct literals in `cvars.rs`'s own defaults-mirror test, the `benilla-cvar-default-trap` discipline. **`CREATE_CHATBUBBLE` is done, and so is `C_ChatBubbles.GetAllChatBubbles`** — one piece of work closed both, plus pfUI's bubble skin. The wall was that a bubble's payload can only be the bubble and this client's bubbles were not Lua frames: they were drawn straight into the overlay quad lane, faithful on screen and unreachable from Lua. They are a **widget pool** now (`benilla-ui`'s `script/chat_bubble.rs`): anonymous `WorldFrame` children with a backdrop (`0x4b0a35` is `SetBackdrop`), a tail texture and a `FontString` — which is what the reference's own `CGChatBubbleFrame` is, and how addons of the era found bubbles before SuperWoW published the event. The app keeps everything a bubble's *existence* depends on — the spawn gate, the lifetime, the fade, the range re-test, the stacking sort and the text measurement that sizes the frame — and hands the result over per live bubble, the same split `vplates.rs` makes for the plates. Three things improved by the move rather than being preserved by it: - The bubbles now sit at frame **levels 2, 3, 4…** over the V-plates at level 1, which is the reference's own order (`0x4b12d5`-`0x4b1312`); the quad band had them *underneath*, a deviation `ui_pass::overlay_z` carried in a comment. That band is gone, and the lane is down to one tenant. - The art is loaded, tiled and bleed-inset by the UI pass like any other frame's, so `chat_bubble.rs` no longer carries its own texture resource or its own half-texel inset. - The system moved **before** the paint. It had been inside `UiQuadAppend`, the window *after* the extract — right for an appender, a frame late for a feed. **What a test had to prove, and does**: pfUI's own `ScanBubbles` loop, transcribed, finds the `FontString` with the line and its chat-type colour, blanks the one Texture region and parents its own frame to the bubble; and a bubble handed over as state comes out of the *extract* as drawn quads — both backdrop halves, the tail, and the text at the em the app measured the box with. A test that only checked the widgets would have passed with nothing on screen. And the CVars it registers, as names: `NameplateRange`, `NameplateMotion`, `ChatBubbleRange`, `ChatBubblesRaid`, `ChatBubblesBattleground`, `ChatBubblesWhisper`, `ChatBubblesCreatures`, `BackgroundSound`, `UncapSounds`, `InterfaceVolume`, `LootSparkle`, `HealingText`, `SelectionCircleStyle`. Three of the bubble four are already live under nampower's `NP_*` spellings on the same fields (`chat_bubble.rs`'s `BubbleConfig`), so those are aliases to add rather than behaviour to build — the same "two mods, one knob" the nameplate reach already is. `HealingText` carries its own description in the pool: *"Toggle display of in-world healing feedback"*. ## nampower — **two different mods share this name; this bundle ships the second** A correction worth stating plainly, because it was wrong here for most of this project's life. The original research targeted **`namreeb/nampower`**, whose entire effect is one cast-timing rule and which exposes *no Lua API at all*. That is a real mod and the rule is correctly ported — but it is not what the OctoWoW bundle runs. This install ships **pepo's nampower v2**, a much richer fork with a real Lua-facing surface: a block of `NP_*` CVars and `GetNampowerVersion()`, driven by the bundled `NampowerSettings` addon. Live addon testing is what surfaced this (the addon asked for `GetNampowerVersion`, which namreeb's mod has no concept of). Upstream has since moved again: `pepopo978/nampower` now 404s, and **`brues-code/nampower`** — same author as the ClassicAPI this bundle uses — is its live continuation and the source of every CVar default below. One consequence worth flagging: the nameplate-distance feature was attributed to SuperWoW earlier in this file on the strength of the namreeb research. pepo's v2 has its own `NP_NameplateDistance`, so **both** mods offer that knob, and both now drive the same reach here. | Area | Status | Notes | |---|---|---| | Cast-ready timing rule (namreeb's, and pepo's simplest case) | done | no Lua surface; `PendingCast::arm` in `crates/benilla-app/src/spell/inflight.rs` opens the guard from the spell's own known cast time at send, not a round trip through `SMSG_SPELL_START`; `SMSG_SPELL_DELAYED` pushback still extends it. This is the always-on version of what pepo's v2 makes configurable — see the stored-only row below for why that configurability is not wired | | `GetNampowerVersion()` | done | `script/compat/nampower.rs`, reporting `2.8.6`. The number is a claim about *surface*, not about benilla's code (same reasoning as `CLASSIC_API_VERSION`). `NampowerSettings` has exactly one version gate, `HasMinimumVersion(2, 8, 6)`, guarding exactly one setting: `NP_NameplateDistance` — which is also the one nampower CVar wired to real behaviour here. So 2.8.6 is the lowest version that still shows the only knob that genuinely works, and no higher: claiming more would assert the queue/buffer machinery of later releases (upstream documents through 4.5.0) that this client does not implement. The addon prints its own "update available" notice below its latest, which is accurate | | `NP_NameplateDistance` | **done, live** | the only `NP_*` row with a real engine reader. Drives both nameplate consumers — `nameplates.rs`'s `NameConfig::range_sq` (overhead names) and `vplates.rs`'s `VPlateMode::range_sq` (V-key plates) — i.e. the same knob SuperWoW's `NameplateRange` drives. **Two mods, one knob: last writer wins**, which is what a player moving either slider expects; neither is silently ignored. Each is clamped to the range its own mod documents: nampower 5..200 (the addon's own slider bounds in `settings.lua`), SuperWoW 10..80. Default `80` — upstream publishes *no* fixed default for this one ("Defaults to whatever was set by the game or vanilla tweaks"), so it keeps benilla's existing reach rather than inventing a number | | The other 22 `NP_*` CVars | **superseded: 20 stored only, 2 live** | registered in `cvars.rs` with the mod's own documented defaults so the settings panel reads, writes and persists correctly — and consumed by nothing. benilla implements no spell-queueing system for them to tune: we carry namreeb's simpler always-on cast rule, not pepo's queue windows, dynamic latency buffer, retry and quickcast machinery. Storing them keeps the addon's UI truthful about what the player chose; claiming they change casting would be a lie the player would feel at the keyboard. They are listed in `cvars.rs`'s `LUA_ONLY` test table (whose rule is "a registered row needs a reader, or a named reason its reader is Lua") with that reason. The rows: the six `NP_Queue*` toggles, the four `*QueueWindowMs`, the four buffer knobs (`NP_MinBufferTimeMs`, `NP_MaxBufferIncreaseMs`, `NP_NonGcdBufferTimeMs`, `NP_OnSwingBufferCooldownMs`), `NP_ChannelLatencyReductionPercentage`, and the seven behaviour toggles (`NP_DoubleCastToEndChannelEarly`, `NP_InterruptChannelsOutsideQueueWindow`, `NP_OptimizeBufferUsingPacketTimings`, `NP_PreventRightClickTargetChange`, `NP_QuickcastTargetingSpells`, `NP_ReplaceMatchingNonGcdCategory`, `NP_RetryServerRejectedSpells`). **Superseded:** `NP_SpellQueueWindowMs` and `NP_QueueSpellsOnCooldown` are now LIVE — they are the explicit queue's own two knobs; see "The queue, and the one verb that needs it" | | pepo v2's wider surface beyond this addon | todo | upstream's README documents much more than `NampowerSettings` drives — later releases add `NP_SpamProtectionEnabled`, `NP_PreventMountingWhenBuffCapped`, the greater-demon autocast pair, `NP_PreventRightClickPvPAttack`, `NP_QuickcastOnDoubleCast`, and a family of events (`SPELL_START_SELF`/`_OTHER`, `AUTO_ATTACK_SELF`/`_OTHER`, `SPELL_HEAL_BY_SELF`, `AURA_CAST_ON_SELF`…). None is registered, because nothing installed asks for them; add them when an addon does, and raise `GetNampowerVersion` to match only then | | `GetNampowerVersion()` now reports **4.0.0** — and what the gate that opens actually reaches | **supersedes the `2.8.6` row above, which is now stale as a statement of the code** | `script/compat/nampower.rs`'s `NAMPOWER_VERSION` is `(4, 0, 0)`, set at the maintainer's explicit request; that file carries the full trade. The consequence this row exists to record: the number is not only read by `NampowerSettings`. **pfUI's `libs/libdebuff.lua:38-99` gates its entire debuff pipeline on `>= 3.0.0`** — below it the library prints "Debuff tracking disabled! Please update Nampower to v3.0.0 or higher" and raises `StaticPopup_Show("LIBDEBUFF_NAMPOWER_UPDATE")`, i.e. fails **loudly**. At `4.0.0` the gate passes, so the library enables five `NP_Enable*` CVars and registers `AURA_CAST_ON_SELF`/`_OTHER` and `DEBUFF_ADDED_OTHER`/`DEBUFF_REMOVED_OTHER` — which, before the rows below, were CVars that did not exist and events nothing fired. The version claim converted a loud failure into a silent one. The rows below are the other half of that claim being made good; `GetPlayerGuid`/`IsPlayerGuid` in the ClassicAPI section are the third half | | The five `NP_Enable*` event switches | **superseded: three live, two stored only** | `cvars.rs`, all five at upstream's own documented `default 0` (`brues-code/nampower`'s `EVENTS.md` states it per row); names taken off the shipped `nampower.dll` (`strings` lists each beside its own "Set to " line). pfUI's `libs/libdebuff.lua:56-88` writes all five at `PLAYER_LOGIN`. **`NP_EnableAuraCastEvents` is LIVE**: a `CvarChanged` observer moves `ui_cast_events.rs`'s `AuraCastFeed::enabled`, and off, the packet path queues nothing at all. `NP_EnableSpellStartEvents`, `NP_EnableSpellGoEvents`, `NP_EnableAutoAttackEvents` and `NP_EnableSpellHealEvents` are **stored only**, in `cvars.rs`'s `LUA_ONLY` table with that reason: benilla emits none of those event classes (see the `todo` row below for what each would need). Registering them is what stops the engine warning; it is *not* a claim that the stream arrived, and the chat line libdebuff prints ("Enabled 5 Nampower CVars") therefore over-reports by four. `NP_EnableSpellEnergizeEvents` and the `NP_EnableUnitEvents*` family exist upstream and are not registered — nothing installed writes them . **Superseded by the rows below and by "The event table, accounted for":** `NP_EnableAuraCastEvents`, `NP_EnableSpellStartEvents` and `NP_EnableSpellGoEvents` are now LIVE, read by `NpCastGates` in `ui_cast_events.rs`; the two unit-event switches stay stored only because nothing produces those events yet | | `AURA_CAST_ON_SELF` / `AURA_CAST_ON_OTHER` | done, gated on `NP_EnableAuraCastEvents` | `crates/benilla-app/src/ui_cast_events.rs`'s `AuraCastFeed`: a second queue beside the `UNIT_CASTEVENT` one, pushed by `spell/net.rs`'s `on_spell_go` (where `Spell.dbc` is already in hand) and drained by the same VM-holding `drain_cast_edges`, behind the two cast families. Folded out of `SMSG_SPELL_GO`'s caster and **hit list** plus the spell's aura-applying effects: **one event per hit target per effect whose `EffectApplyAuraName` is nonzero**, which is upstream's own rule and the reason libdebuff dedupes inside 100 ms. The nine arguments are upstream's (`int spellId, string casterGuid, string targetGuid, int effect, int effectAuraName, int effectAmplitude, int effectMiscValue, int durationMs, int auraCapStatus`, matching the `%d%s%s%d%d%d%d%d%d` signal format in `nampower.dll`), and the shape was taken from the two live consumers' own destructures — `libs/libdebuff.lua:1105-1113` and `modules/innervatecall.lua:58-67`. **The SELF/OTHER split is by target, not caster.** GUIDs use benilla's own bare 16-hex spelling, as every other `compat/` GUID does. A ground-targeted AoE's GO carries no hit list and raises nothing: inventing the caster as the target would report Blizzard's area aura as landing on the player. `arg9` is computed from the target's `UNIT_FIELD_AURA` occupancy (bit 0 buff half full, bit 1 debuff half) | | `DEBUFF_ADDED_OTHER` / `DEBUFF_REMOVED_OTHER` | done, ungated (upstream gates neither) | `crates/benilla-app/src/ui_aura.rs`, inside the existing VM-holding `feed_auras` and after it has pushed the aura lists, so a handler that asks `C_UnitAuras` about the unit it was just told about cannot get a contradicting answer. A **delta on the `UNIT_FIELD_AURA` spell-id dwords**, driven off `net::FieldChanged` edges inside that 48-dword span — the same trigger `creature_anim::spell_visual`'s `arm_aura_state_fx` already uses — so a frame with no aura change does no work at all. First sight of a unit **seeds** the memory and fires nothing: an aura that was already on a mob we had never seen was not newly added, the descriptor keeps no caster for it, and a crowd would otherwise cost a burst of Lua dispatch on streaming in. Seven arguments, upstream's and identical across all eight buff/debuff events: `string guid, int luaSlot, int spellId, int stackCount, int auraLevel, int auraSlot, int state` — every column a field of the descriptor benilla already parses, `auraSlot` **0-based** as upstream reports it (pfUI adds 1 at `libdebuff.lua:1350`), `luaSlot` the `UnitDebuff` index through the same hidden-aura filter the aura lists use and `0` for a hidden aura, `state` 0 added / 1 removed. **Removals fire before adds** within one delta, because pfUI keys slot ownership on the raw slot and clears that key on removal (`libdebuff.lua:1507-1512`) — the other order would wipe the entry the add just wrote | | `durationMs` (`arg8`) for a combo-point or level-scaled `SpellDuration.dbc` row | todo | `ui_cast_events.rs`'s `aura_duration_ms` answers the row's `Duration[0]` when `Duration[1]` is 0, and **`0` otherwise** — including the permanent sentinel and a missing row. `0` is the consumer's own documented "absent" path. Reporting `Duration[0]` for a scaling row would quote 8 s for an 18 s five-point Rupture, and a debuff bar counting down to the wrong moment is worse than one that does not count down. What it would take: settling *which* input the row scales on (caster level vs combo points) per row, and reading the combo points as they stood at the cast rather than after the finisher consumed them. Upstream also promises "includes client modifiers if you are the caster": `spell/mods.rs` stores the server's `SPELLMOD_*` cells but has no duration reader, so this is the unmodified DBC length | | A stack-count change as a buff/debuff edge (`state` 2) | todo, deliberately | Upstream's legend has `2 = modified (stack change)` and its prose says the events fire "whenever a buff/buff **stack** … is added or removed", which reads as: a stack increase is an ADDED and a decrease a REMOVED, both with `state` 2. Not emitted here. Nothing in the corpus reads `arg7` at all; the watch span is the spell-id dwords alone, so a stack move inside a slot that keeps its spell is not an edge this sees; and under that reading a stack *decrease* would make pfUI clear a live debuff's slot ownership and its timer (`libdebuff.lua:1521-1540`), which is the wrong-timer failure this whole family exists to avoid. Costs nothing for the timers: a refresh's duration comes from `AURA_CAST`, which fires on every `SMSG_SPELL_GO`, and pfUI knows DEBUFF_ADDED does not fire on a refresh (`libdebuff.lua:1427`) | | `DEBUFF_ADDED_SELF` / `DEBUFF_REMOVED_SELF`, `BUFF_ADDED_*` / `BUFF_REMOVED_*` | **superseded: six of eight built** | The other six of upstream's eight buff/debuff events, same seven arguments. The `*_SELF` pair is the player's own half of the delta `ui_aura.rs` already computes, and the `BUFF_*` pair the positive half of the slot range it already walks past, so all six are small additions to the same loop. Not built because nothing installed registers one: pfUI's libdebuff takes only the two `*_OTHER` names . **Superseded:** all six `*_SELF` names and the two `DEBUFF_*_OTHER` names fire from `ui_aura.rs`; only `BUFF_ADDED_OTHER`/`BUFF_REMOVED_OTHER` are left, and those stay out deliberately — see "The event table, accounted for" | | `SPELL_START_SELF`/`_OTHER`, `SPELL_GO_SELF`/`_OTHER`, `AUTO_ATTACK_*`, `SPELL_HEAL_*`, `SPELL_CAST_EVENT`, `SPELL_FAILED_OTHER` | **superseded: fired** | `libs/libdebuff.lua:863-869` registers all six families, and its `libdebuff_*_hooks` broadcast surface is what feeds `libs/libpredict.lua` (HoT send tracking), `modules/swingtimer.lua` and `modules/actionbar.lua`'s prowl hook. The start/go pair's packets are already folded into `ui_cast_events.rs`'s `CastEdge::Start`/`Cast`, so most of the data is in hand — but upstream's shapes want `itemId`, `castFlags`, `spellType` and `corpseOwnerGuid` columns this client does not carry, and those consumers key on arguments it would have to guess. `AUTO_ATTACK_*` needs a melee swing edge no path here raises at all (`CastEdge`'s own note). `SPELL_HEAL_*` needs `SMSG_SPELLHEALLOG`'s amount, which nothing reads. Guessing a column here is the same silent-failure shape the four rows above were written to avoid . **Superseded:** `SPELL_START_*`, `SPELL_GO_*`, `SPELL_FAILED_*`, `SPELL_DELAYED_SELF`, `SPELL_CHANNEL_START`/`_UPDATE` fire from `ui_cast_events.rs` and `SPELL_CAST_EVENT` from `ui_self_cast.rs`. Still unfired of this row: `AUTO_ATTACK_*` and `SPELL_HEAL_*`, which are combat-log producers, not cast-queue ones — they belong with the block in "The event table, accounted for" | ## UnitXP_SP3 — confirmed (open source, multiple mirrors) | Area | Status | Notes | |---|---|---| | `UnitXP(command, ...)` dispatcher | done | `compat/unitxp.rs`. No documented detection global for this one — defining `UnitXP` at all is what an addon's `if UnitXP then` finds. **Collision note**: stock 1.12 already has its own single-argument `UnitXP(unit)` (experience points, `PLAYER_XP`); the mod shadows it on a real install exactly as a detour falls through to the original, so this dispatcher's unrecognised-command branch now carries that stock getter's exact behaviour (moved out of `script/unit/bindings.rs`, which no longer registers `UnitXP` itself — only `UnitXPMax` stayed there, untouched) | | `distanceBetween(unit1, unit2)` | done | player side: `inspect.rs`'s `UnitReach::dist_sq` (works whenever one side is `"player"`). Both-non-player side: `UnitState.position` (player/target/targettarget/npc/mouseover only, same reach as SuperWoW's `UnitPosition`) | | `target("nearestEnemy")` | done | queues `SelectionRequest::NearestEnemy` (`script/unit/mod.rs`) onto the same selection queue `TargetUnit`/`TargetLastEnemy` use, so a UnitXP target and a stock one commit through one path in call order. Handled by `target::scan::target_nearest_enemy`, which reuses `scan::best_enemy` over the **existing** candidate scan the tab-target and attack-order paths already run — so "nearest" means exactly what it does there (off-centre-plus-distance score, fighting-me bonus, on-screen first), not a second notion of nearest. Its own message+system rather than an arm in the drain: pulling the whole `TargetScan` into that shared system broke a UI-only test harness and cost +16 ambiguity pairs, the same reason `AttackNearestRequest` is already split out | | `target("nextMarkedEnemyInCycle"/"previousMarkedEnemyInCycle")` | done | `SelectionRequest::MarkCycle { forward }`, handled in the selection drain (`target/click.rs`) off `GroupState::raid_targets`, walked slot-7-to-0 so the order is skull-first as the README documents; steps from whichever marked unit is held, wrapping, and enters at the skull (forward) or star (back) end when nothing marked is held. Only marks on a streamed unit are in the cycle | | `inSight`, `target` distance-cycles/`worldBoss`/`rangeCone`, `notify` | todo | confirmed from `MarcelineVQ/UnitXP_SP3`'s own `README.md` (the authoritative command reference — more precise than this session's earlier research pass, which guessed at `AoE`/`meleeAutoAttack`/`behind`/`timer` variants this README doesn't mention; treat those as unconfirmed until seen elsewhere). `inSight` needs a real raycast with no current binding-layer access; `nextEnemyConsideringDistance`/`nextEnemyInCycle` (the 41-yd distance-banded cycles) would need a third cycle threaded through `TabHistory`, which is keyed to its own two sides; `worldBoss` needs classification data that doesn't reach here; `rangeCone` tunes the scan's own cone constant, not a Lua-settable knob today; `notify` needs OS-level taskbar-flash/system-sound | | `modernNameplateDistance` | **built** | The command's name says distance; its own checkbox says what it does — **"Proper Nameplates Occlusion"** (`UnitXP_SP3_Addon.xml:151`), with three sibling options in that panel gated on it ("Need Proper Nameplates Occlusion to be also enabled"). So it is line-of-sight culling, not reach: benilla's reach is `NameplateRange`/`NP_NameplateDistance`, which have their own rows. 1.12 draws a plate for any unit in range whether or not a wall stands between, which is why a pull through a wall shows its whole room. Stored as the new `NameplateOcclusion` CVar rather than as private state, so it persists, appears in `/console`, and reaches the engine through the one `CvarChanged` path every other nameplate knob uses. **Two readers, as the reach CVars have**: `vplates.rs`'s `VPlateMode::occlusion` (V-key plates) and `nameplates.rs`'s `NameConfig::occlusion` (overhead names), sharing one `camera_can_see` — they must agree, because a unit with a plate has no overhead name and vice versa, so culling only one would swap a unit's plate for a floating name instead of hiding it. The ray is cast from the **camera**, not the player: the plate is drawn where the camera projects it, and in third person the two origins disagree often. It aims `OCCLUSION_AIM_LIFT` above the unit's origin (a transform sits at the feet, so a ray aimed there skims the ground and hides every unit on a slope) and stops `OCCLUSION_BACKOFF` short (so the floor under the unit's feet cannot count as the thing hiding it). Against the body layer only, so a unit standing in front of another cannot hide it — geometry occlusion, as the mod's is, not depth sorting. **The return value is part of the fix**: the addon warns "UnitXP_SP3.dll failed to execute" on a falsy result (`UnitXP_SP3_Addon.lua:108-113`), and the dispatcher's fall-through answers the integer 0, which is truthy in Lua — so before this the toggle reported success and changed nothing | ## ClassicAPI — confirmed (open source, `brues-code/ClassicAPI`) | Area | Status | Notes | |---|---|---| | Lua 5.1 syntax (parsing: `#`, hex literals, long brackets, …) | done | free — benilla's VM is mlua/Lua 5.1 already; 1.12 itself is Lua 5.0 | | Lua 5.1 *stdlib* (`select`, `coroutine`, `string.reverse`/`match`/`gmatch`, `table.maxn`, …) | blocked, needs a maintainer call | `script/lua50.rs` deliberately nils every one of these so a vanilla addon's own `string.gmatch or string.gfind`-style feature test takes 1.12's real branch — that's a fidelity mechanism for *every* addon, not a ClassicAPI concern. Restoring them in `compat/` (always on) would break that test for addons that never asked for ClassicAPI. Documented in `compat/classic_api.rs`'s own header; not resolved here | | `UnitGUID`, `UnitTokenFromGUID` | done | `compat/classic_api.rs`, reusing SuperWoW's `guid_token`/`Model::unit` plumbing — a GUID from either round-trips through both | | `Enum` (`PowerType`, `ItemQuality`, `ItemClass`) | partial | `compat/classic_api/enums.rs`, pure const tables. Only the three groups the installed addons index are built; the modern `Enum` has well over a hundred groups and a constant with the wrong number fails silently at whatever consumes it, so the rest are absent rather than guessed. Values are the **modern** ones — that is what ClassicAPI backports and what addons compare against — which for these three is almost free, since the modern numbering preserved 1.12's: `PowerType.Mana`/`Rage`/`Focus`/`Energy` (0–3) compare correctly against this client's own `UnitPowerType`, and the item classes/qualities line up for every value 1.12 has. **One real collision**: power type 4 is Happiness in 1.12 and ComboPoints upstream, so `UnitPowerType("pet") == Enum.PowerType.ComboPoints` is true for a happy pet. Dropping the name would turn a wrong comparison into one against nil, and 1.12 has no combo-point power type to offer instead (combo points are `GetComboPoints()`), so both names map to 4 and a test pins it. Also carries both spellings where the modern enum renamed a value (`ItemQuality.Standard`/`Common`, `Good`/`Uncommon`) | | `UnitClassBase`, `UnitRaceBase` | done | `compat/classic_api/unit_extra.rs`. The token half of stock `UnitClass`/`UnitRace`, which later clients split into their own verb — `UnitState.class_file`/`race_file`, already fed. Named here because it is the single highest-frequency ClassicAPI call in the installed set: **33 sites in pfUI**, at file scope, so its absence stopped the addon loading outright. Players only: a creature carries no class row, and answering one would make `UnitClassBase(unit)` truthy for every mob | | `UnitSubName` | done | `compat/classic_api/unit_extra.rs`, over `UnitState.subtitle` — the `` line the overhead name and tooltip already draw | | `UnitIsMinion`, `UnitIsFeignDeath` | done | `compat/classic_api/unit_extra.rs`. `UnitIsMinion` is `UnitState.owner != 0`, the same resolution the `"owner"` grammar uses; `UnitIsFeignDeath` reads `dynamic_flags & 0x20`. Both answer `1`/nil, the 1.12 predicate shape | | `UnitPower`, `UnitPowerMax` | done | `compat/classic_api/unit_extra.rs`. The later-client spelling of `UnitMana`/`UnitManaMax`, plus an optional `powerType` — and that argument is the whole point: it reads a slot the unit is **not currently on**, which is how pfUI draws a shapeshifted druid's mana while the active slot is energy or rage (`api/unitframes.lua:1601`'s own comment names this as the reason it uses the two-argument form). 1.12 exposes no way to ask, so this needed **new state**: `UnitState::powers`/`max_powers`, all five `UNIT_FIELD_POWER1..5`/`MAXPOWER1..5` slots, fed in `ui_unit.rs` through the *same* `unit_shown_power`/`unit_shown_max_power` getters the existing active-slot pair uses — so the `UNIT_DYNFLAG_DEAD` gate and the rage/happiness display divisors apply identically and cannot drift. An out-of-range `powerType` answers the number 0, not nil: `Enum.PowerType.None` is -1 and `HealthCost` is -2, so a negative index is a real call, and pfUI's own `UnitPower(unitstr, powerType) or 0` shows a caller assuming a number. **Fidelity deviation**: `script/unit/tests.rs`'s `power_bindings_read_the_active_type` previously asserted both were nil as 1.12 verbs they are not. Updated with the reason rather than deleted, the same way `reference_surface.rs` records intentional deviations — and the assertion was replaced with a stronger one, that the new pair answers *exactly* what the stock pair does for the active slot, so `UnitMana` stays the source of truth instead of drifting from a parallel read | | Focus (`FocusUnit`, `ClearFocus`, the `"focus"`/`"focustarget"` tokens, `PLAYER_FOCUS_CHANGED`, `/focus`, `/clearfocus`) | done | the persistent secondary selection, which 1.12 has no verb, no token and no event for. `compat/classic_api/focus.rs` owns the *state*: the focus is a **GUID** in `Model::focus_guid`, the same shape as SuperWoW's `mouseover_override` and for the same reason — the token has to answer the unit's live health, power and auras, not a frozen copy of the tick `/focus` ran on. `Model::unit` resolves both tokens ahead of the fed-token map so a `FocusUnit`/`ClearFocus` in the current frame wins over the last push, and `unit/mod.rs`'s `classic_api_token_recognised` widens the token check by a `"focus"` *prefix* (so `"focustargettarget"` is recognised and answers nil, exactly as the reference's own `"target"` prefix recognises `"targettargettarget"`). The *data* is the app's: `ui_unit.rs`'s `feed_units` pushes both snapshots and fires their `UNIT_*` edges, `ui_aura.rs`'s `feed_auras` pushes both aura lists and fires `UNIT_AURA` per token, `feed_unit_reach` resolves both for `CheckInteractDistance`/UnitXP_SP3's `distanceBetween`. **No new system and no new `SystemParam`**: each feed already holds the `UiScript` and reads the guid back out of it, so the `UPDATE_ACTIONABLE_CEILING` is unchanged at 5,581 (measured). `feed_units`' gate gained the guid as an input, because focusing a unit that is standing still moves nothing else the gate watches. `PLAYER_FOCUS_CHANGED` is argless and fires only on a real change, assign and clear alike. **Deviation, by design**: a recognised token naming nothing (`/focus` with no target) clears the focus, the later clients' own behaviour; an *unrecognised* token raises `Unknown unit name` through the shared check, as every `Unit*` verb does, rather than silently dropping the focus | | `/focus` argument grammar | partial, inferred | upstream's `docs/API.md` lists `FocusUnit`/`ClearFocus` and **no slash grammar**; pfUI's `modules/focus.lua` is the only evidence the aliases exist at all ("/focus and /clearfocus live in ClassicAPI's SlashCommandsRegistry now"). Bare `/focus` is the later clients' own meaning — focus the current target — and an argument is read as a *unit token*, the only thing this client can resolve, which is also why pfUI ships its own `/focusname` for names. Registered in `benilla-app`'s `ui_script::manifest::install_focus_slash_commands`, not beside the verbs, for a mechanical reason: the stock `ChatFrame.lua:680` **assigns** `SlashCmdList = { }` and `compat/` installs before FrameXML loads, so an entry made there would be dropped — same position and same reason as SuperWoW's macro-letters patch. `SLASH_FOCUS1` and `SLASH_CLEARFOCUS1` are therefore beyond-1.12 globals that the bare-VM surface test cannot see, which is why they are *not* in `reference_surface.rs`'s exception list (`FocusUnit`/`ClearFocus` are) | | focus as an *app-side* token (`TargetUnit("focus")`, `CastSpellByName(spell, "focus")`, `/target focus`) | todo | `ui_unit.rs`'s `UnitTokens::resolve` is the app's own token resolver and is a plain `SystemParam` with no reach into the VM, where the focus guid lives, so it has no `"focus"` arm and `reach_tokens()` (which documents exactly what that resolver knows) stays unchanged. Nothing installed needs it: pfUI's `/castfocus` and `/swapfocus` both go through `TargetUnit(UnitGUID("focus"))`, a raw GUID — which is its **own** separate gap, since `UnitTokens::resolve` does not read SuperWoW's GUID-as-unit-id grammar either. Giving the resolver a focus arm means moving the guid app-side into a resource, which costs the request-pattern round trip `FocusUnit` is deliberately avoiding (`/swapfocus` is `FocusUnit("target")` then `TargetUnit(oldFocus)` in one Lua call, and a token resolved after that retarget committed would focus the wrong unit) — not guessed at here | | `UnitInRange`, `UnitInLineOfSight`, `GetUnitSpeed`, `UnitSpellHaste`, `UnitCreatedBySpell`, `UnitCreatureTypeID` | todo | each needs data no feed supplies, and a **wrong** answer here is worse than an absent one because a caller cannot tell it is wrong: `UnitInRange` is a server-authoritative 40yd party check (not a distance calculation — the real client answers from a flag, and party positions aren't even fed, see the `UnitPosition` row); `UnitInLineOfSight` needs the collision raycast UnitXP's `inSight` also waits on; `GetUnitSpeed` needs per-unit movement-state speed, only the player's is known (`self_movement`); `UnitSpellHaste` has no 1.12 analogue to read (no haste stat exists in 1.12's `UNIT_FIELD`s); `UnitCreatedBySpell` needs `UNIT_CREATED_BY_SPELL`, unfed; `UnitCreatureTypeID` needs the numeric id behind `creature_type_name`, which arrives as a localized string only. Call counts in the installed set are 1–3 each, versus `UnitClassBase`'s 33 | | Detection (`CLASSIC_API_VERSION`, `INTERFACE_VERSION`, `IsAddOnLoaded("!!!ClassicAPI")`) | deliberately not done | tried and reverted this session: a synthetic always-on addon entry is the real mechanism, but it leaked into enable-state file persistence (wrote `"!!!ClassicAPI: enabled"` into the player's own saved file) and inflated `GetNumAddOns()` for every other addon-lister — fixable, but not worth doing *before* a real namespace baseline exists per this file's own stated rule (a flag with nothing behind it is worse than no flag) | | `Time.C_Timer.After`/`NewTimer`/`NewTicker` | done | `compat/classic_api.rs`'s `timer` submodule; a `PendingTimer` row per call (`script/model.rs`), drained by `UiScript::tick` against `GetTime()`. `NewTimer`/`NewTicker` return a handle table with `:Cancel()`; a `ReloadUI`/relog (a fresh VM) drops every pending one, matching the reference's own `C_Timer` lifetime. The reusable building block (an id'd, cancellable, `GetTime()`-driven callback row) UnitXP_SP3's deferred `timer` command can reuse rather than building its own | | `RunNextFrame` | done | `compat/classic_api/timer.rs`'s `install_globals`, a zero-delay `C_Timer.After` row through the same queue — one drain, one place for a scheduling bug to live. pfUI uses it 5 times to defer work out of the handler that triggered it, and it is what broke the moment the addon's frames were driven by input. **It also exposed a real pre-existing bug in `tick.rs`'s `drain_timers`**: the drain empties `model.timers` with `mem::take` before iterating, so a timer armed from inside a callback landed in the emptied vec — and the drain then *assigned* its survivors back, discarding it. A self-rescheduling `After` chain fired exactly once and stopped dead, with nothing logged. Now merged; two tests pin it, and both were checked to fail against the old line (the chain test reported `left: 1, right: 4`) | | `SetShown` on region leaves | done | `compat/classic_api/frame.rs`. It was Frame-only; pfUI calls it on a **FontString** (`f.group:SetShown(...)`, `api/unitframes.lua:582`). Now installed on all three method tables through the same one-line composition, which is sound because a region does have `Show`/`Hide` of its own: `script::REGION_LEAF_SHARED` registers the visibility four onto each leaf separately (Texture `0x79b580`, FontString `0x79cb70`) rather than hoisting them to the Region map, exactly as the reference does. A test pins that the pair the composition needs is actually on the leaf, so a tidying pass that moves them breaks the test rather than pfUI | | `ObjectPoolMixin`, `CreateObjectPool`, `TexturePoolMixin`, `CreateTexturePool`, `TexturePool_HideAndClearAnchors` | done | `compat/classic_api/pools.rs`, the upstream's own structure as a Lua chunk over the existing `CreateFromMixins`. 1.12 has no pooling at all. **Load-blocking**: pfUI builds its tooltip buff-icon pool at module scope (`modules/tooltip.lua:217`), so the missing global aborted the rest of that module — which is also what left `pfUI.tooltip:Update` undefined and made the tooltip raise on hover. One missing global, two reported errors. The shape worth knowing: `EnumerateActive` returns `pairs(activeObjects)` and that table is keyed **by the object**, so `for icon in pool:EnumerateActive()` binds the object; a version storing objects as array values breaks that loop silently, and a test pins it. `table.getn` not `#` — `#` genuinely does not parse in this VM. `CreateFramePool`/`CreateFontStringPool` unbuilt, nothing calls them | | `Totem`: slot constants + `GetTotemInfo` | partial (constants **inferred**) | `compat/classic_api/totem.rs`. **This deliberately reverses `misc.rs`'s earlier "not built" decision**, not because totem data appeared but because the absence turned out to cost most of pfUI's UI: it keys a table with the four slot constants at module scope, and `pfUI.lua:391` loops its modules with **no `pcall`**, so `table index is nil` aborted the loop and every later module plus the whole skins pass. `GetTotemInfo` answers **no values** — right for every non-shaman and for a shaman with nothing down, the only answer this client has data for, and all three pfUI call sites already guard it. Still wrong for a shaman with totems out, which is this row. `MAX_TOTEMS`/the four slots are the standard Blizzard values but unverified against ClassicAPI source; blast radius is bounded because `GetTotemInfo` always says "none", so the slot numbers only decide which of four colours an empty bar slot takes. `GetTotemTimeLeft`/`GetTotemDuration`/`TargetTotem` unbuilt — unreachable rather than broken (pfUI calls `TargetTotem` only from a right-click on an always-empty bar) | | `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.) | | `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 | | colour getters return a `ColorMixin`, not a tuple | fixed | `C_UnitAuras.GetAuraDispelTypeColor` returned `(r, g, b)`, but pfUI's line is `GetAuraDispelTypeColor(dtype or ""):GetRGBA()` (`api/unitframes.lua:1778`) — upstream's return *is* an object, so a tuple raises rather than merely reading awkwardly. Both it and `C_ClassColor.GetClassColor` now build through the real `CreateColor` (`color_mixin.rs`'s `color_object`), which fetches the global at call time rather than holding a `Function` handle, so install order inside this directory stays irrelevant. `GetClassColor` previously hand-rolled a shell carrying two formatters — enough for the `:WrapTextInColorCode` calls pfUI makes, latent for `:GetRGBA()`. Building a fresh object also settles that shell's own stated objection to a mixin (mutating a shared class colour would edit FrameXML's row): the `r`/`g`/`b` are copies, pinned by test | | State (bare globals) | partial | `compat/classic_api/state.rs` + new per-tick posture plumbing: `SelfMovement` (`script/model.rs`), `UiScript::set_self_movement` (`script/self_movement.rs`), fed by `ui_unit::feed_self_movement` from the mover's movement flags (swim, fall), the descriptor (mount display id, `UNIT_FIELD_BYTES_2` form byte) and the area pass's own indoor down-ray. 1.12's Lua can reach none of these at all, so this is new engine data, not a wrapper. Done: `IsSwimming`, `IsMounted`, `IsIndoors`, `IsOutdoors`, `IsFalling`, `GetShapeshiftFormID`, and `IsInGroup`/`IsInRaid` (read the party/raid roster, the same authority `GetNumPartyMembers`/`GetNumRaidMembers` answer from — not duplicated into the posture snapshot). Not built, each for a stated reason in that file's header: `IsStealthed`/`GetSheathState`/`IsAssistingRitual` (no engine state tracks any of the three), `GetMirrorTimerInfo`/`GetMirrorTimerProgress` (benilla's mirror timers are event-only — `ui_mirror.rs` fires `MIRROR_TIMER_START`/`_PAUSE`/`_STOP` and keeps no queryable snapshot), `Dismount`/`CancelShapeshiftForm` (actions; vanilla cancels a form by re-casting its spell, and the engine knows the form *index*, not the spell behind it). Cost: `UPDATE_ACTIONABLE_CEILING` +3, cited in `game_plugins.rs`. **This same plumbing unblocks SuperWoW's own lowercase-initial `IsSwimming`/`IsMounted`/`isIndoors` siblings** (its "New functions" row above) — a later pass can wire those to `SelfMovement` directly instead of building anything | | A **non-player** aura's `duration` / `expirationTime` | done | `crates/benilla-app/src/ui_aura.rs`'s `OtherAuraDurations`: a `(unit guid, spell id)` table stamped by `spell/net.rs`'s `on_spell_go`, joined into every other unit's aura list at the feed. **This was the hole behind "no debuff timers on the target frame".** pfUI-for-ClassicAPI reads its unit-frame timers straight off `C_UnitAuras.UnitAuraBySlot`'s `duration`/`expirationTime` (`api/unitframes.lua:1680,1788`) rather than through libdebuff — its own header says the plumbing "now runs on ClassicAPI's C_UnitAuras (which provides sourceUnit/sourceGUID and **non-player expirationTime**)" and libdebuff's `UnitDebuff`/`UnitOwnDebuff` survive only as adapters over it. So libdebuff could be tracking perfectly (it was: `libdebuff_all_auras` held rows) and the icons still draw no sweep, because the number they read came from us and we answered 0. There is no packet to answer from: `SMSG_UPDATE_AURA_DURATION` is self-only and `SMSG_SET_EXTRA_AURA_INFO`, which carries a caster's durations on others, is a 2.0 opcode. It is learned instead from the GO that applies the aura — which this client sees natively, with the apply instant being the packet's own arrival rather than a combat-log line to parse. Length comes from the same `ui_cast_events::aura_duration_ms` behind nampower's `arg8` and `GetSpellDuration`, so the three cannot disagree, and its `0` (permanent, no row, or a combo-point/level-scaled row — the `todo` row above) stamps nothing, leaving the icon timerless rather than counting to the wrong moment. The join is driven by the unit's own `UNIT_FIELD_AURA`, so a stamp for an aura the target resisted is never read, and a stamp that has run out reports nothing rather than a negative remainder. Not covered, by construction: an aura already on a unit before we streamed it, and one applied out of our range — no GO, no timer, which is what the mods this stands in for also do | | UnitAuras (`C_UnitAuras`) | done | `compat/classic_api/unit_auras.rs`, modern named-table/slot shape over the same `AuraState` list the stock `UnitBuff`/`UnitDebuff`/`GetPlayerBuff` already read (including this session's SuperWoW spell-id extension). `GetAuraDataByIndex`, `GetBuffDataByIndex`, `GetDebuffDataByIndex`, `GetAuraDataBySlot`, `UnitAuraBySlot`, `GetAuraSlots`, `GetUnitAuras`, `GetUnitAuraBySpellID`, `GetPlayerAuraBySpellID`, `GetAuraDataBySpellName`, `GetAuraDispelTypeColor`, and the positional `UnitAura`/`UnitBuff`/`UnitDebuff` siblings. `RegisterComboDuration`/`RegisterAuraDurationModifierByTrigger` accept and discard: they are addon-side registration hooks with no 1.12 data behind them and nothing here would ever read the registration | | Spell (`C_Spell`) | partial | `compat/classic_api/spell.rs`, spell-id-first: `DoesSpellExist`, `GetSpellName`, `GetSpellTexture`, `GetSpellSubtext`, `GetSpellInfo` (named table), `IsSpellPassive`, `IsSpellKnown`, `IsCurrentSpell`, `GetSpellCooldown`, `IsSpellUsable`, `GetSchoolString` (+ the seven `SCHOOL_MASK_*` constants), `CanDualWield`, `CastAtCursor`, `CastAtUnit`, `CancelSpellByID`. **The five display getters answer for any shipped spell**, not just the player's own ~200: `script/spellbook.rs`'s `spell_display` reads the book first (it carries the live icon substitutions) and falls back to the host's `Spell.dbc` probe for every other id — the fix for pfUI's `api/unitframes.lua:42`, which keys a predict table on `strlower(GetSpellTexture(774))` at file scope on every class. The probe carries only name/rank/icon, so the predicates below it (`IsSpellPassive`, `IsSpellKnown`, `IsCurrentSpell`, `GetSpellCooldown`, `IsSpellUsable`) still read the book alone, which is the right source for all five. The rest of the namespace's ~30 functions (`GetSpellDescription`, `GetSpellPowerCost`, `GetSpellReagents`, `GetSpellEffectInfo`, the range/harm/helpful predicates, `UnitCastingInfo`/`UnitChannelInfo`, `GetSpellLevelInfo`, …) are unstarted: each needs a `SpellDisplay` column the probe does not carry, and widening `SpellLookup` field by field is the route, with a reason each time | | Spell — the per-unit cast feed (`C_Spell.UnitCastingInfo`, `UnitChannelInfo`, `CastingInfo`, `ChannelInfo`) | partial — **the feed is built; the `UNIT_SPELLCAST_*` events that drive pfUI's bars are not** | Split out of the `C_Spell` row above rather than folded into it, because this half is a live feed rather than a getter over data already in hand, and because what it does *not* yet reach is worth a row of its own. This closes four of the "ClassicAPI tail" row's 46 measured members — the three it filed under "needs a live feed" for cast state, plus `CastingInfo`, which upstream lists in the same breath (`docs/classic-api-functions.md`, the Spell section) and which is `UnitCastingInfo("player")` exactly. **Built**: `compat/classic_api/spell.rs`'s `cast_info` over `Model::unit_cast`, fed from `benilla-app`'s `ui_cast_events.rs` ([`CastWindows`]), which folds the **same queue of wire edges** that already raises SuperWoW's `UNIT_CASTEVENT` — `SMSG_SPELL_START`, `SMSG_SPELL_GO`, `SMSG_SPELL_FAILED_OTHER`, `MSG_CHANNEL_START`, plus three state-only edges added here (`SMSG_SPELL_DELAYED` pushback, `MSG_CHANNEL_UPDATE`, and `inflight.rs`'s local Esc/move cancel, none of which is one of the wiki's six `UNIT_CASTEVENT` words, so each folds state and fires nothing). One queue, not two: an addon woken by `UNIT_CASTEVENT` polls `C_Spell` for the timing the event does not carry (pfUI, `modules/nameplates.lua:648-656`), and each row's state is pushed to the VM *before* its own event fires so the two cannot contradict each other — `the_event_and_the_window_agree_inside_the_handler_the_event_woke` is that property, and it caught a real one-tick-stale bug on the way in. **Keyed by caster GUID, deliberately not a `UnitState` field**: tokens are a naming layer that moves under units, and a cast carried on `"target"`'s snapshot would hand the next target the old one's half-finished bar. A read folds token -> GUID through `Model::unit`, so every token spelling this client answers (stock, a raw SuperWoW GUID string, `"mark"`, `"owner"`, ClassicAPI's `"focus"`, `nameplate`) reads the right window for free. **Units**: `startMs`/`endMs` are **milliseconds** on `GetTime()`'s clock, stamped at the drain — the same place and frame `ui_cast.rs` stamps the stock bar's `SPELLCAST_START`, so pfUI's bar and `CastingBarFrame` start on the same millisecond. The two verbs are disjoint (a channel answers only `UnitChannelInfo`), and the shapes differ by one field: `castID` exists for a cast and not for a channel, so `spellID` is the 9th return and the 8th respectively — both of pfUI's destructurings (`modules/nameplates.lua:83`/`:86`) are transcribed verbatim as the test, in `script/tests/cast_info.rs`, because a positional shape is only correct relative to the code that reads it. **Not backed, left nil rather than guessed** (each with its reason in `cast_info`'s doc): `isTradeskill` — 1.12 has no tradeskill column in `Spell.dbc` and no wire bit, so pfUI's queued-craft bar merge simply does not engage; `castID` — 1.12 carries no per-cast identifier anywhere; `notInterruptible` — no such flag exists in the client, in `Spell.dbc` or on the wire, and nil reads as false, which is what every 1.12 cast is; return slot 10 of the cast form — upstream puts something there (pfUI skips exactly one slot to reach `delayMs` at 11) and nothing in this tree says what. `text` (slot 2) answers as `name`; the one per-cast string the reference does spell differently is the stock channel bar's `CHANNELING` word, which is an argument of `SPELLCAST_CHANNEL_START` for the local player, not a per-unit field, and no consumer reads slot 2. `delayMs` (slot 11) **is** backed, from the accumulated `SMSG_SPELL_DELAYED` total. **Another unit's channel is not reachable** and is reported as nothing rather than faked: `MSG_CHANNEL_START`/`UPDATE` carry no caster and vmangos sends them to the caster alone, so only the local player's channel has a window. `UNIT_FIELD_CHANNEL_SPELL` (field 144, public) does name *which* spell another unit is channelling, but carries no start and no length, and a bar with an invented timer is worse than no bar — that field plus a `SpellDuration.dbc` column, with the 0 -> id edge as the start and an explicit nothing for a channel already running when the unit streamed in, is the measured route if it is ever wanted. **What this does not finish**: pfUI's cast bars are driven by ClassicAPI's `UNIT_SPELLCAST_*` token events, which benilla fires none of (the row below costs the per-`nameplate` half; the stock `player`/`target`/`focus` half is equally unbuilt). Without them `modules/castbar.lua` stamps a bar only from its `PLAYER_TARGET_CHANGED`/`PLAYER_FOCUS_CHANGED` re-poll (`:340`) — so **a mob already mid-cast when you target it does now show a correct, correctly-timed bar, and one that starts casting while already targeted does not**; the player's own `pfPlayerCastbar` never stamps at all, since `arg1 == "player"` events are its only way in. Nameplates are the same shape: `modules/nameplates.lua:602` seeds `castState[guid]` from `PollCastInfo` at `NAME_PLATE_UNIT_ADDED`, so a plate that spawns on a casting mob draws its bar, while `:655`'s per-cast seed waits on the event. The feed was the blocking half (the row below records that an event with nothing to read behind it "would fire into a handler that learns nothing"); the events are now the whole remainder, and `C_Spell` is no longer what blocks them | | Spell — the `UNIT_SPELLCAST_*` token events | done | **The other half of the row above, and the half pfUI's cast bars are actually driven by.** The feed answered *what* a unit is casting; nothing told an addon *when*, so `modules/castbar.lua` could only stamp a bar from its `PLAYER_TARGET_CHANGED`/`PLAYER_FOCUS_CHANGED` re-poll and `pfPlayerCastbar` never stamped at all. Built in `benilla-ui`'s new `script/cast_events.rs` (`UiScript::fire_unit_spellcast`, the fan-out and the arguments) plus `benilla-app`'s `ui_cast_events.rs` (`CastEdge::token_events`, which edge raises which event), fired by the existing `drain_cast_edges` off the **same queue of wire edges** that already raises `UNIT_CASTEVENT` and folds the windows — so an addon woken by an event and polling `C_Spell` in that handler cannot be told two different things. **Built: nine events.** `_START` (`SMSG_SPELL_START`), `_CHANNEL_START` (`MSG_CHANNEL_START`), `_SUCCEEDED` + `_STOP` (`SMSG_SPELL_GO`), `_INTERRUPTED` + `_STOP` (`SMSG_SPELL_FAILED_OTHER`, and the local Esc/move abort), `_FAILED` + `_STOP` (a failing `SMSG_CAST_RESULT`), `_DELAYED` (`SMSG_SPELL_DELAYED`), `_CHANNEL_UPDATE` and `_CHANNEL_STOP` (`MSG_CHANNEL_UPDATE`, split on its remaining time at 0). **Each is the stock bar's own word for the same packet** — the mapping is `ui_cast.rs`'s `CastBarEdge` one for one, which is the strongest available statement that `pfPlayerCastbar` and `CastingBarFrame` cannot tell two stories about one cast; `every_token_event_is_the_stock_bars_own_word_for_the_same_packet` is that table. The millisecond claim is the two drains' shared frame clock: both are in `UnitFeed`, both stamp off `script.now()`, and `the_players_bar_walks_start_pushback_and_stop_in_step_with_the_stock_bar` interleaves `SPELLCAST_START@100.000` with pfUI's own `startTime` of `100000` to show it. **`arg1..arg5` are `(unit, castGUID, spellID, name, rank)`**, taken from the consumer's own comment (`modules/castbar.lua:354`) and its reads (`arg5` → the bar's rank label at `:357`/`:102-107`, `arg3` → the craft counter at `:370`; the nameplate module reads `arg1` alone behind `strfind(arg1, "^nameplate")`). `arg2` is **nil** — 1.12 has no per-cast identifier anywhere, the same reason `UnitCastingInfo`'s `castID` is nil, and the two must agree. Name and rank come from the one `spell_display` body `C_Spell.GetSpellName`/`GetSpellSubtext` read, because pfUI *prefers* `arg5` over its own `GetSpellSubtext` fallback and two sources would print two ranks. All five slots are pushed on every fire, nil included: the handler is called positionally too, so a short push is a shorter event to anything reading `...`. **The per-token/per-GUID mismatch, which is the whole design.** A cast is per-GUID; these events are per-token; one mob can be `target` *and* `nameplate3` *and* `focus` at once and each drives a different frame, so a new `Model::tokens_for_guid` answers **every** live token and the fan-out fires once per token — `token_for_guid` (`UnitTokenFromGUID`'s singular) returns whichever the map walk reaches first, and firing under that one leaves whichever bar lost the coin flip permanently blank (`one_cast_fires_once_per_live_token_that_names_its_caster` fails with exactly one token when collapsed). The set is the **pushed token keys** only — `player`, `target`, `targettarget`, the focus pair, `mouseover`, `npc`, party/raid/pet, `nameplate` and its target — deliberately not the derived spellings `Model::unit` also resolves (a raw 16-hex GUID, `mark`, `owner`): those are how a *caller* spells a unit it already holds, nothing registers a handler keyed on one, and fanning every edge across `mark1..8` plus an `owner` form per base would multiply the dispatch for no reader. Tokens resolve at **fire** time, not start time, which is what makes token loss safe: retarget off a casting mob and its `STOP` has no `target` to fire under, so the new target's bar survives the old cast ending. **Nothing fires on token *acquisition*** — pfUI already covers that itself (`castbar.lua:343`'s re-poll, `nameplates.lua:602`'s `NAME_PLATE_UNIT_ADDED` seed), so a synthetic start there would both duplicate it and be an event with no wire edge behind it. **Two orderings are read off the consumer, not guessed.** (1) A failing word always comes **before** its `STOP`: `ClearBar(cb, failed)` paints the cancelled-cast red flash only if `wasActive = cb.endTime ~= nil` (`:25`), and a plain `STOP` first nils `endTime` so the flash never paints — reversing the pair makes `failcolor` nil, verified. Both must fire, because the nameplate module registers `_STOP` and *not* the failing pair (`nameplates.lua:506-507`) and that is its only way to clear `castState[guid]`; without it an interrupted mob's plate bar runs to the cast's original end. (2) Events are **event-major, not token-major**, so when one unit answers two of a consumer's tokens (self-targeted: `player` and `target`, and pfUI's target bar handles both through its `UnitIsUnit(unit, "player")` leg) the failing word reaches that bar before any `STOP`. **Gating: `folded` — the window fold's own return — for every edge**, so events and windows are one state machine. A `START` fires exactly when a window opened, which means `Start { window: false }` (`opens_cast_bar`'s verdict, the *same* body `spell_start` uses for `SPELLCAST_START`) raises no token start, so Auto Shot's mid-Aimed-Shot `SMSG_SPELL_START` neither opens a bar nor reaps one; a terminal fires exactly when a window closed, which de-duplicates the `SMSG_CAST_RESULT`/`SMSG_SPELL_FAILED_OTHER` pair and the abort/FAILED_OTHER pair for free; and a pre-start rejection naming some *other* spell folds nothing, so it cannot reach in and wipe the bar of the cast actually running (pfUI's `ClearBar` has no way to tell which spell an event is about). **Two pre-existing window bugs this surfaced and closed.** (a) A failing `SMSG_CAST_RESULT` pushed no edge at all, so a cast the server killed with nothing but that packet — a mid-cast deselect, vmangos `HandleSetSelectionOpcode` → `Spell::cancel` → `SendCastResult(INTERRUPTED)` — kept reporting as live to its original end: a bar that starts and never ends. Now `CastEdge::ResultFail`, queued inside `cast_result` beside the `CastBarEdge::Failed` it mirrors. It raises **no** `UNIT_CASTEVENT`: that row sources FAIL from `SMSG_SPELL_FAILED_OTHER` alone, both packets reach us for one interrupted cast of our own, and doubling a word an addon counts to answer an unsettled question about the real hook would be worse than leaving it. (b) `prune` ran unconditionally before the rows, and a completed cast's `SMSG_SPELL_GO` lands within a frame of its window's own end *either side of it* (the end is `our START receipt + cast time`, the GO is sent at `server START + cast time`), so the GO found nothing to close and `_STOP` fired on a coin flip. `CastEventFeed::reprieve` now spares a window whose resolution is already in this tick's queue, keyed on the spell id like every other removal here, so the expiry sweep still means exactly what it says — a resolution that never came. **Not built, each with its reason.** `UNIT_SPELLCAST_SENT` — it fires at the client's own send, not off a packet, and no addon in the corpus registers it: a new producer in `cast_send.rs` for no reader. No `_STOP` for a window the expiry sweep *does* take (caster despawned, packet for a unit that left streaming range): both consumers end a bar at its own nominal end with no event (`castbar.lua:286-289`, `nameplates.lua:106`), and such a caster is typically named by no token to fire under anyway. No `_CHANNEL_STOP` when a cast `START` replaces a live channel — pfUI's `START` arm re-stamps and its nameplate arm overwrites, so the transition needs no announcement. `_SUCCEEDED` is fired for conformance, not for an effect that can be shown here: its only consumer is pfUI's craft counter, reachable only through `isTradeskill`, which 1.12 has no column for and the feed answers nil for by design (`merged` is asserted nil in the timing test). One **measured divergence left open**: the stock channel bar applies one gate the window does not (`0x6e7550`, `AttributesEx3 & 0x2000`), and it is unreachable — `benilla-formats`' `spells/display.rs:363-367` records that exactly two spells carry the bit, Blood Siphon 24322/24323, neither player-castable, and `MSG_CHANNEL_START` reaches us for the local player's own channel alone. The other arm of that gate (no `Spell.dbc` row) is invisible rather than divergent: a window whose id nothing can name makes `cast_info` answer nil, so no bar is drawn either way. **This also closes the cast half of the per-`nameplate` row below**, which recorded the cast pair as blocked on `C_Spell.UnitCastingInfo`; that is built, the plate tokens do now receive `_START`/`_CHANNEL_START`/`_STOP`/`_CHANNEL_STOP`, and `a_nameplates_token_gets_the_cast_pair_its_module_registers_and_the_stop_that_clears_it` runs that module's transcribed arms. `UNIT_AURA`/`UNIT_FLAGS` per plate stay as that row leaves them. **Evidence**: `benilla-ui`'s `script/tests/cast_info.rs` for the fan-out and the five arguments, and `ui_cast_events.rs`'s own tests for the sequences — the latter transcribe pfUI's `ClearBar`/`StampBar`/`RefreshBar`/`OnUpdate` and its whole `OnEvent` body (`castbar.lua:24-39`, `:89-144`, `:148-175`, `:250-306`, `:340-420`) verbatim and run them against the real fold with both drains in one app, because an event's contract is only correct relative to the code that handles it. **Ambiguity cost: zero, measured.** `Update (actionable)` is 5581 against a ceiling of 5581, unmoved, and `PostUpdate` 466 — no new system (the work is inside the existing VM-holding `drain_cast_edges`, which `WOW_AMBIGUITY_DUMP=1` on the ratchet test, grepped for `drain_cast_edges`, puts at 0 pairs, being wholly in the VM-only explained bucket), no new `SystemParam` on it (the token set and `spell_display` are both reached through `UiScript`, on the far side of the boundary), and the one new app-side param — `Reply`'s `ResMut` — is on `on_cast_result`, a `net_handler` one-shot that is not in `Update` at all (the same dump greps to 0 for it too). No new `benilla-app` file, so `ui_script_wall`'s game count stays at its pre-existing 43. No new bare Lua globals, so `reference_surface.rs` is untouched; the nine event names are modern and absent from `reference/1.12-events.tsv`, so `event_shape_gate` skips them as it does `UNIT_CASTEVENT` and the `NAME_PLATE_*` three. Harness unchanged at 13/13 load clean, 13/13 session start, 13/13 UI probe, 10/13 survived use — it drives no world, so no cast packet ever arrives and it cannot see this feature at all. | | SpellBook (`C_SpellBook`) | partial | `compat/classic_api/spellbook.rs`: `FindSpellBookSlotByID`, `GetSpellBookItemInfo` (named table), `GetNumSpellBookSkillLines`, `GetSpellBookSkillLineInfo`, `GetSpellBookItemSkillLineIndex`. The remaining ~14 (`GetSkillLineIndexByID`, `GetSpellBookItemCastCount`, the loss-of-control pair, `IsClassTalentSpellBookItem`, `ContainsAnyDisenchantSpell`, `GetSpellLevelLearned`, `GetCurrentLevelSpells`, `GetPlayerSpellsByAura`, the skill-line name/rank getters, the auto-attack predicates) are unstarted; some have no 1.12 equivalent concept at all (modern "class talent" vs 1.12's talent tabs, loss-of-control as distinct from 1.12's CC) | | ColorUtil (`C_ColorUtil`) | done | `compat/classic_api/color_util.rs`, all 8. The only namespace here with nothing to wrap: pure colour maths (RGB↔HSV↔HSL, achromatic input answering the upstream's own `h == -1` sentinel) plus the `|cAARRGGBB…|r` escape builders, which is 1.12's own format | | ClassColor (`C_ClassColor`) | done | `compat/classic_api/class_color.rs`. Reads FrameXML's own `RAID_CLASS_COLORS` (`Fonts.xml`, off the player's chain) at call time rather than carrying a second copy of the nine colours that could drift from it; nil before FrameXML loads and for an unknown token. The returned table is the `r`/`g`/`b` fields every caller reads plus `GenerateHexColor`/`WrapTextInColorCode` — the two `ColorMixin` methods that are pure formatting over those fields. The mutating rest of `ColorMixin` (`SetRGBA`, `IsEqualTo`, …) is deliberately absent: a caller mutating a shared class colour would be editing FrameXML's own table | | Expansion | done | `compat/classic_api/expansion.rs`: `GetClassicExpansionLevel` (0, `LE_EXPANSION_CLASSIC`) and the two comparisons. Constants by design — a fork targeting a later era changes the one constant, not three functions | | Input | partial | `compat/classic_api/input.rs`: the six `Is{Left,Right}{Shift,Control,Alt}KeyDown` plus `IsModifierKeyDown`. **Each side of a pair answers the combined bit**: `Model::modifiers` is one `(shift, ctrl, alt)` triple (what the stock sideless `IsShiftKeyDown` family reads) and 1.12 exposes no sided getter, so there is no sided state to report — which is what ClassicAPI itself does on a client that cannot tell them apart, rather than fabricating `false` for one half. Not built: `IsMouseButtonDown` (the only press state is `Model::mouse_down_on`, which the pointer layer fills *only* for a press that landed on a frame — a world press would read as not-down, wrong more often than right) and `GetMouseButtonClicked` (wants the button that triggered the running handler; dispatch passes it to `OnClick` as an argument and keeps no ambient current-event button) | | Lua | partial, by design | `compat/classic_api/lua_ext.rs`: `table.count`, `Mixin`, `CreateFromMixins`, `strreplace`. Only names 1.12 has in neither library nor global, so nothing `lua50.rs` nils gets un-nilled and no addon's `string.gmatch or string.gfind` feature test changes answer — the stdlib-restore question stays the maintainer's call (that file's header, and this table's own Lua-5.1-stdlib row above). A test in that file asserts `select`/`coroutine`/`string.reverse`/`string.gmatch`/`table.maxn` are all still nil, plus `strrev` — which ClassicAPI does provide, but `tests/stdlib.rs`'s `the_rest_of_the_bare_globals` already pins nil for the same era-feature-detection reason, so adding it is that same maintainer call rather than a separate one — so a later pass can't restore any of them by accident. `strsplit`/`strjoin`/`trim`/`wipe` already exist as benilla globals for unrelated TurtleWoW-FrameXML reasons and are not re-registered | | Currency (`C_CurrencyInfo`) | done, delegating | `compat/classic_api/misc.rs`. `GetCoinTextureString` is a *FrameXML* function in 1.12 (`MoneyFrame.lua`), loaded off the player's chain after this installs — so the bare global is left to FrameXML (a Rust copy would be both a second source of truth and dead, since FrameXML's would overwrite it) and `C_CurrencyInfo.GetCoinTextureString` reads that global at call time, forwarding the optional font height. Answers nil before FrameXML has loaded | | Events | partial | `compat/classic_api/misc.rs`. `GetFramesRegisteredForEvent` is real: the registry's own list in registration order, which is also the order `fire_event` walks, and an empty table (never nil) for an unregistered event so `getn` is always safe. `C_EventUtils` exists but is **empty** — `IsEventValid` has no authority to answer from: `RegisterEvent` accepts any string (`script/event.rs`) and the only 1.12 event catalogue is `reference/1.12-events.tsv`, a test fixture that isn't compiled in. Answering `true` for everything would be a lie an addon branches on | | System | partial | `compat/classic_api/misc.rs`. `GetPhysicalScreenSize` answers the UI screen rect's size, with a stated deviation: modern WoW returns true *device* pixels, where this is the logical rect the host pushes through `set_screen_size` — the two differ under a non-1 UI scale or DPI factor, which this crate cannot see. `CopyToClipboard` not built: `benilla-ui` is deliberately engine-free (no windowing, no OS handles) and a clipboard is an OS capability; the workspace's `arboard` sits behind the app's dev tooling, so reaching it means a new host-callback seam through `Model`, which is plumbing rather than a binding | | Combat | partial | `compat/classic_api/misc.rs`. `InCombatLockdown()` → always `false`, and that is the honest answer rather than a stub: the lockdown exists on modern clients to refuse secure-frame edits mid-combat and 1.12 has no secure-template system at all, so nothing here is ever locked down. `StartAttack`/`StopAttack` deliberately not added here — they are 1.12's *own* verbs that benilla hasn't built yet, so registering them from `compat/` would put a stock gap in the wrong layer and hide it from the reference-surface accounting | | Class, Totem, Tracking, Instance | todo, blocked | `FillLocalizedClassList`: the nine localized class names are DBC-resolved app-side (`benilla-app`'s `ui_unit::class_names`) and `benilla-ui` deliberately holds no DBC — the player's own class reaches it through `PlayerRecord`, but not all nine, so this needs a feed like `SelfMovement`'s, not a binding. `Totem` (4 functions): no totem state exists in the model at all; 1.12's own totem bar is unbuilt, so there is nothing to wrap. `Tracking` (3): `Model::tracking` is the one *active* tracking aura (behind the stock `GetTrackingTexture`), not the enumerable list of known tracking spells these index into. `GetInstanceInfo`: `script/instance.rs` feeds only the instance *type*; the modern tuple also wants the instance name, difficulty index and name, and max players — none tracked, and the name would have to be guessed from zone text | | Hooks (`hooksecurefunc`) | done | `compat/classic_api/hooks.rs`, both documented forms (over a global, over a table field). Pure Lua machinery, no game state. The contract it keeps, each covered by its own test: the original runs first with the caller's arguments; **the original's return values pass through untouched** and the hook's are discarded; a hook that errors has its error recorded (`Model::record_script_error`) and the original's returns still come back, while the *original's* own error still propagates; and hooking twice **chains** in install order rather than replacing. A name holding no function is a silent no-op — the reference raises, but an addon written against a client that has the verb it meant to hook is the common case here and taking its UI down over a missing one would be worse. The original and hook live in the Lua registry, since this crate holds no persistent `Function` handle | | Frame (widget methods) | partial | `compat/classic_api/frame.rs`. These are **methods, not globals**, written straight into the stock class method tables (`__benilla_frame_methods` and the two region leaves) — `object.rs`'s `frame_meta_for` *links* those tables rather than copying them ("a later write to a table shows through"), so a late install from `compat/` reaches every widget already made and every one made later. Built: `SetShown`, `GetSize`/`SetSize`, `GetRect`, `SetFormattedText` (FontString), `HookScript`, `IsEventRegistered`, `GetEffectiveAlpha`, and `SetAttribute`/`SetAttributeNoHandler`/`GetAttribute`/`ClearAttribute`. The geometry/visibility/text ones are composed from the stock verbs as a Lua chunk, so they reuse the stock resolution exactly instead of duplicating it in Rust — note the chunks return a function rather than reading `...`, since this VM is 1.12's Lua **5.0** where `...` is a parameter-list token only, never an expression. Skipped, each with the reason in that file's header: the **mask** and **atlas** families (benilla's UI quad carries one texture and a UV rect — no mask slot, no atlas registry; needs a render-path feature first), the **secure-template** family (`RegisterStateDriver`, `RegisterUnitWatch`, `SecureCmdOptionParse`, `SecureButton_*`, … — modern restricted execution, which 1.12 has no equivalent of; `SetAttribute`/`GetAttribute` ARE built because stripped of secure semantics they are just a per-frame key/value store, and that much is honest, but the drivers that *watch* attributes are the actual subsystem and are not), the **per-line** text-measurement getters (`GetNumLines`, `GetWrappedWidth`, `IsTruncated`, … — need per-line layout facts the host measurer is not asked for today; `measure.rs` answers one whole-string width/height). **`GetStringHeight` was on that skipped list and has been built**, because the blanket reason did not apply to it: the whole-string height is the other half of the number `GetStringWidth` already reads, so `measure::current_measure` now owns the staleness rule for both and the method is a read, not a new host question. AceGUI-3.0 calls it **unguarded** (`AceGUIWidget-Label.lua:54` `height = label:GetStringHeight()`; `AceGUIWidget-CheckBox.lua:90,210` `self:SetHeight(28 + self.desc:GetStringHeight())`), so a nil method there raises and takes the library's own zero-height fallback with it. **Correction, and it matters:** those are the `Libs/` (Mainline), `2.4.3/` and `3.3.5/` library trees AI_VoiceOver ships — **not** the `1.12/` tree its TOC actually loads, which reads `label:GetHeight()` instead and has had every one-argument `SetPoint` rewritten besides. This method is therefore ClassicAPI **parity surface**, not the fix for anything on this install, and the earlier claim here that it explained "can't see the VoiceOver options" was wrong. That symptom was **ours and elsewhere**: the ScrollFrame child re-anchor replaced the child's whole anchor list where 1.12's `SetPoint` merges one point into it, so AceGUI's `TOPLEFT`/`TOPRIGHT`-authored scroll child lost the half that gave it a width and resolved to no rect — and nothing below it resolved either. Fixed in `layout.rs`'s `set_frame_anchored`; measured through the same probe, which went from every node under the scroll child answering `GetLeft() == nil` to a populated, correctly placed tree. Kept because the blanket "needs per-line layout facts" reason genuinely does not apply to it and a 2.4.3/3.3.5 client of this library would need it; what was learned is that an addon shipping several library copies has to be read through its own TOC chain before any call site in it is evidence. Also in this family, each plausible and untraced this pass: `IsMouseOver`/`GetMouseFoci`/`IsDragging`/`SetResizeBounds`/`RegisterUnitEvent`/`SetColorTexture`/desaturation/rotation/the EditBox cursor-and-highlight getters (plausible, untraced this pass). All 12 new methods are registered in `widget_surface.rs`'s `ALLOWED_BEYOND` with their reasons | | Container (`C_Container`) | done | `compat/classic_api/container.rs`; almost entirely translation over the stock 1.12 bag verbs' own data (`Model.containers`), not new game logic. `GetContainerItemID/Info`, `HasContainerItem`, `GetContainerItemDurability`, `GetContainerNumFreeSlots`/`GetContainerFreeSlots`, `CalculateTotalNumberOfFreeBagSlots`, `PlayerHasHearthstone`/`UseHearthstone` (scans bags 0-4 for item id 6948), `SwapItems`/`MoveItem`/`AutoStoreItem` (queue the same `ContainerMove`/`BagAutoStore` the stock pickup/place path does), `SortBags`/`SortBankBags` (no-ops — 1.12 has no sort algorithm to run) and the three sort-preference toggle pairs (stored, nothing downstream reads them) are real. Deliberately nil/false, not guessed: `GetContainerItemCharges` (same charge-slot-identity blocker as SuperWoW's), `GetContainerItemRepairCost` (server-only in vanilla), `GetContainerItemQuestInfo`/`GetContainerItemEquipmentSetInfo` (no such tracking exists, and no such feature exists in 1.12 at all for the latter), `IsContainerItemOpenable` (no lockbox flag tracked) | | CreatureInfo (`C_CreatureInfo`) | partial | `compat/classic_api/creature_info.rs`, the 3 of 9 anything installed actually calls. `GetRaceInfo` and `GetFactionInfo` read a **new app→VM feed of the player's own `ChrRaces.dbc`** (`benilla-formats`' `chr_races.rs` → `benilla-app`'s `chr_races.rs` → `UiScript::set_race_info`), built because this crate holds no DBC and a frozen race table gets a modified realm's names wrong: **OctoWoW's row 10 displays "High Elf" where Blizzard's says "Blood Elf"**, while both keep `clientFileString "BloodElf"` for the art paths — the two columns genuinely disagree, so neither can be derived from the other, and a test pins that. The side on `GetFactionInfo` comes from `ui_unit.rs`'s `race_faction_group`, the same answer `UnitFactionGroup("player")` gives, so the two cannot disagree about a race; a non-playable row carries no side and reads nil rather than a guess. **`GetRaceInfo` answering nil past the last race is load-bearing** — pfUI walks `i = 1, 2, 3…` until nil, so an always-non-nil answer hangs the client at load; its own test says so. `GetCreatureID` decodes the template entry out of bits 24-47 of the 16-hex-digit GUID token the rest of `compat/` passes around, accepting **only** the creature high word so a player or pet GUID answers nil instead of a plausible-looking wrong number (the decode is inlined rather than calling `benilla_protocol::guid::entry` — this crate depends on no protocol or engine crate, and one shift is not worth breaking that boundary for). Not built: `GetCreatureInfoByID`, `RequestLoadCreatureByID`, `GetCreatureFamilyInfo`/`IDs`, `GetCreatureTypeInfo`/`IDs` (each needs a creature/family/type table this VM is not fed, and nothing installed asks) and `GetClassInfo` (the class table exists app-side as `ChrClasses` but has no VM feed yet — the same shape of work this row's race feed just did, if an addon ever wants it). With no install the race table stays empty and every reader answers nil, so an addon's race walk finds none and still loads | | Item (`C_Item`) | partial | `compat/classic_api/item.rs`, 37 entries. Modern id-first and `ItemLocation`-keyed readers over the same state the stock item verbs read: `Model::item_templates` (the one `ItemTemplateView` cache every item tooltip renders through — a miss records the same `CMSG_ITEM_QUERY` ask the stock `GetItemInfo` does, so a modern caller warms the one shared cache, not a parallel one), `Model::containers` and `Model::inventory_slots`. **`ItemLocation` is accepted as the plain table addons build** (`{bagID=, slotIndex=}` / `{equipmentSlotIndex=}`): modern WoW's mixin is pure Lua sugar over exactly those two fields and every entry point only reads them, so the table *is* the contract; the mixin constructors themselves belong with `Mixin`/`CreateFromMixins` in the `Lua` namespace and are not faked here. Built: the existence/identity trio, `GetItemData`/`GetItemDataByID` (the upstream's kitchen-sink table, including the `isConjured`/`isOpenable`/`isLootable`/`isWrapper` flag bools off the template's raw `flags`), `GetItemInfoInstant`, the by-id and by-location getter pairs (name, quality, max stack, sell price, set id, inventory type, icon), `GetItemLink`, `GetStackCount`, `GetItemCount` (bags + worn, with the `includeBank` and charge-multiplying `includeUses` flags), `GetItemSpell` (the on-use trigger only — triggers 0/5, not an equip or chance-on-hit effect), `IsUsableItem`/`IsConsumableItem`/`IsEquippableItem`/`IsEquippedItem`/`IsItemOpenable`/`IsBound`, the `IsItemDataCached` pair (which deliberately record **no** ask — polling is not asking), the two inventory-slot name getters, and `UseItemByName`/`EquipItemByName` over the stock container-use queue. Not built, each with the reason in that file's header: the **guid** family (`GetItemGUID`, `GetItemLocation`, `IsItemGUIDInInventory`, …) — nothing in the model keys items by instance guid, and the modern `"Item-Server-…"` string has no source here; the **item-level** family (`GetCurrentItemLevel`, `GetDetailedItemLevelInfo`, `GetAverageItemLevel`) — item level is not in `SMSG_ITEM_QUERY_SINGLE_RESPONSE` and 1.12 exposes no such concept to the client; `GetItemStats`/`GetItemStatDelta` — the modern shape is keyed by `ITEM_MOD_*` *string* names resolved app-side, and returning the raw numeric keys under the modern name would silently mismatch; `GetItemUniqueness` (wants a limit-category pair 1.12 has no counterpart for); the `LockItem`/`UnlockItem` family (`ContainerSlot::locked` is a pending-operation flag the app owns, not a lock Lua may set); and `UseAtCursor`/`UseAtUnit`/`PickupItem`/`RequestLoadItemData*` (each needs an app-drained queue that does not exist yet) | | AddOns (`C_AddOns`) | done | `compat/classic_api/addons.rs`, 10 entries, and deliberately **pure delegation**: every one calls the stock global (`script/addon.rs`) rather than reading `Model::addons` a second time, so the index-or-name parsing, the unknown-name placeholder answers and the load-gate verdicts keep one owner — a caller cannot get a different answer from `C_AddOns.IsAddOnLoaded` than from `IsAddOnLoaded`. `GetAddOnName`/`Title`/`Notes` and `IsAddOnLoadable` are `GetAddOnInfo` returns 1/2/3/5; `IsAddOnLoaded`/`LoadAddOn` pass straight through; `DoesAddOnExist` reads the `"MISSING"` reason rather than "something came back"; `GetAddOnSecurity` maps the stock `SECURE`/`INSECURE` string onto the upstream's `Enum.AddOnSecurityStatus`, answering `NotAvailable` (3) for an absent addon rather than defaulting to insecure (`Banned` (2) is unreachable — it comes from a server addon-banlist this client does not model). `GetAddOnOptionalDependencies` answers **none**, not the required list: 1.12's `.toc` has no optional-dependency directive at all, and returning the required ones would make a caller treat a hard dependency as skippable. `GetAddOnLocalTable` is not built — modern WoW passes each addon file a private table as a load argument and 1.12 passes addon files no arguments, so there is no such table to hand back; inventing an empty one would read as "my locals are empty" rather than "this client has no such feature". A fixture note worth keeping: the addon *index* array stays empty until the server's `SMSG_ADDON_INFO` reply lands, so any by-index read raises before it — faithful to 1.12, and what a test must call `note_addon_info_reply` to exercise | | GameTooltip (reader methods) | partial | `compat/classic_api/tooltip.rs`, 8 widget **methods** (not globals) on the GameTooltip class table, same linking mechanism `frame.rs` documents. The valuable half of the namespace: addons constantly ask "what is this tooltip showing?" before annotating it. This needed **new state** — `TooltipState::subject` (`widget/kinds/mod.rs`, with a `TooltipSubject` item/spell enum): 1.12 keeps the same facts (the reference holds the item id at `tooltip+0x398`) but exposes no getter, so nothing in this client read them. Recorded at the **two renderers every verb funnels through** — `tooltip_item/render.rs`'s `render_view` (12 call sites, each now passing its own `item_id`, so the compiler enforced coverage) and `tooltip_spell.rs`'s `render_spell` — which is why `SetBagItem`, `SetMerchantItem`, `SetLootItem`, `SetHyperlink` and the rest all answer `GetItem` without each needing its own wiring; cleared with the content, so a `ClearLines` drops it. Built: `GetItem` (name, link, itemID), `GetSpell` (name, rank, spellID), `HasItem`, `HasSpell`, `GetUnit` (over the `unit_token` the stock `SetUnit` already kept), `HasUnit`, `GetOwner` (returns the real frame object through the same `frame_wrapper` `GetParent` uses), `IsEquippedItem`. Each returns *no values* rather than nils when there is no subject, the upstream's documented shape and what `if name then` branches on. Not built, each with the reason: `GetUnitGUID` — the modern getter promises the guid as of when the tooltip was set, and resolving the stored *token* now would be right most of the time and silently wrong exactly when it matters (a `"mouseover"` tooltip outliving its mouseover); `GetGameObject`/`HasGameObject` — no GameObject subject is recorded anywhere and there is no object-template cache keyed like `item_templates`; the `Set*ByID` aliases (`SetItemByID`, `SetSpellByID`, `AddSpellByID`, `SetInventoryItemByID`) — the stock verbs they alias already exist under 1.12 names and each needs its own argument-shape check against them; `SetTalentByID`/`SetEquipmentSet`/`SetTotem` — no backing (`Talent.dbc` unreachable from this crate, no equipment-set feature in 1.12 at all, no totem state). Since built: `SetHyperlinkCompareItem`, which this list had turned down on the grounds that the compare chain needs a shopping-tooltip pair — wrong, because 1.12's own merchant and auction compares drive that pair already (its own row below) | | Quest (`C_QuestLog`) | partial | `compat/classic_api/quest_log.rs`, questID-keyed over the same log rows the stock verbs index by position — the gap being that 1.12's API never hands Lua a questID at all, so an addon can otherwise only key by an index that shifts when the log changes. Built: `GetInfo`, `GetQuestLogTitle` (the documented positional list, whose third value is `suggestedGroup` where the stock global keeps the tag — both coexist), `GetQuestIDForLogIndex` (0 for a header, nil out of range), `GetLogIndexForQuestID`, `GetHeaderIndexForQuest`, `IsOnQuest` (counts rows folded under a collapsed header, via `hidden_quest_ids`), `IsUnitOnQuest` (answers for `"player"`; false for any other token, since another unit's quest list is not tracked — the same blocker the stock `IsUnitOnQuest` already has), `GetNumQuestObjectives`. Not built: `RequestLoadQuestByID`/`IsQuestDataCachedByID`/`GetTitleForQuestID`/`GetQuestDetails` — all four probe a *standalone* questID-keyed static-info cache fed by `CMSG_QUEST_QUERY`, which benilla has none of (`QuestLogEntryView::detail` is per-log-row, so nothing is knowable about a quest not in the log, and `QUEST_DATA_LOAD_RESULT` has no sender); a cache plus its query is app-side plumbing. The bare `GetQuestLogLeaderBoardID` is also not built: `QuestLogObjectiveView` carries `kind`/`text`/`cur`/`req` but no creature/object/item id, so there is nothing to return — closing it means carrying that id along from the app's template read | | Faction (`C_Reputation` + 3 bare globals) | partial | `compat/classic_api/faction.rs`. Bare: `GetFactionIDByIndex`, `GetFactionInfoByID` (the same eleven values `GetFactionInfo` returns, keyed by id), `GetFactionParentID`. `C_Reputation`: `GetFactionStandings`, `GetFactionDataByID`/`ByIndex`, `GetWatchedFactionData`, `SetSelectedFactionByID`, `SetWatchedFactionByID`, `ToggleFactionAtWarByID`, `IsFactionActive`/`ByID`, `SetFactionInactiveByID`/`SetFactionActiveByID`. Every setter queues onto the **same** `reputation_sends` queue the stock pane's own toggles use, so an id-keyed call and an index-keyed one are indistinguishable downstream and cannot race; a faction not in the list sends nothing rather than a wrong slot. **One scope limit, stated once in that file's header:** upstream answers for any `Faction.dbc` row including unencountered factions (DBC name + Neutral defaults), and `benilla-ui` holds no DBC, so these answer for a **listed** faction and nil otherwise — narrower reach, identical shape for anything a caller reaches by walking `GetNumFactions()`. Not built: `GetLastStandingChange` — `ReputationState` is a whole-list snapshot with no delta or history to answer from | | FriendList (`C_FriendList`) | partial | `compat/classic_api/friend_list.rs`, named tables over `Model.social`: `GetNumFriends`, `GetNumOnlineFriends`, `GetFriendInfoByIndex`, `GetFriendInfo` (by name, case-insensitive), `IsIgnored`, `SendWhoQueryByName` (queues the stock `SocialRequest::Who`, so modern and stock `/who` share one request path), `GetNumWhoResults` (both counts), `GetWhoInfo`. Not built: `SetFriendNotes`/`ByIndex` — friend notes are a 2.x feature and `SMSG_FRIEND_LIST` carries no note field, so accepting one would make a notes UI look like it worked; `IsFriend`/`IsIgnoredByGuid` — both key on a GUID and the social lists hold names only (the app resolves guid→name before pushing, keeping no reverse map), while the name-keyed `IsIgnored` sibling is built. `IsWhoQueryPending` answers false always: the app drains the request and pushes the reply as a whole new state, keeping no in-flight marker | | Gossip (`C_GossipInfo`) | done | `compat/classic_api/gossip.rs`, all 11. Table-per-row where 1.12 returns flat positional runs (`GetGossipOptions` strides by 2). Selection stays **index-keyed**, documented in-file: `gossipOptionID` on a modern client is a server id 1.12 never sends, so `SelectOption`/`SelectOptionByIndex` both take the 1-based position and both queue onto the stock `gossip_selects`; `SelectAvailableQuest`/`SelectActiveQuest` take a per-side position and map it to the whole-list position, the same mapping the stock verbs make. A coded option queues nothing, as the stock binding skips it. Empty tables never nil, so `getn` is always safe | | MerchantFrame (`C_MerchantFrame`) + Mail link getters | partial | `compat/classic_api/merchant_frame.rs`. Real: `GetItemInfo` (named table over the merchant row), `GetBuybackItemID` (the stock buyback verbs answer name/texture/price but never the id — the whole gap), `GetNumJunkItems` (grey stacks in bags 0-4, real because `ContainerSlot` tracks quality). Honest constants rather than stubs: `IsMerchantItemRefundable` → false (refunds are 4.x; 1.12 has buyback instead, already exposed), `IsSellAllJunkEnabled` → false and `SellAllJunkItems` a no-op (later-era vendor feature, so a UI gating a button on it correctly hides it rather than selling nothing while the caller believes otherwise). **Also closes two SuperWoW rows:** `GetSendMailItemLink` (real per-instance link off the attached cursor item) and `GetInboxItemLink` (the *basic* itemID-only form, which is what upstream specifies here deliberately — built from the row's `item_id`+`item_name`, since the app pushes links for bag/bank/cursor items but not inbox ones). Both take the optional `attachmentIndex` and answer nothing for anything but nil/1, as 1.12 mail holds one attachment | | CVar (`C_CVar`) | done | `compat/classic_api/cvar.rs`, all 8, over the same registry the stock `GetCVar`/`SetCVar`/`RegisterCVar` drive (`script/cvars.rs`). `GetCVarInfo`'s seven returns; `DoesCVarExist` (true exactly when `GetCVar`/`SetCVar` work, which is upstream's own stated invariant and a test pins it); `AreCVarsLoaded` (always true, as upstream documents and for its reason — settings load before any Lua exists); `GetCVarBool` on upstream's documented coercion table (`"true"` case-insensitive, any non-zero numeric, everything else false, nil for no row so false and absent stay apart); `GetCVarBitfield`/`SetCVarBitfield` as real 1..=64 bit arithmetic routed back through the stock `write_cvar`, so a bit write queues to the host and dirties the config exactly as a `SetCVar` would. `isReadOnly` answers `false`, and that is honest rather than a stub: `write_cvar` refuses only a name with *no row*, never a registered one, so `SetCVar` really does work on every CVar this reports on — a fork that later makes some row refuse writes should flip this to read that. `SetTempCVar`/`RemoveTempCVar` are **fully correct, not just Lua-observable**: a new `CvarSlot::temp_original` remembers the pre-temp value and `UiScript::cvars_snapshot` reports *that* to the saver, so a temp override is live for `GetCVar` yet never written to `config.toml` — which is the whole contract of a temp CVar. Not a 1.12 concept at all (the reference has no temp CVar) | | NamePlate (`C_NamePlate`) | done | `compat/classic_api/nameplate.rs`, all 4, over the plate pool benilla already drives (`script/nameplate.rs`'s `NamePlates`, fed by `vplates.rs`). The GUID↔frame mapping existed already for SuperWoW's `frame:GetName(1)` (`guid_for_frame`); this adds its two inverses, `NamePlates::live` and `frame_for_guid`. `GetNamePlates`/`GetNamePlateGUIDs` answer in pool order and pair by index (a test walks both lists and matches each frame to its own GUID); `GetNamePlateForUnit` resolves through `Model::unit`, so SuperWoW's extended token grammar (raw GUID, `markN`, `owner`) works as a unit argument here too; `GetNamePlateForGUID` takes the same 16-hex-digit token the rest of `compat/` passes around. One stated deviation: modern WoW hands back a *container* whose `.UnitFrame` child is the plate, because its nameplates are FrameXML-driven — benilla's are engine frames with no wrapper, so these return the plate itself and an addon reaching for `.UnitFrame` finds nil rather than a fake child. `GetNamePlates`' `includeForbidden` argument is accepted and ignored: 1.12 has no protected-frame split to filter on | | Macro (`C_Macro`) | partial | `compat/classic_api/macro_ext.rs`: `GetMacroIcon`, `CreateMacro`, `EditMacro`, over the same table the stock macro globals drive (`script/macros.rs`). Upstream's stated reason for the namespace only half-applies here — its complaint is that stock's icon argument is an *index* into a list holding no `INV_*` item icons, but benilla's own `icon_path` already took a string or an index, so a string icon was never refused. What these genuinely add is the signature: `CreateMacro`'s fourth argument is `isCharacterMacro` where the stock global's fourth is `localOnly` and per-character is its *fifth*, and `EditMacro` accepts a macro **name** in place of an index. `GetMacroIcon` re-adds the `Interface\Icons\` prefix on read, with the one deviation stated: upstream answers the icon a macro *shows* (the resolved spell's or item's when a `#showtooltip` resolved over a question-mark icon), where this is always the macro's own stored icon — upstream's own documented fallback. Not built, each for a reason in that file's header: `GetMacroSpell`/`GetMacroItem` (both answer for a resolved `#showtooltip`, backed upstream by a spellID cache the editor fills at save time; benilla parses no directive, keeps no such cache, and resolving a body's first `/cast` to `(name, rank, spellID)` needs the rank-aware spellbook lookup `C_Spell`'s row already notes is unbuilt), `StopMacro` (stops the body *running now*, and benilla executes no macro bodies at all — no `RunMacro`, no depth counter, nothing to stop), `C_Macro.SetMacroDisplay` (no per-macro action-button display override is tracked), and the four `Get*MacroIcons` enumerations (they split `loose` on-disk icons against `mpq` archive ones; `Model::macro_icons` is one flat `Vec` with **no provenance**, so that axis cannot be answered at all, and splitting on basename alone would silently under-report whichever source the host did not push) | | Action (`GetActionInfo`) | done | `compat/classic_api/action.rs`, one bare global, over the same action table the stock `GetActionTexture`/`GetActionCount`/`IsConsumableAction` read (`script/action.rs`'s `Model::actions`). Upstream unpacks a `uint32` type tag and then has to disambiguate its one ambiguous tag (macro-or-bag-item) by walking the macro-slot map; benilla needs none of that, because the host pushes `ActionSlot::kind` already separated into the three `ACTION_KIND_*` bytes — the tag is read, not inferred, and that ambiguity does not exist here. One place this is *better* than upstream: upstream returns `"item", nil` for a bag-instance reference because extracting the id "needs more reversing than we've done", where benilla's slot carries the resolved id for every kind. `subType` is `"spell"` only for a spell action, as upstream documents | | `GameTooltip:GetUnitGUID` | **done — a deliberate reversal, with a new mechanism** | `compat/classic_api/tooltip.rs`, the 9th reader method. The `GameTooltip (reader methods)` row above lists this as *not built*, and the objection recorded there was and is correct: a unit *token* is resolved against the live snapshots per call, so re-resolving `TooltipState::unit_token` at read time answers for whatever that token names **now** — right most of the time and silently wrong exactly when it matters, because a `"mouseover"` tooltip routinely outlives its mouseover. What changed is not the standard but the mechanism: the guid is now **recorded beside the token at `SetUnit` time** (new `TooltipState::unit_guid`, `widget/kinds/mod.rs`; written by `tooltip_unit.rs`'s `set_live_token`, cleared by `tooltip/mod.rs`'s `clear_content` with the rest of the content). The getter therefore hands back a set-time fact and resolves nothing, which *is* the modern getter's promise — and the stale-token case it used to be wrong about is now the case it is most right about. Shape pinned by the live call site, not by memory: pfUI's `modules/tooltip.lua:81` is `local _, guid = GameTooltip:GetUnitGUID()` feeding `UnitTokenFromGUID`, so it returns `(name, guid)` — `GetUnit()`'s two-value shape with the guid in the token's place, and **no third value**, since nothing pins a third position. The *name* is resolved from the guid (through `Model::unit`'s SuperWoW raw-guid arm), not from the token, so the pair always describes one unit; a guid nothing can reach any more answers the guid with no name, because the identity outlives the snapshot. Nothing at all when `SetUnit` saw guid 0, which leaves `HasUnit()` true and is what pfUI's own `guid and UnitTokenFromGUID(guid)` reads. Built because the absence cost **14 errors in one play session** — that call site is on the tooltip's `OnShow`, so it fires on every unit hover. The test fails against the rejected mechanism, not merely against absence: `get_unit_guid_keeps_answering_for_the_unit_set_unit_saw_after_the_token_moves_on` | | `INVSLOT_FIRST_EQUIPPED` / `INVSLOT_LAST_EQUIPPED`, `GetInventoryItemDurability` | done | `compat/classic_api/item.rs`'s new `install_globals` — bare globals, which is how upstream's own list spells them (it writes `C_Macro.CreateMacro` in full and leaves these unqualified). The two constants are **1 and 19**, read off this tree rather than a later client's headers: `char_stats.rs`'s `InventorySlots` is documented "0 ammo, 1..=19 equipment, 20..=23 the equipped bags", its `SLOT_INFO` is `PaperDollItemFrame.dbc`'s own table with `HeadSlot` 1 and `TabardSlot` 19, and `item.rs`'s existing `inventory_slot_token` enumerates the same 19. Neither name is 1.12's (`reference/1.12-globals.tsv` has no `INVSLOT_*` row at all — 1.12 spells each slot as `GetInventorySlotInfo("HeadSlot")`), and their absence was a session's **first** error: pfUI uses them as `for` bounds at module scope four times (`modules/panel.lua:414`, `:445`, `modules/itemcount.lua:7`) and a nil bound raises "'for' initial value must be a number", taking the enclosing block with it. The rest of the `INVSLOT_*` family is deliberately left out: nothing installed asks, and 1.12 already answers each slot by name. `GetInventoryItemDurability(slot)` is the one member of the `GetInventoryItem*` family 1.12 lacks — the reference ships the `GetInventoryItemBroken` predicate and never the numbers behind it — and needed **no new feed**: `InvSlotView::durability` already carries `ITEM_FIELD_DURABILITY`/`ITEM_FIELD_MAXDURABILITY` (fed by `ui_char.rs`'s `slot_view`), which the item tooltip's "Durability X / Y" line and the doll's red tint already render. Slot-only, no unit argument, as the modern function and every pfUI call site have it. Three answers kept distinct: **no values** for an empty slot (what `if cur and max then` branches on), the live pair for a damageable item, and **`(0, 0)`** for a worn item with no durability at all — the feed stores that as `None`, so it is unfolded here, because a worn ring is not an empty finger | | `GetSpellBonusDamage` | done | `script/char_stats.rs`, beside the `UnitDamage` that reads the same wire field. A school's spell power: 1.12 has no character-sheet line for it and no global to ask, which is why it reads as a later-client function — but **the number is already streamed**. A vanilla server folds item spell power into `PLAYER_FIELD_MOD_DAMAGE_DONE_POS`/`_NEG` (`_ApplyItemBonuses` → `ApplySpellDamageBonus`), the very field `UnitDamage` already reports index 0 of as the physical pos/neg pair, so schools `[1..7]` were arriving and being dropped at the feed: `ui_char.rs` read `player_mod_damage_done_pos(0)` alone. The snapshot now carries `damage_bonus_pos`/`_neg` as `[i32; 7]` (the shape `resistance_pos`/`_neg` already use) and `UnitDamage` takes `[0]` of it, so there is one model of the field rather than a scalar and a derived duplicate. **Why it mattered**: pfUI's player module polls schools 2..7 every 0.25s from its throttled `OnUpdate` to draw a "+N SP" readout (`modules/player.lua:92`), so on a caster the missing global raised *per tick* — 433 runtime errors in one logged session on a fresh warlock, the single largest error source in the log, and invisible on a warrior because the readout resolves to 0 and never shows. Schools are 1-based (1 physical … 7 arcane) and anything off that range answers **0, not nil and not a raise**, which is what the call site's own `or 0` assumes. Its sibling `GetSpellBonusHealing` stays **unbuilt and is not approximable**: vanilla has no healing player field at all — 0x40A is `PLAYER_FIELD_BYTES`, the healing-done fields are a 2.0 addition — so there is nothing on the wire to read, and the number would have to be summed from equipped item stats. Nothing installed calls it. `UnitSpellHaste`, which the same pfUI function calls one line earlier, stays `todo` for the reason its own row gives (no haste stat exists in 1.12); it is reached only when that module's haste display is switched on, which this player's config has off | | `GetAddOnEnableState` (the legacy global) | **deliberately not done — checked, not assumed** | Upstream's own `docs/API.md` AddOns section lists the ten entries above and **not** this one, and keeps 1.12's `GetAddOnInfo` with `enabled` 4th. So the DLL actually loaded on this install does not define it either: adding it here would be a *divergence from the player's own client*, not parity with it. It would also break the one addon that asked for it. `AI_VoiceOver/Compatibility.lua:121` gates **two** definitions on one `if not GetAddOnEnableState` — its own enable-state fallback *and* a modern-shaped `GetAddOnInfo` that drops the `enabled` return. Define the global and VoiceOver keeps 1.12's 8-tuple while reading it at modern offsets, so `VoiceOver.lua:84`'s `select(5, GetAddOnInfo("VoiceOver")) ~= "MISSING"` reads `loadable` where it means `reason`, is true for a folder that is not installed, and raises the "delete your old VoiceOver folder" popup every login — twice, `AI_VoiceOver_112` too. Recorded because this was first diagnosed the other way round: the three `AI_VoiceOverData_*` modules look unloaded in the log and are not. They load inside AI_VoiceOver's own `ADDON_LOADED`, 224 ms of it, before the walk ever reaches their rows — and the walk's `is LoadOnDemand — not loaded (no LoadAddOn() yet)` line used to be printed unconditionally, so it went on saying "not loaded" about addons already in memory. That line is now gated on `UiScript::addon_is_loaded` | | `C_Item.GetItemInfo` | done, delegating | `compat/classic_api/item.rs`. Calls the **stock 1.12 global** (`script/item_stats.rs`), which owns the template cache and the `CMSG_ITEM_QUERY` warm an uncached id records — so a modern caller feeds the one shared cache and a hook over the stock global is honoured, same discipline as `C_AddOns`. The shape is modern WoW's, by exactly the transform `item_stats.rs`'s own comment already names ("later clients insert `itemLevel` at 4"): returns 1..3 pass through, **position 4 is nil**, stock's 4..9 land at 5..10. Nil rather than a number because item level is not in `SMSG_ITEM_QUERY_SINGLE_RESPONSE` and 1.12 exposes no such concept (the same reason `GetCurrentItemLevel` stays unbuilt) — a fabricated 0 would read as a level-0 item where nil reads as "not known here", which a caller can tell apart. Modern positions and not 1.12's because a live call site against the real DLL pins them: pfUI's `modules/bags.lua:457` reads `local _, _, q, _, _, _, itype` and tests `itype == "Quest"` at `:475`, and position 7 is the subType *string* in the modern run against the stack *count* in 1.12's — forwarding the nine verbatim would have left that test silently never firing. Same evidentiary standard as the `ColorMixin` row below. Stops at 10: modern's 11th onward (sellPrice, classID, subclassID, bindType, …) are facts this client holds, but nothing available pins their *order* and each already has its own `*ByID` entry, so they are left off rather than placed from memory | | `IsSpellKnown` (bare) | done | `compat/classic_api/spell.rs`'s new `install_globals`, matching upstream's own heading "Spell (`C_Spell`, plus several bare globals)". It hands out **the same function value** `C_Spell.IsSpellKnown` holds, lifted out of that table rather than written twice — so the two spellings cannot drift and there is no second body to maintain (`the_bare_is_spell_known_is_the_very_function_c_spell_holds` asserts the identity, not just the behaviour). Not a 1.12 name: 1.12 asks "do I know this" only by walking the book with `GetSpellName(i, "spell")`, never by id. pfUI's `modules/bags.lua:703` calls it bare, 3 occurrences in one session | | `GetMacroSpell` (bare) | done — **the "not built" note in `macro_ext.rs` was wrong about this client** | `compat/classic_api/macro_ext.rs`'s `install_globals`. The triage item that produced this said `C_Macro.GetMacroSpell` already existed and only needed exposing; it did not exist anywhere, and that file's header gave a reason — "resolving a body's first `/cast` line needs the rank-aware spellbook lookup `C_Spell`'s row notes is unbuilt". **Both halves do exist and are already load-bearing.** `benilla-app`'s `ui_macro/run.rs` is a traced reimplementation of the reference's own macro→spell derivation (`0x4efe00` writing `[rec+0x564]`, reading the `SLASH_CAST%d` aliases as Lua globals exactly as `0x703bf0` does, plus a `CastSpellByName(` arm) and resolves through `spellbook.rs`'s `resolve_spell_by_name`, the same resolver `CastSpellByName` and `/cast` use; the action bar has been reading its result for a macro slot's cooldown, usability and range all along. So this is **not new derivation** — it is the existing one, fed into the VM as `Model::macro_bound_spells` by the existing `rebind_macro_spells` system (no new system, no new file, no ceiling movement) and read back by id through the same `spell_display` `C_Spell.GetSpellName` uses. Returns `(name, rank)`, two values only: pfUI reads `effect, rank = GetMacroSpell(id)` (`libs/libpredict.lua:1027`) and compares the first to a spell name (`modules/swingtimer.lua:430`), so a bare-spellID return (what the newest clients give) would fail both, and no third value is appended because nothing pins its position. Rank is a string, `""` when rankless, the rule the stock `GetSpellName` already keeps. Only resolved spells cross the feed: `BoundSpell::None` and `BoundSpell::Unresolved` are both "no spell" to Lua, which has one way to say it, while the bar needs them apart. Stated latency: one frame after an in-Lua `EditMacro`, the identical delay the action bar already has, from the identical source. `GetMacroItem` stays unbuilt — the reference's macro record binds a spell and has no item counterpart to derive | | `GetInventoryItemRepairCost` | todo, blocked | pfUI reaches it in the *hover* arm of the durability panel widget the two `INVSLOT_*` constants above unblocked (`modules/panel.lua:416`), guarded as `(GetInventoryItemRepairCost(id) or 0)` — which does not help, since a nil *global* raises before `or` runs. Deliberately not built, and not stubbed to 0: 1.12 computes repair cost **server-side only** and the client never receives or derives a per-item figure (the same blocker already recorded for `C_Container.GetContainerItemRepairCost`, `container.rs`'s header). `0` would read as "this item repairs free", which is a wrong answer a caller cannot detect, where a nil-call error names the function. The client-side repair numbers that *do* exist are the merchant window's own `CanMerchantRepair`/`GetRepairAllCost`, which are a different fact (the whole-inventory total at an open vendor) and cannot be divided back into per-slot costs | | `C_Container.GetContainerItemCharges` (restated, measured) | todo, blocked — now the harness's first session error | No change made; recorded because fixing the `INVSLOT_*` `for` bound moved this to the front of the queue. `container.rs` installs it returning nil by the stated decision (`item_spell_charges` is a raw signed field and no item-template column says which of an item's five spell-trigger slots is a consumable charge rather than an equip/proc effect — the same blocker as SuperWoW's charges extension above). pfUI's `modules/bags.lua:452` then does `if charges > 1`, which raises "attempt to compare number with nil" and is now the harness's first session error for pfUI, two lines **above** the `C_Item.GetItemInfo` call this pass fixed. It did not appear in the live session log at all (bag stacks there were not 1, so the guarded block was skipped) — the harness's synthetic bag fixture is what reaches it. Unblocking it needs the item-template charge column, not a change here | | `SetScript("OnInputLanguageChanged")` refusal | **examined and deliberately kept** | Not a DLL feature and not an addon's call at all: both occurrences in the live session (`errors…18-53-31.log`, world entry and `/reload`) are **stock FrameXML** — `ChatFrameEditBox` in `FloatingChatFrame.xml`, off `ChatFrame.xml:121`'s script slot. `object/events_regions.rs`'s `set_script` raises for real 1.12 script kinds nothing fires, by a stated decision ("an accepted handler that never runs fails silently"), and `OnInputLanguageChanged` is named in that comment. Accept-and-ignore was weighed and rejected: the reference accepts it because the reference has an IME to fire it, benilla has none, and silently installing a handler that can never run is the exact failure that comment exists to prevent. It is already recorded as a **warning**, not an error, and two tests pin it by name — `world_entry_tests.rs`'s `a_clean_world_entry_raises_only_the_warnings_we_have_named` (where it is one of exactly two named deviations) and `script/tests/input.rs`'s `the_unfired_script_kinds_still_raise_rather_than_silently_accepting`. The name leaves that list when something fires it, not to quiet a log line | | ~60 namespaces, remaining ~515 functions, 50+ events | todo | full per-namespace function list fetched from the upstream repo's own `docs/API.md`: `docs/classic-api-functions.md` at this repo's root — re-fetch upstream before resuming, that file is a point-in-time names-only snapshot. One file per namespace under `compat/classic_api/` (`container.rs` done; `spell.rs`/`spellbook.rs`/`unit_auras.rs`/`state.rs` may be in progress or done by the time this is read — check their own status before re-doing them) | | `ItemLocation`, `ItemLocationMixin` | done | `compat/classic_api/item_location.rs`, a Lua chunk over the existing `CreateFromMixins`. **The single largest error source measured in a real session: 12,570 lines**, one `attempt to index global 'ItemLocation'` per bag slot per `OnUpdate` pass of pfQuest's quest-item watcher (`database.lua:207` walks five bags, `:216` all 19 equipment slots). `item.rs` was already designed for this — its header states that the mixin is pure sugar over `{bagID, slotIndex}` / `{equipmentSlotIndex}`, that the plain table *is* the contract, and that the constructors "belong with `Mixin`/`CreateFromMixins`"; this is that deferred half, so nothing in `item.rs` changed and a hand-built table still works. Every member has backing and all are built: the two setters, `Clear`, `IsBagAndSlot`/`IsEquipmentSlot`/`HasAnyLocation`, `GetBagAndSlot`/`GetEquipmentSlot`, `IsEqualTo`, and `IsValid` — which is literally `C_Item.DoesItemExist(self)`, so it cannot disagree with the `GetItemName` on the next line. Three shapes are load-bearing and pinned: the constructors are defined with `:` (every installed call site writes `ItemLocation:CreateFromBagAndSlot(...)`, and so does Blizzard's own `ItemLocation.lua`); `IsBagAndSlot` compares against **nil**, not truthiness, because the backpack is bag `0` and pfQuest's loop is `for bag = 4, 0, -1`; and the setters clear the other kind's fields, because `item_loc` tests `equipmentSlotIndex` first and a half-cleared object would resolve to the wrong slot. Not built: `IsEqualToBagAndSlot`/`IsEqualToEquipmentSlot` (nothing calls them, and `IsEqualTo` plus a constructor is the same answer) | | `C_CreatureInfo.GetClassInfo` + a new `ChrClasses.dbc` feed | done | `compat/classic_api/creature_info.rs`, closing the gap that row's own "Not built" list named. **Costliest absence in the surface by blast radius rather than by count**: pfQuest calls it at *file scope* inside `for id = 1, 32` (`database.lua:408`, building `pfDB.bitclasses`), so the nil call aborted `database.lua` there and **every `pfDatabase` function defined below line 408 was never defined at all** — the session log's separate `GetBitByRace` (`:609`), `AddCustomIcon` (`:1897`) and `GetQuestIDs` (`:1928`) failures were all this one abort, not three gaps, and the addon harness ranked it as one of only two first-load errors in the whole corpus. Built as a feed, not a table: `benilla-formats`' `chr_classes.rs` gains its name block's enUS column (field 5) and the class file token (field 14) beside the three columns it already read, `model.rs` gains `ClassInfo`/`class_info`, `script/class_info.rs` the 19-line `set_class_info`, and `benilla-app`'s new **`ui_chr_classes.rs`** the `PostStartup` push — the `ui_*` name because `ui_script_wall.rs` only lets that family name `UiScript`, and a second file because `chr_classes.rs` loads the same table for three engine readers and must not learn the VM exists. Read rather than carried for `ChrRaces`' reason: a patched realm owns the display name, and `classFile` is an independent column — a test pins both against the shipped DBC and confirms fields 5 and 14 by their nine-rows-with-gaps-at-6-and-10 shape. **Stated rather than glossed**: on this shipped enUS table the two columns *coincide*, `name:upper() == classFile` for all nine rows, and a test asserts that too — so a feed that derived the token from the name would pass every real-data assertion and go wrong only on a renamed or localized realm. That is why the app-side test uses a deliberately renamed row (the class-side shape of `ChrRaces` row 10's "High Elf"/"BloodElf"): without it the property is untested, which the discrimination pass caught. **Nil for a gap is load-bearing**, differently from `GetRaceInfo`'s nil: ids 6 and 10 carry no row in 1.12, pfQuest walks straight through them, and borrowing a neighbour would key `bitclasses[2^5]` to the wrong class rather than leave it empty. No `Update` system, so no `UPDATE_ACTIONABLE_CEILING` change | | `TableUtil.Execute` | done | `compat/classic_api/table_util.rs`. **Not a ClassicAPI function and not 1.12's** — `docs/classic-api-functions.md` has no `TableUtil` at all; it is Blizzard's own FrameXML helper (`Blizzard_SharedXMLBase/TableUtil.lua`, 58 functions) and this install's pfQuest calls one of them at **file scope** (`map.lua:21`, renaming its minimap pins for Gatherer-compat). Line 21 of a 2000-line file, so the nil index aborted `map.lua` and `pfMap` was never defined — which is the whole of the harness's other first-load error (`pfQuest-octo/patchtable.lua:137`) and the two `pfQuest.ResetAll` failures under input (`quest.lua:616`). One function closes all of it. The body is **upstream's, fetched not remembered** (Gethe/wow-ui-source `live`): it walks `pairs`, not `ipairs`, and the callback takes the **value only**, never the key — two details the one installed caller cannot distinguish (it passes an array and reads one argument), which is exactly why the source was read instead of inferred. Tests pin both against upstream's body rather than against the call site. The other 57 members are not installed: nothing calls one, and several (`CopyTable`, `tInvert`, `tFilter`, `tContains`) collide with names the addon corpus and the stock chain already define | | `table.wipe` | done | `compat/classic_api/lua_ext.rs`, **aliased** to the bare `wipe` `script/stdlib.rs` already defines — literally the same function value, so the two names cannot drift. That matters because `wipe`'s body is not a `pairs` clear: it walks `tremove` down from `getn` to reset Lua 5.0's *remembered table size*, which a second copy would get wrong and a refilled list would then read holes out of. pfUI's nameplate module calls `table.wipe(plate.cache)` on every plate refresh (`modules/nameplates.lua:1023`) and with only the bare name it raised **2,859 times in one session**, stopping the plate styling mid-refresh — the module was running (the `NAME_PLATE_*` events had just started firing), it simply could not finish. A test asserts identity (`table.wipe == wipe`), not merely presence, and was verified to fail without the alias | | `UnitCreatureTypeID` | done | `compat/classic_api/unit_extra.rs`, over new `UnitState::creature_type_id` — `SMSG_CREATURE_QUERY_RESPONSE`'s `CreatureType`, which `ui_unit.rs` was already receiving and discarding after converting it to the localized word. Both are carried now: the word is what 1.12's `UnitCreatureType` answers, the number is what this answers, and deriving either from the other would mean a reverse lookup through a localized string — wrong on any non-enUS realm. **The argument is a GUID, not a token**, in its only caller (`pfUI/modules/nameplates.lua:295`), which resolves through the SuperWoW raw-GUID arm so neither form needs a special case. This supersedes the earlier `todo` that listed it as needing data the client lacked; the number was always there. **Why it mattered visually**: the call sits in `TotemPlate` → `CreatureType`, reached from the plate refresh at `:1111` — *above* the whole block that `SetPoint`s and `Show`s `plate.name`/`level`/`health`/`glow`/`guild`. So every refresh aborted before laying its own art out, and the user saw plates that were styled-but-misaligned with wrong colours rather than unstyled. Its own comment is `-- nil retries next call`, so it re-raised per plate per frame: 1,014 times in one session | | `table.unpack` | n/a — not reached | 5.0's table library has eight members and this is not one, so it is genuinely absent. Left that way: its only call sites are in `pfQuest/toolbox/extractor.lua`, a data-extraction dev tool **not listed in pfQuest's `.toc`**, which never loads in play. Recorded so the next sweep does not re-flag it | | `PlayerUtil.GetClassFile` | done | `compat/classic_api/player_util.rs`. The **second** of two walls in pfQuest's `database.lua`, found by re-running the harness after the `GetClassInfo` row above cleared the first: the abort moved 478 lines down, from `:408` to `:886`'s module-scope `local isdruid = PlayerUtil.GetClassFile() == "DRUID"`. Both are needed for the measured cascade — with only the first fixed, `GetBitByRace` (`:609`) exists but `AddCustomIcon` (`:1897`) and `GetQuestIDs` (`:1928`) still do not, and `icons.lua` still dies on the first of those. Not a ClassicAPI function either (`PlayerUtil` is Blizzard's own FrameXML helper, absent from `docs/classic-api-functions.md`). **Unlike `TableUtil.Execute`, the body here is not upstream's verbatim** — every path to `PlayerUtil.lua` 404s and `warcraft.wiki.gg`'s FrameXML index lists the name with no signature — so it is built on a *forced* reading rather than a fetched one: the namespace means the player so it takes no argument, "class file" is the engine's own word for the locale-independent token, and this client already answers that exact fact as `UnitClass("player")`'s second return. It reads the same `PlayerRecord::class` pair that binding reads, so the two cannot drift, and a test asserts they agree. nil before the record lands, which is both what `UnitClass` answers then and what the `== "DRUID"` call site needs. The other eleven members are unbuilt: four spec getters plus `CanUseClassTalents` describe a specialization system 1.12 has none of, and the rest would be second spellings of `UnitClass`/`RAID_CLASS_COLORS`/`C_CreatureInfo.GetClassInfo` | | Map (`C_Map`) | partial | `compat/classic_api/map.rs`, the 3 of upstream's 27 that read data already in this VM, over the world-map catalog `ui_world_map.rs` pushes once at world entry — **no new feed and no second DBC read**, so these cannot disagree with what the map window itself draws. This supersedes the `C_Map.GetMapOverlays` entry in the "ClassicAPI tail" row above. The error count (19) badly understates this one: pfQuest's modified copy has **deleted** its shipped zone-size, subzone-placement and area-trigger tables in favour of these calls (`db/minimap.lua`, `db/zones.lua`, `db/areatrigger.lua` are now stubs saying so), and almost every call site is guarded with `if C_Map and C_Map.X then`, so an absent namespace degraded *silently* into a pfQuest with no zone sizes and no subzone index. Only pfUI's is unguarded (`modules/mapreveal.lua:119`), and that is where the 19 came from. Built: `GetMapWorldSize(areaID)` → the `WorldMapArea` loc rect's two spans in yards, `left - right` across and `top - bottom` down, the same pair `worldmap.rs`'s own `area_grid_cell` un-lerps hovers through and the order pfQuest divides by (`map.lua:1367`); it answers **no values** for a degenerate rect rather than a truthful `0`, because the caller's next line is `mapZoom / mapWidth` and an infinite scale stacks every pin in one place. `GetMapAreaIDs()` → `{ [mapFolder] = areaID }`, the first reader of a `WorldMapZoneView::area_id` column nothing had ever read, keyed by `GetMapInfo()`'s own string because a city or instance map has no zone-dropdown index to look itself up by. `GetMapOverlays([areaID])` → **every** `WorldMapOverlay` row of the zone, explored or not, which is the one thing separating it from the stock `GetNumMapOverlays`/`GetMapOverlayInfo` pair (those are the *revealed* list, gated on `PLAYER_EXPLORED_ZONES_1`) — pfUI diffs the two to find what is still fogged, and a test asserts they disagree on an unexplored zone. That needed two new `WorldMapOverlayView` fields: `texture_name` (the bare DBC leaf, carried rather than split back out of the path so `textureName` and `texturePath` are the same string by construction) and `area_id` (slot 0, the sub-area the overlay *is*, as against `explore_bits`, which is all four slots' reveal bits). Nil, not an empty table, when no zone resolves — a zone with no overlays and no such zone are different facts and both callers branch on it anyway | | `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo` | todo — feed shape known | Both are what pfQuest deleted its own tables for, so both are silent functional loss rather than errors, and neither is guessable. `GetAreas()` wants `{ [areaID] = name }` for the **whole** `AreaTable.dbc` (~2700 rows); the model holds only the ~80 rows that have a `WorldMapArea` map, because that is all the map window needs. `AreaTableCatalog::rows` already exists app-side (`benilla-formats/src/area_table.rs`) and `benilla-app`'s `AreaTableRes` is already a live resource, so the work is exactly the `ui_chr_classes.rs` shape this pass just built: one `BTreeMap` feed and a `PostStartup` push. `GetAreaTriggerInfo(id)` wants `AreaTrigger.dbc` **plus a projection**: the caller reads `areaID`, `mapX` and `mapY` (`database.lua:790`), which means resolving the trigger's world position through the `WorldMapArea` rects — `AreaTriggers` is loaded app-side (`area_trigger.rs`) and never reaches the VM, and the projection is `map_proj`'s job, not Lua's. Not built here because each is a new feed and this pass's budget went to the error-producing members; the remaining 22 (nine waypoint members, the layered-art trio, the four-member projection family) are later-era or app-side by nature — except `GetMapInfo` and `GetPlayerMapPosition`, which are 1.12's own globals and already exist bare (`script/worldmap.rs`), so a `C_Map.` spelling of either is an aliasing decision rather than a gap and nothing in the corpus asks for one | | `C_PlayerInfo.CanUseItem` | done | `compat/classic_api/player_info.rs`, superseding that member's entry in the "ClassicAPI tail" row above. It is the "would the tooltip print this item's requirements in red" gate, and that gate already existed here as exactly one function — `item_stats::item_usable_by_id`, the port of the reference's `0x5ea930` (required level, class, race, proficiency, skill + rank, required spell, honor rank, city rank, reputation standing). So this is the **same function value** `C_Item.IsUsableItem` answers from, under the second name upstream also publishes it under; a test asserts the two agree, because pfUI tints a bag slot from one and its tooltip red lines come from the other, and a second implementation that disagreed would red-tint slots whose tooltip is black. pfUI's own call-site comment (`modules/unusable.lua:18-21`) names the requirements-not-condition split and says it depends on it, which is what `item_usable` already implements. A bad argument answers **false**, not a raise — the hook fires per bag slot — and deliberately not the gate's own "an unpushed player declines to judge, answer usable" default: an unresolved player genuinely cannot be judged and greying the whole bag at login would be worse, but a nonexistent item id is not a judgement call. The namespace's other nine are unbuilt: nothing installed calls one, and the five getters take a modern `PlayerLocation` object whose facts (another player's class by guid, their connected state) are a name cache this VM is not fed | | `nameplate` / `nameplatetarget` unit tokens | partial — **recognised, deliberately naming nothing** | `script/unit/mod.rs`'s `classic_api_token_recognised` now takes a `"nameplate"` prefix beside `"focus"`. 206 `Unknown unit name: nameplateNtarget` lines in one session, and the cost was not the lines: pfUI's aggro scanner walks its whole `pfValidUnits` table — which `env/tables.lua:48-49` fills with `nameplate1..40` **and each one's `target`** — calling `UnitExists` on every entry (`api/unitframes.lua:186`), and an unrecognised token raises, which longjmps out of `GetAggroState` entirely. So each of those 206 lines was also an aggro scan that never reached its *party and raid* tokens. Recognition alone fixes that and is the same answer the reference gives for `"raid5"` solo. **Resolution is deliberately not built, and this is the honest half of the row.** Three separate things block it, measured not assumed: (1) `Model::unit` can only turn a GUID into a `UnitState` that some *other* token already names (`unit_by_guid` scans `units_by_lower`), and no feed pushes a snapshot per plate — so `nameplate1` would answer only for a plate that is also your target or a party member, i.e. almost never, and unpredictably; (2) `NamePlates::live()` compacts retired slots out, so its dense rank is **not** slot-stable — a plate's index shifts when an earlier plate dies, and modern WoW's token is slot-bound, so building on `live()` would make `nameplate1` mean a different unit frame to frame; (3) `target` needs the plate unit's `UNIT_FIELD_TARGET`, the same hop the `focustarget` comment in `Model::unit` already records this crate cannot make. A token that names the wrong unit is worse than one that names none, so these name none. What it would take: a per-plate `UnitState` feed (`ui_unit.rs`'s `token_snapshot` is the ready-made builder, but `feed_units` is at Bevy's 16-`SystemParam` ceiling and would need a sub-struct first) and a new `NamePlates` accessor exposing the **pool slot** rather than the dense rank. Note the pool's order is arrival order, not distance: the one sort in the pipeline (`vplates.rs:461`, screen-space distance from a fixed gx point) exists for the overlap solve and is discarded before `sync()` | | Newly surfaced by the above: `Item`/`ItemMixin`, `GameTooltip:GetGameObject` | todo — measured, not guessed | Both were **masked** before this pass and are reported here so the harness's numbers are readable. Clearing pfQuest's two `database.lua` walls (`:408`, `:886`) moved its abort to `:2241`, `local item = Item:CreateFromItemID(HEARTHSTONE_ITEM_ID)` followed by `item:ContinueOnItemLoad(...)` — modern WoW's `Item` mixin, which pfUI also reaches for (the harness counts `CreateFromItemID` and `ContinueOnItemLoad` at two addons each). **That line is 5 lines from the end of a 2246-line file and no function definition follows it**, so the residual cost is only the locale name-check bootstrap in that last block — every `pfDatabase` function now exists, which is the whole point of the two rows above. Not built because `ContinueOnItemLoad` is an *async* data-load callback and `item.rs`'s header already states why that family is out: `RequestLoadItemData*` needs an app-drained queue that does not exist, and a `ContinueOnItemLoad` that fired synchronously off a cache miss would call back with no data. Separately, `map.lua` now loads, which makes `map.lua:296`'s `GameTooltip:GetGameObject()` reachable for the first time — that is why the harness's pfQuest input-raise count went **2 → 7** while its load errors went 2 → 1: the same one unbuilt method, now hit by a tooltip hook that previously never ran. It is a *widget method* (so `tests/widget_surface.rs`, not the global list) and already carries its reason in `tooltip.rs`: no GameObject subject is recorded at any renderer and there is no object-template cache keyed like `item_templates`. Honest net: pfQuest went from dying at 19% of `database.lua` to dying at 99.8% of it, and from `map.lua` not loading at all to loading and then raising on one tooltip hook | | `C_Item.GetItemUniquenessByID` | todo — **the fact is here, the signature is not** | Deliberately not built, and not for want of data: 1.12's whole uniqueness story is `ItemTemplateView::max_count`, which `item.rs` already hands out as `maxCount`. What is missing is the **return shape**, and the two candidates are behaviourally different. Upstream publishes `GetItemUniqueness` *and* `GetItemUniquenessByID`, and every other `X`/`XByID` pair in that namespace returns the same thing — which argues the pair is modern WoW's `(limitCategory, limitCount)`, hence `(0, max_count)` here since 1.12 has no `ItemLimitCategory.dbc` at all. But the one installed call site is `C_Item.GetItemUniquenessByID(id) == 1` (`pfUI/modules/itemcount.lua:16`, suppressing stack counts on unique items), which only works if the return is a **single** uniqueness number. Guessing wrong is invisible to the caller in both directions — the pair shape makes that test silently always-false and pfUI prints counts on unique items; the single shape would mislead any modern-shaped caller — which is exactly the failure mode this file's scope rule refuses. Resolving it needs upstream's `docs/API.md`; note the path `docs/classic-api-functions.md` cites now **404s** on `raw.githubusercontent.com` (`brues-code/ClassicAPI` `main`), so re-fetching it is itself a prerequisite task. The compat doc's earlier `GetItemUniqueness` row reached the same "no 1.12 counterpart for the pair" conclusion from the other direction | | `C_NewItems` (`IsNewItem`/`RemoveNewItem`/`ClearAll`) | todo — reason sharpened, not changed | Left unbuilt, agreeing with the "needs a 1.12-absent subsystem" classification in the tail row above, with the measurement that makes it conclusive: **building it would deliver nothing**. pfUI's module refreshes its glows from `EventRegistry:RegisterFrameEventAndCallback("BAG_NEW_ITEMS_UPDATED", ...)` (`modules/newitem.lua:60`) and benilla fires no such event, so even a perfect `IsNewItem` would never repaint. The tempting cheap version — `IsNewItem` → false, the other two no-ops — is not free either: it is a *positive* claim that a just-looted item is not new, indistinguishable by the caller from a correct one, and it would turn a loud failure into a feature that is permanently and silently off. A real implementation needs app-side state nothing feeds: a container-arrival diff gated on whether the bag UI was open when the item landed, plus the event. Cost of leaving it: an error per bag-slot update while a bag frame is open (`pfUI.newitem:UpdateSlot` is hooked onto `pfUI.bag.UpdateSlot`), which is why the measured count is low — the harness never opens a bag | | `C_NewItems` (`IsNewItem`/`RemoveNewItem`/`ClearAll`) | done — **floor**, reversing the row below | `compat/classic_api/new_items.rs`. The row below is correct about *function* and is kept: benilla never fires `BAG_NEW_ITEMS_UPDATED`, so the glow would never refresh even with the namespace present, and `IsNewItem → false` is a positive claim about a just-looted slot. What it did not weigh is the cost of the **absence**. `pfUI/modules/newitem.lua:16` indexes `C_NewItems` from inside a bag-slot update that runs on hover, click and drag — so a nil namespace does not skip the glow, it *raises* and takes the rest of that handler with it. Measured the moment every addon began loading cleanly: **7 of 13 addons went dead to the touch on this one name**, against 1 before. "No glow" and "no working bag interaction" are different failures, and this is the same trade the `Totem` row records. `IsNewItem` answers the boolean `false` (not nil — upstream returns a boolean and nil is the one shape a `== false` could tell apart); the two removers are honest no-ops, since there is no marker state they could fail to clear. **Still wrong for a slot that really was just looted** — that is this row. The real feature needs benilla to track bag arrivals across a session and fire the event on the edge; when that lands this file becomes a getter instead of a floor | | `C_Item.GetItemUniquenessByID` | done — reverses the decline below | `compat/classic_api/item.rs`, one `by_id!` line over `max_count`, whose own doc comment reads "1 prints Unique, N > 1 Unique (N); not the stack size". **The fact was never the problem** — it was declined twice because retail's shape is a `(limitCategory, limitCount)` pair, 1.12 has no limit categories, and upstream's `docs/API.md` truncates before the Item section so reading it settles nothing. What settles it is the **one real caller**: `pfUI/modules/itemcount.lua:16` reads `... or C_Item.GetItemUniquenessByID(id) == 1 then return end`, comparing the *first* return against 1 to mean "unique, draw no count". A limit *category* compared against 1 is meaningless there; the limit *count* is exactly right and answers every case — unique → 1 (skip), unlimited → 0, `Unique (3)` → 3 (draw, since three may be held), all three pinned by test. **One value, not a pair**: inventing a category id for a game with no categories is the part that would actually be wrong. Declining cost 27 raises per session on plain mouseover, the same trade the `Totem` and `C_NewItems` rows record | | The modern nameplate contract: `NAME_PLATE_CREATED` / `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED`, slot-bound `nameplate` and `nameplatetarget` | done — **this is why pfUI was not styling nameplates at all** | Supersedes the `nameplate` row above, which was right about the symptom and wrong about one of its three blockers. The user-visible fault was total: `pfUI/modules/nameplates.lua` registers these three events (`:498-500`) **and nothing else** — there is no `WorldFrame` child scan anywhere in that 1936-line file — and benilla fired none of them, so pfUI's module never received a single plate and every plate drew as benilla's own. All six of its styling paths are `C_NamePlate.GetNamePlateForUnit(arg1)` over the token those events carry. Built in `benilla-ui`'s `script/nameplate.rs` beside the existing 1.12 widget pool, **not** in a feed system of its own, and that is the central design call: the token, the plate and the event edge are one state machine, and the correct order inside a single tick is (1) fire every removal while its token still names the departing unit *and* its plate is still findable, (2) free those slots, run the widget sync, claim new slots, push every live snapshot, (3) fire `NAME_PLATE_CREATED` then `NAME_PLATE_UNIT_ADDED`. Split across two systems, step 1 lands after the plate is already retired, and pfUI's removal handler is `local plate = GetNamePlateForUnit(arg1) if plate then visiblePlates[plate] = nil end` (`:611`) — a nil plate there leaks that plate into its visible set for the rest of the session, with no event left to take it out. Its own source comment states the dependency ("UnitGUID still resolves inside the handler (the slot is freed after dispatch returns)"). **On blocker (2), slot stability, which the old row called the heart of the problem**: the token slot space is a new `NamePlates::token_slots` (`Vec>`, guid per slot, lowest-free claim) and is deliberately *not* the widget pool index. A unit keeps its slot for the life of its plate; a slot is freed only after its removal has been dispatched. The pool cannot serve as the token space because the pool recycles a slot in the very tick it is freed — `sync`'s own "retire first, so a departing unit's slot is free for an arriving one", the reference's single free list — which would make one index mean two units inside one dispatch. Nothing an addon can observe ties the two numbers: `GetNamePlateForUnit` is token → GUID → `frame_for_guid`, never token → pool index. `NamePlates::live`'s dense rank is still the wrong answer for the reason the old row gave, and a test proves it — three plates up, the *middle* one dies, and the third must still be `nameplate3`. **On blocker (1), the per-plate snapshot**: `PlateState` became `PlateFeed { state, unit, unit_target }` and `sync_nameplates` takes that. One struct, not two parallel slices, so the plate an addon styles and the unit its token names are the same unit by construction. The snapshots come from `ui_unit.rs`'s own `token_snapshot` body, extracted into `SnapshotCtx::full` and now shared with `vplates.rs` — one body for `"targettarget"`, `"npc"`, `"focus"`/`"focustarget"` and the plate pair, so no two of them can come to disagree about one unit. Built in `drive_vplates`' own gate loop rather than a second system, because a second system re-deriving the plate set would be a second gate: the first time the two disagreed, a token would name a unit with no plate. **On blocker (3), `target`: the old row was wrong — it is reachable and is built.** `UNIT_FIELD_TARGET` is a plain descriptor field (`unit_target()`), and the app has made exactly this hop for `"targettarget"` since that token existed; what the `focustarget` comment in `Model::unit` records is that *benilla-ui* cannot make it, which is a statement about the VM crate, not about the client. pfUI needs it: its aggro scanner walks every `nameplate`/`target` pair in `pfValidUnits` with `UnitExists`/`UnitIsUnit`/`UnitCanAttack` (`api/unitframes.lua:186`), which is how a nameplate drives the aggro glow on a unit frame. `nameplatetargettarget` is **not** built: it is one more hop for a third snapshot per plate (120 a frame at 40 plates) and the one scanner that reads it already scores through `target`. **Prefer absent over wrong, three places.** A plate whose unit the app could not snapshot names *nothing*, not a blank unit — `UnitExists` answering true for a token with no GUID, no name and no health is undetectable by the caller, and a zero GUID would additionally make `Model::unit`'s raw-GUID fallback resolve `"0000000000000000"` to it. `UNIT_FIELD_TARGET`'s zero ("targeting nothing") is rejected before the object-manager lookup rather than after. And a free slot's token names nothing rather than the last unit that held it. **`NAME_PLATE_CREATED`'s `arg1` is a frame**, the one event in this client whose argument is a widget, so `ScriptValue` gained a `Frame(u32)` variant carrying the arena id — still plain data, no mlua handle, so the no-persistent-`Function`-handles rule (`LUAI_MAXCSTACK`) is untouched. It resolves through the cached wrapper (`0x701bd0`), because pfUI guards on `registry[arg1]` object identity and parents its whole overlay to that table; a fresh table per fire would build a second overlay per plate and lose it. **Not built, deliberately**: `UNIT_AURA`, `UNIT_FLAGS` and `UNIT_SPELLCAST_*` do not fire *for* `nameplate` tokens, though pfUI's module has arms for all of them (`:628`, `:651`, `:665`, `:681`). Those are field-transition events and would mean running `ui_unit.rs`'s `fire_transitions` 40× a frame; without them pfUI falls back to its own throttled `OnUpdate`, which it has and which is its no-ClassicAPI path, so the module works — it just refreshes on a throttle instead of on an edge. Tracked as its own item, not folded in here. **Harness: unchanged at 11/13 load clean, 10/13 session start, 10/13 survived use — and that is the honest reading, because `examples/addon_harness.rs` drives no world, no camera and therefore no plate, so it cannot see this feature at all** (it never calls `sync_nameplates`; `grep` finds no `nameplate` in it). The end-to-end check it cannot make is instead a test: `script/tests/nameplate_tokens.rs`'s `pfuis_own_oncreate_body_runs_on_the_frame_the_created_event_hands_it` transcribes pfUI's `OnCreate` (`:756-897`) — `parent:GetChildren()` positionally, the six regions by index against its own `NAMEPLATE_OBJECTORDER`, `DisableObject` on each, `HookScript("OnValueChanged")` on the health bar, the raid icon reparented onto its own frame, `SetScript("OnUpdate", nil)` — and then its `_UNIT_ADDED` body, and asserts the module reaches the end on every plate. **Ambiguity cost: zero, measured.** `Update (actionable)` is 5581 against a ceiling of 5581, unmoved, so `UPDATE_ACTIONABLE_CEILING` is untouched. No new system (the work is inside the existing VM-holding `drive_vplates`, so `every_vm_holder_in_update_declares_its_side_of_the_tick` sees no new holder), and the four new `PlateWorld` fields are all read-only (`Res`, `Res`, one `Query<(&ObjectStore, Option<&Transform>), With>`), which Bevy does not count against another reader; `WOW_AMBIGUITY_DUMP=1 … | grep drive_vplates` gives 48 pairs, every one a `Transform`/visual writer the plate loop's pre-existing queries already paired with, and none naming `GuidIndex` or `ChrClassTable`. No new benilla-app file, so `ui_script_wall`'s game-file count stays at its pre-existing 43. No new bare Lua globals (`C_NamePlate` already existed) and no new widget methods, so neither `reference_surface.rs` nor `widget_surface.rs` changes; the three event names are modern, absent from `reference/1.12-events.tsv`, so `event_shape_gate` skips them as it does `UNIT_CASTEVENT` | | Per-`nameplate` field events (`UNIT_AURA`, `UNIT_FLAGS`, `UNIT_SPELLCAST_*`) and `nameplatetargettarget` | todo — reachable, costed, deferred | Split out of the row above rather than folded in, because these are the measurable remainder of that feature and nothing else blocks them. pfUI's nameplate module has an arm for each (`modules/nameplates.lua:628` `UNIT_FLAGS`, `:651`/`:665` the cast pair, `:681` `UNIT_AURA`), each guarded by `strfind(arg1, "^nameplate")`, and each only sets a dirty flag that bypasses its own throttle — so their absence costs *latency*, not function: the module falls back to the throttled central `OnUpdate` it keeps for exactly the no-ClassicAPI case, which is why the plates style correctly without them. The work is not a binding: `UNIT_AURA` and `UNIT_FLAGS` come from `ui_unit.rs`'s `fire_transitions`, which diffs a token's previous snapshot against its current one, and running that per plate means 40 snapshot diffs and 40 memo entries a frame on top of the 40 builds the token feed already does — a real per-frame cost for an earlier repaint, which wants measuring against a live session rather than guessing. The cast pair additionally needs `C_Spell.UnitCastingInfo` / `UnitChannelInfo`, which the "ClassicAPI tail" row above still lists as unbuilt (pfUI's `PollCastInfo` calls both), so a cast event with no way to read the cast behind it would fire into a handler that learns nothing. `nameplatetargettarget` is one more descriptor hop for a *third* snapshot per plate (120 a frame at 40 plates); the only caller is pfUI's aggro scanner, which reads it as the second of two terms and already scores through the first (`nameplatetarget`, built), so the token currently names nothing — recognised, as the prefix rule has made it since the earlier pass | | `IconDataProviderExtraType` | todo — the constant is the smallest part | Not built, because installing it fixes nothing and the values are unverifiable. pfUI only ever passes it *through*: `CreateIconPicker(name, parent, extraType, ...)` hands it straight to `CreateAndInitFromMixin(IconDataProviderMixin, extraType)` (`api/ui-widgets.lua:1288`), and the picker then calls `provider:GetNumIcons()`, `:GetIconByIndex(i)`, `:SetIconTypes({ IconDataProviderIconType.Spell })` and `:Release()`. So `IconDataProviderMixin`, `IconDataProviderIconType` and `CreateAndInitFromMixin` are all absent too (the harness lists `CreateAndInitFromMixin` among its missing globals), and the mixin itself needs an **icon catalog** — an enumeration of every `Interface\Icons` texture in the patch chain — which this VM has no feed for at all. Installing the enum alone moves the raise from module scope into `EnsureProvider`. And the values cannot be taken from the one call site, which only reads `.Spellbook` and `.Equipment` as opaque seeds: modern WoW builds this through `EnumUtil.MakeEnum` (1-based) where a plain table literal would be 0-based, and nothing available here distinguishes them. Two call sites, `modules/macroicons.lua:36` and `modules/equipmentmanager.lua:388`; both lose an icon picker, neither loses anything else | | `C_Map.GetMapOverlays` — the `tiles` array | done — the shape the caller actually reads | Supersedes the `Map (C_Map)` row above on this one member. The function existed and was returning every field *except* the one pfUI's reveal pass walks: `modules/mapreveal.lua:151` is an unguarded `for _, tile in ipairs(overlay.tiles) do`, so "reveal unexplored map" raised `bad argument #1 to 'ipairs'` on the first row and drew nothing. Each cell now carries `file`/`width`/`height`/`texCoordX`/`texCoordY`/`offsetX`/`offsetY` — exactly the seven the call site hands `SetTexture`/`SetSize`/`SetTexCoord`/`SetPoint`, and no others. **The grid is the reference's own**, `WorldMapFrame.lua:112-159` off the player's chain: 256-px cells, row-major numbering from 1, the remainder cell on the last row and column. **The file sizes are measured, not predicted**, through the `TextureSizeProbe` `ui_script/lifecycle.rs` already installs — and that is the whole reason the member needed work rather than a rename, because this install's art is **not** the reference's: every `Interface\WorldMap\\` overlay is re-exported at 2x. Measured over the whole table: **684 of 707 rows ship tile files whose dimensions are exactly 2x what the stock loop assumes, and not one matches 1x**. The stock loop survives that only because its `span / assumedPad` fraction is scale-invariant; it shears the moment a shipped pad is not the assumed one times that factor, which is what pfUI's own comment warns about and names (`Icepoint/KANEQNUUN`). So the per-overlay factor is read off tile 1 and every fraction is taken against the real denominator. Verified on real art, not a fixture: `world_map_tests.rs`'s `pfuis_reveal_body_tiles_the_real_alterac_overlays_rects_exactly` runs the addon's loop over Alterac's 16 shipped rows with the live probe and pins `GALLOWSCORNER` at the rect's 183x185 with a 366/512 crop — 183/512 if the factor is missed. **What this still cannot do**, stated because a caller cannot tell: the grid *shape* comes from the DBC rect, because a power-of-two pad bounds its content without determining it — a 1-wide/2-tall row (`Alterac/CORRAHNSDAGGER`) and a 2-wide/1-tall one ship indistinguishable pad sequences, so fitting the grid to the files alone misreads it. 23 of the 707 rows have art that disagrees with their rect, in three shapes: 6 ship **fewer** files than the grid asks for (those cells are **omitted** rather than listed, so a caller never acquires a texture for a file that is not there), 3 ship **more** (ignored — and 121 further rows ship extras past a grid that otherwise matches exactly, so appended files are ordinary in this data rather than a signal), and 14 ship the right *number* of files at the wrong *sizes* — `KANEQNUUN` among them, which has a sliver column the rect rounds away entirely and whose own tiles are trimmed on the other axis too, so no consistent grid fits it at all. Those 14 keep the rect's geometry with the measured denominator, which is undistorted-and-clipped rather than stretched, and is as close as a pad that bounds its content can get. Cost: one texel probe per tile per zone, memoised by path in the existing probe (~20-40 ms on the first open of a zone with overlays, nothing after); a probe-less VM (a bare `UiScript`, the addon survey) gets an empty `tiles` array, which is still an array | | `C_Container.GetContainerItemCharges` (third entry) — **and why pfUI's bag loop is not the bug it looks like** | done — a reversal, and the earlier reason was looking in the wrong place | Supersedes the two `todo, blocked` rows above. The recorded reason was that nothing in **`benilla-formats`'** item template says which of an item's five spell-trigger slots is a consumable charge pool — true, and irrelevant: item templates do not come from a DBC here, they come over the wire. `benilla-protocol`'s `ItemSpellEntry` carries all five blocks with their own signed `SpellCharges`, so "which block is the pool" is answered by the block itself, and `ui_items/feed.rs` was **already** making exactly that choice — the tooltip's "N Charges" line has been printed off it all along (`charges_count`, the port of the builder's `0x52d8a0` gate). All that was missing was carrying the *instance's* countdown of the chosen block onto `ContainerSlot`. Now `charge_pool` returns the block and the two readers take different halves of it (the tooltip the template's full pool, the bag verb the live `ITEM_FIELD_SPELL_CHARGES + index`), so they cannot come to disagree about which block an item's charges live in. **The second half of this row is the fault that was reported as separate.** `pfUI/modules/bags.lua:210` raising `attempt to index field '?'` while looping `for bag = -2,11` over `pfUI.bags[bag].slots` is **not** an independent bug and needs no client-side special case: `pfUI.BACKPACK` and `pfUI.BANK` union to exactly `{-2..11}`, `CreateBags` creates a frame for every id in its list **unconditionally** (`:313`, before any `GetContainerNumSlots` read), and both calls run from one `PLAYER_ENTERING_WORLD` handler (`:133`, `:136`). So nothing about a real client's `GetContainerNumSlots`, its keyring at bag -2 or its bank bags answers differently — the loop is safe there because the table is complete. It was incomplete *here* because `:452`'s `if charges > 1` raised **inside** `CreateBags`' own `UpdateSlot` call (`:322`), aborting the handler before line 136 ever ran and leaving `pfUI.bags[-1]` and `[5..11]` absent for the next `BAG_UPDATE`. One nil return, two reported faults; the cascade is visible in `logs/errors_2026-10-07_16-40-25.log:327-339`. Hence the shape rule here: a **number for every `(bag, slot)`**, 0 for an item with no pool, an empty slot or an absent bag — the same 0-not-nil choice `GetContainerNumFreeSlots` already takes, and all three are true statements. **What is chosen rather than confirmed**: the arithmetic. "Uses left" is the member's documented meaning, but the other plausible candidate is `stackCount x usesPerItem`, and the two differ for a *stack* of charged items. That is safe here and the safety is checked rather than assumed — pfUI reaches the verb only inside `if count == 1`, where the candidates coincide, and only ever tests `> 1` (`container.rs`'s `container_item_charges_answers_a_number_for_every_slot_so_pfuis_bag_loop_cannot_raise` transcribes that block). SuperWoW's own `GetContainerItemInfo` negative-stack-count row above is blocked on the *same* mistaken reason and should be re-read next to this one | | `GameTooltip:GetGameObject` / `HasGameObject` | done — a deliberate reversal, same mechanism as `GetUnitGUID`, 17x the cost | The `Newly surfaced by the above` row records this as `todo` with two reasons. One was right and is now fixed the way `GetUnitGUID`'s was; the other was simply wrong. **Right**: nothing recorded a GameObject *subject*, and re-deriving one at read time would answer about whatever the pointer is over *now*, where the getter promises the object the plate on screen is describing. Fixed by recording it at build time — a third `TooltipSubject::GameObject { object_id, name }` variant written by `world_tooltip_gameobject` and dropped by `clear_content` with the lines it described, exactly the `TooltipState::unit_guid` shape. **Wrong**: "there is no object-template cache keyed like `item_templates`". `benilla-app`'s `GameObjectTemplates` is a `QueryCache` keyed by precisely that entry, and `ui_tooltip/mod.rs` was already holding the resolved template *and* the guid in hand when it called over to build the plate — only the id was being dropped at the seam. Built because the absence cost **236 raises in one play session**, seventeen times the 14 that justified `GetUnitGUID`, and for the same structural reason: pfQuest hangs its reader on a frame **parented to** `GameTooltip` (`map.lua:253`), so every tooltip that becomes visible fires it — unit plates, item plates, pfUI's xp bar, pfQuest's own tracker rows. Shape pinned by the live call site, not by memory: `map.lua:296` is `local _, oid = GameTooltip:GetGameObject()` feeding `pfMap.tooltips_object[oid]`, so it is `GetUnit`'s two-value `(name, id)` with the entry in the token's place and **no third value**, nothing pinning one. The id is `OBJECT_FIELD_ENTRY` and deliberately not the guid: that is what the caller's own comment at `:294` asks for ("sourced from the spawn-packet template field rather than parsed out of a GUID") and the only thing a static node index can be keyed by — a per-spawn guid matches nothing. The corpse plate borrows the same builder for its one gold line and records **no** subject, so `HasGameObject()` stays false there. The test fails against three rejected designs, not merely against absence: `tooltip_anchor_tests.rs`'s `pfquests_own_tooltip_hook_reads_the_object_entry_off_the_plate_the_world_hover_built` transcribes the addon's `OnShow` body, and a missing method raises at the call, a name-only return leaves `oid` nil so the index is never reached, and a guid in the id's place matches nothing. Widget method, so `tests/widget_surface.rs`'s `ALLOWED_BEYOND`, not the global list. **Honest correction to that earlier row's framing**: these 236 raises were *not* why pfQuest showed no tracker and no icons. `script/event.rs` records a handler error rather than re-raising it (`:157-171`), so the raise only truncates the tail of pfQuest's own `OnShow` — it costs the addon its GameObject tooltip annotations and 236 log lines, nothing more. The icons had a different cause; see the `GetQuestLogLeaderBoardID` row below | | `GetQuestLogLeaderBoardID` + `C_QuestLog.GetQuestDetails` — **this is why pfQuest drew no tracker and no map or minimap icons** | done — two reversals, one of them the actual blocker | `compat/classic_api/quest_log.rs`. Both were refused in that file's header for reasons that have to be separated, because only one of them was about data. **`GetQuestLogLeaderBoardID`**: the stated reason was that `QuestLogObjectiveView` carries `kind`, `text` and the `cur`/`req` pair but no creature/object/item id, "so there is nothing to return". True of the view and untrue of what fed it — `ui_quest_log.rs`'s two line builders had `ReqCreatureOrGOId` and `ReqItemId` in hand when they formatted the sentence and were discarding them. The view now carries `QuestLogObjectiveView::id`, with the wire's `0x80000000` GameObject marker **stripped** (the bit is what `kind` already says; leaving it in would make every object id a nonsense 2-billion number). **Why this matters far more than its 0 logged raises suggest.** `pfQuest/database.lua:1470` calls it *unguarded*, inside `pfDatabase:SearchQuestID` — the one function that turns each quest in the log into map and minimap nodes — in a `for i = 1, GetNumQuestLeaderBoards(qlogid)` loop that this client's own objective count makes non-empty. So for any quest in the log with objectives the nil global raised and `SearchQuestID` returned before it handed back its map table: no pins, no minimap icons, and a tracker with nothing to list. It did not appear in the error log because the loop only runs once a quest with objectives is in the log; the 236 `GetGameObject` lines were the loud symptom of a different, harmless gap. The id is the function's whole point — pfQuest sorts by it into `parse_obj["U"]`/`["O"]`/`["I"]` — so the test transcribes that scan (`pfquests_node_builder_scan_sorts_real_objective_ids_into_its_unit_and_item_tables`) and fails against a nil global, a 0 id and an unstripped marker alike. Argument order is the stock verb's on purpose: the addon calls the two side by side on the same pair (`database.lua:1469-1470`, `map.lua:366-367`). **`C_QuestLog.GetQuestDetails`**: refused as one of four probes of a standalone `CMSG_QUEST_QUERY` cache. That classification is right for the other three and wrong for this one — both live call sites ask only about quests they already walked out of the log by an id `GetQuestIDForLogIndex` gave them (pfUI `modules/questitem.lua:5`, pfQuest `database.lua:732`), so the honest subset is the whole of what is asked, and nil off the log is a branch both callers already take. Shape from the call sites, the only specification available: `title`, `level`, `objectives` (the objective **paragraph**, because `GetQuestText(id, "O")`'s other branch is one shipped locale string and a list there would be the wrong type), `description`, and `requirements`, an `ipairs` array of `{ kind, id, count }` plus the `have`/`finished`/`text` the row already knows. The `id` is what makes pfUI's module do anything at all — it gates on `req.kind == "item" and req.id and req.id > 0` — so without the feed change above it would have been a verb that never fired. 4 raises for `GetQuestDetails`, and pfQuest's own quest-text reader stops falling back to its shipped locale table for quests in the log. `RequestLoadQuestByID`, `IsQuestDataCachedByID` and `GetTitleForQuestID` stay unbuilt for the original reason, unchanged | | `UnitCanAssist` | done | `script/unit/bindings.rs`, beside `UnitCanAttack`/`UnitIsFriend`/`UnitCanCooperate` — **not** in `compat/`, because it is a real 1.12 verb (`reference/1.12-globals.tsv:18496`, `0x516bb0`, registered at `.data 0x8504c8`) and so needs no `allowed_beyond_1_12` entry; adding one would fail `the_exception_list_has_no_dead_entries`. It was simply never installed, and the cost was **649 raises in one session, the largest single count in the log**: `pfUI/modules/nameplates.lua:14` captures it as a file-scope upvalue, so the local stays nil for the module's life, and `:214` is an early-out inside the per-plate combat-tint path — every raise aborted one plate's styling partway. `libs/libpredict.lua` uses it three more times as the heal-prediction self-cast switch. All four call sites read a single value for truthiness with no destructuring, so `flag(bool)` (1 or nil, never a Lua boolean) is the whole contract. The predicate is `CanAssist`'s own three terms (`0x6066f0`), spelled the same way `benilla-app`'s port of the same engine function already spells them (`target/relations.rs`'s `can_assist`, which gates `UnitBuff` visibility) so the Lua verdict and the engine's cannot disagree about one unit: `UNIT_FLAG_NOT_SELECTABLE` clear, a reaction of at least `UnitReaction`'s 1-based 5 — the same cut `UnitIsFriend` takes, and the internal `>= 4` the engine applies plus the 1 it adds at `0x51683e` — and, for a unit that is not player-controlled, its PvP bit. That third term is the one a reaction-only stand-in gets wrong, and it is why a priest cannot heal a quest giver; a test enumerates all three. Two narrowings, each inherited from what `UnitState` carries rather than chosen: `IsPvP` reads the *owner's* flag first (`0x5ee5a0`) and the snapshot carries `owner` as a guid with no flags beside it, so a hostile minion that is not itself player-controlled reads its own bit (a player's pet carries `UNIT_FLAG_PLAYER_CONTROLLED` and takes the arm above, which is the case that matters); and the reference's player-controlled arm reads an unnamed `[obj+0xe68]` record that is not ported app-side either, so such a unit passes. `CanAssist(P, P)` is true in the reference (`0x6061e0` answers 5 for a unit toward itself) but every feed site holds the reaction *toward* the player, leaving the player's own snapshot at 0 "not fed", so a guid compare restores that one answer instead of leaving it silently nil | | `MouselookStart` / `MouselookStop` / `IsMouselooking` | partial — `IsMouselooking` is real, the pair are documented no-ops | `script/pointer.rs`, beside `GetCursorPosition`/`GetMouseFocus`; all three are 1.12 engine verbs (`reference/1.12-globals.tsv:11821`, `:11822`, `:10611`), so no surface exception. `IsMouselooking()` is **backed by the real camera rig**: `CameraControl::mouselooking` — the reference's own `[cam+0x90] & 1` (set `0x50fe41`, cleared `0x50fddd`), which is `freelook`, a right-held turn or a both-button run and deliberately **not** a left-button orbit — pushed each tick as a new `SelfMovement::mouselooking` field. That rode into `ui_unit.rs`'s existing `feed_self_movement`, a system that already holds the VM, for **zero measured ambiguity cost**: `WOW_AMBIGUITY_DUMP=1` shows three new pairs naming `feed_self_movement` and the actionable total stays at 5581, so `UPDATE_ACTIONABLE_CEILING` is untouched. **The pair do not start or stop a look session, and that is a behavioural deviation rather than an absent value, so it is stated plainly.** benilla's mouse-look is not a mode the engine can be put into from outside: it is latched per frame from the buttons *the world owns* (`player/camera.rs`'s `latch_world_mouse` then `run_look_session`), the session begins on a world press and ends when that press is released, and a press captured by a frame is dropped before the latch sees it. A Lua `MouselookStart()` has no press behind it and therefore no release edge either, so forcing the flag on would leave the camera turning with nothing able to stop it — worse than doing nothing, and not what the reference does, where the call joins a session the hardwired `BUTTON2` binding owns and that binding's release ends. `bindings/commands.rs` records the same fact from the other side (`TURNORACTION` declared absent, "right-button mouse-look is hardwired, not routed through a binding"). The one corpus caller is pfUI's nameplate right-click emulator (`modules/nameplates.lua:1899-1934`), which calls `MouselookStart()` on a plate's `OnMouseDown`, polls `IsMouselooking()` to tell a drag-look from a click, and synthesises the click once the poll says no look is running. The nil global aborted that handler **before** it armed the poll, which is why right-clicking a pfUI nameplate did nothing at all; with these installed the poll runs and reads false, which is the truth, because the press that armed it was captured by the plate frame and so is a press the world never owned. 2 raises, and a feature back. Making the pair real needs a look session the VM can open and close — a camera state-machine change, not a binding. `script/tests/input.rs`'s `pfuis_nameplate_right_click_emulator_arms_and_then_synthesises_the_click` transcribes the emulator and fails against the absent global | | `C_PlayerCache` (`RememberPlayer`, `GetPlayerInfoByName`, the four enable toggles) | done — the store's writer and its reader are the same addon | `compat/classic_api/player_cache.rs`, superseding this member's entry in the "ClassicAPI tail" row above, which classified it as needing a 1.12-absent subsystem. It needs no feed at all, and the reason is worth stating because it is the thing that makes the one unverifiable part unverifiable-but-safe: pfUI **fills** the cache itself from every roster, inspect and whisper scan it already runs (`libs/libunitscan.lua:42-47`, `RememberPlayer(guid, name, class, UnitRace(unit), UnitSex(unit))`) and **reads it back** by name to colour a player link in chat (`modules/chat.lua:830`). So every value here is the caller's own, round-tripped — nothing derived, inferred or invented. The one thing that could be wrong is which return slot carries the class token, and it cannot be, because that slot carries exactly the argument the caller named `classToken`. Return order is `GetPlayerInfoByGUID`'s and is pinned by that call site's destructuring rather than by memory: `local _, class, _, raceKey, sex = ...`, five values with the class token second and the race key and sex fourth and fifth. The two slots the store has nothing for answer **nil rather than a guess** — slot 1 is the *localized* class name and slot 4 the *English* race key, where `RememberPlayer` is handed a class token and `UnitRace`'s localized first return, and faking the other spelling of either means a reverse lookup through a localized string, wrong on any non-enUS realm. A miss answers **no values at all**, which is what lets the caller's `if not class then class = GetUnitInfo(real) end` fall through to its own scan instead of colouring with an empty string. The honest limits: benilla's `names::NameCache` is guid→name only, so a player this session never scanned is absent even if they are standing in front of you, and nothing persists across a logout where upstream's hooks the client's own cache and does. The four toggles are preference state with nothing downstream, exactly as `container.rs`'s bag-sort pair is (upstream's gate the DLL's background name scan; this client runs none), both defaulting **on** so an addon that gates on `IsEnabled()` is not told the store it can write to is closed. 7 raises. `GetPlayerInfoByGUID` and `UnitNameFromGUID`, the two bare siblings, stay unbuilt — neither is called, and a guid-keyed class/race lookup genuinely has no backing here | | `C_Item.GetItemUniquenessByID` (restated) — the 404 was a wrong branch | todo — reason sharpened to something actionable | No change to the decision; the row above is still right that the *fact* is here (`ItemTemplateView::max_count`) and the **return shape** is not, and that guessing is invisible to the caller in both directions. What is new is that the prerequisite it named is half done. Upstream's `docs/API.md` does not 404 because it is gone — it 404s because the path cited there is on `main` and the repository's default branch is **`master`**. `brues-code/ClassicAPI` is public, its tree is readable, and the two files that answer this question outright are `src/item/Uniqueness.cpp` and (for the charges row above) `src/item/Charges.cpp`. It is still `todo` because the fetcher available in this pass could not read them honestly: `docs/API.md` resolves on `master` but is **truncated before the Item section**, and the summariser demonstrably confabulates past the cut — asked for the source tree it invented directories (`src/romance`, `src/witch`, `src/worship`) that cannot exist in a WoW API DLL. Anything it "quoted" past the truncation is therefore unusable as a signature, which is exactly the standard this row was written to protect. Next pass: read those two files directly (a git clone or an authenticated API read, not a summarising fetch) and the shape falls out of the `Push*` calls and the `return N`. One raise, `pfUI/modules/itemcount.lua:16` | | `C_NewItems` (restated) — re-verified, unchanged | todo — the objection survives new information | Re-read against the live log rather than re-decided, and the two reasons the row above gives both hold. (1) benilla fires no `BAG_NEW_ITEMS_UPDATED`, and pfUI's module refreshes its glows from `EventRegistry:RegisterFrameEventAndCallback` on exactly that event (`modules/newitem.lua:60`), so a perfect `IsNewItem` would still never repaint. (2) The cheap version is a *positive* claim that a just-looted item is not new, indistinguishable by the caller from a correct one, which turns a loud failure into a feature that is permanently and silently off — the same objection that keeps `GetItemUniquenessByID` out. What the upstream tree would add, for whoever builds it: `src/item/NewItems.cpp` is the file to read, on `master`, and the event is upstream's own to fire — so firing it is part of the job, not an excuse not to do it. 3 raises, and they are low because the harness never opens a bag | | `IconDataProviderExtraType` (restated) — re-verified, unchanged | todo — still fixes nothing on its own | Checked against the current surface rather than re-argued: the row above's claim still holds in full. `IconDataProviderMixin`, `IconDataProviderIconType` and `CreateAndInitFromMixin` are all still absent (the addon survey still lists `CreateAndInitFromMixin` among its missing globals), pfUI still only passes the constant *through* `CreateIconPicker` into `CreateAndInitFromMixin(IconDataProviderMixin, extraType)` (`api/ui-widgets.lua:1288`), and the mixin still needs an enumeration of every `Interface\Icons` texture in the patch chain that no feed provides. Installing the enum alone moves the raise from module scope into `EnsureProvider`, which is not a fix, and the values are still unavailable from the one call site, which reads `.Spellbook` and `.Equipment` as opaque seeds. 2 raises, both an icon picker neither call site can show anyway | | `HEARTHSTONE_ITEM_ID` — **this, not the quest verbs, is why pfQuest's tracker and icons were still blank** | done — one global, and it turns the whole addon on | `compat/classic_api/container.rs`'s `install`, beside `PlayerHasHearthstone`/`UseHearthstone`, which is where modern FrameXML declares it; `reference/1.12-globals.tsv` has no row for it. **The seven stock quest-log verbs were never the problem.** Measured with the harness read-back probe (`--probe pfQuest --eval …`), which is the only instrument that could see this: `GetNumQuestLogEntries()` answered `3, 2` for the seated log, `GetQuestLogTitle(2)` answered `The Fargodeep Mine, 10, nil, nil, nil, nil`, and forcing `pfDatabase.localized = true` by hand made `pfQuest:UpdateQuestlog()` return `change=true` and fill `pfQuest.questlog` with `62=The Fargodeep Mine/2 176=Kobold Candles/3` — the whole chain below the gate was already correct. The gate: `database.lua:2215-2246`, the last block of the last file of pfQuest's ``, decides whether the server's locale matches its shipped databases by comparing the Hearthstone's live name against `pfDB["items"][loc][HEARTHSTONE_ITEM_ID]` — and `pfDatabase.localized = true` is set *inside* that comparison's `if`. With the global nil, `C_Item.IsItemDataCachedByID(nil)` is false, `Item:CreateFromItemID(nil)` builds an **empty** item whose `ContinueOnItemLoad` poll can never resolve, and the flag stays nil for the session. `quest.lua:352` — `if not pfDatabase.localized then return end` — is the first statement of pfQuest's `OnUpdate`, so every frame returned before `UpdateQuestlog`, before `SearchQuests`, before the queue drain that calls `SearchQuestID`: no tracker rows, no map pins, no minimap icons, for the whole session. **Silent by construction**, which is why it outlasted two earlier passes: reading `t[nil]` is legal Lua, a callback that never fires raises nothing, and `nil == nil` in pfUI's two comparison sites (`libs/libbagsort.lua:19`, `modules/itemcount.lua:16`) is a branch that is simply never taken. The value is not from memory — `container.rs` already held the same const for its two hearthstone verbs, and pfQuest's own shipped table is `[6948] = "Hearthstone"` (`db/enUS/items.lua:5642`), indexed *by* this global, so a wrong id would miss the key and leave the flag nil just as surely. The test transcribes that block whole (`pfquests_locale_check_sets_its_run_gate_from_the_hearthstone_the_bags_hold`) and fails against a nil global and a wrong id alike. **A note on the instrument**: the harness *survey* is blind to this entire class of fault and its table is byte-identical before and after the fix — 13/13 load, 13/13 session, 13/13 UI probe, 10/13 survived use, and the missing-globals ranking never listed `HEARTHSTONE_ITEM_ID` because the scan ranks names that are *called or indexed*, and this one is only ever compared or used as a table key. Only `--probe`'s read-back found it | | `C_CreatureInfo` reaches every VM, not just the boot one | fixed | `ui_chr_races.rs`/`ui_chr_classes.rs` fed their tables from a **`PostStartup`** system — once, at app boot — so the VM that existed then got them and no other. World entry and `/reload` each build a *fresh* VM, which saw an empty `C_CreatureInfo`. Not cosmetic: pfQuest builds its race and class bitmasks at **file scope** (`database.lua:402-413`) and `GetBitByClass` has no fallback (unlike `GetBitByRace`), so an empty table made it answer nil and `bit.band(…, nil)` raise in `QuestFilter` (`database.lua:1707`) **every frame** — 380 raises in one real session, with `SearchQuests` aborting before its queue drain. That is the FPS collapse the user reported, the missing map icons, and the tracker that only filled when some other path happened to populate it. Each feed's body is now a `seed_*` function the VM-construction path in `ui_script/lifecycle.rs` calls directly — seeded **there** rather than by moving the systems to `Update`, because pfQuest reads the tables at file scope, so a push one frame later is a push too late. A test transcribes pfQuest's own file-scope loop and was verified to report an empty bitmask (`left: 0, right: 1`) without the seeding | | Harness fixture: `C_CreatureInfo`'s race/class tables, the Hearthstone's item template, and a zone | done — three states the live client always has and the harness never did | `addon_harness/mod.rs`'s `seat_a_session`. Not client code; recorded here because each one was hiding a feature the harness therefore could not reach, and two of them looked like client faults. **Race/class tables**: the live client pushes them once in `PostStartup` off the player's own `ChrRaces.dbc`/`ChrClasses.dbc` (`ui_chr_races.rs`, `ui_chr_classes.rs`); the harness seated none, so pfQuest's `for id = 1, 32` walk (`database.lua:402-412`) built empty `pfDB.bitraces`/`bitclasses`, and `GetBitByClass` — which unlike `GetBitByRace` has **no fallback** — answered nil, which reached `bit.band(…, nil)` in `QuestFilter` (`:1707`). That raise aborts `SearchQuests` at `quest.lua:383`, which runs *before* the queue drain in the same `OnUpdate`, so it also cost every `SearchQuestID` map node, and `updateQuestGivers` is only cleared after the call, so it re-raised every frame. Seated from this crate's own `ui_unit::race_names`/`class_names`/`race_faction_group` rather than a second copy, so the fixture cannot disagree with its own `UnitRace`/`UnitClass`; the 1.12 class gaps at ids 6 and 10 stay gaps. **Item template for 6948**: a live bag push is itself what queries each slot's template (`ui_items/feed.rs` calls `items.template(entry, …)` per slot), so a logged-in character always has it; the harness held a Hearthstone in bag 0 with no template behind it, which made `IsItemDataCachedByID` take the 600-tick poll arm that a ten-tick session start never finishes. **A zone**: `area.rs` pushes four host globals on every area change, and pfQuest's default `trackingmethod` 5 matches a quest's log header against `pfMap:GetCurrentZone()`, whose fallback is `GetRealZoneText()` (`map.lua:678`) — with no zone its tracker lists nothing however full the log is. Elwynn Forest, to agree with the seated log's header row. With all three in place the probe now walks the whole feature end to end: `localized=true`, `questlog=2`, `nodes=194`, `trackerrows=2 [The Fargodeep Mine, Kobold Candles]`. The survey table is unchanged by all of it | | Quest-watch persistence (`Model::quest_log_watched`) | todo — **buildable, but the reference behaviour is the unconfirmed part, not the plumbing** | The set lives only in the VM (`script/model.rs:977`); nothing in `crates/` reads or writes it to disk, so a relog or `/reload` — which rebuilds the VM — drops every tracked quest. Two facts are settled and worth recording so the next pass does not re-derive them. **It is client-side**: `reference/1.12-message-catalog.tsv` has no quest-watch opcode at all, and `reference/1.12-shapes.tsv` lists `AddQuestWatch` (`0x4e1930`), `RemoveQuestWatch` (`0x4e1970`), `IsQuestWatched` (`0x4e1890`) and `GetNumQuestWatches` (`0x4e1860`) as engine globals on one vtable (`0x84b0f0`), so no server round trip is involved either way. **And the implementation is cheap and safe**, correcting an earlier worry: `quest_log_watched` is a `Vec` of **quest ids**, not log indices (`script/quest_log.rs`'s `watch_id_at` resolves the index to `QuestLogEntryView::quest_id` on the way in), so persisting it restores the right quests even when the log order changes — a `benilla-config/` file through `local_state.rs` plus a resolve once the log lands. What is **not** established is whether the reference 1.12 client persists the array at all: nothing in this tree says so either way (`AUTO_QUEST_WATCH` is the auto-track CVar's label, not the list), and the only claim to that effect was an investigation note with no source. Building it on that would be a silent fidelity deviation in whichever direction the guess went, which this file's own rule ranks below an absence. Not the pfQuest symptom in any case — that tracker's inclusion is not gated on watch state (`tracker.lua:524` uses `watched` for ordering and styling only) | | `QUEST_TURNED_IN` | **built** | `quest.lua:261` registers it and `:310` uses `arg1` as the questID to stamp `pfQuest_history[arg1]`, the authoritative turn-in record that keeps the questlog REMOVE path from having to guess turn-in vs. abandon by name. Not a 1.12 event — `reference/1.12-events.tsv` has no row for it — so it is declared in `reference_ui.rs`'s `ALLOWED_BEYOND` with its reason, beside SuperWoW's `UNIT_CASTEVENT` and the `DEBUFF_*_OTHER` pair. **The earlier note here turned it down on the grounds that the questID was not available at the moment of the turn-in. That was wrong**: `SMSG_QUESTGIVER_QUEST_COMPLETE` carries `quest_id`, `xp` and `money`, `messages/quest/giver.rs` already parses all three, and `ui_quest/net.rs`'s `quest_complete` already had the struct in hand to build its chat lines — the fact was sitting unused one field away. Queued there (`QuestGiver::push_turned_in`, since a net handler has no VM) and drained in `feed_quest` before the snapshot's early-out, so it fires even though the reward click has already closed the panel. Measured cost of its absence: a quest already handed in kept its map and minimap markers. The old note claimed `pfQuest-octo`'s addon-message sync covered this; it does not — that sync **replaces** `pfQuest_history` wholesale with whatever arrives inside a 3-second window (`patchtable.lua:196-219`), so a slow or absent reply empties the history rather than filling it, which makes `/db query` a way to lose the record, not restore it | | `ItemMixin:ContinueOnItemLoad` — the wait now **asks** | done — a one-line correction to a function already built | `compat/classic_api/item_mixin.rs`. Found while closing `HEARTHSTONE_ITEM_ID` above and worth its own row because it is a second, independent way for the same pfQuest feature to stay off. Upstream's `Item:CreateFromItemID(id) … :ContinueOnItemLoad(cb)` is how a caller *requests* an item's data — `RequestLoadItemDataByID` is the same namespace's bare spelling of the same act — and this port implemented only the waiting half: a `C_Timer` poll on `IsItemDataCached`, because 1.12 has no `GET_ITEM_INFO_RECEIVED` edge. But `C_Item.IsItemDataCachedByID` records no ask *on purpose* (its own registration says so: a caller polling "is it here yet?" should not trigger the query), so the poll only ever resolved for ids something else happened to read, and ran its full 600-tick bound to no effect otherwise. Concretely: a character whose Hearthstone sits in the bank has no bag slot reading template 6948, so nothing asks, so pfQuest's locale check never completes and the addon is off for that character even with the global present. The fix is one read through the `*ByID` family, whose shared `template()` helper puts a miss into `Model::item_stat_asks` for `ui_items/feed.rs` to drain — one source of truth for item data, unchanged, and no new plumbing. Once is enough: the app's pending set keeps an unresolved id across frames. The test drives the whole arm — ask recorded, nothing fired while the answer is absent, callback on the first tick after `set_item_template` — and fails on the first assertion with the line removed | | `C_Map.GetAreas` — **this is why pfQuest drew map icons but no minimap icons** | done — one member, and it is the whole of the minimap half | `compat/classic_api/map.rs`, fed by `ui_world_map::seed_area_names` over the `AreaTableRes` the map catalog is already built from; supersedes the `GetAreas` half of the `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo` todo row above, which still stands for the trigger member. **The asymmetry was the whole clue and it is not in the minimap code at all** — pfQuest's two passes resolve the player's zone *differently*. `UpdateNodes` (world map) takes `GetMapZones()`'s name and, on a miss, `customids[GetMapInfo()]`, a table built at file scope from `C_Map.GetMapAreaIDs` (`map.lua:632-660`), which has shipped here since that member landed. `UpdateMinimap` has no such fallback: one line, `pfMap:GetMapIDByName(GetRealZoneText())` (`map.lua:1365`), over `pfDB.zones.revloc` — and this install's pfQuest has **deleted** its shipped name table (`db//zones.lua` is a five-line stub saying the names now come from `C_Map.GetAreas`), so with the member absent `pfDB.zones.loc` was empty, `revloc` was nil, the name lookup answered nil, and the pin loop's own gate `if data[mapID] and minimap_sizes[mapID] …` (`map.lua:1383`) indexed by nil and skipped. **Silent by construction**: `data[nil]` is a legal read, so there is no raise, no log line and no missing-global the harness survey can rank — the name is only ever a table key here, the same blind spot `HEARTHSTONE_ITEM_ID` sat in. Measured with the read-back probe before and after (`--probe pfQuest --eval`): `pfDB.zones.loc` 0 entries → 1,480, `revloc` nil → built, and `pfMap:GetMapIDByName(GetRealZoneText())` nil → **12**, the same id the world-map pass already reached through `GetMapAreaIDs`, which is why the two passes now agree on the key pfQuest's node tables are built under. Two further silent losses close with it: `GetMapNameByID` (the browser's and the tracker's zone labels, `UNKNOWN` for every row) and `GetZoneIndex`, which walks the keys of `pfDB.zones.loc` as the zones to ask `C_Map.GetMapOverlays` for (`database.lua:1104`) — with no keys the subzone index was empty and `SearchZoneID` returned early for every subzone-placed objective. **The shape is pfQuest's own generator, not a guess**: `toolbox/extractor.lua:2084-2104` writes `pfDB["zones"][locale][id] = name` straight out of `SELECT * FROM AreaTable ORDER BY id ASC`, so the answer is the **whole** table (1,480 rows here, 1,400 of them subzone rows with no `WorldMapArea` map — the catalog keeps only the ~80 that have one) keyed by id, ascending. What it cannot fix, said plainly: the caller's reverse `name → id` index keeps the *first* id it meets for a name (`database.lua:285-287`) and 70 of the 1,480 names are carried by more than one row (`Westfall` by both 40 and 206, both top-level), so those names resolve by Lua's own table order. That ambiguity is the shipped table's too and nothing in the DBC says which row a name means; picking one here would be this file inventing a fact. **One correction rode along, and it was forced**: `GetMapOverlays`'s `tiles` array is now resolved on **first read** through a per-VM row metatable instead of being built eagerly (`overlay_row_meta`). A cell is measured by *decoding* its file (`benilla-assets::sprite_dimensions`), and until this member landed nothing ever walked every zone's overlays — `GetZoneIndex` now does, and it reads only `areaID` and the four `hitRect*`. Measured on this install: **1,019 tile files across 707 overlay rows, 1.5 s**, which eager tiles would have spent inside pfQuest's `OnUpdate` at the first quest search. pfUI's reveal is the only reader of `tiles` and is itself gated on `C.appearance.worldmap.mapreveal`, so a player with reveal off now pays none of it; a read of `overlay.tiles` answers the same array it always did and is cached on the row after the first. Also seated in the survey fixture (`addon_harness/mod.rs`), beside the race/class tables and the zone globals and for the same reason: an in-world session with an empty `AreaTable` is a state only the harness reaches, and without it the harness is blind to this whole fault class. Tests: `pfquests_minimap_gate_opens_on_the_zone_the_area_name_table_names` transcribes `database.lua:283-289`, `map.lua:583-595`, the `minimap_sizes` metatable and the `map.lua:1383` gate, and asserts the gate is **shut** on an empty table and open on a fed one; `the_whole_area_name_column_reaches_the_vm_including_the_unmapped_rows` fails a feed narrowed to mapped rows; `an_overlay_rows_tiles_are_measured_on_first_read_and_cached_after` counts probe calls and fails an eager build. **One interaction left standing, with the reason.** pfQuest-octo knows about the duplicate names and dedups them — `overwrites.lua:922-963` deletes 16 "phantom" ids whose names collide with a functional zone's, its own comment naming the exact mechanism ("GetMapIDByName scans the locale table with pairs() (non-deterministic order), so it may return a phantom ID instead of the functional one, causing quests and minimap nodes to not appear inside those dungeons"). That deletion lands on `pfDB.zones.-turtle`, which `patchtable.lua:92` merges into `pfDB.zones.` — and then `patchtable.lua:150` calls `pfDatabase:Reload()`, which **re-points** `pfDB.zones.loc` at this call's table and drops the merged copy entirely. So the dedup no longer applies, and those seven dungeon names resolve by table order again. Left alone on purpose: which id a name "means" there is a fact about *pfQuest's database* (which rows carry NPC and object references — the fork says so in as many words), not about `AreaTable.dbc`, and the client has no way to know it. It is also not a regression in either direction — an empty `loc` resolved **every** name to nil, so all seven were already dead — and the custom zones the deleted `-turtle` names existed to supply are in this install's own `AreaTable.dbc` anyway (Dragonmaw Retreat 5600/5601, Hateforge Quarry 5098/5103, Gilneas City, Frostmane Hollow, The Rock of Desolation), which is the whole reason the fork deleted its shipped tables for this call | | 3D unit portraits on an addon's own `PlayerModel` pane (`CreateFrame("PlayerModel", …)` + `:SetUnit`) | todo — **genuinely not built, not a bug**; the 2D path beside it works | What pfUI asks for: `api/unitframes.lua:1455-1456` creates one pane per unit frame (`pfPortraitModel