Four pointer defects: OnEnter and OnLeave set `arg1 = true`; a frame being moved or sized still ran the hover walk; the pointer leaving the window fed `f32::MIN` to a move in flight, flinging the frame off-screen; and the mouseover token read the world pick, not the published mouseover.
In 1.12.1 both scripts run through `0x702690`, which sets no arg global. While `[root+0xcfc]` holds a move or size target, the walk pumps it (`0x7655b0`) and returns before any probe (`0x766164`–`0x7661a0`); a hide clears that slot (`0x76492e`). The pump only sees real cursor events. The token resolver reads the published pair `[0xb4e2c8]` (`0x515970`).
OnEnter and OnLeave fire with no arguments, `mouse_move` pumps a move or size and returns, a hide ends it, the pointer leave has its own call, and the aura feed reads the published mouseover.
Tests bite on the parent for each.
Fixes#932Fixes#933Fixes#936Fixes#938🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moving from the world onto a window, the world mouseover's loss reached the tooltip a frame after the window's OnEnter, so a gate kept the fade off any tooltip the world had not put up.
In 1.12.1 the hover walk runs the old frame's leave before the new frame's enter (`0x766229`, `0x766239`). The WorldFrame's leave (`0x483b10`) runs OnLeave, then publishes the empty mouseover, whose publisher fades GameTooltip whoever holds it (`0x492890` → `0x530ae0`). A nameplate's leave publishes first (`0x7cb8b0`); the Minimap's hides the tooltip (`0x4eb880`), and its probe drops a held blip on a miss (`0x4eb0c0`).
The VM now holds the published mouseover and runs these leaves in the walk, the hide and every mouse disable, freelook's plates included (`0x60f830`); the ownership gate is gone.
Tests: OnEnter no longer sees "mouseover", an addon tooltip fades on a change, blip and Minimap leaves hide it.
Fixes#901🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GameTooltip's lifecycle took shortcuts the reference does not: a 0.5 s linear fade armed once, a fade cancelled by new lines, cursor-anchored plates that faded instead of hiding, a SetOwner that never hid first, the last anchor kept after a hide, and a health bar shown and hidden without its handlers.
In the reference, FadeOut re-arms a 2 s countdown that holds full alpha for half and then ramps, or hides at once at the cursor anchor. Only the owner core, the show and FadeOut write the fade. SetOwner runs the tooltip's own hide first. The un-own leaves ANCHOR_NONE. The clear hides the bar through its own transition.
Every builder, verb and world or minimap seat now goes through these functions; an uncached item hides and un-owns as in the reference.
Tests pin each of these.
Fixes#889Fixes#134Fixes#896Fixes#893Fixes#895Fixes#843
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UI scale was folded into the pixel seam, with the root and `UIParent` at scale 1. Frames outside `UIParent` (dropdown menus, need/greed rolls, the world map) shrank with it, `UIParent:GetScale()` answered 1, and `GetCursorPosition()` was off by the scale.
In 1.12.1 the scale is `UIParent`'s own frame scale (`0x494550` → `0x76ac10`, floored at 0.64) over a 768-unit root. `GetScreenWidth`/`GetScreenHeight` divide by it (`0x494590`), and `GetCursorPosition` (`0x48b820`) does not.
Now the seam is `height/768`, and the scale is applied to `UIParent` at the load's end and on a resize. The nameplates' compensation is gone. Backdrops, hit insets and minimap pings now follow a frame's own scale (`0x76b580`, `0x77e8d0`); tooltips at the cursor and `OnCursorChanged` divide by it (`0x530b20`, `0x77db23`). Tests cover each, and a stock-UI comparison shows nothing inside `UIParent` moves.
Fixes#172🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- capture: trainer scenarios shoot the service description field (#793)
- fix: trainer services show their spell's description, as 1.12.1 does (#793)
- review: reuse the caster closure, cut repeated comments, pin the profession rows
- trainer: the pet's skill value is the tooltip's one helper, not a copy
- trainer: the recipe row's real-data test reads a product that has text
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
- spell text: $g/$G take the active player's gender and the chosen form is space-trimmed, so a female character reads "her" (0x508180, 0x508214)
- spell text: trim the $g docs, drop the colon-less $g pin
- spell tooltips: $g reads the view's player store; nets cover gender
- item tooltips: re-substitute $-texts when the gender byte moves
- tests: share one real-spells loader beside Spells
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
Percentage costs took their share of the max pool, so Bloodrage cost 20% of max health. The reference's basis table (`0x612c50`, reached from `GetPowerCost` `0x6e31b0`) reads the caster's fields:
| power type | basis |
|---|---|
| health | base health (`UNIT_FIELD_BASE_HEALTH`, 163) |
| mana | base mana (162) |
| rage | 1000 |
| focus, energy | 100 |
| anything else | 0 |
`cost_basis` now holds that table, and `power_cost_at` reads through it for the tooltip, the press-path gate and the action bar. The spell tooltip's rebuild inputs watch base health in place of max health, for the player and the pet. Tests pin each type's basis, and Bloodrage on real data: "337 Health" at level 60, whatever the gear.
Fixes#604
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
A warrior chasing a running mob was refused Hamstring at 6-7 yd with "Out of range.", where 1.12.1 lets it go out.
After both arms, `GetMinMaxRange 0x6e3480` adds 2.6667 yd (`[0x811914]`) to the max when all of these hold (`0x6e3648`-`0x6e36a2`):
- the caster and the target unit both move, with a bit of `0x200d` set;
- both run faster than twice their walk speed (`0x5fc350`);
- the row is melee, or the target is a player.
On the melee row, the auto-attack target stands in when no unit is passed.
`min_max_range` now takes each unit's motion and adds the bonus. Every caller passes what its reference call site passes. The ambiguity ceiling rises by the 11 pairs this adds, with the reason beside it.
Test: Hamstring goes out at 7.5 yd with both units running and is refused standing still.
Fixes#678
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`UnitCreatureType` read only a creature's template type, and fell back to "Humanoid" for a player. The form stage was not built. So a druid in cat, bear, travel or aquatic form and a shaman in Ghost Wolf all answered "Humanoid", where 1.12.1 answers "Beast".
`UnitCreatureType 0x51a280` names the type the resolver `0x605570` returns, and 0 names nil (`0x51a2c3`). The resolver tries three stages in order:
1. **Form:** the form byte (`UNIT_FIELD_BYTES_1` byte 2) indexes `SpellShapeshiftForm.dbc` column 12, which applies only above 0 (`0x60559a`).
2. **Template:** else the cached creature record (`0x6055a8`).
3. **Race:** else the race's `ChrRaces.dbc` type (`0x6055ca`).
A token naming no unit answers nil (`0x51a2b8`). The unit tooltip's level line takes the type only for a non-player (`0x52a4a6`).
Changes:
- `ui_unit::snapshot` sets `creature_type_name` for every unit through the shared `CreatureTypeSources::of`: players, creatures, pets and group tokens alike. `enrich_unit` no longer sets it.
- `UnitCreatureType` reads that field, with no fallback.
- The feeds at Bevy's parameter limit carry the sources in a `SnapshotTables` param.
- A player's tooltip level line is unchanged, since it reads race and class.
Behaviour that now matches the reference beyond the issue:
- a held pet, `partypetN` and `raidpetN` answer their template's type rather than nil;
- a party member in a form reads the form's type;
- a roster member with no object, and the pre-arrival player seat, answer nil.
Tests:
- `the_shipped_forms_name_a_beast_or_fall_through_to_the_race` reads the real `SpellShapeshiftForm.dbc`.
- `unit_creature_type_names_the_form_before_the_race_and_a_creature_its_template` runs `feed_units` and asks the Lua binding.
- Mouseover, party, raid and pet feeders each have a test.
- `a_players_level_line_ignores_the_snapshots_creature_type`.
Fixes#641🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`min_max_range` took one `target_reach` that served two roles: the melee arm's fallback to the auto-attack target and the ranged arm's explicit target. The ranged arm padded whenever it was `Some`. The tooltip passed the auto-attack target's reach, so while you swung, every ranged spell's tooltip read its row plus both reaches: Fireball read "38 yd range" where 1.12.1 reads 35.
`GetMinMaxRange 0x6e3480` counts its target (`[ebx+0x10]`) only when it is a unit (`0x6e34e1`-`0x6e34f9`).
- **Melee arm** (`0x6e3552`-`0x6e3594`): with no unit target, it falls back to the caster's auto-attack target (`0x6e356a`), then to the caster's own reach.
- **Ranged arm:** pads only for a non-null target (`0x6e35d8`, `0x6e35ec`). The max always takes the pad, and the min only when nonzero.
What each caller passes:
| Caller | Call site | Target |
|---|---|---|
| Tooltip | `0x52e9c2` | none |
| `ActionHasRange` | `0x4e5852` | none |
| The cast check and `IsActionInRange` | through `CanTargetUnit 0x6e4440` → `0x6e47b0` | the bound or selected target |
Changes:
- `min_max_range` takes `RangeTargets { target, attack_target }`.
- The melee arm reads the target, else the attack target, else the caster. The ranged arm pads for the target alone.
- Each caller passes what its reference call site passes. The action button's two verbs are now two calls: `has_range` with no target and `in_range` with the selected one.
- `min_max_range_reads_reach` narrows to melee rows. The tooltip safety net then counts 813 reach-dependent views instead of 10,298, so starting to swing no longer rebuilds every ranged tooltip.
Tests:
- `ranges.rs`: `the_ranged_arm_pads_only_for_the_explicit_target`, `the_melee_arm_falls_back_from_the_target_to_the_attack_target_to_the_caster`, and the narrowed exactness test.
- `range_cell_on_real_data` reads Fireball as "35 yd range" and Charge as "8-25 yd range" with an auto-attack target present.
- `cast_range_refusal_follows_the_two_compares` covers a bound target's pad.
- `a_ranged_button_pads_its_range_by_the_selected_target_alone`.
Fixes#643🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every spell-tooltip invalidation rebuilt every view the feed had ever pushed. The remembered set only grows: the book, every talent rank and trainer service, and every aura ever seen. So in a long session each of these frame-hitched for 1.5 to 4 ms:
- a crit or dodge change;
- a worn-slot change;
- a target swap while auto-attacking;
- a spell-modifier packet;
- a reagent cast.
The reference builds a tooltip at the hover (`0x52e610`) and keeps nothing. benilla pushes views ahead of the hover, so it has to know what each view was built from.
Each pushed view now records, as it is built, the inputs it read: the `$z` home area, the form line, the worn slots the equipped-item search reads, and so on. Each record sits at the line that reads the input.
The inputs:
- the `$z` home area;
- the form line (a spell with `Stances`);
- the worn slots the equipped-item search reads, each slot's item, template and broken state, and the disarm flag;
- the chance line's percentage (`Effect[0]` 20, 22, 23, 78);
- the combat reach the range cell reads;
- the skill lines the `$`-tokens scale by, the spell's own and those of each spell it cross-references;
- the modifier family-flag bits of the spell and of those it cross-references;
- the reagents it lists;
- the player's level, base mana, max health and max power where the cost or cast cell reads them;
- the ranged attack time the cooldown cell pads by.
The feed diffs those inputs frame to frame and rebuilds only the views whose recorded reads intersect what moved. A class-family change still rebuilds everything, and the pet's own inputs still rebuild the pet views.
The feed moves out of `ui_tooltip/mod.rs` into `spell_feed.rs` (the builder and the system) and `spell_deps.rs` (the records and the diff). `TokenContext::home_area` becomes a closure, so a `$z` read is recorded exactly.
Measured in release on the real data with 2,000 remembered ids, per forced change:
- crit or dodge: 3.3 ms → 26-38 µs;
- one worn slot: 3.5 ms → ~100 µs;
- one modifier cell: 3.5 ms → ~60 µs;
- all 64 modifier bits: 3.5 ms → ~0.6 ms;
- reach: 3.3 ms → 1.3 ms, still high because the range cell also pads ranged rows by reach (#643);
- a quiet frame: unchanged.
The bench is `ui_tooltip/bench.rs`, `#[ignore]`d.
Tests:
- `deps_tests.rs`, the safety net, builds all 22,357 real views. For each input it moves the input alone and fails on any view that changed without having recorded it, naming the spell and the input.
- It also moves everything each view did not record at once, from both sides.
- It pokes every player-store field (and every pet-store field for the pet views) to discover reads mechanically.
- It enumerates the equipment slot states the item search distinguishes.
- `feed_tests.rs` drives `feed_spell_tooltips` itself. A dodge change re-pushes only the dodge chance-line view, and a modifier cell only its family's views.
Fixes#535🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`SetPetAction` (`0x532888`), `SetSpell` on the pet book (`0x532e23`) and a pet-learn
`SetTrainerService` (`0x533a7f`) pass the spell builder `0x52e610` its unit selector. With it set,
the level lookup `0x6e3130` asks the player's charm, else its summon (`6e3159`-`6e317f`), for
`UNIT_FIELD_LEVEL × 5` capped at `maxLevel × 5` (`0x60cd80`), and `GetPowerCost` `0x6e31b0` costs
against that unit: its level term and `0x612c50` on it, base mana for a mana spell (`6e327d`).
`GetCastTime` takes the same level (`6e3397`). No such unit reads level 0 and no cost.
The VM keeps a second view store for the pet, which the selector's setters read; the app builds
those views against the pet's fields and rebuilds only them when the pet, its level or its bases
change.
Verified: real-catalog tests (Sacrifice rank 1 at pet level 22 reads 318, Blood Pact rank 1 at 14
reads 3, Seduction costs 449 from the pet's base mana 1874), each failing with the change undone;
the player's views of the same spells are unchanged. Live, a level-60 warlock's succubus shows
Seduction at "449 Mana" (main computes 329 from the warlock's base mana 1373) and its voidwalker's
Sacrifice rank 1 absorbs 414, the capped level-22 value with the warlock's 30% talent mod, where
main reads 396. Built on macOS only; no platform seam.
Fixes#600Fixes#601
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- fix: apply per-level scaling to spell descriptions
- The description's per-level terms scale by the player's skill in the spell's line, not the character level
- The skill value reads its bonus word as the reference does: the high half unsigned over a positive value, the low half signed
- The $d and $o durations take the row's per-level term at the spell's own skill level
- $d reads a duration at or below 0, or no duration row, as until cancelled
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
- fix: spell modifiers
- fix: round scaled spell tooltip values to client tenths
- fix: refresh only modifier-dependent item tooltips
- The pet leg of the SPELL_GO cooldown insert takes op 11, and the item leg takes none
- The tooltip's cast cell is GetCastTime's unclamped result
- The Improved Fire Blast tooltip test reads Fire Blast's real recovery columns
- The effect-points aura ops follow the reference's jump table
- The template filler rounds %f and %g ties half up, as the client's CRT does
- The $s, $S, $m, $M and $o tokens compute and print their values as the reference does
- The $t, $r, $e and $a tokens follow the expander's arms
- The unclamped cast-cell test skips without an install, as its GlobalStrings come from one
- start_gcd's doc names the reference's gate: category or time nonzero, then op 21, and nothing armed for a zero result
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
- Spell tooltips for pet spells, quest reward spells, Beast Training, enchant links and pet or target-of-target debuffs show whole on the first hover
- The spell-tooltip tests' doc comments say the one thing the test name does not, in a line
- The spell feed builds each view once per frame, the pet-bar Firebolt test slot is one helper, and the wait's comment reads in one pass
- A spell tooltip completed by its view fires no second OnTooltipCleared: one setter call, one event, as in 1.12.1
- The spell feed reads every aura and the tracking spell from the VM alone, dropping the three app-side aura reads that duplicated it
- The spell feed reads the auras from the VM's player list and every guid's list, the aura model main moved to
- A spell tooltip waiting on its view re-renders only while it still shows what the miss drew, so a line Lua rewrote in place, by AppendText or a cell's SetTextColor, is kept
- The spell tooltip's one-frame re-render is stated as a known difference from the reference, which builds every spell tooltip at the call
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
`"mouseover"` now names nobody once the mouse leaves a unit, as in 1.12.1, where the publisher `0x492890` zeroes the mouseover guid pair on teardown (`0x4928e8`, `0x4928f2`) and a corpse or GameObject hover leaves it naming no unit. Before, `UnitExists("mouseover")`, `UnitName("mouseover")` and `UnitIsUnit("mouseover", …)` kept answering for the last unit hovered.
- The world tooltip's health bar follows its subject's guid, as the builder `0x529fe0` registers its health watcher on the guid (`0x52a0a8`), so clearing the token leaves the fading plate and its bar as they were; the clear fires no `UPDATE_MOUSEOVER_UNIT`, whose one fire site is the unit arm (`0x4929b3`).
- One definition of the hovered unit (none when a nearer GameObject wins the pick) serves the tooltip, the unit tokens and the aura feed, so they clear together.
Fixes#500
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Hovering a unit now makes `UnitIsUnit("mouseover", "target")` (or any other token naming the same unit) answer 1, as in 1.12.1, where `UnitIsUnit` (`0x516070`) resolves both tokens through `0x515970` and compares their guids, and `mouseover` reads the hovered unit's guid (`0xb4e2c8`/`0xb4e2cc`, written by `0x492890`). The `mouseover` unit state was pushed with guid 0, so every such comparison answered nil.
`ui_unit::snapshot` now takes the unit's guid and sets it, so every unit state built from a snapshot carries it; the callers that set it by hand afterwards no longer do.
Fixes#496
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Targeting an NPC whose faction you are Exalted with no longer opens a script error. `UnitReaction` answered 8 there, and stock `TargetFrame_CheckFaction` indexes `UnitReactionColor[reaction]`, which has seven entries (`TargetFrame.lua:6-14`, `:183`).
- **The cap.** `UnitReaction 0x6061e0` takes `0x606530`'s answer through `min(rank, 6)` at `0x606439` (`cmp esi,6` / `jl` / `mov esi,6`), so Revered and Exalted both read 6, and the Lua binding adds one (`0x51683e`), so Lua sees at most 7. `ring_reaction` now caps its answer there.
- **The ladder stays whole.** `reputation_rank` is untouched: the reputation pane and the item requirement lines still read Exalted.
- **Every caller** of `ring_reaction` models `0x6061e0` and tests at 1, 3, 4 or 5, so none changes but the four Lua unit feeds (target, targettarget, npc, mouseover), which now share one `unit_reaction` helper. The vendor discount's Exalted row reads as Revered, as in the reference, and still takes 10%.
- **Comments** that gave the scale as 0..7 or 1..8 now say 0..=6 and 1..=7.
Checked: `the_reputation_rank_is_capped_at_revered` walks both sides of the Honored, Revered and Exalted thresholds; `an_exalted_npc_target_reads_revered_and_its_plate_is_green` runs stock `TargetFrame` on an Exalted Stormwind unit. With the cap loosened to 7 both fail, the second with the issue's `TargetFrame.lua:183: attempt to index field '?'`; restored, both pass. Gates green on macOS; no platform seam touched.
Fixes#440🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Fix trainer tooltip on first hover
- The spell-tooltip feed keeps its in-game gate, and a test holds it after the trainer feed
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
The strings pass for `benilla-app`, the last crate: string literals and test names lose the private record numbers and the workspace's words that a comment pass cannot touch.
Fixes#92. Fixes#36.
- **Messages.** About 230 assertion, panic, `expect` and log messages, printed tool labels, and comments inside embedded Lua fixtures drop their record numbers; the eight that named "the director" keep their fact in plain words. Stale claims in a few messages are corrected on the way (a cast-bar step, a font line, the every-shot recompute).
- **Names.** Seven test and helper names that carried a record number or "director" now say what they check. `git grep` found no other use of any of them.
- **Player files.** The key-bindings file's header and `config.toml`'s header each lose a record citation. Neither header is compared when the file is read back: the bindings reader skips every `#` line, and `config.toml`'s header is a TOML comment.
- **Fixtures.** Test fixtures use neutral character, account and frame names.
- **#36.** The quest-material test labels GameObject type 10 GOOBER (in 1.12, 3 is CHEST, 9 TEXT, 10 GOOBER); the code under test was already right, and the comments were fixed with the comment pass.
- **Verification.** With comments stripped and every literal's contents masked, each file is identical to main and keeps its literals' line counts, apart from the renamed functions and one rustfmt reflow. `cargo fmt --check` is clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Part of #41: the second of five `benilla-app` pieces. This one is the UI windows: chat, the NPC windows (gossip, merchant, trainer, taxi, bank, mail, auction, quest giver), the unit, party, raid and aura feeds, the action bar, bags, tooltips, macros, the spellbook, the world map and the smaller panels. Their comments are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 26,648 comment lines become 9,221.
Fixes#98.
- **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal.
- **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them:
- Several client addresses carried a `0x400000` slip (`UPDATE_MACROS` is `0x852460`, not `0x452460`; the macro store's globals likewise).
- `QUEST_WATCH_UPDATE` carries the quest-log row (`0x4df880` scans the log table at `0xbb71c0`), not a watch-list position.
- The stock `ChatFrame_DisplayTimePlayed` now ships, so benilla's own `/played` lines duplicate it (#194); trainer refusals are logged, not shown on the error line; the player's cast result is `SMSG_CAST_RESULT`.
- Stock FrameXML and `Blizzard_*` line citations are checked against the stock 1.12 files, and vmangos line citations against the vmangos revision nearest the one the local server runs.
- Dozens more turned up on the way: docs sitting on the wrong item, removed functions, stale counts, broken intra-doc links.
- One test comment in `ui_script/questlog_tests.rs`, which landed with #184, carried the same watch-index slip and gets the same fix.
- **Deviation labels.** "Deviation:" now marks only a choice with a reason. An unbuilt behaviour, or one whose reference side is untraced, says so plainly.
- **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Six second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the code, the stock files, vmangos and the reference.
Behaviour differences the rewrite surfaced are filed as their own issues (#188, #204, #222 and #259 among them). The comments here state what the code does. String literals are untouched; they are #92.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comments that pointed into the maintainer's private reverse-engineering notes (a note by name, a
path, a section or finding id, the analysis's own name for a client function, a process word) now
carry the fact itself, cited by the reference client's address, by vmangos file and line, or as
"the reference". Nobody outside could follow those pointers.
Comment text only, plus the test-assert and log strings that carried the same pointers. No claim
changes: a comment the notes have since corrected keeps its wording here, and correcting it is
separate work.
The two reference generators, `scripts/gen-reference-globals.py` and
`scripts/gen-reference-verb-events.py`, named the private notes' layout to find their inputs. They
now take the input files as arguments (`--capture`, `--framexml`, `--names`, `--extents`,
`--disasm`). Run on the same inputs, both write the committed tables row for row; only the two
header lines that named the old environment variable change.
Checked per file:
- the code is identical: each file with its comments stripped compares equal before and after; a
changed string literal is compared under a mask that proves only literal text moved and every
literal kept its line count;
- no client address the old text cited was dropped;
- a second reviewer checked every added address against its derivation;
- a detector for the notes' names, paths, ids, function names and process words finds none left;
- `scripts/gates.sh` is green.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- a Vanish, a stealth or a unit leaving range clears your target at once, not two seconds later, and TAB reaches a mob behind you (2345)
- a fully painted terrain layer covers fully: roads and rock no longer let about 6% of the grass underneath show through (2345)
- companion pets show their names, the saved camera pitch survives a login, and a missed resurrect offer no longer blocks later ones (2345)
- ESC on a quest giver closes it, browsing the auction house by category searches the right items, and abandon removes the quest you chose (2345)
- a profession window open at logout no longer reopens for the next character (2345)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- an item is an object in the one index, and its enchant and lifetime countdowns are its own (2334, 2340)
- the server's stand state applies, a bag equipped into a bag slot fires BAG_OPEN, and the inspect echo is dropped on purpose (2339)
- the schedule census counts declared orders only, and the architecture walls gate leaks rather than the API (2337, 2338)
- one queue for every event into the UI is not built: the reference orders its events per mechanism, not by the wire (2341)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- the cast ladder moves under the spell: try, target bind, the usable walk and the refusal rungs live in one place, and the cast reply sends a chained cast itself (2330)
- every server packet decodes through one exhaustive match, and an opcode-coverage script joins the reference's 387 handled opcodes against ours (2331)
- a landing that touches a platform seam compiles Linux and Windows first, and the seven WMO portal-cull audits join the tests (2331)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- the spell owns its lifecycle state: the in-flight cast, the local cancel, the cooldowns and the spell-modifier tables live in one module (2328)
- a malformed MPQ, DBC, M2 or WDT file is refused instead of crashing the client, nine read sites bounded
- a test that needs the install or the addon corpus fails instead of skipping when the data is there; the Bagnon render suite runs again (2329)
- the dev crash injector is armed on every platform, and no instrument writes a path relative to the working directory
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- the CVars are one engine-side table that outlives every interface reload: a change fires its observer, the eight latched rows wait for RestartGx, and /console gains set, cvar_reset, cvar_default, cvarlist and help (2303)
- every push into the interface runs in one feed phase after the net drain and before the tick, held by a structural test on the built schedule (2304)
- subsystems own their packet handlers, dispatched in packet order; the auction house is the first family out of the 273-arm drain (2305)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- inside a battleground the battle map draws its terrain, overlays, arrow and blips: the world map's selection carries the instance maps as the reference's third cell (2298)
- talent spell modifiers land: the 45 packets a level-60 login threw away fill the reference's two tables, and a spell's power cost reads them, so a cast you can afford is no longer refused (2298)
- a ghost's Accept on the spirit-heal dialog can no longer be eaten by frame order, and a latched spirit guide no longer follows you into the next character's session (2298)
- the battleground probe refuses GM mode, exits when done, and reads the scoreboard before toggling it (2298)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- a channelled spell's bar reads "Channeling" rather than the spell's name, and stays orange: 1.12 tells a channel from a cast by its label and its drain, not by a green bar (2284)
- a shot's muzzle blast fires only when a projectile is queued, the reference's own gate, so a target-less shot shows none (2289)
- the eight hand-copied query caches, names, items, objects, page texts, quests, guilds, petitions and mail, are one generic cache, and a read that asks no longer takes the whole store exclusively (2288)
- the schedule's ambiguity ratchet counts by class, and the cast-fail argument table is closed: MIN_SKILL is the prospecting leg and cannot fire on 1.12 (2287, 2292)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- a panic writes benilla-config/Diagnostics/crash-<time>.txt with the build id, the location, a backtrace and the last 200 log lines, and names the file as its last line, so a bug report can attach it (2266)
- every wire-count allocation hint is capped and the compressed update object reads through the cursor, so a malformed packet can no longer ask for gigabytes (2266)
- the 23 in-world run conditions are one set, the 45 probe variables one registry, the install-gated tests say when they skip, and every gate is timed (2266)
- one workspace-level lint allowance replaces 413 per-site attributes, and the decision corpus gets a lint in the gate (2266)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- your character's own name no longer goes missing on the first login of a fresh start, so KLHThreatMeter stops erroring: the four "player" verbs read the login record, as the reference does (2260, 2261, 2263)
- four pipelines that compiled live mid-play now warm under the loading screen: the model tile atlas's orthographic view, the minimap composite's quad, the glow combine pair and the indoor pool's liquid layout (2262, 2264)
- a hoverable sign seats its tooltip where its template says, at the cursor or in the corner, not by its object type: Brill's signpost still follows the cursor (2259)
- three warnings that fired when nothing was wrong are quiet, and the lane-coverage gates walk every crate instead of one (2264)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- a signpost or mailbox tooltip shows with Questie loaded, and after opening the map: the cursor-seated plate carries an owner, so Questie's OnShow hook no longer hides it (2255)
- picking up a quest whose template is not cached no longer blanks the quest log or jumps the selection: the missing quest is skipped, not the pass (2256)
- the spellbook's skill-line tab flashes when a spell lands in it (2252)
- the GPU meter's resolve rides the frame's own encoder, with no submit or poll of its own: −1.8 ms on Intel's DX12 driver when armed (2254)
- the audio starvation watch no longer charges a stream's own spin-up as silence (2253)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- a quest objective that carries both a creature and an item is two lines in the quest log, as the reference's four passes enumerate it: Graverobbers shows its Embalming Ichor (2249)
- the quest log's detail follows the live selection inside a frame, so a quest addon's log walk reads the right quest; Questie no longer files a chain quest under its shortest sibling (2247)
- a UI tint change is not a material change: a run's colour rides the batch's instance tag, so the PlayerFrame glow no longer re-prepares a material every frame (2245)
- the workspace lints clean on rustc 1.98: constant-size chunks become as_chunks, one slice fill, two unused globs
- WOW_HOVER_PROBE aims the pick in a headless run, and the inspect card names the tooltip's ladder stage (2248, 2250)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- a melee spell's tooltip says 5 yd range instead of Melee Range and grows with the reach of whatever you are auto-attacking; Heroic Strike and Bloodrage show no range line at all, as the reference does (2158)
- an item's resistances list Arcane first, and its type word comes from ItemSubClass.dbc, so every container names itself: 24 Slot Soul Bag, 8 Slot Quiver, 6 Slot Ammo Pouch (2080, 2158)
- weapon damage rounds the client's way, floor on the low bound and ceiling on the high, so Fang of the Mystics reads 38 - 86 not 39 - 86; ammo reads Adds 1.5 damage per second (2158)
- an arrow's slot line reads Projectile | Arrow and a bullet's Projectile | Bullet, the word the reference takes from ItemClass.dbc where we printed the type alone (2158)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- a cooldown still running when you log out and back in keeps sweeping instead of vanishing, and another character no longer inherits the previous session's cooldowns (2116)
- the server's Welcome and Patch 1.12 lines print at login in system yellow, and a character whose General, Trade and LocalDefense had gone silent for good gets them back at the next login (2119, 2120)
- the stock Interface Options window loads hidden beside ours, so pfUI's options skin finds the real thing; the Auto-join Guild Recruitment Channel row has a real store, and a /reload no longer trips on its camera dropdowns (2115)
- the combat log words a power gain on the display scale, so MikScrollingBattleText shows +1 Rage where it showed +10; a 1.12 predicate answers 1 or nil rather than a Lua boolean, the test ColorPickerPlus makes (2117, 2118)
- a Lua error quotes a program element with a backquote the way 5.0 does, so AceLibrary's argument checks report their caller instead of raising inside the error path (2122)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- the last app-side lines become GlobalStrings keys — tooltip cells, party, petition, duel, quest, PvP and rest lines read the install's own wording, so a non-enUS chain shows its own text (2045, 2080)
- a locked chest says Requires Pick Lock, ammo and thrown weapons drop their invented slot word, a met set bonus reads Set: without the count, and a 100 s cooldown reads 1.67 min as the template prints it
- the level-up block and the free-professions line printed twice in chat and print once; a declined invite, a friend coming online, a duel request and a failed quest play the sound 1.12 gives them
- Duel cancelled. is the yellow line at the top of the screen, and a trainer refusal or a failed quest with no cached template shows nothing, as on 1.12
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- `UnitHasRelicSlot` is real in 1.12 and benilla had no binding for it: `0x519e50` reads `ChrClasses.dbc` field 16 — set for Paladin, Shaman and Druid in `patch.MPQ`'s 17-column copy (the base `dbc.MPQ` copy has 16 columns and no such field, which is how the "always false in vanilla" belief survived twice in decisions and seven times in code) — and stock `PaperDollFrame.lua` calls it unconditionally, so the character sheet would have raised for EVERY class; a druid, paladin or shaman's sheet now hides the ammo slot, shows the relic plate where the bow was, tooltips "Relic", and reports ranged Attack/Damage/Power as N/A, all four branches transcribed from the reference
- the loot window is the reference's own `LootFrame.xml`: `LootButton` is built as a real widget type rather than aliased (a Button plus one slot dword and one overridden OnClick — right-click loots exactly like left, no shift/ctrl/alt takes, a row whose Lua handler errored still loots, a fast double-click takes two slots), `LOOT_SLOT_CLEARED` fires per row again because the stock file hangs hide-in-place AND the emptied-page auto-advance off it, `EditBox:Insert(nil)` is a no-op as `lua_tostring` makes it, and an alias `GameTooltip.xml` had quietly become a reader of would have turned every item tooltip white, silently, had it just gone nil
- the vendor cursor: `PickupMerchantItem` is two verbs behind one name, decided by what the cursor holds and never by the caller — a held bag item SELLS (index ignored, no merchant-open gate), otherwise the vendor row is grabbed as cursor mode 5, which is invisible to `CursorHasItem` (modes 1 and 9 only) so all thirteen stock gates stay closed while it is held; the drop is the buy, `CMSG_BUY_ITEM_IN_SLOT`, count 1, into a bag or an equipment slot, and every refusal is silent and clears the cursor; the four `Model_*` rotation handlers land in UIParent where the reference keeps them
- the gap report reads `:Method(` sites now — it found `SetMerchantCompareItem`, which would have raised on the first vendor-row hover with every check green, and `LootButton:SetSlot`, and 28 "free" windows became 25; the shared coin kit moved out of the merchant window into `MoneyFrame.xml` so the eight windows that draw coins through it can reach it without loading a vendor
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- the "Resurrect Now" prompt was never a click: release sends the corpse query, the position is cached, and a per-frame range check (d² ≤ 1600 — 40 yards) raises CORPSE_IN_RANGE, whose Lua raises the prompt, whose button sends CMSG_RECLAIM_CORPSE — a chain we already ran at exactly 40.0; what we were missing was the bones gate: every one of the reference's three latch writers requires the corpse's bones bit clear as well as ownership, and RetrieveCorpse itself carries no guard at all, so our reclaim guid could have named a pile of bones — the gate is in now, at create and on the in-place flip
- a corpse publishes a mouseover: the "Corpse of %s" name plate and the +64 brighten, applied before the publisher's type switch with no type test — any real body qualifies, owner irrelevant, only a bone pile with nothing to take is dropped — and it fires no event, so UnitName("mouseover") over a body stays nil, here as there; sitting refuses the corpse loot click with the client-local red not-standing error and no send; and the insignia doc is corrected — bit 5 unlocks spell 22027 "Remove Insignia", the corpse's actual PvP flag is bit 2, and there is no skinning of players in 1.12.1
- the size tripwire counts code, not lines: 145 flagged files → 15, because in this codebase the comments are the knowledge base and the tests are the gate — one flagged file was 4357 lines of which 5 are code, another 1566 with ZERO — so the budget (unchanged at 1000) now counts non-test, non-comment source, calibrated so the one split on record that actually mattered still fires
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- a corpse is a body you can hover, cursor and click: it enters the same ray pass as units with the winner switched on type at publish (the reference's own shape — one trace over every object), the cursor classifier is transcribed whole (lootable-by-me → the pickup cursor, grayed by range; else the insignia leg behind the learned-skill latch; your own body falls through to the clear), and right-click is "not mounted AND lootable" → CMSG_LOOT; CORPSE_FIELD_FLAGS bit 5 is the PvP insignia flag, not "reclaimable" as an old note had it; the widening also caught that a GameObject anywhere on screen could steal the click from a nearer corpse, and that a left-click on a body cleared your target as if you had clicked ground
- the cinematic sweep closes eleven holes: a shot plays its sequence band's WIDTH, not its end — two shipped fly-bys band away from zero and ran long with their opening frames frozen (the Night Elf intro measured 102333 → 102000 ms); CMSG_NEXT_CINEMATIC_CAMERA announces every shot, the first included — the old "between shots" reading meant it was sent NEVER, since every 1.12 sequence row carries exactly one camera; the ack invariant is stated once (one CMSG_COMPLETE_CINEMATIC per accepted trigger, over-acking tolerated, under-acking never — a dropped ack despawns the world around you until relog); the hardware cursor hides for the shot; and the music cut lands in the frame that starts it — it was one frame late, exactly long enough for a first login's zone fanfare to start under the narration and be stamped as played
- RunBinding exists, and a fly-by stops handing the world its mouse: CinematicFrame's OnKeyDown hands the SCREENSHOT chord back to its binding through RunBinding, a global we never had, so every press during a fly-by raised and took no picture; and the mouse was fully live under a shot — right-drag turned the character, clicks picked targets through the flying camera, both buttons walked the body — so the pointer now reads as over the UI while a shot plays
- AdvanceTime and ReplaceIconTexture are carved off the bytes: AdvanceTime really is a no-op in the reference (its one reachable call is mov eax,1; ret), ReplaceIconTexture is a material swap over a model's type-14 texture slots that stores nothing on the widget, and SetModel(nil) RAISES — the "documented clear" our binding claimed was invented, not read; and the addon roster now says WHOSE package is short when a .toc names files the folder doesn't hold — nine of the corpus's twenty-nine load failures are the addon's own manifest, which the real client also shrugs off ("Couldn't open %s", carry on), so they stop reading as our defects
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- priest tooltips drop their bogus red lines ("Requires Shadowform" on Psychic Scream and Inner Fire, "Requires Spirit of Redemption" on Flash Heal): `Spell.dbc`'s Stances mask is an allow-list, not a requirement, when `AttributesEx2` bit 19 is set — 244 of the 522 masked spells carry the waiver, and the tooltip now consults the same gate the cast validator already did; Charge's "Requires Battle Stance" still asserts
- a hovering mover refuses the jump, silently — no packet, no toast — exactly as `CMovement::Jump` does: the HOVER test sits ahead of the swim select, so a levitating swimmer can't breach either; the old gate refused on ROOT only
- opening a lock gates on your skill, never your level: mining with 1 Mining no longer appears to open a 300 vein only to be bounced by the server — the client-side "Requires Mining ≥N" refusal fires with no packet sent, and the level term rides the skill block through the same clamp the reference uses
- the Mod/Mod2x fade was never a deviation — preset 5 IS the identity-lerp, byte-exact, five comment sites re-lawed with no behaviour change; and the garble word pool (`LanguageWords.dbc`) loads, the first hop of the language scramble that lands next
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- the in-game UI ends at the character screen and is rebuilt at the next login, so addons that read the character at file scope stop being frozen on whoever logged in first
- everything the host remembers about a UI is keyed on that VM's identity — keybinds, macro icons and every feed survive the rebuild instead of silently pushing nothing
- a ribbon trail rides the room its model is drawn by: Caverns of Time's ninety energy trails stop burning through 200 yards of rock
- the visibility census gains the sky and ribbon lanes it could not see, and the logout leak probe measures after the release instead of accusing every run
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- a player build ships beside the dev one — dev chords and probe surfaces go dark, and a gate enforces what a player can still reach, by medium not by module
- the binary carries its own shaders and its shipped UI, finds the WoW folder by itself, and keeps its local state in one benilla-config folder
- the glue screens get a capture shutter, and the engine stops depending on the protocol crate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- timed quests get their countdown — a Quest Timers box under the minimap and a Time Remaining line in the quest log, kept against the server's own clock
- interiors go honest: footsteps stop crunching the terrain's snow through a building's floor, and tavern voices are authored dry, so the fake cave echo is gone
- snow is the reference's again — point-sprite flakes at the era's pixel size, and a spawn slab that leans into your run so a runner no longer outruns the field
- Stormwind stops drawing Elwynn through its walls, a looted quest crate's lid settles, cold zones puff visible breath, and the collect-quest toast fires only on a real advance
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- clicking a book in the world opens it and turns its pages: letters and world books share one reader
- the cursor's object rules are the client's own 31-row strategy table now, so decorative objects stop pretending to be usable
- trainer and tradeskill windows draw each row's icon by the reference's own law, and the trainer's filter dropdown toggles again
- water color follows the zone's light blend, so lakes stop snapping between area tints
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- the whole loop plays: the line arcs rod tip to bobber, the bite splashes with its own sound, and clicking the bobber reels in
- the verdicts read like 1.12: yellow "your fish got away" lines, the reel-in sound, the fishing loot window
- "You feel rested." prints on the transition, rest states come from the client's own Exhaustion table, and level 60 trades the XP strip for the max-level rail
- the Skills pane lists what the real client lists: the hidden rows, the sort, gray numberless single-rank lines
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- a chat link edits as one atomic token: the caret can't land inside it, its colors survive, one keypress removes it whole
- spell tooltips print the resolved mana cost through the reference's own table walk, and the "% of base mana" line dies
- the spirit healer's confirm comes back after a cancel and wears its warning icon
- the dressing room's pane is a live animating model, and a tried-on bow lands in the hand
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>