434d514879f8981a5d9fd2ebbbc155d5e28daddb
641
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
434d514879 |
superwow: the chat bubbles are widgets — CREATE_CHATBUBBLE and pfUI's skin
The last real SuperWoW feature, and one piece of work closed three things: `CREATE_CHATBUBBLE` (which could not honestly be fired, because its payload can only be the bubble), `C_ChatBubbles.GetAllChatBubbles` (which answered an empty list, truthfully, for the same reason) and pfUI's whole bubble skin, which walks that list. `benilla-ui`'s `script/chat_bubble.rs` is a pool of real frames — 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 it over as `BubbleState`: the same split `vplates.rs` already makes for the plates, so the engine decides what is on screen, the widget layer decides what Lua sees, and neither can disagree about a bubble because there is one feed. Three things improved by the move rather than being preserved by it: - The bubbles 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 `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 this file drops its own texture resource and its own half-texel inset helper. - The system moved BEFORE the paint. It sat in `UiQuadAppend`, the window *after* the extract: right for an appender, a frame late for a feed. The test that matters transcribes pfUI's own `ScanBubbles` loop — it 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 then checks the EXTRACT for 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. Two architectural gates caught this and both were answered rather than silenced. The one-shot gate: `drive_bubbles` now holds the VM and drains a queue, so it is EXEMPT with the argument — the queue's product is app-side state, the widget sync is idempotent and binds on the first frame there is a WorldFrame, and gating the system instead would pile lines up and spawn them all at once with fresh lifetimes. The VM wall: its ceiling rises by one with the reason, which is that this file is `vplates.rs`'s exact twin. `CREATE_CHATBUBBLE` fires per APPEARANCE, not per pool slot ever built — the opposite of `NAME_PLATE_CREATED`'s rule, because a recycled frame has been re-anchored, re-sized and re-texted since it was last seen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
0e02401325 |
superwow: CastSpellByName at a unit, FoV, NameplateMotion, the raw log
The DLL publishes its own **usage strings**, in one block beside the names it registers, and reading them turned up four things this file had wrong — three of them bugs rather than gaps. `CastSpellByName(name [, "unit"])`. A unit ID, which is what every real call site passes and what `Model::unit` already resolves. 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 (a GUID) and /castmouseover (a token), Decursive's cure-at-guid, BigWigs' spell requests and two encounter scripts. Each asked for a unit and got whatever was targeted. The stock `onSelf` meaning keeps its behaviour: only a string that resolves to a live unit takes the new path. `CombatLogAdd`'s second argument names a FILE, not the event. There is a third log literal in the same pool — `Logs\WoWRawCombatLog.txt` — and it is the GUID-form log, the same lines RAW_COMBATLOG carries as arg2. `ui_chat/logging.rs` keeps it beside the ordinary one now, opened by the same `LoggingCombat`, which also makes the arg2 composition worth paying for when nothing is registered but the log is on (`wants_raw_form`). My first reading, "also fire the event", was an inference from the name alone, and the second literal settles it. `FoV`'s hold-back is resolved, and the answer was already in the tree. The row was held because 1.57 looked irreconcilable with CAM_FOVY's 0.785. It is the same number: 1.57 is π/2, the reference's own `[camera+0x40]` default of 90° in radians, and that field is not a vertical FOV at all — `scoped_view.rs` already divides by it as a DENOMINATOR, because what it expresses is a ratio. So upstream's own default is this client's existing lens by construction, which is what the test asserts; the DLL even registers the literal `1.57079`. Live, where upstream's panel says "Requires UI Reload". `NameplateMotion` asked for an investigation that was also already answered: benilla spreads plates with the reference's own SmartScreenRect solver. 0 (Overlap) bypasses it, 1 (Default spread) is it; 2 and 3 are the mod's own algorithms and fall back to 1, a documented silent-equal rather than a refused row. Also: `GetTradeSkillItemLink(index[, spellLink])`, the only way to link a recipe that makes no item — an enchant, whose product_item is 0 and which answers nothing at all on the stock surface. And four stored-only rows (SelectionCircleStyle, UncapSounds, HealingText, InterfaceVolume), each with its own reason, so SuperAPI's options panel round-trips. Three ghosts retired: outline/shadow text, force-show-auras and XP text are in no part of the DLL's string pool, nor is NameplateVerticalSpacing. They came from the early wiki reading, not from the binary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
323f3ae03c |
superwow: the map pair, GetQuestLinkForID, GetWeaponEnchantID
Four more off the DLL's manifest, and two measured corrections. GetMapPositionWorldLoc is the INVERSE of GetPlayerMapPosition, which is what the test asserts rather than three numbers: `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 cannot depend on benilla-world) and the round trip is welded, which is the thing that actually keeps the two in step. GetMapBoundaries shares one new `worldmap::loc_rect_at` with the hover grid's own windowing, so no two readers of a (continent, zone) selection can disagree about which rect it means. GetWeaponEnchantID is the one thing GetWeaponEnchantInfo cannot say: the reference reads that very dword as its gate and then reports only that there WAS an enchant, so nothing on the stock surface can tell Windfury from Rockbiter. The id was already in hand and discarded as a `?` gate, so this was one field on the snapshot, plus a test that the two functions cannot disagree about which hands are enchanted. GetQuestLinkForID answers for a quest in the log and nil otherwise — which is also what upstream answers on the first call for one it fetches. The gap is one feed, not new protocol: the ask-once CMSG_QUEST_QUERY cache already exists app-side. That row also corrected the LINK FORMAT for both quest getters. The DLL's own formatter is `|cffffff00|Hquest:%d:%d|h[%s]|h|r`; ours was built without the colour wrapper on the reasoning that it matched the stock quest-log shift-click — but 1.12 has no quest links at all, so there was no stock shape to match and the measured string is the only authority. The four sound-driver getters are deliberately NOT built, with the reason recorded: every one of their return shapes is unknowable from here, none is called in the corpus, and this client has one backend and no enumeration to report — the same bargain KEY_DOWN/KEY_UP lost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
3b6e91be35 |
superwow: RAW_COMBATLOG and CombatLogAdd, the only two names his addons call
Of SuperWoW's whole surface these were the two names grom's corpus calls
that this client lacked (`scripts/dll-surface.py --wanted`), and the first
is load-bearing out of proportion to its size: `SuperCleveRoidMacros`
funnels FIVE subsystems through one `RAW_COMBATLOG` frame — immunity and
resist learning, hidden-CC detection, debuff-duration learning, and the
reactive-proc window that lights Overpower and Revenge. Its own comment
says why the last of those can come from nowhere else: "SPELL_GO only
provides hit/miss COUNTS, not miss TYPES".
`route` is the fire site: the one funnel every combat line already passes
on its way to a window and to WoWCombatLog.txt. `arg1` is the printed
sentence; `arg2` is the same sentence with each unit name spelled as its
GUID, composed a second time through the same `compose_line` so the two
can only differ in the name slots. Fired before the `CHAT_MSG_*`, which is
what "raw" means upstream. Both the fire and the `arg2` composition are
gated on `has_listener` — a raid's combat log is tens of lines a second.
`CombatLogAdd` is also BigWigs' SuperWoW detection (`if CombatLogAdd
then`), so defining it turns that module's path on.
Three corrections came out of reading the DLL's own usage strings, which
it publishes beside the names — the arity and argument meaning of every
remaining SuperWoW function is now measured rather than inferred, and
`docs/ADDON_COMPAT.md` carries the manifest:
- `CombatLogAdd("text" [, addToRawLog])` has a SECOND argument. Nothing
in the corpus passes it; it would have been missed.
- `SUPERWOW_COMBAT_LOG_EVENT` does not exist. The DLL registers exactly
three events, and that name is in no part of its string pool.
- `IsIndoors` is capital-I beside `IsSwimming`/`IsMounted`, not the
lowercase `isIndoors` two files guessed at — all three are already live
off one feed, so that clause of the "blocked" row was already answered.
- The detection pair is `SUPERWOW_STRING="SuperWoW 2.2 by Balake"` and
`SUPERWOW_VERSION="2.2"`, both quoted. Ours had the string without its
author clause and the version as a number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
68f397af72 |
app+protocol: nampower's combat-log family — 35 events off one decode
Every packet this family reports was already decoded: `combat_log/` builds its chat lines from exactly these structs. `ui_np_events.rs` is a second consumer of that decode, not a second decoder, which is also why its arguments cannot drift from the chat log's. Built: `SPELL_DAMAGE_EVENT_*`, `AUTO_ATTACK_*`, the three `SPELL_HEAL_*`, the three `SPELL_ENERGIZE_*`, `SPELL_MISS_*`, `DAMAGE_SHIELD_*`, `ENVIRONMENTAL_DMG_*`, `SPELL_DISPEL_BY_*`, `UNIT_DIED`, the `*_UPDATE_DURATION_SELF` pair, the thirteen `UNIT_*_GUID`/ `PLAYER_GUILD_UPDATE_GUID` rows and `UNIT_COMBAT_GUID`. **A registration is the switch.** Upstream's README calls every `NP_Enable*Events` CVar deprecated — "registering the event is preferred" — and three of the eleven families have no CVar at all, so a CVar-only reading would leave `SPELL_DAMAGE_EVENT_*` unreachable for every addon and `SPELL_ENERGIZE_*` unreachable for all of them, since nothing installed writes its CVar. `refresh_listeners` reads the VM's own registration table (the same one `GetFramesRegisteredForEvent` reads), so a family is wanted when its CVar is set OR something is listening. That is also what keeps the cost honest: with nobody registered a damage packet queues nothing, rather than building eight arguments for a fire that reaches no frame. Nine `NP_*` CVars move from stored-only to LIVE, and `NP_EnableSpellEnergizeEvents` is registered for the first time — the first pass took the names off `NampowerSettings`' writes rather than off the DLL's own CVar table. The two reasons written at the auto-attack and heal rows were both about the *cast* path, and both were answered by the combat-log decode that was already there. The descriptor indices the GUID-keyed family watches are written out and then cross-checked against the two tables the tree already keeps — `unit_rec::FIELDS` and the `FIELD_UNIT_*` constants. A hand-written descriptor index is exactly the number that is plausibly wrong and silently so: the event simply never fires, which no amount of play distinguishes from "the server never sent one". `AttackerState` gains `sub_damage_count`, which is `AUTO_ATTACK_*`'s arg6; vmangos writes one block, but a server splitting a swing across schools sends more. Two test notes worth keeping: the edge tests run through the schedule, because `run_system_once` builds a fresh system each call and the `MessageReader` cursor restarts — a test written that way passes whatever the system does with the second edge. And the data-gated suite held at exactly 269, after the event gate correctly named all 35 new names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
702eb475a4 |
app+ui: the queue, the disenchant sequencer — 62/62
QueueSpellByName, QueueScript, ChannelStopCastingNextTick, DisenchantAll. Every one of nampower's 62 registered Lua functions is now defined; the `comm -23` in the doc prints nothing. **Upstream's queue has two halves and one is deliberately not built.** The Lua surface — remember this spell, send it when the client will accept it — is `spell/queue.rs`. The other half intercepts *every ordinary press*, so a keypress during a cast is held rather than refused, and it is governed by NP_QueueCastTimeSpells, NP_QueueInstantSpells, the buffer rows and the quickcast pair. That half is not a binding: it changes how every key in the game feels, its bugs are double-casts and dropped casts, and it is not something to switch on as a side effect of porting three function names. Those twenty CVars stay stored-only, each row saying so; the machinery they would sit on is now here, and turning it on is its own piece of work with its own live test. Two of the twenty-two went live, because they are the explicit queue's own knobs: NP_SpellQueueWindowMs and NP_QueueSpellsOnCooldown. Three slots, because upstream has three and its own event says so: SPELL_QUEUE_EVENT's code is 0/1 on-swing, 2/3 normal, 4/5 non-GCD, so the queue is keyed by what kind of cast a spell is — and the kind comes off its own Spell.dbc row through the same three tests the send path makes. Each pops on its own condition: non-GCD and on-swing as soon as no cast guards (neither uses the global cooldown), normal when the GCD has run out too. Three decisions at the queue, each pinned: - A replacement raises no popped event. A second queue of one kind replaces the first — upstream's own log line is `Replacing queued nonGcd spell X with Y` — and the replaced entry was never sent, so a consumer counting pops against queues would see one too many. - An expiry raises nothing either. `*_POPPED` means "this was sent". - The expiry is measured from the end of the cast in flight, not from the moment of queueing, so queueing early during a long cast is not punished. A lapsed deadline cannot shorten the window. **`DisenchantAll` is specified completely by the DLL's own strings**, which is why it is built this way rather than designed. `0x93087` onward carries the argument note, the precondition, the three quality keywords, every stop reason and the three chat lines — one of them as a Lua chunk that builds the item link itself. "move during cast to cancel" is the abort, and it needed nothing new: moving already cancels a cast here, the cast then fails, and a failed cast stops the run. The one piece of machinery a sequencer usually needs is the client's own, and the player's way out is the one upstream documents. How a step's outcome is read: the item is gone, or it is not. There is no "disenchant succeeded" packet — the item leaves the bag. Each step remembers the guid it cast at and waits for it to vanish with a ten-second deadline. An observable test with no new wire plumbing, right for the same reason the cast bar's own edges are. The queue's drain and the sequencer both live inside `drain_spell_casts`, which that file's own note explains: only one system may hold `CastLadder`, because a second would share its resources with every other cast-send system in an order the ambiguity gate cannot know is safe. `spell/queue.rs` names no VM — the first draft mentioned `UiScript` in one comment and `ui_script_wall.rs` counted it as gameplay code naming the VM, which is the gate working as intended: that module really is gameplay state and not a feed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
b652c90a4b |
app+ui+protocol: the four measured-out names, and the unit descriptor by name
Each of these had a reason for being out. Each reason turned out to be
answerable, and two of them were simply wrong.
`GetSpellPower(school)` is **delegated to `GetSpellBonusDamage`**, which
this client already answered — one number under two mods' names, as
SuperCleveRoidMacros says itself ("Reads the same client field
nampower's GetSpellPower does"). Delegating is the only way to guarantee
they cannot drift, and the test asserts equality across all seven
schools and the off-range values rather than a literal. The earlier
"needs a descriptor band fed" was wrong: it was already fed.
`GetRaidTargets()` is the 8-slot **board**, now pushed beside the party
roster. The earlier reason was right about the scan it rejected and
wrong about the alternative: `GroupState::raid_targets` is the
reference's own table and copying it costs eight words a push. A mark
with nothing on it is absent, because a zero GUID is a string that
passes `if t[4]`.
`LearnTalentRank` is one `LearnTalent` press per missing rank, which is
what a player clicking the button does. The earlier reason — the burst
cannot be checked before it is sent — is true and is not a reason to
withhold it: the check that matters is the server's and the client has
never had another. What can be checked locally is, so a rank already
held queues nothing and one past the maximum is clamped.
`GetUnitData` is the whole descriptor, from a new raw block on
`UnitState`. The earlier reason — "a decision about which fields" — is
now a table of seventy columns under the DLL's own names, read off its
string pool in UnitFields order; `GetUnitField`'s scalar half reads the
same block, so the two cannot answer differently for one name, and a
test walks the table asserting exactly that.
**The indices are derived and the derivation is checked.** Only thirteen
`FIELD_UNIT_*` constants existed here; the rest come from 1.12's block
sizes. Those thirteen span index 22 to 147 and every one lands where the
layout puts it — thirteen independent confirmations, run as a test
rather than asserted in a comment, with two more that no columns overlap
and that nothing falls inside the aura blocks' 47..=124.
Three decisions at that mapping:
- An unstreamed column is absent, not zero. `ObjectFields::at` is
`get_raw`'s strictness rather than `get_u32`'s absent-is-zero
convenience: `health = 0` for a unit whose health has not arrived is a
number a caller divides by.
- `aura`/`auraApplications` come from the filtered per-unit aura list,
not the raw slots — the same place `GetUnitField` has always answered
them from. An unfiltered second answer would be a different unit by
the same name.
- `status` and `zoneId` are left out of nampower's seven computed
columns: nothing says what `status` is a status *of*, and a zone is
only knowable for the player. The other five are filled by the caller,
which knows them.
`SpellRecValue` is now `RecValue`: it serves both record readers, and a
second copy of the same four shapes would drift.
58 of 62 registered names. The four left are `DisenchantAll` and the
queue trio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
e716709bca |
app+ui: the nine file and credential verbs, fenced and keyed
Built on grom's explicit say-so, after a round where they were withheld and the reason recorded. Both halves of that decision stay in the doc, because the reasoning is what makes the second half a decision rather than a drift. ReadCustomFile/ImportFile (one operation, two upstream names), WriteCustomFile/ExportFile, CustomFileExists, ExecuteCustomLuaFile, StartItemExport, EncryptPassword, EncryptedServerLogin. **The fence is app-side and it is not a `..` scan.** `local_state::custom_file_path` admits one relative name whose every component is an ordinary name, inside `benilla-config/CustomFiles/`, and refuses a leaf that is already a symlink. Each escape a string check misses has its own rule and its own test row: an absolute path (`join` *replaces* the base rather than appending), a Windows prefix, a `..` nested inside a longer path, and a planted symlink — the one no amount of sanitising the string will catch. A subdirectory rather than the config root, because a verb that could create `config.toml` or `autologin.toml` by name would let an addon rewrite the client's own settings; that is a different power from "keep a file of your own". The VM side holds three closures and no paths, so no spelling of a `filename` reaches outside that directory. **The credential pair's deviation is forced, not chosen.** Both are registered by nampower under "Registering *Glue* Lua functions" and this client has no glue Lua VM at all — login, character-select and character-create are Rust. `EncryptPassword` is pure and works the same anywhere. `EncryptedServerLogin` cannot mean "submit the login form" in the only VM there is, so it stores the credential for the next login: `enabled` on, the plaintext field cleared, the blob kept, and `AutoLogin::effective_password` reads it back at the refill. What that protects is an `autologin.toml` or a SavedVariables file read by someone else or pasted into a forum: the blob is authenticated and keyed on 32 random bytes in a different file, mode 0600. What it does not protect is anything running as this user, which can read the key as easily as the blob — there is no user secret in the chain and there cannot be, because the point is logging in without typing one. Both halves are at the module. Encrypt-then-MAC with HMAC-SHA1 as the PRF for both halves, from the two primitives the tree already had (sha1 moves from a dev-dependency to a real one). The HMAC is tested against RFC 2202's own vectors, because a hand-written one still round-trips with itself when it is wrong — and a first draft of the key reader did exactly that kind of silent wrong: it returned raw bytes on creation and hex text on read, and the caller guessed between them with "32 bytes and not all hex digits". It guessed wrong the moment a random key came out all-hex. Returning one thing is the fix, and the note is at the function. **The existing plaintext path is untouched.** Storing the password in plaintext was grom's own explicit choice when the alternatives were a keyring or nothing, so this is additive and nothing migrates by itself. The machinery to make the Remember Password tick-box write a blob instead is now all here — one line — and that is his call. Base64 was missing from the whole tree; it is in `benilla-ui`'s `script/encoding.rs` with RFC 4648's own vectors, and `C_EncodingUtil.EncodeBase64` will want the same one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
f87ef148b8 |
app+ui: the trinket cooldown — and a stock verb that always said "cold"
Built: GetTrinketCooldown, UseTrinket, GetSpellRangeData, SetLocalRaidTargetIndex, GetQuestLogQuestIds, GetQuestDialogQuestId, CombatLogFlush. The find on the way through is not a nampower verb. **`GetInventoryItemCooldown` is a stock 1.12 verb and it answered `(0, 0, 1)` unconditionally** — right shape, always "cold", so nothing errored and no worn trinket ever drew its cooldown swirl on the character pane. Stock PaperDollFrame.lua:691 calls it on every slot update. Feeding an equipped slot's cooldown for GetTrinketCooldown fixes that one off the same column, and the test runs `CooldownFrame_SetTimer`'s own arithmetic against it. The feed keys its query on the (use spell, item entry) pair exactly as the bag feed does, so one trinket reads the same cooldown from the doll and from a bag. **The risk was the gate, and my first test for it was vacuous.** `feed_char` sits behind a change gate and no descriptor moves when a cooldown is armed, so `Cooldowns::feed_epoch` had to become a gate input. The first test passed with that input deleted: `run_system_once` builds a fresh system each call, so the gate's `Local` memo resets and `vm_reset` opens the gate every time. A test for a gate input has to run the system the way the schedule does. Rewritten with `add_systems` + `update`, it fails with the input removed — verified by probe, and the reason is written at the test. `feed_char` also hit Bevy's sixteen-parameter ceiling, which was a fair signal its parameters wanted grouping by what they are for: `ItemCooldownInputs` and `RepairInputs` replace six. Two decisions recorded at the code: - `GetSpellRangeData(rangeIndex)` answers three values, not a table. Every table-returning verb in this DLL has its key names in the string pool and this row's are not there. `flags` bit 0 is the melee family, where the engine takes the combat-reach sum instead of the authored pair — a caller ignoring the bit reads 0..5 for every melee ability. - `CombatLogFlush()` is a deliberate no-op. Upstream buffers its combat-log writes and this empties the buffer, so what an addon asks for is "my lines are on disk now" — and `ui_chat/logging.rs` writes each line to an unbuffered File as the window shows it, so that holds at all times. Defining it is the honest answer: the capability is present, and a missing global makes `HasFeature` report it absent. Also fed, both one wire column each: `QuestState::quest_id` (the wire names the quest on all three giver panels and 1.12's Lua names it nowhere — a title is not a key) and `SpellDisplay::stack_amount`'s neighbour work from the previous commit. Five names stay out, each now with the field list that makes it a decision rather than a shrug — `GetUnitData` (80 raw descriptor columns, read off the pool, zero call sites, and a partial answer is worse than none), `GetRaidTargets` (the reference keeps a board; this client feeds the mark per unit, and a scan over named tokens would silently omit a marked unit), `DisenchantAll` (a server-paced sequencer, which is the queue), `GetSpellPower`, `StartItemExport` (writes a file — it belongs with the withheld seven). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
f06caa9968 |
ui+app+formats: the Spell.dbc row, and seven columns that answered nil
`GetSpellRecField` knew three names. Counting what the corpus actually asks it for: `name` (4 sites), `school` (3), `stackAmount` (2), `spellIconID` (2), then one each of `rank`, `rangeIndex`, `mechanic`, `manaCost`, `effectMechanic`, `effectApplyAuraName`. Seven of those ten answered nil, and each was a feature quietly off rather than an error — DoiteAuras caps its stack display on `stackAmount` (DoitePlayerAuras.lua:539) and SuperCleveRoidMacros classifies a crowd-control effect off the two arrays (Utility.lua:7776-7794). The eighty field names are the DLL's own, read off its pool in Spell.dbc column order. The mapping to `SpellDisplay` lives in `ui_script/spell_rec.rs`, beside the struct it reads — a copy on the VM side would be free to drift — and `GetSpellRec(spellId)` is that same field list walked through that same per-field reader, so the two verbs cannot answer differently for one name. A test asserts exactly that, key by key, over one row. The array columns are a 1-based table of three, which is measured, not guessed: SuperCleveRoidMacros walks `effectMechanic` as `for i = 1, 3 do local em = effectMechanics[i]`. `totem`, `reagent` and `reagentCount` take the same shape, two and eight long. One new DBC column — `StackAmount`, 39, bracketed by two constants this tree already had (38 ModalNextSpell, 40 Totem[0]). It is the only one added: fifteen further names upstream answers are left nil rather than putting fifteen unused u32s on a struct the spell catalog holds one of per row. They are listed at `FIELDS` with that reason, so the gap is a decision rather than an omission. `GetSpellRecField`'s three string columns keep the spellbook fallback the verb had before the probe, so a VM with a book and no catalog — a capture, or the boot VM before the catalog loads — still answers a learnt spell's name and rank. Every other column is a DBC row and has nowhere else to come from, which is why the fallback is exactly three names wide and says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
cf0e157f34 |
ui+app: the bag walk, the modifier fold, and two shapes the DLL's pool corrected
Built: `GetBagItems`, `GetSpellModifiers`, `GetSpellIdCooldown` — plus
fixes to two verbs that already existed and had the wrong *shape*,
found the same way `GetItemIconTexture`'s wrong argument was: by
comparing what we answer against nampower's own string pool.
`GetBagItems()` hands back every occupied bag slot as
`{ [bagIndex] = { [slot] = row } }`. DoiteAuras asks seven times over
and walks both levels with `pairs`, reading `itemId` and `stackCount` —
it is how that addon answers "do I have this item" without 100
`GetContainerItemInfo` calls per condition evaluation. Empty slots and
empty bags are absent, because a nil value in a Lua table *is* absence
and `pairs` is how both levels are read.
`GetSpellModifiers(spellId, modifierType)` answers
`flat, percent, hasModifier`, where percent is the final multiplier (90
means 90% of the original) — the consumer says so in its own words and
multiplies by `percent / 100`. The 26 modifier types come from the
DLL's usage string. The fold is `spell/mods.rs`' existing reader,
reached through a new `spell_modifier_probe` the lifecycle reinstalls
whenever the wire moves the tables: a VM-side reimplementation of a
four-gate 64-bit fold would be free to drift from the mana cost the
action bar greys a button on. The third return is the point — the
consumer tests it before touching either number, and the duration path
multiplies, so `0, 0` for an unmodified spell would zero every debuff
timer that asks.
`GetSpellIdCooldown(spellId)` answers the twenty-key table, from a new
`Cooldowns::legs`: `info`'s three legs kept apart, same walk and same
arms, so the folded number a swirl draws and the legs an addon reads
cannot describe different timers.
**`GetBagItem`'s row had the wrong key names** — `bag`, `slot`,
`count`, `quality`, `link`, `locked`, this client's own
`GetContainerItemInfo` spellings. nampower's row is contiguous at
`0xa8a68` and is the block `GetEquippedItem` already used, so a caller
reading `.stackCount` got nil and no error. One `bag_slot_table` now
serves both, with three columns absent rather than zeroed: this client
resolves a bag slot's enchants to names at the app boundary and keeps
no random-properties id for a container slot, and a zeroed enchant id
would be a claim.
**`GetItemIdCooldown` returned the wrong kind of value** — the stock
`start, duration, enable` triple. At the version we report it is a
table, and the addon layer settles it: `API.GetItemCooldownInfo` and
`API.GetSpellCooldownInfo` return these calls' results *directly* and
their callers index them. `cd.gcdCategoryRemainingMs` is the primary
path of `API.GetGCDRemainingMs`, which the `[gcd]`/`[nogcd]` macro
conditionals divide by 1000 — against a triple that is
`(0.0).gcdCategoryRemainingMs`, which raises. Latent rather than live
(nothing calls those wrappers today), still wrong.
Two things the schedule gates caught and improved:
- The probe refresher started as an exclusive system, which is
ambiguous against everything in Update it is not ordered against:
+274 pairs over the ceiling. `NonSendMut` is all it needs.
- Its memo started as a bare `Local`, which `session.rs`' own gate
refuses — a memo about what this VM has been told must die with the
VM. As a `VmMemo` it now reinstalls on the first frame after a
`/reload`, which is better than the reasoning I had written for the
`Local`: a fresh VM has no closure at all until something installs
one.
One count the earlier sweep got wrong, corrected in the doc:
`GetSpellPower`'s "7 call sites" are BetterCharacterStats' own method
`BCS:GetSpellPower(school)`, not nampower's global. Nothing in the
corpus calls the global at all. A bare name match is not a call site.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
dc59594389 |
app+protocol: nampower's ten cast events, off the queue that already had them
All ten ride `ui_cast_events.rs`'s existing `CastEventFeed` rows — the same rows `UNIT_CASTEVENT` and the cast windows are folded from — with a new `NpCastColumns` carrying the fields nampower's shapes want and SuperWoW's one word has no place for. One row per packet, folded once, which is this file's own rule: an addon woken by one of these and then asking a getter must not be able to get a contradicting answer, and a parallel queue over the same packets is exactly how that happens. `NP_EnableSpellStartEvents` and `NP_EnableSpellGoEvents` go from stored-only to LIVE. Their rows said the shapes wanted "itemId/spellType/corpseOwnerGuid columns this client does not carry"; two of those three turned out to be a Spell.dbc lookup and a guid hop the item-cooldown path already made. `itemId` is the cast item's entry through `item_entry` (now shared with `item_cooldown`, which had the same two hops inline) — `SessionEvent::SpellStart` was dropping the guid the GO always carried, so it carries it now. `spellType` is `AttributesEx & 0x44` for a channel and the auto-repeat bit for a shot. `channel_ms` is `SpellDuration.dbc`. The GO's two counts are the packet's own lists, capped at 255 because the server writes both as `u8`s. `SPELL_FAILED_SELF`'s result byte is the failing `SMSG_CAST_RESULT`'s reason — the number 1.12 turns into a red line and then discards, so no addon on that surface can see why a cast failed. `corpseOwnerGuid` is the one column that really is absent, and it is not sent at all: a trailing nil in Lua is indistinguishable from an absent return, and upstream sends nil for every cast without a corpse target. For the handful that have one the gap is bounded and written down — the targets block reads the corpse guid and drops it, and the owner is a further hop through that corpse's own CORPSE_FIELD_OWNER. Two subtleties, both pinned by tests: - A channel's START is queued although its wire cast time is 0, and `UNIT_CASTEVENT`'s START is still suppressed for it. Without the row, `SPELL_START_SELF` would never fire for Mind Flay or Blizzard — pfUI's reader expects exactly that row and falls back to arg7 "only for true channels like Blizzard where arg6 is 0/nil". With the word, every cast-tracking addon on this realm would get a SuperWoW START whose arg5 is 0, which is a cast bar told to run for no time. So the suppression moved from the producer to the fire, and SuperWoW's word behaves exactly as it did before the row existed. - The `_SELF`/`_OTHER` split is by caster here and by *target* for the aura family next door. Neither is a guess: upstream's wording for the aura pair is "casts that land on the active player", and for this one pfUI handles both names in a single arm and then reads arg3 to learn the caster, which only makes sense if the name already settled it. A gate hole closed on the way past: `AURA_CAST_ON_SELF`/`_OTHER` were fired through one `&str` variable, and `reference_ui.rs`'s beyond-1.12 event gate reads the string literal *at the call* — so it had been blind to that pair for as long as they had existed. Both are literals now, with their rows, and the probe confirms it: renaming either row makes the gate name the event. Tests read pfUI's own `local` bindings back out rather than a list of values in push order, as the aura-cast block beside them does. A positional shape got backwards here fails in silence — the handler binds arg3 to casterGuid whatever arg3 happens to be — and this family is where that would be most expensive, since pfUI's swing timer resets off the START and its whole debuff bookkeeping gates on the GO's two counts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
ecee994ab6 |
app+ui: the player's own cast state, and SPELL_CAST_EVENT off the commit
`GetCurrentCastingInfo` and `GetCastInfo` are the two most-called
nampower functions in grom's corpus — 25 and 20 call sites outside
`NampowerAPI.lua` — and one of them calls with no existence guard at
all: `local _,_,_,_,_,_,autoattack = GetCurrentCastingInfo()`
(SuperCleveRoidMacros/Core.lua:94), in the frame that keeps clearing the
target while an autoattack is queued. On this client that line raised.
The earlier doc row said the client had "a spell id, a deadline and a
guard bit — no start instant, no channel state, no GCD end". That was
true of `PendingCast` and wrong about the tree: `ui_cast_events.rs`
already folds a cast window per caster off the same packets, with a
start, an end, a channel bit and the accumulated pushback, behind
`C_Spell.UnitCastingInfo`. What was actually missing was the caster's
own machinery — the reference's cast globals, which have no descriptor
field anywhere — and the GCD, which `spell/cooldowns.rs` has run
correctly all along and never published.
So `GetCastInfo` folds two halves: its timing columns come from the
player's own window, its identity and GCD columns from a new per-tick
`SelfCast` snapshot. The feed declares itself after the window drain, so
the two halves can never describe different casts. Added:
- `SelfCast` (benilla-ui) — the inflight spell `0xceca88`, the bar's
spell `0xceac48`, the auto-repeat key `0xceac30`, casting/channeling/
onswing/autoattack, the cast counter, the cast type, the target, and
the GCD's start and end on the GetTime clock.
- `ui_self_cast.rs` (benilla-app) — the push, plus `SPELL_CAST_EVENT`.
- `Cooldowns::gcd` — the running global cooldown as one timer. `info`
still answers the per-category question every cooldown swirl asks;
this answers "when does the GCD end", which has no spell to key on
because its consumers divide it (`API.GetGCDRemainingMs`) or compare
it against seconds (`[gcd:<1]`).
- `PendingCast`'s cast counter, in the one place every class of commit
already passes through.
Three decisions where the tempting answer was the wrong one:
- `GetCastInfo` is **nil** when nothing is casting, and an instant's GCD
does not count. Answering a table whenever the GCD is up would make
the consumer's fast path always hit — and `API.IsCasting()` *is*
`GetCastInfo() ~= nil`, so `[casting]` would read true for a second
and a half after every instant. The wrapper's own comment says what to
do instead ("GetSpellIdCooldown … works for instant casts").
- `castId` is two different numbers under one upstream name: a counter
in `GetCastInfo`'s table, beside a separate `spellId`; a **spell id**
in `GetCurrentCastingInfo`'s returns, which is how both of its readers
treat it (Init.lua:89 stores it as castingSpellId, Core.lua:5427 hands
it to C_Spell.GetSpellName). Each is implemented as what its own
consumer reads, and both are spelled out at the binding.
- `SPELL_CAST_EVENT` fires for commits, not refusals. Upstream's
`success` can be 0; this client's local refusals funnel through
`CastErrors`, which carries a spell id and a reason byte and neither a
cast type nor a target — a row from there would invent two of five
fields. Nothing in the corpus reads the failing branch
(Core.lua:5177 is one `if success == 1 then` with no else). Written
down at the type rather than left to be found.
Tests run the consumers' own bodies transcribed, as `cast_info.rs` next
door does: `API.GetCastInfo` with its `GetCurrentCastingInfo` fallback
arm, `API.IsCasting`, `API.GetGCDRemainingMs`, `API.GetCastRemainingMs`
and `CleveRoids.UpdateCastingState`. A positional seven-return shape and
a ten-key table are only correct relative to the code that reads them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
f120e8003d |
ui: six of nampower's reads, and the item-icon probe behind two of them
First tranche off the DLL work list. The method that produced it is worth recording with the code: `SuperCleveRoidMacros`'s `NampowerAPI.lua` is a capability registry — a `VERSION_REQUIREMENTS` table of every nampower name with its minimum version, reached through `HasFeature`, which tests the version **and** `_G[name] ~= nil`. So none of this was broken, and raising `GetNampowerVersion` would not have helped: the only honest way to make one of those features live is to define the global. - `PlayerIsMoving()`, `PlayerIsRooted()`, `PlayerIsSwimming()` — `1` or nil, as upstream's own doc lines state, never a boolean. All three read the per-tick posture snapshot `SelfMovement` already carries, which gains `rooted` (`MOVEFLAG_ROOT`) and `moving` (any of the six movement bits, the keyboard turns included — they are input to the same word the client tests, and a turn in place does interrupt a cast). - `IsAuraHidden(spellId)` — from a new `SpellLookup::hidden_aura`, filled with exactly `ui_aura::shown_in_aura_ui`'s predicate inverted, so it cannot contradict the aura list an addon reads beside it. An unknown spell is nil: "not hidden" is the honest answer about an aura this client knows nothing about, and the lists show a catalog miss too. - `GetItemIconTexture(itemId)` — two hops, because an item template carries a display id and not an icon: the cache row's `displayInfoID`, then `ItemDisplayInfo`'s `Icon` through a new `ItemIconProbe`. A known item with no display row is nil (a data gap); an item the cache has never seen raises and records the ask, as every `item_record` verb here does. ClassicAPI's `GetItemIcon` wants the same probe, which is why it is a probe and not a closure. - `GetTrinkets()` — slots 13 and 14 in `GetEquippedItem`'s own row shape, an empty slot absent rather than nil-valued, since the consumer walks it with `if t[i] then`. Each is in `reference_surface.rs`'s `allowed_beyond_1_12` with why 1.12 has no spelling to prefer — the client reads its own movement word and tells Lua nothing, applies the aura attribute test inside `UnitBuff`/`UnitDebuff` without exposing either, and can only reach an item icon through a bag slot it already holds. All nine gates green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
cae59cdc11 |
app+protocol: fire CHAT_MSG_HARDCORE, the realm's own chat type
`reference_ui`'s `UNPRODUCED` carried `CHAT_MSG_HARDCORE` as a chain event nothing here fires: the window registered for it and benilla routed no such message, so the realm's hardcore-death broadcasts simply never arrived. Three of grom's addons listen for it too. The missing piece was one number, and the realm's patched `WoW.exe` has it in the PE section it names `.tdata`, beside the string `"HARDCORE"` at `0xd06000`. Two detours do the whole job: `0x49f7aa`, in the chat type-name lookup, jumps to `0xd06020`, which sets the name pointer to that string and the type to `$0x5e`; and `0x49b193`, inside the `CHAT_MSG_*` fire helper `0x49b0b0` whose format string this file already cites, jumps to `0xd06040`, which tests the type against `$0x5e` and swaps in the event-name index `0x21f`. So wire type `0x5E` is `ChatEventKind::Hardcore`. Nothing in 1.12 claims it — its last type is `CHAT_MSG_BATTLEGROUND_LEADER` at `0x5D` and its `CHAT_MSG_BG_SYSTEM_*` block sits at `0x52`-`0x54` — so there is no collision to arbitrate, and the test pins that adjacency along with the decode, since a wrong id would look exactly like the silence it replaces. The chain's `ChatFrame.xml` registers the event and its `GlobalStrings.lua` has the `[Hardcore] %s: ` format, so the window needed nothing from us but the fire; it takes the window's default colour, as `FILTERED` does, the realm adding no `ChatTypeInfo` row. Not covered: sending. The ruRU locale carries `SLASH_HARDCORE1 = "/h"` and `CHAT_HARDCORE_SEND` where enUS has only the receive format, so the send side needs its own look. 269 data-gated failures, identical to the baseline, all nine gates green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
02e6fc4208 |
formats+app: a stale snapshot archive mounts below the realm's patches
grom is keeping `patch-A.mpq` — it is almost certainly textures — so the shadowing it causes is handled in the chain instead of on his disk. `mount_order` stays the reference's law, letter above digit and all, and `demote_stale_snapshots` runs after it: an archive whose `FrameXML.toc` is a **strict subset** of another's is an earlier edition of the realm's manifest, so it mounts just below the patch set — above `patch.MPQ` and the base archives, so anything only it carries still resolves. A toc that differs by *removal* is a different manifest rather than an older one and is left exactly where its name puts it. `WOW_CHAIN_DEMOTE=0` keeps the reference's order for comparison. On his install `patch-A.mpq` (Aug 7) now loses to `patch-5.mpq`, so the realm's current data is what loads: its `FrameXML.toc`, its `GlobalStrings.lua` and a `Spell.dbc` with **28019** rows against the 27992 his last session reported — 27 spells the update added that were invisible. Reading the realm's real files then broke ten tests, all of which had pinned August's behaviour, and each is now pinned the realm's way: - `AIOHandlers.lua` claimed the `OctoRadio` block as a no-op on the evidence that the radio file carried no AIO call. The update built the radio out into a file twice the size whose `RegisterNowPlayingFeed` registers that name at `VARIABLES_LOADED`, and `AIO.RegisterEvent` asserts on a duplicate, so our shim raised through seven tests. The row is gone, and the shim now claims at `PLAYER_LOGIN` through `pcall` so a realm that implements one of these takes the name and we step aside. `no_aio_block_we_shim_is_one_the_realm_now_handles` fails a row the realm has implemented — verified by putting `OctoRadio` back. - `BATTLEFIELDS_CLOSED` no longer hides the battlefield list, by the realm's decision: the server stamps `SMSG_BATTLEFIELD_LIST`'s battlemaster guid with the player's own, so the client anchors the window to the spot it opened and closes it when you step away, on foot or on a boat (bug tracker #330 and its follow-up). Pinned that way, with the close button proving the list still closes. - `MAX_QUESTLOG_QUESTS` is 25, not 1.12's 20. The test reads the realm's global now, so the next bump is data and not a failure. - The realm's `OctoQuestLogPoller` redraws the log every 0.2 s, which legitimately costs a layout gate walk. `POLLS_ON_A_TIMER` stands it down by name before the quiet-frame bench measures, rather than raising the threshold and blunting the one instrument that catches an every-frame writer. A timed realm handler that is not listed fails. 269 data-gated failures, identical to the baseline at HEAD name for name, and all nine gates green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
73bc9a9e8e |
app+formats: name the realm files a shadowing archive holds stale
The archive found holding an old copy of the realm's own `FrameXML.toc` is holding an old snapshot of the realm's interface, not one old file. `Chain::copies` answers which archives hold a name and at what size off the hash and block tables alone, decompressing nothing, so the boot log can now name every row of the realm's toc whose winning copy comes from that archive while the realm's own has a different one. On grom's install that is ten, and it is the better half of the finding: `GlobalStrings.lua`, five `OctoLocale` files, `AIO.lua` — the client and server's whole transport — the calendar, the challenge tips and the radio all load as an August `patch-A.mpq` has them, because a letter patch outranks the realm's `patch-5.mpq`. `Spell.dbc` is shadowed the same way, 20 KB behind. Reported and not repaired, deliberately: shadowing a file with different content is what a patch archive is *for*, so outside the toc — where "holds every row the winner does, plus more" is a decidable relation — this client cannot tell a stale copy from an intended override. The real 1.12 client reads the same copies, so this is a fix on disk, and the line is what makes it findable instead of invisible. `richest_toc` splits the superset rule out of `realm_toc_rows` so it is testable without client data, which is where it ships from: the winner stands unless a shadowed copy drops nothing, a longer copy that drops a row is passed over, and the caller is told which archive it read so it can say so. The staleness report is checked against the real chain and every row it names must really differ in size between the two archives, so it cannot be noise. The 269 data-gated failures remain identical to the baseline at HEAD, name for name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
eaadc9f73b |
app+formats: take the realm's own FrameXML list, not a curated copy of it
`benilla.toc` enumerated every interface file, so a row the realm adds in a client update was *silently* absent: no "not found", no warning, just a global nobody defined. That is what `/xpboost` was, and the only way to find it was to diff two MPQ archives by hand. The manifest now decides order and substitution, and the realm's own `FrameXML.toc` decides membership: every row it carries that the manifest does not name loads after it, in the chain's order, and the boot log names them. `DECLINED` is the short list of rows this client must not run (`StatsFrame.xml`, whose OnUpdate is the only reader of a `GetDebugStats` we do not have), each with its reason; a row we merely have no verb for still loads and raises where it is used, which beats silence. `WOW_FRAMEXML_TAIL=0` opts out and says what it is leaving out. Finding that out turned up the reason the row was missing in the first place, which is worse and is now fixed too. `Chain::open` mounts `patch-?.MPQ` in the reference's order — case-folded, last wins — so `patch-A.mpq` mounts **above** `patch-5.mpq`. The realm ships its interface in `patch-5`; this update added six rows to that toc; and the 140-row copy inside an older `patch-A.mpq` kept winning. The interface was loading August's list of files. So `Chain::read_all` returns every archive's copy of a name in ascending priority, and `realm_toc_rows` reads a shadowed copy when it holds every row the winner does *plus* more. That case is unambiguous — nothing was removed, so nothing can be lost — and it is a packaging accident with no intent behind it either way. A copy that disagrees by removal is a real change of mind we cannot arbitrate: the winner stands and the log says what was passed over. Both paths say which archive, so the clean fix on disk is one line of log away. Verified against grom's own install and his addon corpus: with the `OctoXpBoost` row deleted from the manifest — tomorrow's update, simulated — the tail finds it, resolves it to a readable chain path, and `/xpboost` is a live slash command with a function behind it. The tail is empty with the manifest as it stands, which is the point: the mechanism is armed and changes nothing today. Tests: the merge takes both lists as arguments so it runs where there is no client data, since that is the machine where a regression in it would otherwise skip unseen; the letter-above-digit mount order is pinned on its own, because that fact is what cost the six rows; `read_all`'s ordering contract is checked against a real chain; and a `DECLINED` row that the realm stops shipping now fails as stale. The 269 data-gated failures are the same 269, name for name, as at HEAD. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
ef7f823d9a |
ui: teach the producer gates the reach-reporting fire
`every_event_a_chain_file_registers_has_a_producer` finds producers by scanning our own `.rs` for `fire_event(`, `fire_event_into(` and `pending_events.push((` followed by a string literal. `49b4f7b5` moved `area.rs`'s minimap fire to `fire_event_reach(`, which matches none of them, so the gate began reporting that `Minimap.xml` listens for `MINIMAP_ZONE_CHANGED` and nothing fires it. Both scanners here now match that spelling too. The break rode green through a full gate run and a commit because this test and its sibling need `WOW_DATA`, which `scripts/gates.sh` does not set — on a machine without the client data 1609 tests skip silently. Found by running `WOW_DATA=<Data> cargo test -p benilla-app --lib` and diffing the failures against a baseline at HEAD: 3 before, 2 after, and the two that remain fail at HEAD as well. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
ce8992bc3e |
Draw flight masters: C_TaxiMap.GetTaxiNodesForMap over TaxiNodes.dbc
grom: "can't see flight master location marks, even tho I have them on."
Nothing was broken — the layer was silently empty. pfQuest-octo has deleted its
shipped pfDB.meta.flight table in favour of this call (db/meta.lua:181 is a stub
whose comment says the data "always comes live from the client (ClassicAPI)"),
and SearchFlightNodes opens with
if not (C_TaxiMap and C_TaxiMap.GetTaxiNodesForMap) then return maps end
so with the namespace absent the option stays ticked, no error is raised, and no
icon is ever drawn. Same silent-by-construction shape as C_Map.GetAreas, same
consumer.
The contract is half read off the bundled ClassicAPI.dll's string pool
(C_TaxiMap, GetTaxiNodesForMap, nodeID, faction, reachable, state and the
FlightPathFaction/FlightPathState enum names) and half off the caller: the
areaID/mapX/mapY triple is not separately visible there, since the pool holds
one copy of each string and C_Map.GetAreaTriggerInfo already uses all three, so
for those SearchFlightNodes itself is the authority. It reads six fields, gates
on `node.areaID and node.mapX and node.mapY`, and feeds the pair into
pfMap:AddNode as meta.x/meta.y, which UpdateNodes divides by 100 — so they are
percentages of the zone rect. Both files say which half is which.
Faction comes from the two MountCreatureID columns, the reference's own gate
(0x4a2650 asks for the mount of the player's half): one column filled is that
faction, both is neutral, and neither means the row is not a flight master at
all — on this install those are exactly the eight Transport/Ferry/zeppelin
endpoints, and passing them on would put "Flight Master" pins on the Menethil
docks.
Attributing a node to a zone needed the file's own names. Geometry over
WorldMapArea boxes proposes — these are axis-aligned boxes around irregular
zones, so a node near a border sits inside two or three of them — but no purely
geometric rule works: "smallest box" misfiled a dozen of ~70 rows (Crossroads
under Mulgore, Aerie Peak under Alterac Mountains), and "most central box",
much better and what zones_at now sorts by, still loses Talonbranch Glade to
Darkshore by about one percent of inset. TaxiNodes.dbc names its rows
"<place>, <zone>" and a city row leads with the city, which is itself a zone, so
trying each comma-separated part against the candidates gets Ironforge over Dun
Morogh *and* Felwood over Darkshore from one rule. zones_at also reaches the
instance-map list, so Alterac Valley's two nodes place on the battleground map
instead of nowhere.
Seeded before the interface loads, after the map catalog it reads the rects
from. That deadline is tighter than file scope: pfQuest replays its saved
tracking selections once on PLAYER_ENTERING_WORLD and then unregisters every
event (database.lua:131-151), so a push one frame later is a push after the only
read.
Verified against the real addon offline, not just by unit test:
SearchFlightNodes now adds 69 nodes through the probe harness. Three tests pin
the rule against the DBC itself — the capitals and the border nodes by hand, and
every named row held to the zone its own name claims, which is the test that
rejected both earlier geometric rules. It also pins the four rows that name no
zone, three of which are this realm's own additions (Northshire Abbey and two
Darkshore canoe routes), so the next custom node shows up as a failing
assertion rather than a pin in the wrong zone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
cd6731799b |
ui: memoise the texture-existence probe, as its two neighbours already are
`set_texture_probe`'s closure answers every path form of `SetTexture`, and it was the only one of the three probes installed beside it without a cache — `set_texture_size_probe` and `set_font_probe` both memoise, and the font one's comment already gives the reason that applies here verbatim: a path's answer cannot change, and an addon may ask every frame. A miss is the expensive answer. An addon texture is never in the MPQ chain, so both candidates fall through to `loose_addon_file`, which walks the path one component at a time and, whenever a component's real spelling is not the lower-cased one it was handed, enumerates that whole directory to compare case-insensitively. `Interface\AddOns` holds 94 folders here and `pfquest` never matches `pfQuest` on a case-sensitive filesystem, so an uncached call read that directory twice over, on the main thread, between two systems. grom's 2026-10-21 17:08 session priced it: `[ui-verbs]` reports `texture:SetTexture` at **45-59 us a call across all 33 windows** — stable enough to be a fixed charge rather than a tail — against the ~0.2 us a cached existence answer costs. 1.75 ms a frame over 34 calls while the world map was up, because `WorldMapFrame_Update` re-textures every overlay tile and POI on each `WORLD_MAP_UPDATE`. Replaying the same walk over the real AddOns tree reproduces 26-42 us for a single candidate. Misses are cached too, as the size probe's are: an addon probing for optional art is the common case and it is the one that pays for the walk. This is the smaller half of the map's cost — `scrollframe.rs`' derivations were the larger — and it is a ~1-2% of frame fix, not the dip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
e6ae2e0f67 |
ui: a scroll names the child it moved instead of giving up on the graph
`SetVerticalScroll` and `SetHorizontalScroll` called `Model::touch_layout` on the line before `reanchor_child`. That gave up naming for the whole graph and then *erased* the precise touch that followed: `reanchor_child` re-anchors the scroll child through `write_anchor`, which reads an offsets-only change (same point, same target) and names the child node — but `touch_layout_node` will not un-say an imprecise touch, so the give-up won on every call. Naming the child is the whole truth here. A scroll offset reaches the layout only as that one anchor: anchor targets are ids fixed at `SetPoint`, so no edge moves and the roster is unchanged, and the clip a scroll child draws within is the ScrollFrame's resolved rect rather than the offset — `clip::scroll_clip_sources` rebuilds it per call and caches nothing. Measured on grom's 2026-10-21 17:08 session, which was a map-and-panel run. These two lines were **472 of the run's 806 whole-graph derivations**, the largest single source by a factor of two, and the cost lands on the Lua that reads a frame edge afterwards rather than on the pass's own `resolve` phase: | | n | fps | UI pass | `frame:GetLeft` | |---|---|---|---|---| | windows with >= 10 scroll derivations | 12 | 68.5 (min 53) | 5.93 ms/f | 2.18 ms/f | | windows with no derivations at all | 11 | 92.4 (min 77) | 1.62 ms/f | 0.05 ms/f | `r(scroll derivations, GetLeft us/f) = +0.907` over 57 in-play windows, against `+0.014` for every other derivation site. `[ui-verbs]` has `frame:GetLeft` at 94-376 us a call in those windows and 0.6-1.5 us in the quiet ones, on ~14 calls a frame, because stock FrameXML reads frame edges constantly and each read past the clean epoch re-derived. That is why a scrolling panel — the world map, the quest log, a profession list — was the expensive thing on screen. The test scrolls both axes on the shipped interface and asserts zero derivations, with a positive control so it cannot pass on a dead counter. Put one `touch_layout` back and it reads exactly 10 for 10 scrolls. `WOW_LAYOUT_VERIFY=1` over the real 92-addon interface gives `frames 13379 -> 13379, regions 29597 -> 29645` — identical to the recorded pre-existing baseline, so this narrowing introduced nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
754d577eaf |
ui: a trade-skill list mutator signals inside its own call
`ExpandTradeSkillSubClass`, `CollapseTradeSkillSubClass`, `SetTradeSkillSubClassFilter` and `SetTradeSkillInvSlotFilter` fire `TRADE_SKILL_UPDATE` from inside the call now, as the one helper all four tail-call in the reference does (`0x4fd710`, `0x4fd730`, `0x4fd750`; `reference/1.12-verb-events.tsv`). It used to be a `trade_skill_touched` flag drained by `ui_tradeskill::drain_trade_skill` and answered the following frame. Inline is load-bearing. These four verbs are called *from* a `TRADE_SKILL_UPDATE` handler: a profession library reads the recipe list by unfolding every header, walking it and folding back what was folded. The signal is therefore re-entrant by construction, and every addon that does it carries its own guard — LibCraftingProfessions has `trade_skill_scan_in_progress`, ATSW an `atsw_scans < 2` counter. Those guards are written against an inline signal and only an inline signal lands inside the window they cover. Deferred to the next frame it landed after the guard had cleared: the library rescanned, the rescan called the verb, the verb re-armed the flag, and the two kept each other alive at one fire a frame for as long as the window was open. grom's 2026-10-21 session shows it directly — `[ui-events]` reporting `TRADE_SKILL_UPDATE · 1.01 fires/f · ~900 call(s)` across 37 consecutive windows, three unbroken minutes, at 0.58-0.84 ms a frame in handlers over four addons rebuilding their lists for nothing. It is the 118-second and 61-second stretches at 55-70 fps in a session whose in-play median was 86. Two tests: each mutator's fire is read with its own `run` still on the stack, so a deferred signal fails; and a library-shaped in-progress guard turns one real update into exactly one scan, with nothing owed to a later tick. The bench's `[events]` block also fires the five non-quest events the live session named. They cost ~0.2-0.5 ms there against 12-72 ms live, because this harness has no NPC in front of it, no trainer list and no open map — recorded as a gap, not an answer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
c1a56f98ab |
layout: name the write that asks for a whole-graph derivation
`[ui-phases]` could say a window paid for 61 whole-graph derivations. It could not say which of the forty-odd `touch_layout` sites asked for them, and that is the half a fix has to be aimed at. Measured on the player's interface, one derivation costs ~15.5 ms — from two directions: the derivation count explains total resolve time at r = +0.977 across twenty windows (solve count: r = -0.33, so solves are free and derivations are everything), and 26 of 78 worst-single-call figures over 1 ms land in the 15-16 ms bin across eleven unrelated handlers by six authors, two of which fired exactly once for their whole window. Eleven handlers cannot each independently be 15.5 ms slow; it is one shared charge landing on whoever reads the graph first, which is also why the same session shows `resolve` p99 at 14.4 ms and worst frames "mostly tick". One defect, two accounts. So `#[track_caller]` down the naming chain and a tally of `file:line`, printed in the report. Not a backtrace: `WOW_LAYOUT_DERIVE_TRACE` already captures those and has to be rationed to `n` prints because of what they cost. This is a `(&'static str, u32)` key off `Location::caller()`, cheap enough to leave armed, and take-and-rearm so a window reports its own rows and a VM replaced by `/rl` re-arms without anybody noticing it was replaced. Also a `[churn]` window in tick_bench, which records a negative: a plate roster turning over 4-in-40 every ten frames costs **zero** derivations on the player's own interface. `sync_nameplates` pools the plate frames, so a new key rewrites an existing plate and mints nothing. With `resolve_bench`'s hover-sweep, tooltip-content and moving-region tests all asserting zero, four candidate triggers are now ruled out offline and the live tally is what names the real one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
e4851df7df |
ui: WOW_UI_PHASES — the UI pass's phase split, windowed
`WOW_UI_TICK` measures `UiScript::tick`. `[sys-timing]` measures the Bevy system around it, `ui_script::extract::tick_script`, which also runs the FontString measure round-trip and `UiScript::resolve`. In a combat session the first read 38 ms/s — its lowest of the whole run — while the second read 157 ms/s. Neither instrument was wrong, and 76% of the cost was in the space between them, which is where the stutter players report lives. `UiFrameCost` has carried the whole split since the hover recorder needed it, and `WOW_UI_COST=1` prints it one line per frame: at this client's framerate, twenty thousand synchronous stderr writes a minute from the thread being measured, which is the trap `sys_timing`'s header records costing 15 ms a frame. The split existed and was unusable in exactly the situation it was for. So a window, and a worst column — a phase that is ruinous on one frame in a hundred has a mean near nothing and is the only thing a player feels. `derives` is in the per-frame line because `UiFrameCost::derives` is documented "expected zero", so a nonzero one names a whole-graph re-derivation on sight. `UiFrameCost::stamped` comes with it: the paint can return before any of its three arms wrote this frame's phases, and a reader that cannot tell counts the previous frame twice. Armed in ./measure.sh at 5 s, with the per-frame form left alone for the hover recorder. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
497570a68c |
addons: an on-demand load says what it loaded
The startup walk names every addon it loads; `LoadAddOn` named none. So when the client froze on 10-19 while learning recipes, the session log could not say whether the trainer window was even open — and `Blizzard_TrainerUI` is LoadOnDemand, so that was the whole question. `benilla-ui` has no logger, so the line travels the way its errors and warnings already do: a `notices` channel on the model, drained to `info!` beside them in the same pass. Silent for a redundant load, because the stock `*_LoadUI` glue calls `LoadAddOn` on every open and a line per call would bury the one that did the work. A refusal carries the reference's own reason token, and a name that is not installed says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
9782b07fd4 |
lua: a frame that never leaves the VM fails its call, not the client
The load has had an instruction bound since `3b6975fd` — "so a hung load reports instead of freezing the loading screen" — and the end of the load disarmed it, "so no handler is killed for being slow". Right about slow, and it leaves a handler that never returns at all able to freeze the client until the player kills it, which is what happened to him this morning. So the bound does not go at the end of the load, it changes shape. Wall time rather than instructions, because the number has to be obviously beyond anything legitimate and a count is not: a frame still inside Lua after fifteen seconds is not slow, it is stuck. `tick` restamps every frame, so no frame is charged the one before it and a handler reached later in the frame — a click, a fired event — measures from that mark, which can only make the bound tighter. The raise names the chunk and the line, so his log says which addon rather than that something somewhere looped. It shares the one count hook, which costs 5-13% of pure Lua execution time (measured: 30M instructions, 235-260 ms bare against 267-274 ms hooked, which also puts his machine at ~125M VM instructions a second — what sizes the load's 200M at about 1.6 s). Lua is a small part of a play frame, so that is a fraction of a percent of frame time, and the load was already paying it. `WOW_LUA_RUNAWAY=<secs>` moves the bound, `0` disarms it. Two tests: a loop with no exit fails its own call, names its chunk and leaves the VM usable; and a frame is not charged the wall time that passed before it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
b7ddc18782 |
load: a reload says what it cost and what the chunk cache did for it
Both lines sat on the world-entry path, so the load the player actually waits through — the `/reload` — reported nothing at all. His 2026-10-19 session has two of them, 4,981 ms and 6,276 ms against the 7.5-9 s he started from, and the log cannot say where either went or whether the bytecode cache was hit, although the cache directory proves it was: 1,585 entries written at first load, 14 added across both reloads. `report_load` now runs on both roots and names which one. The reload's clock starts before the saved-variable writes, because those are part of the one blocked frame he sees, which is also what the `frame hitch` warn measures. `chunk_cache_line` resets its counters on read, so each line is that load's own work rather than a running total in which a warm reload hides behind the cold login before it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
a48dc95586 |
lua: the cache says what it did, and takes an off switch
Two small things the chunk cache needed to be trustworthy on a machine I cannot
watch. `WOW_LUA_CACHE=0` turns it off without a rebuild — a cache earns a kill
switch, and it is also how its own saving is measured. And every in-game UI load
now logs what it did, beside the load's own cost:
ui_script: lua chunks: 0 compiled in 0 ms, 1591 loaded from cache in 157 ms
ui_script: in-game UI up in … ms
because "the cache is installed" and "the cache is working" turned out to be
different claims, and the first build of it satisfied only the first.
A capture run resolves no state folder, so it has no cache and compiles
everything — which is right (a capture writes no player state) and worth knowing
before reading one of those logs as a regression.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
d5ddaa1286 |
lua: a compiled chunk is kept, so a load parses only what changed
The compile is a UI load's biggest removable cost and the only half of it that can be kept — running the chunks is what an addon *is*. Measured through the addon harness's `--together` load of the player's own 91 addons, three passes: ``` no cache 4,788 chunks compiled 3,324 ms cold 1,592 compiled, 3,196 from cache 1,482 ms warm 0 compiled, 4,788 from cache 471 ms (+24 ms of keys) ``` **About 940 ms off every login and every `/reload`**, and the first load after an addon changes pays only for the files that changed. `benilla-config/cache/lua/`, one file per chunk, keyed by a hash of the chunk's own bytes and its name. Both chunk paths go through it — `run_chunk_named` and the XML loader's own runner, which is how the biggest files in the corpus arrive (pfQuest's database is 33 MiB of `<Include>`d Lua). It cannot go wrong quietly. A key is a property of the bytes, so an edited file is a different entry and a stale one is never asked for; an entry that is truncated, corrupt or from another Lua build fails `lua_undump`'s header check and the source is parsed instead, so a bad entry costs the parse it meant to save. The dump keeps its debug info — `debugstack` is an addon-facing verb and a stripped chunk would lie about its own lines — which the test asserts by raising from a cached chunk and demanding its file and line back. The folder is capped at 512 MiB (one load's worth is 159 MiB) and emptied wholesale when it is over: a cache that needed an eviction policy would need bookkeeping that costs more than it saves. The first build of this looked like it worked and saved nothing, because `ahash::AHasher::default()` is seeded per process: every session hashed the same file to a different name, found nothing and rewrote the whole cache (4,776 entries, 477 MiB, a steady 1,592 misses a run). Fixed seeds, and the instrument that caught it — `benilla_ui::script::chunk_cache_line()`, hits and misses with the time each spent — stays, because "the cache is installed" and "the cache is working" turned out to be different claims. Verified: the harness's verdicts over all 91 addons are **identical** with the cache and with `WOW_LUA_CACHE=0`, the full gate chain is green, and the `WOW_DATA` failure sets are unchanged (benilla-app 270, benilla-formats 80). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
0fe0a99d8a |
layout: a size the author set is not a question for the solver
`GetWidth`/`GetHeight` answer against current layout, so they resolve first — and a resolve that follows any `CreateFrame` re-derives the whole layout graph, because minting a frame id drops the cached one (`Model::frame_id` → `touch_layout`). A UI load is nothing but creation interleaved with reads, so the player's interface paid **839 whole-roster derivations in one load, 2.84 ms each: 2.4 s**, against 650 `GetWidth` calls. `WOW_LAYOUT_DERIVE_TRACE` named the giver-up: every sampled one was `create_frame`. Most of those reads cannot learn anything from a resolve. An edge takes its anchor scan first and only then `combineEdge(center, opposite, size · scale)` (`resolve_left`, `resolve_right`), so an authored size IS the span unless the anchors pin **both** of that axis's edges. `LayoutInput::authored_span` is that rule, written beside the solver whose precedence it mirrors, and it refuses the two cases where only the solver knows: both edges anchored, and a clamped frame, whose rect the screen can squeeze. Measured on the player's own 91 addons through the harness's `--together` load, three passes each: ``` derives per load 839 → 235 preamble per load 2,414 → 560 ms frame:GetWidth 686 µs → 60 µs per call (1,241 ms → 118 ms over three loads) ``` **1.85 seconds off every login and every `/reload`**, and the steady state gains the same way: an addon reading a size it set in an `OnUpdate` no longer walks the roster for it. Proven as well as measured: six UI captures (character, bag, merchant, quest, quest log, tooltip) are **pixel-identical** before and after, MAE 0.000, max 0; the layout suite passes under `WOW_LAYOUT_VERIFY`, which re-derives every frame from scratch and demands the same rects; and `resolve_bench` now asserts both halves — an authored, singly-anchored span answers with no gate walk, while one whose anchors pin both edges still forces the resolve and reports the anchors' distance, not the authored number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
e95de4a5ad |
log: the session's warnings are 330 down to 129, and the 201 that went were ours
A full inventory of the player's 2026-10-18 session: **330 WARN lines, 67 distinct
shapes, and not one ERROR**. Read one at a time against the code and the
reference, most of what remained was the client complaining about itself. Three
causes, measured by replaying the new rules over that same log: **330 → 129**.
**A label is not a key (33 lines).** `text=` resolves as a GlobalStrings key and a
miss shows the raw value, which is faithful; the warning on a key-shaped miss is
ours. But a miss is already a string no locale defines, so a single all-caps word
is a label the author typed — `OK`, `UP`, `DOWN`, `MT`, `TPS`, `TOP`, `BOTTOM`,
`WIM`, `TWA`, 42 lines of them in one session, every one rendering exactly as the
reference renders it. The predicate now wants an underscore, which keeps the shape
that cannot be anything else: `ATSW_ALLREAGENTLISTFRAME_CHN`, an addon's own locale
row missing from the locale it shipped, which really does paint its key name on
screen. The shipped-XML sweep keeps its own broader rule, where `text="DELETE"`
must resolve and a collision is a bug either way; its comment now says so.
**Prose is not a missing file (12 lines, and phantom load failures).** A `.toc` is
read line by line, `#` opening a metadata line and everything else a filename, so
a wrapped `## Notes:` paragraph and the `--` section headers many addons write
become entries that no file answers. The reference opens them, fails, and says
nothing. We called each one a load failure, which put it in `/errors` and counted
it in the "N addon load failures" line at every login — ColorPickerPlus shipping
"two files it does not contain" that are really one two-line sentence. A line that
cannot name a file is now a debug line; one that looks like a file and is absent
stays a warning, because then the package really is short a piece (BigWigs'
`Raids\Other\Nerubian.lua`). The addon harness shares the predicate, so its
package audit stops making the same claim.
**One cause speaks once (168 lines → 9).** The stock inspect panel's sixteen slots
each name a frame `Blizzard_InspectUI` has never been asked to build, and
QuickHeal's one undefined template is inherited ten times — per UI load, so 78
lines a session for two causes. A document's log warnings now collapse by shape:
the first two keep their detail, the rest become one "N more like …" line, and
`/errors` still gets every one, because the panel is where detail belongs.
Three shapes were checked and deliberately left, because the client is right:
`SetScript("OnInputLanguageChanged")` must keep raising (an unfired script kind
raises by this project's own rule, and nothing can fire an IME edge here); the
`SetOwner: unknown anchor 'BOTTOMRIGHT'` line is TurtleWoW's own
`Turtle_ShopUI.lua:530`, where our mode-0 fallback already matches the reference
and the line is already once per session; and a `.toc`-listed `Bindings.xml` walks
as FrameXML with `<Binding>` an unknown frame type, which the reference logs too
(the bindings themselves load by their own path and work).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
56fb28fa2b |
gates: clippy clean again, and the mail repro that never was for commit
`scripts/check.sh` has been failing before it ran a single test, on 62 clippy findings in code none of the recent rounds touched — 32 in `benilla-world`, 30 in `benilla-app` — so the in-loop gate could not tell anyone anything. Every one is a newer clippy noticing older code (`as_chunks` and `is_multiple_of` are both lints younger than the lines they fire on), and they are fixed, not silenced, except where the silence is the point: - **Argued allows, the house idiom.** `clippy::type_complexity` on ten Bevy system signatures and two inline caster queries, each with the one-line reason beside it, as the other 67 in this workspace have. `clippy::approx_constant` on `liquid/waves.rs`'s `TWO_PI`, whose own doc already says why it is spelled `6.2831853`: it mirrors a shader line, and `f32::consts::TAU` would stop reading against the file it copies. - **Two runtime assertions became compile-time ones** (`EXT_SEL_EMPTY is index 255`, the endless-stream frame count): a claim about constants should fail the build that breaks it, not a test run after it. - **Three real, if harmless, redundancies gone**: `len > MAX_TILT_TAN && len > 0.0` (the constant is `tan(4°)`, so the second test never spoke), `u32::from(..) + 1 | flags` now parenthesised where `+` binds tighter than `|`, and `palette.get(0)`. - The rest is mechanical: `chunks_exact(3)` → `as_chunks::<3>()`, `map_or(true, ..)` → `is_none_or`, two `Default::default()`-then-assign test fixtures, a doc line that began `1.` and so turned its own paragraph into a list item, and `local_utc_offset_secs` moved above the test module it had drifted below. `tests/repro_mail.rs` is deleted. Its first line says "TEMPORARY repro harness — not for commit"; it reads a loose mirror of FrameXML out of `/tmp/opencode/ vanilla-ui`, which no longer exists, so it has been failing on a missing directory rather than telling anyone about mail. What is left of the gate's red is one test: `world_api_wall` counts 174 crossings of the `benilla-world` boundary against a ceiling of 107 — 90 of them already filed with a close plan, 84 unsorted. That is not cleanup; the wall's own message says the remedies are to close the crossing or to publish it as engine API with the record that says so, and 84 of those is a design pass on what the engine's public surface is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
3e616e5cb3 |
reload: one VM and no flush storm, 615 ms off every rebuild
Two costs read straight out of the player's own `WOW_UI_HANDLERS=1` session log (2026-10-18 20:33), where a `/reload` blocks the main thread for 7.84 s. **The saved-variables flush, 305 ms.** The reload's first act is to write every loaded addon's saved variables, one file each, and `write_atomic_bytes` ends each with `sync_all()` — a disk round trip per file, 60 of them, inside the one blocked frame. Measured on this project's own btrfs home: **327 ms flushed, 6 ms not**, which is the +27→332 ms the log spends between `ReloadUI — ending the UI session` and the last `wrote …/saved/*.lua`. `write_atomic_bytes_bulk` keeps the temp file and the rename (so a file is whole or absent) and gives up only a power loss in the seconds after; the reference writes these with plain buffered CRT writes and a `.bak` rotation whose result it discards, so this is still the stronger of the two. **A whole VM built and thrown away, 310 ms.** `end_ui_session` installs a fresh boot VM, which is right for the roots that stop there — the character screen runs on it — but `run_pending_reload` then calls `load_ingame_ui_on_world_entry`, which mints its own in `mint_entry_vm`. So every reload ran `UiScript::new`, `GlobalStrings.lua`, the emote table and the font registry twice and let the first one go without running a line: the log prints `GlobalStrings loaded` at +350 ms and again at +662 ms. `end_session` now takes what to leave behind, and the reload leaves the dying VM until the mint replaces it — safe because the two calls are one exclusive system with nothing in between, the old VM is still dropped before any file loads, and `fold_dying_vm_cvars` drains what it folds, so the mint's second fold of the same VM finds nothing. Only when the rebuild is certain to mint: with no UI wanted (a capture) the load returns early and the world would be left with no VM at all. `chunkbench` is the instrument the next item needs, and the one that priced this one's neighbours: it runs a file list three ways — a VM per file, one VM with the stock collector, one with it stopped — so parse, execute and the GC's share are separate numbers. Over his enabled addon set (1,471 files, 70 MiB) the parse is 703 ms against 74 ms to compile the same chunks from bytecode, which is what a bytecode cache would be worth; the collector is not the problem (829 ms against 731 ms stopped), and pfQuest's 23 database files are 417 ms and 110 MiB of heap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
4764e4fb2d |
framexml: the twenty-one rows of the chain's own toc the manifest still lacked
benilla loads `assets/ui/benilla.toc`, not the player's `Interface\FrameXML\FrameXML.toc`, so a row the chain carries and the manifest omits is silently absent — no read attempt, no warning. Six went in with the arena templates; this is the rest of the diff but one, each with a comment on what it is for and in the chain's own order. The six baked Octo extensions are the player-visible half. `/pvp` now asks before a War Mode character spends its permanent, persisted opt-out. The exhaustion tooltip reads this realm's 175% instead of 1.12's baked 150%, and `/rested` answers. A player's hover shows the realm's own two challenges, Samurai and Together Forever: the server was already setting bits 10 and 11, and `TargetFrame_UpdateChallenges` walks a list that stopped at 10. The one-shot War Mode offer on arrival from the hardcore realm renders instead of being dropped. `/bug` and `/suggest` exist again — Turtle's FrameXML dropped the BugReport frame, and this rebuilds the dialog over chunked `.bug` SAY commands. And on C'Thun the Ghost debuff's tooltip carries the hardcore afterlife line, which is why the realm reverted that string in Spell.dbc. With them: `OctoToolReqTip`, the realm's own 40 KB recipe-to-tool table wrapping `GetTradeSkillTools`, so a recipe whose tool OctoWoW changed names its real one; six `OctoLocale` rows, each gated on `GetLocale()` and inert here; and the three Turtle windows the manifest said needed verbs built first — arena (4 rows), barbershop, guild bank (2). They need exactly one, `CameraZoomOut`, now built. All three publish their frames and open only on an exact gossip trigger string, so answering nil for `GetGossipText` cannot hide ordinary gossip. `StatsFrame.xml` is the row left out, and the manifest says so where it would sit: the debug-build overlay whose OnUpdate is the only reader of `GetDebugStats`, a binding we do not have, so the row would publish a `ToggleStats` that raises twice a second once pressed. `UNLEARNED_SPELL` joins `UNPRODUCED` rather than getting a producer. It is the only entry there that is not a 1.12 event at all: 5875's event-name pool carries `LEARNED_SPELL_IN_TAB` and `SPELLS_CHANGED` and no unlearn sibling, so the reference ignores that registration exactly as we do, and the War Mode re-scan rides PLAYER_LOGIN and LEARNED_SPELL_IN_TAB, which both fire. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
02265cc224 |
compat: nampower's equipment and item-cache getters, for the character panel
"then we gotta fix bettercharacterpanel, that one is driving me nuts."
BetterCharacterPanel is built on five nampower getters and had none of them.
`GetEquippedItem` was 8 raises in one session of his log; the other four failed
inside the addon's own `pcall`s, so its item-level column, quality colours,
missing-enchant warning and two-hand test were all dead at once. DoiteAuras
reads the same getter in six places.
The spec is the installed `nampower.dll` itself rather than inference, which is
what unblocked this — the earlier note here said to find the upstream spec
first, and the DLL *is* the spec:
- its usage strings give the signatures,
- "Slot must be a number between 1 and 19" gives the band, so the slots are
1.12's own `GetInventoryItemLink` numbering and `NampowerAPI.lua`'s
`slot - 1` in `IsItemInSlot` is that shim's bug, not the contract,
- its string pool gives the key names in order, and
- "Item not found in DBCache" is a raise, which is exactly why the addon
`pcall`s these three and calls `GetEquippedItem` bare.
`itemLevel` is the one field 1.12 cannot reach at all: `GetItemInfo`'s fourth
value is the *required* level, and later clients are the ones that insert
`itemLevel` there — so the panel's item-level column had no possible source
before this. The cache row now carries it, with `buyPrice` and `displayInfoID`
beside it, and an equipped item carries the raw descriptor numbers the tooltip's
resolved enchant lines could not answer (an id whose `SpellItemEnchantment` row
hides its name is still an id).
Fields left absent rather than guessed are listed in code and in
`docs/ADDON_COMPAT.md`; nothing installed reads any of them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
85cd750a4e |
vplates: no plate for a critter, by the client's own no-TAB flag
grom asked for the name and level over every rabbit to be gone. pfUI cannot do it: its option is
"Hide Healthbar On Critters", and the branch it takes is `level:Show() name:Show() health:Hide()`
— bar hidden, name and level kept, a label floating in empty space.
The reason it shows up here and not on his real client is our own compat. That pfUI branch needs
`UnitCreatureTypeID` on a plate GUID, which needs the `NAME_PLATE_UNIT_ADDED` → `C_NamePlate` →
`UnitGUID` chain; stock 1.12 has none of it, so the check returns nil there and a critter gets an
ordinary plate nobody notices. We answer all three, so the feature wakes up and strips the bar.
So the gate goes where the plate is decided. It keys on `CreatureType.dbc`'s **no-TAB flag**
rather than on the id 8: that bit is the client's own mark for a creature the player does not
mean to interact with, it is set on Critter alone in the 1.12 data, and `target::scan`'s TAB
gate already reads it — one notion of "critter", from the data, with two consumers. An
unresolved type keeps its plate, as the scan treats an out-of-range index as targetable.
A deviation, and `nameplateShowCritters` says so: the reference's ladder (`0x60f600`) has no
creature-type test at all. Off by default, because the floating name is the thing nobody wants;
`SetCVar("nameplateShowCritters", "1")` restores vanilla. Removing the plate leaves nothing
behind — the overhead-name path gates a creature on `UnitNameNPC`, which is "0".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
07d41dc321 |
portrait: measure SetPosition's unit instead of fitting it
One `WOW_BOOTH_LOG` run with Tmog open printed every term, so the factor is arithmetic now rather
than a fit:
- The cards draw through the paper-doll camera, logged `eye=(0.003, 1.103, -2.919)`, and the same
run logged the dwarf's `head_y=1.364`.
- Tmog's browse cards frame on the slot being browsed, so its per-race **head** row is a request
to put that race's head on the camera's axis. The dwarf row's vertical is `-1.2`.
- Centring the head needs `1.364 - 1.103 = 0.261` model units against a request of `1.2`:
**0.2175**, rounded to 0.22.
The same run confirmed the whole chain end to end: the logged pose inverted back to
`(3.72, 0, 0.489)` at facing 3.2, and `Tmog.lua:332` reads `{ 3.8, 0, 0.5, 3.2 }` — the small gap
is the previous factor, `G48` at grom's actual aspect.
It cross-checks on depth: the head row's `6.3` then moves the model 1.37 of its 2.92 standoff,
leaving it 1.55 out — a view about 0.75 units tall around a head of roughly 0.35. The two earlier
values are what make the arithmetic worth trusting: 1.0 put every card *behind* its camera and the
grid drew empty; `G48` ≈ 0.49 left them 1.06 out, the extreme close-up in the screenshot.
Still a measurement of an addon's table against this client rather than of the reference's own
root, and the constant says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
751cac062b |
compat: C_Spell.GetSpellEffectMechanics, the per-effect sibling
`SuperCleveRoidMacros/ClassicAPI.lua:267` calls it unguarded, so its absence was a raise a
session. It is not a second spelling of `GetSpellMechanicByID`, which this tree already answers:
that one reads `Spell.dbc`'s spell-level `Mechanic`, and vanilla stores some mechanics on an
*effect* instead. The consumer's own comment names the case — Rake's bleed is effect-level, so
the spell-level column says 0 while this answers `{0, 15, 0}` — and a macro conditional asking
whether its target is bleeding gets the wrong answer from the spell-level field alone.
`SpellDisplay::effect_mechanic` was already parsed for the cast-immunity scan, so this is a field
on the probe payload and a reader, not a new lookup. The three-way answer is its sibling's, for
the sibling's reason: nothing at all for an id no spell owns, three zeros for a real spell that
carries no mechanic anywhere, the ids otherwise — collapsing those two would reclassify crowd
control as ordinary damage, which is the hand-kept table this function exists to replace.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
fe00d16ec6 |
trainer: a service's skill line is the profession, not its group header
`GetTrainerServiceSkillLine` answered the group header text. At a class trainer the two are one string, so nothing showed; at a tradeskill trainer they are not. Every group there is labelled from `TRADESKILL_SERVICE_STEP`/`_LEARN` (`0x4d7786`), and `TRADESKILL_SERVICE_STEP` is the string "Development Skills" — so standing at the mining trainer, the stock confirmation dialog, which formats this return straight into `PROFESSION_CONFIRMATION2` (`Blizzard_TrainerUI.lua:28`), asked whether to learn **Development Skills** as a second profession. `TrainerService` now carries the taught spell's own line beside the group label, resolved through the same `SkillLineAbility` lookup the group key uses, and the getter prefers it. The header row is left alone: "Development Skills" there is the client's own label for a skill-stepping tradeskill group, not a lookup that went wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
072d691449 |
portrait: fit SetPosition's unit to the one table that calibrates it
Shipping one model unit per `SetPosition` unit put Tmog's browse cards behind their own cameras and the grid drew empty: its per-slot table asks for depths of 3.8 (gnome) to 11.8 (night elf) against authored cameras that stand about 2 to 6 units off. That table is itself the measurement — each row has to land that race's head in a card — and across gnome, dwarf, human and night elf the displacement they need sits at 0.45–0.55 of what they ask for. `G48`, the client's own `1/√(a²+1)` at `[0x832a48]`, is 0.50 on a 16:9-ish panel and 0.6 at 4:3, and it is the constant `Model::layout_unit` already inverts for the orthographic leg. So the translate is `pos · G48`. Fitted, not traced, and the extract's arm says so: a capture of the reference's root would settle it, and the wheel zoom this landed for in the first place works at either value because the player drives it by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
06011e5e5b |
ui_script: the LoadOnDemand log line stops claiming "not loaded"
A LoadOnDemand addon can already be in memory when the walk reaches its row, because an addon ahead of it loaded it from its own `ADDON_LOADED` — which is how every AI_VoiceOver data module actually arrives: 224 ms of Lua inside AI_VoiceOver's handler, before the walk gets to the `AI_VoiceOverData_*` rows at all. The line was printed unconditionally, so it went on saying "no LoadAddOn() yet" about addons that had loaded. That reads as a bug and is not one; it cost a session chasing a missing `GetAddOnEnableState` that was never the problem. Gated on a new `UiScript::addon_is_loaded`. `docs/ADDON_COMPAT.md` now records why that global stays unbuilt, since the evidence was gathered anyway: upstream ClassicAPI's own `docs/API.md` does not define it either, and AI_VoiceOver gates a modern-shaped `GetAddOnInfo` on the same `if not GetAddOnEnableState`, so defining it would leave `VoiceOver.lua:84` reading `loadable` where it means `reason` and pop the "delete your old folder" dialog on every login. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
7fef604615 |
portrait: a unit pane honours Model:SetPosition
`SetPosition` is the widget's model-root translate and the extract spent it only on the file-pane
path. The booth path dropped the value and posed `R(yaw) · S(s)` alone, so Tmog's wheel zoom, its
right-drag pan and the per-slot framing of every browse card moved nothing at all.
It shows on a `<PlayerModel>` and not on a plain `<Model>` for one reason: this widget's camera is
frozen (`0x7acf10`) where `<Model>`'s perspective leg republishes its authored camera through the
very root being moved (`0x718960`), which cancels. `capture::probe_model_camera`'s `position` leg
pins that half; this is the other.
The extract publishes the translate in bevy's axes into a new `PanePositions`, keyed by slot so the
four stock window panes and the stock dressing room are served by the same channel rather than a
second path; a slot that is not drawn keeps the translation it has rather than snapping back to the
origin. The booths apply it as one `RootPose { yaw, scale, pos }`, latched together — keyed on
`(yaw, scale)` alone a position-only change compares equal and writes nothing, so a wheel would
zoom once and then stop. `WOW_BOOTH_LOG` gains `pane pose slot=… yaw=… scale=… pos=[…]`, one line
per change.
One model unit per `SetPosition` unit, and deliberately not the reference's `pos · layoutScale`:
that factor belongs to the group the reference's root scales with it, and this booth's root carries
only the renormalizer (`framing::pane_root_scale_at` drops `modelScale` and `layoutScale` both), so
taking the translate's share alone would move a `scale="4"` pane's model four times its own size.
The magnitude is the one part nothing in the tree pins — the only two calibrated `SetPosition`
sites are the ortho leg's pixel ladder and the `<Model>` leg where it cancels — and the extract's
arm says what to revisit if a wheel notch turns out to zoom too hard.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
da0f647be6 |
portrait: size a pooled pane's bake to the pane, without touching what it shows
Tmog's full-body preview is ~500 physical pixels tall and was drawn from a 256² bake, so it read soft beside its own grid of cards — which 256² oversamples. One size cannot serve both. A pane larger than its bake now steps up to 512², so a page of fifteen cards plus one large preview pays for a single upgrade rather than sixteen. This is the second attempt. The first widened `BoothPanes`' value type, which `sync_body_booth` and the dressing room read to frame their cameras: a change meant for resolution reached into the path deciding what a pane *shows*, and a page of previews stopped showing their own looks. The size now travels in `PaneBakeSizes`, written by the extract and read by `resize_pane_targets` alone. Nothing on the content path sees it. The first attempt also called `Assets::get_mut` to *read* a target's size. That marks the asset modified, and a modified `Image` is re-uploaded from its CPU pixels — the zero-fill it was created with — silently erasing whatever the booth had just rendered. The size is now probed read-only and `get_mut` is reached for only when it must change, pinned by `a_target_already_the_right_size_is_not_touched`. An absent entry means "not drawn this frame", never "wants the default", so a paced pane cannot shrink its own target between renders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
81d96af35d |
Everwood: the HD/OctoWoW line of benilla, imported onto upstream
This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (
|
||
|
|
706cd717a3 |
merge: absorb samwhosung/main on top of the upstream merge
Same fork policy: upstream structure wins, local-unique re-instated. Cargo.toml unions both measured dev-profile philosophies (codegen-units=1 + lto=off from samwhosung, debug=line-tables-only + split-debuginfo from the fork's disk measurements). glue_booth.rs takes samwhosung's typed GlueScene (token/of_race/from_model_path) and re-adds the fork's TurtleWoW races (Goblin=9, BloodElf=10) as variants; their glue fog stays on Human's row, exactly as the fork's scene_fog fallback did. ui script takes samwhosung's mouseover/hyperlink rework (world_mouseover, minimap_blip, hyperlink.rs) over the fork's inline hover_link/span_at/pointer path — the rework covers the same OnHyperlinkEnter/Leave behaviour. game_plugins.rs keeps the fork's re-measured ceilings (466 / 5,509); mail.rs keeps the explicit 13-tuple with a float daysLeft; MAP.md takes the superset. |
||
|
|
26d714fd1d |
merge: absorb upstream/main into the Turtle fork
Fork policy (docs/METHOD.md): upstream structure wins; local-unique work re-applied on top. This commit is the upstream half of the reconciliation. Upstream is the same program at a rewritten history ( |
||
|
|
2af917044d |
Stock login fetches the server alert; LaunchURL behind the gate (#1040)
The stock login's Breaking News box (`WOW_GLUE_STOCK=1`): - At glue init, as `0x46a400` does, the client reads the Lua global `SERVER_ALERT_URL`, which the player's own GlueStrings or a server's patch sets. It fetches the URL once on a background thread: plain HTTP, 5 s timeouts, redirects followed. - The body builds the reference's buffer (`0x46a580`: 0x7ff cap, appended chunks). A case-insensitive `SERVERALERT:` prefix fires `SHOW_SERVER_ALERT` once from the update (`0x46b939`). Every failure is silent. - `LaunchURL` opens the browser only for URLs that pass the reference's gate (`0x5abc10`): `http://`, a strict charset and the 10-host whitelist. An over-long host is refused rather than cut. - No fetch or browser ever runs in a test, capture or unattended run. Tests run against a local fake server: prefix, 404, redirects, chunked, timeouts, caps, and the frame shown end to end. Part of #1026 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
5c7eb5fc75 |
Stock glue screens draw their 3-D scenes through the glue booth (#1039)
Part 4b of the stock GlueXML (`WOW_GLUE_STOCK=1`): the stock screens' ModelFFX scenes now go through the glue booth instead of the model-tile path, which drew them black. - The scene, look and facing come from the glue VM. `SetCharSelectBackground` runs the frame's own SetModel and swaps its fill callback (`0x472128`, `0x472144`), so the select scene fogs until a character sets its stage. - An ModelFFX pane draws the booth's scene at its own bucket's ARTWORK layer (`0x46fa90`), framed at the pane's aspect with the diagonal law (`0x7ada40`, `0x5c3cc0`). The native box Deviation now holds for the native screens only. - Each SetModel restarts the stage. The select glow holds through an empty roster until the screen shows again. - An empty roster shows the Orc scene fogged, and after the last delete the last stage stays, as `CharacterSelect.lua` leaves them. Part of #856 Fixes #978 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
bb82dd1ac8 |
Lua getters answer the reference's miss shapes; tooltip setters' flag (#1030)
Lua bindings answer the reference's shapes on misses, bad indices and tooltips: - Indexed getters push their full miss tuple (`GetCraftInfo`, `GetPetActionInfo`, the trainer requirements and others). Indices go through `ftol` and an unsigned compare, so a negative misses instead of raising. Verified slot readers keep their own raises (`IsInventoryItemLocked`, `0x4c8520`). - The eight GameTooltip setters return the builder's refresh flag, gated as `0x53267c` gates it, so a hovered cooldown tooltip refreshes. `LOCKED` follows the item object (`0x52bdb3`). - Quality goes through the InventoryType gate (-1), and a pending template shows the question-mark icon. - `GetLootSlotInfo` returns four values. `SetSpell`, `CreateMacro`, `JoinChannelByName`, `GetQuestReward` and the shapeshift and trade-skill filters raise or answer as the reference does. - The shape gate probes each getter's miss legs through an argument ladder. Fixes #145 Fixes #993 Fixes #994 Fixes #995 Fixes #996 Fixes #997 Fixes #998 Fixes #1016 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |