Four defects out of his log, and the 65 warnings that are not ours

His 19-minute session put 8 errors on screen from two causes, and 73 WARN lines
of which 8 are ours. The whole triage is written down in the play-test memory,
because most of the log is the addons' own and nothing should re-chase it: five
files their `.toc`s name and do not ship, a QuickHeal template that is never
defined, an ATSW global string that only exists in its deDE branch, inspect
frames from a load-on-demand addon with no `## Dependencies` line, `<Binding>`
elements the frame loader is *supposed* to log, and a world-map lag line that
reported its own recovery 5.2 s later. Four of the eight are fixed here.

`GameTooltip:SetSpellByID` was nil, and it is not a keypress: the traceback is
pfUI's `ButtonEnter` into `SuperCleveRoidMacros/Core.lua:4217`, so it raised on
every action-bar hover of a macro that resolves to a spell — 7 times, each
leaving the button with no tooltip at all. `compat/classic_api/tooltip.rs` had
declined the whole `Set*ByID` family pending "an argument-shape check against
the stock verb each aliases". Done, and the check is why this is four lines:
there is no aliasing to do, because `tooltip_spell::set_spell_by_id` is the body
all four of 1.12's spell verbs already funnel through, and it takes a bare id —
the book slot or list row is what *they* have to convert first. `show_rank` is
the action's rather than the spellbook's, because the book draws "Rank N"
itself as the button's `subSpellName` and an id-named caller has no such label.
The caller's own comment says it reaches this only for a spell in neither
spellbook, so the cold-view path is the common one here; that needs no special
case, since the app's feed builds a view for any id in the catalog and an action
button re-asks its tooltip every `TOOLTIP_UPDATE_TIME` while hovered.

`resolve_spell_fx` despawned an entity the world no longer had. `FxInstance`'s
root is a *stored handle*, and `attach_spell_fx` is the system that notices one
lost with the unit's whole visual and re-arms or drops the instance — so between
that loss and that pass, the same-slot replace, the spell-id reap and the
self-termination clock all hold handles to entities that are gone. `try_despawn`
at those three, as `lifecycle.rs` already does; `tend_world_plants` keeps its
plain `despawn` because its entity comes from a query, not from a record.

The first paste of every Wayland session could never have worked.
`smithay-clipboard` sets `latest_seat` only on a `wl_keyboard` Key/Modifiers
event or a pointer press (`state.rs:522`, `:386`) — `Enter` sets `has_focus` and
nothing else — and refuses to load a selection without it. `HostClipboard`
opened its backend on first *use*, so the backend that read the clipboard was
created by the very Ctrl+V asking for it and its keyboard object was younger
than that chord's own key events. It is now opened as soon as the window can
give the `wl_display`. Costs +2 on the `Update` ambiguity ceiling, both pairs
against the schedule's two exclusive `&mut World` systems, which pair with
everything they have no declared order against.

And the one `changed_windows` ERROR a login prints had no remedy behind it: no
client can warp a free pointer on Wayland, so the loading screen's cursor
hand-back now asks only where the answer can be yes — off Wayland, or with the
pointer locked, which is winit's own rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-28 20:26:05 -06:00
co-authored by Claude Opus 5
parent 31b7d4f228
commit c8c4e7f855
8 changed files with 305 additions and 13 deletions
+1 -1
View File
@@ -256,7 +256,7 @@ separately, so "cannot say" has to be distinguishable from "no".
| 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) |
| 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 rest of the `Set*ByID` aliases (`SetItemByID`, `AddSpellByID`, `SetInventoryItemByID`) — the stock verbs they alias already exist under 1.12 names, each needs its own argument-shape check against them, and no installed addon has asked for one; `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); and `SetSpellByID`, where the deferred shape check found there was no aliasing to do at all — `tooltip_spell::set_spell_by_id` is the shared body every 1.12 spell verb already funnels through and it takes a bare id, so the verb is the existing body reached by the argument it wants. `showRank` is the action's, not the spellbook's, because a caller naming a spell by id has no `subSpellName` label to read the rank off. 7 raises and 7 empty action-button tooltips in one session of grom's: SuperCleveRoidMacros tooltips a macro's resolved spell through it (`Core.lua:4217`), off pfUI's `ButtonEnter` |
| 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 |