Item tooltips lacked the lock's sub-line and a wrapped gift's giver.
The reference: the lock check (`0x52bd57`) takes the Lock row's slots in place. The first slot picks the arm: a key names its item in white (`0x52bf30`), and a skill names the LockType and the required value, coloured by the walk at `0x5da5e0`. A blank LockType name prints blank; only a missing row reads `UNKNOWN` (`0x52bede`). A wrapped item (flag `0x8`) prints `ITEM_WRAPPED_BY` with `GIFTCREATOR`, from the object or, for the trade partner's slots, from the block (`0x534559`, `0x534574`). Real lockboxes use locks whose first slot is empty, so they show no sub-line, as in 1.12.
The rows keep all eight slots, and both trade sides carry the wrapped word and the giver. Tests cover each arm, a blank name, and gifts in bags and trade.
Fixes#1440Fixes#1441🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under `WOW_GLUE_STOCK=1` the login engine still wrote the hidden native `GlueDialog`: `drive_policy`, `to_select_on_roster`, `tick_login_caret`, `answer_dialog` and `exit_login` all ran ungated.
The engine moves to `glue_session/login.rs` (intent, attempts, the env fast path, the account guard, resubmit, quit) and tells a screen what it decided as a `LoginNotice`. The native login screen's `show_login_notices` turns each notice into its dialog, in the slot `drive_policy` held. `LoginPlugin` is now native-only, and the stock manager's frame moves into `UiScriptPlugin` with its ordering kept. `login/queue.rs` becomes `glue_session/queue.rs`.
Test: a schedule census fails on any login or glue-session system that writes `GlueDialog` without the native gate; it names the five above on main. Policy tests now assert notices. Live: native smoke, stock login, stock logout round trip, and a wrong password on both paths.
Part of #856.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`GetInventoryItemCooldown` answered `(0, 0, 1)` for every slot, so worn trinkets had no cooldown spiral. A charged item at 0 charges kept its cooldown.
In 1.12.1, `0x4c8a60` answers the item cooldown query (`0x6e2ed0`) for the active player's slot. It needs a use spell in the template's first block (`0x4c8b47`) and a charge left on a charged template (`0x4c8b55`). Anything else answers `(0, 0, 0)`. `SetBagItem` and `SetInventoryItem` take the plain leg for a spent charged item (`0x5347e3`, `0x53318a`), as does `GetContainerItemCooldown` (`0x4f9a48`).
Now the verb answers from the worn slot's cooldown under those gates. The slot feeds read `ITEM_FIELD_SPELL_CHARGES[0]`, and a held item with no use spell answers enable 0, as the query leaves it.
Tests: the verb's gates and errors, the spent-charge rule over five cases, and the container triple.
Fixes#1439Fixes#1442🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The character roster, the pick, `lastCharacterIndex` and the session edges (logout, connect, refused login, disconnect) lived in the native character-select screen's module, mixed with its UI. They now live in `glue_session`, ungated, so the stock screens own them. The native screen keeps only its UI and the refused-login dialog.
The logout smoke under the stock screens re-entered by sending the pick straight to the server, skipping `CGlueMgr::EnterWorld` (`0x46b500`): no state 8, no `lastCharacterIndex` save. It now selects the row and presses Enter World, as a player does.
Second step toward the stock screens as the default.
Tests: the refused-login dialog opens only off the stock screens; the smoke's stock re-entry goes through Enter World.
Part of #856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An item tooltip's Locked line was always red, and no tooltip drew "Cooldown remaining".
- In 1.12.1, the lock check `0x5da5e0` colours Locked (`0x52bdde`): red when nothing opens it, the skill ramp `0x529fa0` for a known open-lock spell, green for a held key or an empty lock. benilla now feeds Lock.dbc and the player's openers and runs the same check.
- The spell builder draws the line when its recovery is at least a minute (`0x52f537`). The item builder draws it with no floor (`0x52e0fd`) for `SetBagItem`, `SetInventoryItem` and the `SetAction` item leg, which are the three setters that pass a cooldown. Worn items now carry theirs, and the line counts down while hovered.
Tests: each colour and ramp edge, the unit and rounding, the spell floor, and the line order.
Fixes#1024Fixes#1017🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under the stock glue screens a scripted run ignored `WOW_REALM` and parked at the realm list, because the realm policy lived in the native screen's module and ran only under `native_glue`.
The realm tables and the run-mode realm choice now live in `glue_session`, which serves both paths. The stock manager's connect dials the realm already chosen, then `WOW_REALM`, then `realmName` (`0x5ab6f0`), then the unattended pick. A run-mode pick sets `realmName` (`0x5ab7b0`) and the chosen cell (`0x46f1d1`), as a selection does. A player's run is unchanged and still gets the list or the wizard. `ClientState` moves out of `char_select` into `client_state`.
This is the first step toward the stock screens as the default.
Tests: the connect ladder; a scripted run with no `realmName` reaches character select over the fake realmd.
Fixes#1433
Part of #856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three defects in the stock glue path.
- `SetPreferredInfo` turned pvp and rp into booleans. 1.12.1 reads them as integers (`0x6f1c10` returns a number unchanged) and compares them raw with each realm's 0 or 1 (`0x46ea20`), so a 2 matches nothing.
- The queue's "Estimated time" climbed while queued. `0x46ae50` stores an estimate per packet, only when the position moved forward against the oldest sample (`0xb41df0`), and the dialog counts down from it (`0x46bcb8`). Now it does the same.
- The realm walk and the glue round-trip took an empty roster for "not at character select". Both now key on the roster arriving, time out, and run under the stock screens.
Tests: raw integer compares, the countdown and its keep rule, empty-roster arrival and the timeouts.
Fixes#1315Fixes#1312Fixes#458🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two pieces of stock UI state were set at the wrong point of the load.
- The buff rows were repositioned right after the manifest loaded, before any addon. In 1.12.1 `CGGameUI::Initialize` loads FrameXML, then the addons (`0x4900a3`), then the saved variables and `VARIABLES_LOADED` (`0x4900b2`), whose stock `UIOptionsFrame.lua` arm runs `BuffButtons_UpdatePositions`. The early pass is gone, so addons load against `BuffFrame.xml`'s anchors.
- An addon's minimap mask survived a relog. 1.12.1 reloads `Textures\MinimapMask` (`0x4ed810`) only from `InitializeGame` (`0x48f590`) at world entry, and `ReloadUI` (`0x48fbf0`) keeps it. World entry now restores the default; a reload keeps the addon's.
Tests: an addon sees the XML anchor at file scope and in `ADDON_LOADED`; a relog restores the mask; a reload keeps it.
Fixes#314Fixes#695🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The in-game EditBox caret was a 1-pixel white bar, the PC keymap lacked 1.12.1's Ctrl editing letters, and a word was an alphanumeric run.
In 1.12.1 the caret is 4 script units wide (`0x77ba2d`, the `SetWidth` scale) in the box's text colour (`0x77e2a0`). `0x77b160` binds Ctrl+B/F/D/W/U/K/P/N beside A/C/X/V (tables `0x77b6cc`, `0x77b69c`), and tests Shift before Ctrl on Delete, so Ctrl+Shift+Delete cuts. A word is a run of non-spaces (`_iswspace`, `0x77c3f0`, `0x77c7a0`), so Ctrl+Right ends at the next word's start.
Now the caret, the chords and the word rule follow the reference. Ctrl is its own key on the Mac, so it takes the same letters and word deletes there. The Cmd and Option chords are unchanged.
Tests: caret width and colour, each chord's edit, the word rule.
Fixes#183Fixes#749🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A creature not in view is named by the entry in its guid. 1.12.1's combat-log namer reads guid bits 32-47 for it (`0x6265c4`–`0x6265e0`). The servers in use put the entry at bits 24-47, so the client's read would name every such creature wrongly. benilla reads bits 24-47; this records that as a deviation, so `docs/DEVIATIONS.md` lists it. Comment only.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Taxi route lines drew tiled copies beside each line. benilla chose repeat or clamp from a texture's current UVs. Lua-set textures with UVs past [0,1] tiled.
In 1.12.1 the wrap is fixed when the file loads (`0x770200`, both axes, sampler table `0x80a254`: clamp or wrap). An XML `<Texture>` passes 1 only when its own `<TexCoords>` leave [0,1] (`0x76fe20`, each template level with its own flag). Lua `SetTexture` passes 0 (`0x79bda5`). `SetTexCoord` never changes it. A backdrop's background follows `tile`, its sides wrap, its corners clamp. A same-file call keeps the loaded texture (`0x770225`).
Now each texture carries its load-time flag and the sampler follows it. Route lines draw as one line. The stance bar's middle strip clamps, as its XML loads it.
Tests: the Lua, XML, template, same-file and backdrop flags, and the sampler pick.
Fixes#870🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wheel did nothing over action buttons, unit frames or the minimap. benilla gave the notch to the topmost wheel frame or its parents, and zoomed only off the UI. A sideways scroll also reset the idle timer.
In 1.12.1, `0x7664f0` walks the wheel list under the cursor, strata then level then age, and the first visible frame with `OnMouseWheel` consumes it (`0x76c180`). The world frame's slot runs the zoom binding (`0x483d20`), and an unbound chord passes the notch on. Only `WM_MOUSEWHEEL` stamps idle (`0x76651f`).
Now the wheel walks once, and the world takes what is left. Scroll frames are no longer born wheel-enabled (`0x786a40`). On macOS a Shift+wheel, which the system turns sideways, counts as vertical again.
Tests: no-handler frames reach the zoom, handlers consume, off-cursor parents get nothing, a sideways scroll leaves idle alone.
Fixes#176Fixes#224🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three defects a fork found.
- A worn item the server answers as unknown left the player unattached. 1.12.1 holds the player while a query is out (`0x5ed771`, `0x5ec8f0`) and releases it on a refusal without applying the item (`0x55bdf5`). Now the slot stays bare.
- Templates and names were read by the guid's entry bits. 1.12.1 asks the GameObject cache by the descriptor's `OBJECT_FIELD_ENTRY` (`0x5f7d77`) and keys a creature's record by it too (`0x604629`, `0x60b151`). Every reader of a streamed object now goes through its descriptor; guid bits are left only for a guid not in view.
- A negative aura duration read as 49 days. The span is signed: below zero is lapsed (`0x4e4472`), except `i32::MIN`, which reads as 2^31 ms, as the client's wrapped compare does.
Tests: readers keyed by a field entry unlike the guid bits; the refusal; the signed span.
Fixes#1402Fixes#1404Fixes#1405🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unit events fired from a per-frame diff of each token's values: a new target re-fired its events, a roster packet fired member events, a `targettarget` token appeared, and cost-modifier changes fired no UNIT_MANA.
In 1.12.1 they come from field watchers (`0x51bbb0`, `0x5e2510`, `0x5dd8a0`) and the flag callbacks `0x5ee990` and `0x5ff580`, signalled per values block through `0x515e50`, and from party records compared on member stats (`0x5f0b80`).
Now every unit event fires from a field-watch notice or its packet. That covers health, power, flags, auras, rest, PvP, play time, regen, pet attack, the pet bar and the guild tag. Name answers signal only on a cache miss. The per-frame fires are gone.
Tests: each arm's notice and order, and no-fire tests that fail on the old feeds.
Fixes#1389Fixes#1390Fixes#1393Fixes#1394Fixes#713🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
**What was wrong.** The loose-file index walked every folder below the install on the first open. The walk followed links and blocked the frame. A large tree under the install, such as a Wine prefix whose `z:` links `/`, left the window empty while memory climbed (6.6M keys, about 4 GB).
**Reference.** The walk at `0x646ea0`/`0x646910` follows links and keeps no record of visited folders.
**What changed.**
- A lookup lists only the folders along its name, each once, and caches answers until the index empties.
- Keys and skip rules are unchanged.
- A folder reached through a link now answers, as the reference's does.
- Listing on first reach is recorded as a deviation.
**Tests.** Lookups match a full walk over 1.9M probed keys. The prefix install lists 8 folders, down from 207, and reaches the menu in 0.77 s.
Fixes#1398🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Places that read the client's locale in 1.12.1 now follow it:
- **Creature type words.** `CreatureType.dbc` at the locale column (`0x51a2dc`). The tooltip gates type 10 itself (`0x52a2c6`), so `UnitCreatureType` answers it and that deviation is gone.
- **Race and class names.** `ChrRaces`/`ChrClasses` at the locale column (`0x518301`, `0x518451`), feeding `UnitRace`, `UnitClass`, `$R`/`$C`, who, friends, guild, raid, the battlefield score and the glue screens.
- **Name check.** Charsets come from the Wow.ini Region or the locale (`0x6c91b8`, `0x86ca0c`), with the Hangul syllable list (`0x86ca30`) and the per-charset caps.
- **Display.** koKR chat bubbles are left-justified (`0x4b16e7`). The loading tip font is picked by locale (`0x82f018`).
- **CVar defaults.** AutoInteract on koKR, gxWindow and gxMaximize on zhCN (`0x603368`, `0x63a738`). A saved value still wins.
Tests run at non-zero locales throughout.
Fixes#1361Fixes#1362Fixes#1363Fixes#1364Fixes#1365🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`SelectTarget` (`0x493540`) checks two things before it selects:
- a charmed player returns (`0x49359c`);
- with the spell targeting cursor up, a unit is bound to the cast and nothing is selected (`0x4935ca`–`0x4935d5`).
Every benilla selector skipped this. Now one select front models it, and every caller runs through it: the script verbs, TAB (which still advances), `AttackTarget` and the Interact attack, nameplate and body clicks (both greet first, `0x4927fd`), loot select, `/target`, and the attacker acquire. With the cursor up, `AttackTarget` and the pet Attack send the cast first.
`CastBookSpell` (`0x4b3474`) stops targeting when the cursor is the same spell.
Tests: each verb with the cursor up and down, a stock TAB with a heal's cursor up, the charmed cases, and the wire order.
Fixes#1380Fixes#1378🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In 1.12.1 the stat, XP, rest, honor and faction events come from field watches. The notify pass (`0x465330`) runs each watch once per values block, in field order, only when its bytes changed (`0x4655bb`). A first create and `/reload` run none. benilla diffed snapshots per frame, fired on first sight and lumped fields together.
`field_watch` now writes one notice per block per watch from the object handlers. The events fire from those notices:
- the unit-field bridge (`0x51bbb0`/`0x51bd50`), one per field, to every token of the guid (`0x515c50`);
- the player watches (`0x5dd8a0`);
- the skill watches (`0x5ed990`: UNIT_ATTACK, UNIT_DEFENSE, UNIT_RANGED_ATTACK_POWER);
- the weapon slots (`0x5dee30`).
The feeds only push. POSSTAT/NEGSTAT and `has_offhand` fire nothing, and the AP multipliers fire.
Tests:
- field order and per-block counts;
- silence on create;
- bridge ids pinned to `events.tsv`;
- push-before-fire order.
Fixes#207Fixes#286Fixes#1379Fixes#1381Fixes#1382Fixes#1383Fixes#1384🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Item counts and trade state, as 1.12.1 holds them.
- Counts walk mask 0 (`0x622420`): worn gear, bags, backpack and keyring, skipping broken and flagged items. A charged item counts its uses left (`0x4e6d20`). A reagent is red only when no copy is carried (`0x52f49f`). The equipped border asks the walker at mask 1.
- The trade keeps both item offers after a close until the next open (`0x4bf549`). Money zeroes on the ending statuses (`0x5d4884`), and the player getter is gated (`0x4c0706`). Traded-away items clear before `TRADE_CLOSED`.
- `TRADE_UPDATE` fires only from a getter's landed item record (`0x4c034f`).
- A stale own-side status is dropped (`0x5d49ba`). Every edit sends whatever the window state and bumps the counter, money verbs included.
Tests: each change, including the stock money re-entry.
Fixes#1356Fixes#1357Fixes#1358Fixes#1359🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The protected-action gate (`0x494a50`) now runs its input arms. Tags 2, 4 and 10 pass tainted code only under an input flag:
- the edit box's Enter (`[0xcf4dc4]`);
- a button's click or double-click (`[0xcf4da0]`);
- a running binding.
Without one they refuse silently. This covers casts through `TryCastSpell`, the pet actions, `SpellStop*`, `SpellTargetUnit`, item targeting, `TargetNearest*` and `AttackTarget`, and `ReloadUI`.
Branches the reference runs before the gate still run when the cast is refused: aura and form cancels, and the bind-on-use and soulstone legs. Queued calls replay the input events they ran under, around their own events too. So a TAB-driven `PLAYER_TARGET_CHANGED` handler casts as in 1.12.1.
`SetEuropeanNumbers` now also swaps separators in tooltip lines, both message frame classes (at show for scrolling ones), SimpleHTML, chat bubbles and nameplates.
Tests: a 30-verb census, and the stock buttons, bindings, `/cast`, spellbook and macros.
Fixes#1354Fixes#1355🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-vendors `reference/` at benilla-re `c854c4e`, which now resolves computed event ids at their fire sites. 97 events gain a producer: the 93 `CHAT_MSG_*` events and the player-field watcher's `"player"` events.
The fires change to match:
- `PLAYER_XP_UPDATE`, `PLAYER_PVP_KILLS_CHANGED` and `PLAYER_PVP_RANK_CHANGED` carry `"player"`, once per changed watch (`0x5de4b0`). A level-up fires twice.
- `UPDATE_EXHAUSTION` fires bare from the rest byte (`0x5de4e0`) and `"player"` from the pool. `UPDATE_FACTION` fires bare from the list, twice on initialize, and `"player"` from the watched index. Everything fires in field order.
- `PLAYER_DAMAGE_DONE_MODS` is produced (`0x5ddbc0`, `0x5ddbe7`, `0x5ddc0e`), and `UNIT_DAMAGE` keeps only its own fields.
- The rank watch covers all of `PLAYER_BYTES_3`, so its deviation goes.
- The shape gate reads the chat fire's shared ten-value list.
Tests: the shape gate, plus feed tests for each watch and its order.
Fixes#237Fixes#244Fixes#296🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing listened to the player's item objects after they changed. Action and trade slots diffed snapshots, so an addon missed restacks, enchants and destroys.
- **The seam.** `item_watch` emits one notice per reference handler run, in wire order. The handlers are destroy `0x5d8660`, stack `0x5d9400`, charges, entry, enchant `0x5d9a90`, record `0x5d8440`, seats `0x4f8db0`/`0x4f90d0`/`0x4f8ec0` and visible items `0x5dee30`.
- **Action bar.** Each notice recounts every item slot holding the entry and fires `ACTIONBAR_SLOT_CHANGED`, with no change test (`0x4e5c70`, 16 callers). Arrival recounts quietly, then fires slot 0 (`0x5dea50`). The per-frame poll is gone.
- **Trade.** Our slot re-sends or clears its item and fires `TRADE_PLAYER_ITEM_CHANGED` (`0x4bfb10`, `0x4bfb60`). Slot 7's enchant spell signals its side and names the enchantment (`0x4bf2b0`). `ClickTradeButton` refuses bound, quest and bag items (`0x4bfeb5`).
Tests cover every watcher, consumer order and refusal.
Fixes#1064Fixes#1062Fixes#1063Fixes#1041🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
benilla read every DBC string from the enUS column and answered enUS everywhere. 1.12.1 reads its locale from `Wow.ini` in the archives (`0x40249d`). It joins `Language` and `Country` into a tag at `0xc2a2a4` (`0x402513`). It looks the tag up in the eight locale names to get the index at `0xc0e080` (`0x402562`); enGB keeps index 0.
- The chain reads `Wow.ini` with the client's ini rules: no trimming, and the first value wins.
- Every localized DBC read takes column `first + index`.
- `GetLocale` and the TOC `Title-xxXX` pick follow the index (`0x48d8b0`, `0x46ce40`).
- The logon challenge (`0x5b2115`) and the name-cache header carry the tag, so this install sends enGB.
Tests:
- A synthetic WDBC per locale.
- 43 loader probes at an empty locale.
- A byte-exact golden for the challenge.
- GetLocale in glue and in game.
Smoke: green.
Fixes#1338🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Five Lua API gaps, settled at the bytes.
- `FrameXML_Debug` sets its flag only from a number (`0x488440`); it always returns the flag.
- The eight debug globals exist and return nothing (`0x83de68` rows).
- `GetTalentTabInfo` returns the tab's SpellIcon path (`0x4f30d0`).
- `SetEuropeanNumbers` stores its flag (`[0xcf4d24]`). The flag survives a reload. FontString and Button `SetText` swap `.` and `,` inside a number under the exact byte tests of `0x771c50`.
- The protected-action gate (`0x494a50`) fires `MACRO_ACTION_FORBIDDEN` or `ADDON_ACTION_FORBIDDEN`. It covers the 23 tag-0 movement calls, PitchUp/Down included, by context: a `RunScript` chunk, an insecure addon's files, inline scripts and binding bodies, or the client's own. Stack scan; the unmodelled taint cases are listed in `taint.rs`.
Tests: a test for each fact, including the gate through `LoadAddOn` of a real chain addon.
Fixes#60Fixes#410Fixes#162Fixes#566Fixes#486🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every character model frame, named or anonymous, now draws the unit its own `SetUnit` names, in a booth of its own. The seven stock-name booths are gone: the character window, pet doll, inspect, stable, dressing rooms and tabard designer are ordinary instances, driven by their FrameXML calls.
- `SetUnit` stores the GUID and builds a copy when shown and the unit is held (`0x505b50`).
- `RefreshUnit` rebuilds from the stored GUID (`0x505e40`); hide drops the copy (`0x505ce0`).
- `SetRotation` and the paint timer turn it, with the model's fallback ids (`0x505bb0`, `0x505c50`, `0x711bf0`).
- The constructor's directional light lights a fresh pane (`0x505680`).
- The stable shows the live pet's creature-template display (`0x4cb99e`).
Each change has a test that fails without it.
Fixes#1346Fixes#1347
Co-authored-by: migwynkriid13 <335116281+migwynkriid13@users.noreply.github.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: migwynkriid13 <335116281+migwynkriid13@users.noreply.github.com>
The Warden `Deviation:` lines now give the reason, not only the fact. Warden's answers come from a module, native Win32 x86 code the server sends and the client maps, links and runs in its own process (`0x7a79d0`). The client parses none of the payload itself (`0x6ca5c0`). benilla runs no such code on any platform, so it has nothing that could answer, and a server that requires Warden would kick it 30 s in (`Warden.cpp`).
Fixes#1349🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tooltip lines, EditBox byte caps and button slot textures, as 1.12.1 has them:
- A tooltip line with no text on either side is not added, and only a non-empty side is written (`0x530270`).
- A tooltip holds as many lines as it has `TextLeft<n>`/`TextRight<n>` pairs at load (`0x529650`), so a bare `GameTooltip` holds none. `AddFontStrings` adds one (`0x529940`).
- A clear blanks the left text and keeps the right (`0x530050`).
- An EditBox's `<FontString bytes="N">` and `SetMaxBytes(N)` cap it at N−1 bytes (`0x77a225`, `0x798f30`); `GetMaxBytes` answers N. Over the cap, the box drops a character or a whole link. The chat and who boxes stop at 255 bytes.
- A button texture path that does not load makes no slot region and keeps the old art (`0x7790ce`).
Each change has a test that fails without it.
Fixes#165Fixes#177🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
`docs/DEVIATIONS.md` lists every place benilla deliberately differs from 1.12.1, generated by `scripts/genmap.sh` at every land beside `docs/MAP.md`:
- The settings that ship off the reference default, with each one's reason, from the `Deviates` rows.
- Each `Deviation:` comment's first sentence, linked to where it lives, grouped by crate and module.
A test fails if a `Deviates` row takes a shape the generator would misread. README, METHOD and CONTRIBUTING link the doc.
Fixes#1289🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Loose files in the install now answer the opens 1.12.1 flags, ahead of the archives (`0x647e60`, benilla-re `storm/mount`).
- The index walks the install folder, then `Data\`, keyed case-insensitively; the first entry wins.
- It answers interface files and textures, fonts (bare name first), cursors, sound, model frames and the minimap arrows.
- A `.MPQ` file answers its name from inside it.
- A name written with `/` misses, as in 1.12.1.
- A found file that fails to read does not fall back.
- The index empties where the reference empties it (`0x646fc0`): after saved variables, `AddOns.txt` and the glue sweep.
- A `Blizzard*` addon folder holding its `.toc` is skipped unread (`0x51fb50`), so the archive's copy loads. Nothing in the install is moved.
Each change has a test that fails without it.
Fixes#1144Fixes#1145🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
44 Lua functions 1.12.1 registers were undefined, so an addon calling one raised.
- Units: `UnitCanAssist` (0x516bb0, computed as the first unit toward the second) and `UnitIsTrivial` (0x519d70). The assist rule now runs player-to-unit where the client does (0x6066f0), which corrects UnitBuff's and the TAB scan's answers for reputation factions.
- Quests, trainers, crafts: `GetActiveLevel`, `GetAvailableLevel`, `OpenTrainer` and seven skill-line functions.
- GM survey, LFG, lottery, minigame, skill points and the file verbs, from the install's tables: the survey (id 6) and LFGDungeons are loaded, and `GMSurveySubmit` (0x32a), `LFGQuery` (0x1ff), the lottery verbs (0x334, 0x336) and `BuySkillTier` (0x21f) send what the client sends.
Every send has a byte-exact golden; usage strings are the client's.
Fixes#333Fixes#335Fixes#338🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A fresh-clone walk-through as a new contributor found four gaps in the docs:
- No quick test loop: `scripts/check.sh` has nothing to check on a clean clone, and `cargo test -p <crate>` was not mentioned. "The loop" now says both.
- The doc-link lint is a full `cargo doc` of the workspace; METHOD and the script's header now say so.
- What to cite while a benilla-re gap is still open: the client address, with the benilla-re issue or pull request linked.
- When `reference/PIN` moves: only with a change that needs a newer benilla-re row, to a commit on its main. Stated in CONTRIBUTING and `reference/README.md`.
Docs only.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`reference/` held benilla's own copies of the reference's tables, some derived by its own scripts from older inputs: the fire-site table kept one event id where a site fires two, and credited glue-screen sites to in-game events.
Now `reference/` is benilla-re's tables at the commit `reference/PIN` names, byte for byte: bindings, events, glue events, messages, event arguments, verb events, the glue surface, the Lua globals, chat types and groups, and CVars. `scripts/sync-reference.py` copies them, and its `--check` runs in the gates and in CI. Four generators and nine old files go, and every reader moves to the new columns.
New tests hold the chat type, chat group and CVar tables to the copies; the CVar rows already filed are listed as known.
Fixes#245Fixes#960
Part of #244🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Contributors and their agents had no word on how to use the reference beyond a link.
- `AGENTS.md` sends an agent to benilla-re before it builds: clone it beside the repo and grep it, or look an address up on its site.
- `docs/CONTRIBUTING.md` gets "The reference": how to look a fact up (the site, `symbols/`, `tables/`, `systems/`), how to weigh a row's confidence, and that a gap or an error goes to benilla-re as an issue or a finding.
- `docs/METHOD.md`, the pull request template and the issue chooser say the same in a line each.
Docs only; `scripts/check.sh` is green.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
benilla's reference facts come from reading the 1.12.1 client at its bytes. That work is now public as [benilla-re](https://github.com/samwhosung/benilla-re): one row per function, global and field, a doc per system, and the data tables, each cited by client address.
This links it from the README, from METHOD's "The reference first" and from CONTRIBUTING's step on stating the reference fact, so a contributor knows where a client address leads. Docs only; no code changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Item locks came from pending-op bookkeeping, so unlocks reached the wrong items or none.
Now a lock is a bit per item guid, set by LockItem (0x4953e0) and lifted by UnlockItem (0x495420) at the client's own sites:
- pickups, places, swaps, the delete and the wrap;
- UseContainerItem's arms, taken in the call in 0x4fa0e0's order (bank, deposit, sell, equip, open);
- the watchers: a seat's guid changing, an arrival, a restack;
- the inventory and sell refusals, loot closes, the bind-confirm cancel, the revive.
Readers refuse a locked item where the client does (0x4f9e76, 0x5d8d32, 0x4fa1cd, 0x4fa4bf). Ammo equips by class and unlocks.
A /reload carries the locks and the cursor, minus what the sweep's loot, mail and auction legs unlock. A despawned item loses its lock.
Tests cover each site, reader, watcher and the reload.
Fixes#1033Fixes#242Fixes#228Fixes#354Fixes#227🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In 1.12.1, logging out or a refused character login returns to character select on the same world connection and re-lists there (`0x5aaeb0` → `0x401ee0`, then `0x46b880` → `CMSG_CHAR_ENUM`). There's no realmd logon and no new `CMSG_AUTH_SESSION`. Enter World closes the realmd link (`0x46b70c` → `0x5b3320`), and only the glue's own re-dials reopen it.
- benilla used to drop the socket and log on again from cached credentials. Now the world session splits and rejoins on one connection, and the header cipher carries its state across.
- The realmd link closes at the pick.
- A leftover packet at character select is skipped rather than fatal.
- The write thread applies a pending writer swap before any command.
- A kept-link teardown ends the character without a "server down".
Fake-server tests pin one logon, one auth and one more enumerate. The logout smoke fails without a same-link relist.
Fixes#984Fixes#983🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part 4a of the stock GlueXML (`WOW_GLUE_STOCK=1`): character select's roster.
- The select verbs run on the reference's roster (`0x4736c0`): `GetCharacterInfo` (race and class from `Name_Lang`), `SelectCharacter` with its clamp and event, the facing gate, and `GetCharacterListUpdate`. They are backed by the roster load and teardown (`0x4724d0`, `0x472580`) and screen state 3.
- Under the stock screens the character list is fetched only when the screen asks, as `0x46b880` does. The world connect completes on the auth response (`0x5aab80`), not on a roster.
- Every character-park request goes through one ordered channel. A roster reply completes only the current link's query. A late auth after a cancel is dropped, Warden stays terminal, and a failed list shows `CHAR_LIST_FAILED`.
The native screens are unchanged. Fake-server tests cover the ordering, the cancel, the realm change and the native policy.
Part of #856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An unattended run (`WOW_UNATTENDED`, `WOW_RIG`) resolved the same `benilla-config/` as a person. In a dev build, every linked worktree maps that to the primary checkout's folder. So a probe read the player's CVars, bindings and layout, and wrote its realm, settings and window positions back over them.
`local_state::home` now gives an unattended run a fresh folder of its own (`$TMPDIR/benilla-run-<pid>/benilla-config`) and prints it at boot. `BENILLA_HOME` still pins one, and a capture still resolves none. CONTRIBUTING's "Running it unattended" says so.
Test: `an_unattended_run_keeps_its_own_state_folder` checks the rig path, the override and the capture, and that the folder is never the resident one.
Fixes#975🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Player reports and the trace, in one batch.
- SetFactionInactive/SetFactionActive raise `UPDATE_FACTION`, so the list moves at once.
- NPC greetings: the latch, a dead unit, then the reaction pair with the active mover (`0x60c270`); a dead or ghost player hears none. Only a passing click re-tracks the NPC, and a window hello resets it (`0x60c3d1`).
- "You are now <rank> with <faction>." prints on first contact and rank change (`0x62c670`), in packet order, with the real gender.
- A standing packet prints its "increased by" line for the first entry only, never a hidden one (`0x4d57dd`).
- `WOW_MOVE_TRACE` carries each packet's bytes as hex; chat bodies are opt-in, and dot/bang commands and channel passwords are always redacted.
Tests cover each gate, the line order and the redaction.
Fixes#927Fixes#882Fixes#881Fixes#867Fixes#965Fixes#966Fixes#967🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under `WOW_GLUE_STOCK=1` the stock login screen now logs in. Separately, realmd's 0x0E answer was refused by the native login too.
In 1.12.1 the glue manager runs the screen states (`0xb41da0`): `DefaultServerLogin` (`0x46afb0`) needs the agreements accepted and opens the status dialog; each tick re-texts it from the logon's state and result (`0x46b140`, tables `0x836b40`, `0x836b78`); the world connect re-texts it every frame (`0x46be8b`). The agreements seed from `readTOS`/`readEULA` (`0x46a400`). Realmd 0x0E is a success (`0x5b2cd4`); a proof's 0x0A changes nothing (`0x5bada2`).
The glue core verbs, the glue manager over the native login, the agreement CVars, `SimpleHTML file=`, case-insensitive loose files and a benilla Realmlist layer file are built; the protocol accepts 0x0E. A recorded `Deviation:` keeps the interface signature checks off.
Tests: fake-realmd refusals and 0x0E; stock-screen login reaching the native submit.
Part of #856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With `WOW_GLUE_STOCK=1` (dev builds), benilla now loads the stock GlueXML off the player's own patch chain into the glue state and draws it; with the switch off nothing changes.
In 1.12.1 the glue builder (`0x46a7b0`) opens `Logs\GlueXML.log` (`0x46a8ce`), runs `GlueXML.toc` through the shared toc runner (`0x6edb90` at `0x46a9b3`), fires `FRAMES_LOADED` (`0x46aa34`) and opens on "login" or "movie" (`0x402b3b`), or "charselect" back from the world (`0x402084`). Glue `GetScreenWidth`/`GetScreenHeight` answer the root unscaled (`0x46dd30`, `0x46dd80`).
Under the switch the glue state replaces the boot state, takes input, and fires `SET_GLUE_SCREEN` on those edges; the native screens are hidden but still drive the network. A data-gated test loads the stock files with only seven unbuilt verbs raising, a list that may only shrink.
Part of #856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first piece of running the stock GlueXML: benilla had no glue Lua state, and no reference data for its surface.
In 1.12.1 the glue state holds the base set (`0x7039e0`) and seven registrars (`0x46abb0`): 119 functions, 105 of them glue-only, with 26 glue events (`0xb41e70`, `0x46a88f`). `RegisterEvent` matches names case-insensitively and drops an unknown one (`0x702140`). The glue builds the 13 base frame types plus `ModelFFX` (`0x46a4fe`), a Model with its own show and paint (`0x46fa00`); the in-game types come only at world entry (`0x495940`).
`reference/` gains the glue event and surface tables, generated and checked against the shapes table. `UiScript::new_glue()` builds that state; a gate holds its globals to the table, with the 108 unbuilt names listed. Nothing in the app changes yet.
Part of #856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chat bubbles were quads drawn straight into an overlay with no widget behind them, so `WorldFrame:GetChildren()` never returned them and bubble-skinning addons such as pfUI's found nothing.
In the reference, a bubble is an anonymous Frame under WorldFrame from a reused pool: eleven regions (the nine backdrop pieces, the tail, the text in the `NAMEPLATE_FONT` global), the text's bottom on the speaker's projected point, its own fade, and an OnShow on each reuse. It fades out and hides when the speaker is out of range or was not drawn last frame.
Bubbles are now built that way, fed by the app and drawn by the frame pass; the overlay path is gone. Item links show their bare names, and a speaker behind the camera fades out.
Tests cover the child, its regions, a pfUI-style skin, reuse and the fades.
Fixes#884
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a PvE realm, zone names were still coloured friendly, hostile or contested.
In 1.12.1, `GetZonePVPInfo` (`0x48d540`) answers a type only when its display gate is open (`0x48d5c2`/`0x48d5cc`). The gate opens on the realm's PvP flag or in a zone with Flags `0x10`, which is the six capitals. The realm flag is read from `Cfg_Configs.dbc` for the selected realm's type and stored once per world entry (`0x4015de`). A type with no row skips the store. Otherwise the type is nil, so the stock zone splash shows cream and the minimap zone text gold.
benilla now reads `Cfg_Configs.dbc` instead of a transcription that stopped at type 8, so realm type 10 lists as PvP. It latches the realm flag at world entry and applies the gate.
Tests cover the gate, the six capitals and every shipped Cfg_Configs row; each failed before the fix.
Fixes#816
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A crate built on benilla through `run_with` has its own `benilla-config/`, but could not ask where it is: `local_state::home` is crate-private. A crate that ships an addon has to write it into that folder's `AddOns/`, where benilla discovers addons and resolves their art and fonts, so without the path it needed a fork.
- `benilla_app::config_dir()` answers `local_state::home()`, asked from `run_with` on, since a dev build resolves it from the launcher's folder set there.
- CONTRIBUTING's "Building on top" says a crate's files and addons go there, and names `run_with` and this folder as the two seams opened for crates alone.
- The example launcher logs its `AddOns/` folder through it, so the example, which clippy and the tests build, is the outside caller that keeps it public.
No 1.12.1 behaviour changes.
Fixes#807🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The spell target binder went straight to the relation checks and tested none of `BindTarget`'s unit gates. So:
- a heal with a dead friendly player selected was sent at the corpse and refused by the server;
- Hibernate, Banish or Turn Undead at the wrong creature type was sent and refused, instead of refusing locally with "Invalid target";
- a self-excluding spell with yourself selected bound you.
`BindTarget 0x6e5b40` refuses a unit silently, before any relation arm, when any of these holds:
- the unit is the caster and the spell has `AttributesEx & 0x80000` (`6e5bd8`-`6e5bf7`);
- the unit has `UNIT_FIELD_FLAGS & 0x10000` (`6e5c1d`);
- the spell's `TargetCreatureType` mask excludes the unit's type (`6e5c26`-`6e5c53`). A zero mask admits every unit. Otherwise a unit of type 0 is refused and a typed one needs bit `type - 1`;
- the unit is dead (signed `UNIT_FIELD_HEALTH` not above 0), unless `UNIT_DYNAMIC_FLAGS & 0x20`, a word in `0x8600` or `AttributesEx2 & 1` admits it (`6e5c5f`-`6e5c8b`);
- the unit is alive and the word has `0x400` (`6e5ca2`).
A refused selection falls to the player behind `autoSelfCast`, then to the enemy-word refusal or the cursor. `SpellCanTargetUnit 0x6e6460` runs the same five gates (`6e6507`-`6e65ab`).
- `spell/bind_gates.rs` holds the five gates. `cast_target::unit_binds` is the one predicate for the selection, the `autoSelfCast` fallback, the cursor's click, `SpellTargetUnit` and the hover verdict. The cursor's separate `excludes_caster` checks fold into it.
- The dead test now matches the reference, which catches a corpse whose create block sent no health. The `0x400` bit is now cleared by a dead unit's bind (`6e5d89`, `6e5ddb`), where before any non-self candidate cleared it.
- `creature_type.rs` is the one `0x605570`, reading these in order:
- the form's `SpellShapeshiftForm` creature type;
- the creature template's type, by `OBJECT_FIELD_ENTRY`;
- `ChrRaces`'s type.
The minimap's tracking dots used their own copy, keyed by the guid's entry bits and missing the fall-through past the form. They now call the shared resolver.
- `SpellDisplay` reads `TargetCreatureType` (column 14).
Tests:
- `bind_gates.rs` has a truth table per gate. Its `ladder` module drives `resolve_cast_target` through:
- a dead heal target, with `autoSelfCast` on and off;
- Hibernate at the wrong type;
- a self-excluding spell;
- the `0x10000` flag;
- the `0x402` word.
- `the_press_the_click_and_the_hover_bind_the_same_units` runs one table through all three paths.
- `real_spell_catalog_reads_the_bind_gate_columns` pins the real rows for Hibernate, Banish, Turn Undead, Mind Control, Bloodlust and Revive.
Fixes#449🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A unit torn down (out of range or destroyed) before its visual was built still got one built, and the build's plain entity commands could apply after `apply_despawn_fade` had despawned it. That chain is unordered against the attach chain, and Bevy's default error handler panics.
The reference frees the object at once on destroy (`0x4674a0`, `0x465ec0` → `0x464920`), and only an already-built model fades (`0x672df0`). So a torn-down unit now gets no new visual work: `Without<DespawnFade>` on the queries of `attach_entity_visuals`, `refresh_live_display`, `heal_rig_starved`, `redress_player_looks`, `reseat_mounts` and `attach_held_items`.
`a_torn_down_unit_is_not_built` drives the real `attach_entity_visuals`, despawns the torn-down unit between the system's run and its command flush, and panics with "Entity despawned" on `insert<AppliedMount>` without the filter. `a_torn_down_starved_unit_is_not_rebuilt` covers the heal path. Built and gated on macOS.
Fixes#612🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- `docs/CONTRIBUTING.md`, "What does not": what 1.12 client mods add (a new key binding, a spell queue, new Lua functions, graphics past 1.12's) is named as a feature 1.12.1 does not have, so that what benilla does is what 1.12.1 does.
- A new "Building on top" section: such a feature lives in a crate started through `benilla_app::run_with`, or in a fork. A client started that way says `extended` in its build line. The crate keeps its own settings. benilla makes a piece of itself public to a crate only when its own 1.12.1 work creates it, never for the crate alone, with no stable API: a crate pins the revision it builds on.
- "What gets in": whatever the stock 1.12.1 client loads from a patch, benilla loads.
- `docs/METHOD.md`'s rule for every change points there, where it said "Anything else is a fork."
Docs only; no code or platform seam touched.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The core now loads the chain's `Interface\FrameXML\Bindings.xml` after the `FrameXML.toc` rows, as the reference's UI load does. The load checks the file at `0x48fff9` and loads it through the addons' loader `0x4b6f70` at `0x490018`. The default keys come from the chain's `WTF\DefaultBindings.wtf` (`0x4b62b0`). Every command now runs the stock file's own body, and the registry compiled into benilla, with its 91 retyped bodies, is deleted.
- **The loader's rules (`0x4b6f70`).** It reads the root's children only and skips empty, `debug="true"` and non-Windows rows. A second definition of a name is skipped, and duplicate headers are refused.
- **Keys and commands are separate tables, as in 1.12.** `SetBinding` never reads the command (`0x4b7490`). `RunBinding` runs the body in the VM. `RunCommand` sets `keystate` around the body and clears it after (`0x4b7c42`).
- **Engine behaviour sits behind the Lua globals the bodies call.** These are the movement, turn and mouse-steer bits. Focus loss clears the direction bits and keeps the mouse pair (`0x514490`). One key's stop clears its bit, as `0x514b70` does, so releasing W while UP is held stops the character.
- **Behaviour changes that follow the stock bodies.**
- The nameplate keys (V, Shift-V, Ctrl-V) now run the stock bodies.
- ALT-Z runs `CloseAllWindows(); UIParent:Hide()`.
- The minimap, sound, stance and bonus-bar keys call their stock functions.
- The wheel zooms one `CameraZoomIn(1.0)` per notch.
- **Platform.** benilla answers as the PC build, so a `platform="mac"` row does not register on any OS.
Verified:
- New tests: a fixture `Bindings.xml` (reordered, edited, and with debug, empty, nested and Mac rows) drives the command list, defaults and bodies. The V keys follow the stock bodies. No stock body's text remains in the tree. Every stock body's calls resolve after a production load. Each test fails with its change undone.
- `scripts/gates.sh` and `scripts/smoke.sh` are green.
- A live run with `WOW_GM=off`:
- W and Space moved and jumped.
- B toggled the backpack.
- 1 reached `UseAction`.
- V, Shift-V and Ctrl-V behaved as the stock bodies do.
Built on macOS; no platform seam touched.
Fixes#570Fixes#563🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
**#549: the stock video window's CVars.** Its check boxes, `ffx` and `gxRefresh` are registered at the reference's defaults. `pixelShaders` and `specular` ship as `overridden` "1" (`hwDetect`, `0x639a60`), and `gxFixLag` ships "1".
- `ffx`, `ffxGlow` and `ffxDeath` gate the full-screen effect passes every frame, as the reference's gate does (`0x6cd890`; `CFFXGlow` reads `ffxGlow` at `0x6cc5a8`, `CFFXDeath` reads `ffxDeath` at `0x6cdf10`).
- `gxMaximize` is latched. At the `RestartGx` commit it makes a windowed client borderless over the monitor, as the reference's window rebuild does (`0x58cf10`).
- The rest are registered with no effect, and each row says why: `useWeatherShaders` (snow's fallback leg is a separate CPU draw), `lod`, `M2UseShaders`, `M2UsePixelShaders`, `movieSubtitle`, `gxTripleBuffer`, `gxCursor`, `gxRefresh`, `DesktopGamma`.
- `cvars.rs` is split into the runtime and the table.
**#329: Spell Detail.** benilla's options window gains 1.12's Spell Detail Level slider on the Graphics page, over `spellEffectLevel` on the stock 0-2 grid.
**#583: `FrameXML.log`.** Load failures now go to `benilla-config/Logs/FrameXML.log`, as the reference's go to `Logs\FrameXML.log`, and no longer to chat.
- The log uses the reference's lines ("Couldn't open", "Error loading", "Couldn't parse XML in") and its banners.
- A UI load that reports something rewrites the file, and a runtime `LoadAddOn` appends to it (`0x490187`, `0x46aac0`).
Verified:
- New tests for each switch, the maximize latch, every box's stored value with no unknown-CVar warning, the Spell Detail row and the log file's rewrite and append, each failing on main.
- A region dump of the options window shows the new row, and the rows below it moved 35 px, and nothing else.
- `scripts/gates.sh` and `scripts/smoke.sh` are green.
- A live run unticked Full Screen Glow in the stock window, which turned the glow pass off and saved `ffxGlow = "0"`, and a missing addon file produced its four log lines.
Built on macOS; no platform seam touched.
Fixes#549Fixes#329Fixes#583🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>