Six places where benilla's Lua answered unlike 1.12.1's:
- The lexer took `[=[` long-string levels; 1.12.1's `luaX_lex` tests one byte after `[` (`0x6ff793`) and `--[=[` is a line comment (`0x6ff6c9`).
- `math.random` ran a glibc-style `rand` seeded with 1; now MSVC's (`0x7400e5`, `% 0x7fff` times 1/32767 at `0x7fb310`), clock-seeded as the client is (`0x5d1c8b`), fixed in captures and the addon harness.
- `getfenv`/`setfenv` honour a raw `__fenv` field (`0x702ba0`), and `setfenv` returns nothing.
- An object's text is `type: %08X` in the embedding API too (`0x74301d`).
- `time` converts a date table through MSVC's `_mktime` (`0x74066b`).
- `date` formats with MSVC's C-locale patterns (`0x8323a0`): `%c` is `09/09/01 01:46:40`, `#` drops zeros.
Each has a test that fails without it; the addon corpus parses as before.
Fixes#1257Fixes#1258Fixes#1510Fixes#1511Fixes#1503Fixes#1504🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loading screen's "Tip:" could read "Tlp". benilla drew the tip with Bevy's own text raster at a fractional size, which hinted the i's dot onto its stem at some heights, 1080p among them.
1.12.1 draws the tip through its font engine (`0x5c1c30`, from `0x40679c`). The tip now goes through benilla's font engine, the one every FontString uses, at the reference's size, line spacing, bottom-left justification, trimmed line ends and pen step. The shadow takes the body's alpha (`0x5cd65b`), and the shadow offset and the anchor snap to device pixels (`0x5c7010`, `0x5cdf70`). The Bevy text reshape plugin, now unused, is removed.
Tests: the i keeps a gap row at six window heights, the layout matches the engine's for the DBC line, and placement holds at DPI 1 and 2.
Fixes#1089🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The collector now paces as the client's Lua 5.0 does.
The threshold:
- The state opens at 4× its count (`0x6f6ec8`), and InitLua then sets 256 KB (`0x7039f2`).
- After a collection it is the count plus the larger of the old headroom and a quarter of the count (`0x6fae00`). The headroom is capped at the free space in the client's six block pools, which a new book models (`0x6fae90`).
- The memory watch collects from a threshold of 0 (`0x703bd6`), and varargs never collect.
The stack and call records start at 45 and 8 slots and grow by 5.0's rule (`0x6f5f60`). They are checked once per collection, during mark, and halved under a quarter's use (`0x6f7949`). A tail call checks the stack where the call is made (`0x6f922c`).
A collection of a 10 MB heap takes about 1 ms; each change fails its test without it.
Fixes#1783Fixes#1784🤖 Generated with [Claude Code](https://claude.com/claude-code)
The AddOns screen's script memory setting did nothing.
In 1.12.1, `scriptMemory` (KB, default 49152, `0x402e42`) is one limit for the whole Lua state. It is checked each frame in the world by `0x495590`:
- Over the limit, the client does a full collection and counts again (`0x4955d5`).
- Still over, it fires `MEMORY_EXHAUSTED` with the limit in MB (`0x49563d`), so the stock popup shows, and starts a 15 s deadline.
- Still over at the deadline, it quits (`0x495602`).
- Back under, it fires `MEMORY_RECOVERED` (`0x495656`).
- A limit of 0 or less turns the watch off.
benilla now does the same. Its count still uses 64-bit object sizes (#1779), recorded as a deviation.
Tests: the state machine, plus two full stock-UI logins. A 1 MB limit shows the exact popup and freeing memory hides it. The default shows nothing (the stock UI is 13 MB).
Fixes#1081🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
benilla-lua's lexer and compiler now behave as the client's 5.0:
- A control byte is "invalid control char" (`0x6ffabb`).
- Every chunk lexes as source, binary ones too (`0x6f6a15`).
- Lines end at `\n` alone (`0x6ffb94`); a dot is never a NUL (`0x6ff98a`).
- A `while` condition over 100 instructions is too complex (`0x6fd03b`), counted as 5.0's code generator counts it.
- Upvalues get no name in errors (`0x6fc5e0`).
- `string.dump` refuses closures with upvalues, and dumps the stack top (`0x6f438a`, `0x6f4373`).
The client installs no Lua hook (`lua_sethook` has no caller), so the load's runaway guard has no client mechanism to follow. It is now an abort no `pcall` can catch, recorded as a deviation.
An audit re-derived every commit from the bytes. Each fix fails its test without it.
Fixes#1755Fixes#1756Fixes#1757Fixes#1758Fixes#1759Fixes#1767Fixes#1434Fixes#1445Fixes#1777Fixes#1778🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A spell visual kit naming animation 0 (Stand) was read as no animation, so 41 kits lost their play. In 1.12.1 the field is skipped only when negative (`0x60f366 jl`): a state kit naming 0 recomputes the base (`0x60f383`-`0x60f399`) and never pins (`0x5fdeb5 jle`); a cast or impact kit plays it through `0x5fe2f0` (`0x60f3c5`); the holds and the missile release need above 0 (`0x612a0a`, `0x6e8295`).
Playing it needed two driver rules benilla lacked: PlayTrack drops a request for the id a track already asks for (`0x5fdcab`), so a recompute no longer restarts an unchanged gait; and a Stand request over a swing, ready stance or cast is dropped (`0x5feb00`-`0x5feb56`).
Tests: the parse, the recompute without a pin, the holds and release, the dedup, each demotion arm, and Stand ending at its window. Each fails without its part.
Fixes#1719🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A server that sent BillingTimeRemaining got no warning: benilla dropped the auth response's billing minutes and plan flags. In 1.12.1 the handler (`0x5b41b0`, gate `0x5b41f9`) keeps the group from AUTH_OK or the first AUTH_WAIT_QUEUE; `InitializeGame` arms a deadline at each world entry for 1-4320 minutes on a non-recurring plan (`0x48f636`-`0x48f679`); the game UI's per-frame poller (`0x495679`-`0x495826`) fires `BILLING_NAG_DIALOG` once at 30 minutes or fewer and prints `BILLING_NAG_WARNING` each 5 minutes, then each minute from 5 (`IGR_BILLING_NAG_DIALOG` at 600 for an internet-cafe plan).
The protocol keeps the group across the queue; `ui_billing` transcribes the arm and the poller. `GetBillingTimeRested` was set on the glue VM; it is now seeded into every in-game VM.
Tests: the four vmangos body shapes byte-exact, the queue's group outlasting a bare AUTH_OK, the cadence, the stock popup and chat line, and the arm at entry only.
Fixes#813🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no WoW install found, or a `Data` folder whose archives will not open, benilla booted into a window of textless buttons, since fonts, art and the stock interface all come from the install.
It now ends before any window exists. A native error dialog (`rfd`: AppKit, Win32, `zenity` on Linux) names every folder searched, or the folder and the archive error, and says where to put the install; then it exits 1. An unattended run prints the same text to stderr and exits without a dialog.
1.12.1 ends at a fatal dialog for a present but corrupt base archive (`0x403b6f`) and only logs a missing one (`0x403b00`); ending at no install at all is recorded as a `Deviation:`.
Tests cover both messages and the failing exit; both cases were run live unattended. Built on macOS, Linux and Windows.
Fixes#836🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stock 1.12 glue screens are now the only glue screens, so benilla's native login, realm list, character select and create screens are deleted (−17k lines). So are the native switch, its census tests, the art-extent module and its extract command.
What still drove the native screens now presses the stock ones through their own Lua verbs (`capture/glue_press.rs`):
- the rig;
- `WOW_PROBE_CHARCREATE`;
- the `glue-login`, `glue-charcreate` and `glue-realmlist` captures.
The three world-entry tests run on the glue VM. The schedule's ambiguity ceiling drops from 3939 to 3348.
Live: smoke, a glue round trip, a stock pick, a rig teleport, a create and delete, all three captures, and the player build's boot.
Fixes#856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stock 1.12 GlueXML screens are now the default in every build: login, realm list, character select and create. benilla's native screens run only in a dev build with `WOW_GLUE_NATIVE=1`, or under the rig and capture runs, which still drive them, until the native screens are deleted. `WOW_GLUE_STOCK` is gone.
On the way:
- An unattended reconnect re-dials the kept realm and lists the roster. Under the stock screens it stalled at the realm list.
- `WOW_CHARSELECT_PICK` selects on the stock screen.
- `smoke.sh` counts the stock run's VMs and fonts.
The stock screens fix the native screens' defects listed below.
Tests cover the switch, the pick and the reconnect. Live: smoke, a kick and re-entry, the player build's boot.
Part of #856.
Fixes#849Fixes#850Fixes#834Fixes#812Fixes#360Fixes#463Fixes#79Fixes#977🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fifth open loot roll was announced at once. The stock frames hold four, so its frame never opened and it could not be voted.
1.12.1 starts a roll through `0x61b430`: below four open it counts up and fires `START_LOOT_ROLL` with the wire countdown. Otherwise it defers the roll. Every close (own vote, WON, ALL_PASSED) fires `CANCEL_LOOT_ROLL` and then promotes the oldest deferred roll (`0x61b760`). An uncached item takes its turn when its record arrives (`0x61b460`).
benilla now does the same. Each roll waits, defers, starts or is open; at most four are open, and every close promotes oldest first. The client's server-close paths decrement the count even for a deferred roll that never counted (`0x61b6f8`, `0x61bc17`), so a fifth frame can open there. benilla keeps the count exact, as a `Deviation:`.
Tests: five announce four; each close path promotes; the wire countdown; a deferred close.
Fixes#1061🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With `WOW_GLUE_STOCK=1`, character select, realm list, create and the glue strings still ran native systems. They spawned hidden trees and could write the hidden `GlueDialog`. A refused scripted login showed nothing on the stock screens.
Every native glue system is now gated on `native_glue`. The cross-switch instruments (round trip, login shot, realm smoke) move into `glue_session`. Under stock, `WOW_CHAR` enters by the stock Enter World. The account guard's refusal opens the stock `OKAY` dialog, the path `CGlueMgr::Login` takes for an empty account (`0x46b090`). The schedule's ambiguity ceilings drop to 3939 and 372.
Test: a census fails on any system that touches the native screens' components or resources without the gate; it lists 44 on main. Live: native smoke, a stock login to create and back, a stock logout round trip, the stock realm smoke, and a guard refusal.
Part of #856.
🤖 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>
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>
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>
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>
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>
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>
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>
`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>
`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>
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>
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>
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>
The fourteen 1.12.1 functions #336 lists are defined, each derived from its reference address, and benilla's layer drops the two places it stood in for the stock bag code.
- **Keyring:** `KeyRingButtonIDToInvSlotID` (`0x4c8150`) exists, and the keyring slots answer as inventory slots 82-113. The stock `ContainerFrame.lua:616-619` hover path now runs unchanged, and the layer's keyring `OnEnter` wrapper is deleted.
- **Bags:** the layer no longer replaces `CloseAllBags`. The stock one (`ContainerFrame.lua:702-707`) closes the backpack only when it was not open before the vendor or mailbox opened it (#541). The deviation that opens every bag stays, and its comment now names its only callers.
- **Inventory:** `ShowInventorySellCursor`, `SetInventoryPortaitTexture` and `UpdateInventoryAlertStatus` exist.
- **Character and pet:** `GetDamageBonusStat` (ChrClasses field 2), `GetPetTimeRemaining` (armed from `SMSG_PET_SPELLS`) and `GetActionAutocast` exist.
- **Mail:** `ClearSendMail`, `GetNumPackages`, `GetPackageInfo` and `SelectPackage` exist, over a new `Package.dbc` loader. A selected package rides in the mail send. The compose reset now also clears the stationery, the package and the attachment, as `0x4acdc0` does.
- **Tabard:** `GetTabardInfo` opens the tabard designer through the registrar (`0x5e00e0`).
- **World map:** `GetWorldLocMapPosition` projects a map position onto the displayed map (`0x4a7360`).
- **Loot:** `SetLootPortrait` binds a unit loot source's portrait (`0x4c2b40`).
Verified:
- A test per function for its shapes, raises and effects, failing on main.
- A keyring hover test through the stock path, failing on main.
- A backpack-over-vendor test, failing on main.
- `scripts/gates.sh` and `scripts/smoke.sh` are green.
- A live A/B run: hovering a key asked for `player:82` and showed the key's tooltip. After a vendor, opening the Help panel left the backpack open and closed bag 1; on main the backpack closed.
Built on macOS; no platform seam touched.
Fixes#336Fixes#541🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
benilla's own interface now loads as one layer on top of the stock UI, as `docs/METHOD.md` now states: the core loads the stock FrameXML rows, the layer (`assets/ui/layer.toc`) loads after the last of them and before the third-party addons, and like FrameXML it has no AddOn API row and fires no `ADDON_LOADED` (the reference loads `FrameXML.toc` outside `AddOn_Load`, `UI_Init` `0x48fbf0`).
- Moved into the layer unchanged: `ScrollTemplates.xml`, `KeyBindingsPage.xml`, `OptionsFrame.xml`, `ContainerFrameAdapters.xml`, `SpellBookAdapters.xml`, `ScriptLogFrame.xml`. `GameMenuFrame.xml` stays the core's last row until #553.
- The host's Lua fixes to stock code (`FCF_ChatTabFadeFinished`, the `DurabilityFrame` re-seat) move into the layer's `FrameXMLFixes.xml`; the resize re-seat stays host-side as an engine adaptation.
- The core now loads `StatsFrame.xml` and runs `LocaleProperties.lua` once, from `UIParent.xml`, so its chain rows are exactly the stock `FrameXML.toc` rows but the menu; the order is #555.
- A dev build boots without the layer under `WOW_STOCK_UI=1`; the host's calls into the layer are guarded.
Verified: new tests for the layer's file list, the core list against the stock toc, the load position (a `_G` definition log), absence from the AddOn API and the stock-only boot, each failing with the change undone; a before/after dump of every global, frame, draw order and saved variable differs only by `StatsFrame` and the moved fixes' source; `scripts/gates.sh` and `scripts/smoke.sh` green, and a live logout/re-login under `WOW_STOCK_UI=1` clean. Built on macOS; no platform seam touched.
Part of #552Fixes#542Fixes#543🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A dev build finds its project folder, where its `WoW` link, `benilla-config/` and `.probe-identity` live, from its launcher, not from a benilla crate's own `CARGO_MANIFEST_DIR`. For a crate built on top of benilla, where benilla is a git dependency, that manifest dir is cargo's checkout of benilla.
- `benilla_buildstamp::emit()` stamps `BENILLA_PROJECT_DIR`, the launcher's workspace root (the nearest folder up holding `Cargo.lock`), only when the launcher's `dev` feature is on. A player build stamps it empty and carries no source path. The shims pass it in as `BuildId::project_dir`.
- `benilla_formats::project_folder` is the one home for the folder. Each entry point records the stamped folder once, before anything resolves from it. The install's dev rung, the state folder (which keeps its step from a linked worktree to the primary checkout) and the probe identity all read it. With nothing recorded (a test, an example, a tool) it is benilla's checkout, as before, and in a player build it is `None`.
- Live `assets/ui` and the addon corpus stay on benilla's own source.
- The example launcher's header now says a dev build of a crate on top keeps these in its own folder.
- The comment above the stamp's resource names the build banner, the stamp's reader beside the panel footer.
Fixes#494Fixes#493🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A script's calls that touch the selection, the player's cast or the targeting cursor now take effect in the order the script made them, as in 1.12.1, where each such call is done when it returns (the macro runner `0x4f14e0` fires every line in one pass; `TargetUnit` `0x4899d0` and `TargetByName` `0x489d60` commit through `SetSelection` `0x493540`; `CastSpellByName` `0x4b4ab0` casts at the selection as it stands then).
- The VM keeps one ordered queue of these calls (`ScriptCall`) in place of fifteen per-kind queues, and one system applies it front to back in the target chain, each call through its owner's applier.
- A macro's lines run inside the `UseAction` that pressed it (`0x4e6098 call 0x4f1460`), so the calls they make are applied before the next queued call.
- `ClearTarget` deselects whatever is selected when it applies, so `TargetUnit("player") ClearTarget()` ends with no target.
- `SpellStopCasting`, `SpellStopTargeting`, `TargetNearestFriend`, pet bar presses and orders and pet-book casts join the queue, since they touch the same state: `/script SpellStopCasting()` then `/cast`, and `/assist` then `/petattack`, now apply in order too.
Tests run the real dispatcher on the headless client: target then cast casts at the new target, cast then target at the old one, a clear after a target ends with nothing selected, and a macro's calls land inside the `UseAction` that ran it. Built on macOS; no platform seam.
Fixes#451
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With realms in more than one category, the realm list shows the stock category tabs, so every realm can be reached. Before, it listed only the first category's realms and had no way to the rest.
- **The Region** is `Wow.ini`'s `[WoW Config] Region`, read through the archive chain as `0x40249d` loads it (the stock install ships it in `interface.MPQ`), with the section and key compared case-insensitively (`0x445356`, `0x4453cb`) and the value parsed as `0x445470` does. It is 0 when the file or the key is missing.
- **The categories** are the `Cfg_Categories.dbc` rows whose Region column equals it, in file order (`0x46e300`): id from column 0, name from the row.
- **Placement** (`0x46e510`): a realm's category byte 0 takes the first category's id (`0x46e5b8`), then joins every category with that id (`0x46e60b`). A byte matching no category puts the realm on no tab, as in 1.12.1. Tab ordinals count the non-empty categories only (`0x46edf0`), and the load-band statistics still run over every realm.
- **The tabs** are built from `RealmList.xml`: hidden for one category, `RealmListTab1`..`N` with the category names for two to eight, the front one raised and disabled. A click fronts the tab and re-lists (`RealmListTab_OnClick`), the highlight falls back to the current realm or none, and the scroll clamps to the new tab. Change Realm and `OnShow` keep the stored category (`0x46f188`, `0x46f400`).
- **No Region rows**: every realm lists on one untabbed page. The reference reads a NULL category there (`0x46f26a`, `0x46e5b8`), and benilla does not copy the fault.
- **Logging in** by name, the remembered or current realm, still searches every realm, as the reference resolves by name.
- A dev capture, `WOW_CAPTURE=glue-realmlist`, shows two tabs over five invented realms.
Checked: tests for the Region read, the category rows for a US and an EU Region from the install's DBC, placement (byte 0 folded, an unmatched byte on no tab), the strip hidden for one category and named for two, a tab click through the real click path, the highlight and scroll on a switch, the stored category across Change Realm, the statistics over every realm, and the no-rows fallback. Each fails with its piece undone. Gates and the smoke run green on macOS; no platform seam touched.
Fixes#362🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Logging in to an account or realm with no characters lands on the empty character list. Until now the login cycle created a male Human Warrior on the server, named `WOW_CHAR` or "One", on every login that found an empty roster, a typed one included and again after Change Realm.
- **The reference.** The client builds `CMSG_CHAR_CREATE` in one place, `0x5aac50`, reached only from the glue binding `CreateCharacter` (`0x4719e0` → `0x4711d0` → `0x46b290` → `0x5aac50`), which stock `CharacterCreate.lua` calls only from `CharacterCreate_Okay`. With an empty roster, stock `CharacterSelect.lua`'s `UpdateCharacterList` disables Enter World and Delete and shows Create New Character, which benilla's select screen already does.
- **The change.** The create-if-empty step is gone from the per-realm cycle, and `NetConfig`, which existed only for it, goes with it. The create screen's own path is untouched.
- **Scripted runs.** `WOW_CHAR` must now be on the account: a driverless run whose `WOW_CHAR` is missing fails at select, as it already did on a non-empty roster, and a player's run shows the list. `docs/CONTRIBUTING.md` says so, and names `WOW_PROBE_CHARCREATE` as the explicit way to make one.
Checked: `an_empty_account_parks_at_select_without_creating_a_character` drives the real login cycle against a fake realmd (the server half of SRP6) and a fake world server answering an empty roster. It fails with the old step restored (the client sends `CMSG_CHAR_CREATE` on its own) and passes without it. Live against a local vmangos on a fresh account with no characters: before, the server gained a level-1 "One"; after, the client parks on the empty list and the account still holds no character. With a `WOW_CHAR` not on the account, a driverless run exits at select, and nothing is created. Gates and the smoke run green on macOS; no platform seam touched.
Fixes#343🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The second read of `benilla-protocol`'s comments, the last step #41 asked for: that crate's comments were rewritten to the rule before second readers existed. Four readers compared every file's comments against their state before the rewrite and against the code as it is now.
Fixes#41.
- **Citations.** vmangos line citations are re-pointed to the pinned revision: off-by-a-few lines (`Pet.cpp`, `Guild.cpp`, `Petition.cpp`, `LootHandler.cpp`, `ChatHandler.cpp`), and paths the rewrite dropped where a bare file name is ambiguous (`Server/Packets/Spell.cpp` against `Spells/Spell.cpp`, the two `Item.cpp`, `Group.cpp` and `Chat.cpp`). The gossip-hello citation is `NPCHandler.cpp:347`.
- **Facts restored.** The move-time-skipped chain (`0x603b40` → `0x601560` → `0x61ab90`), the addon-message TAB surviving vmangos's chat sanitizer, the zone-under-attack sender, `SMSG_ADDON_INFO` answering the auth session's addon block, the officer-note permission gate, and `SMSG_ITEM_COOLDOWN` on equipping an item with an on-use spell (the old text said a proc).
- **Deviation labels.** A label now carries its reason, or goes when the behaviour is only unbuilt (the survey window, the group XP bonus, the NPC-text language garble).
- **Two examples.** `park_probe` and `login_refusal_probe` took a hard-coded local account; they now read `WOW_USER` and `WOW_PASS`.
- **Verification.** Every file strips to identical code apart from the two examples; `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>
Until now this repo was an export of a private tree, pushed as squashed snapshots. From here on
it is the working tree itself, and the private repo is archived.
The root holds AGENTS.md and the Rust and GitHub files only. Everything an agent needs to orient
is in docs/: METHOD.md (the rules and how the project is built, 123 lines, project method only),
MAP.md (generated from the code at every land) and CONTRIBUTING.md (what gets in and how a
change is judged). One skill, `probe`, holds the procedure for running the client unattended
against the local server.
Brought over because the project needs it: reference/ (the 1.12 name catalogues 34 source
files read; the export failed its own tests without them), the gate chain and the pool
(scripts/), the hooks that enforce the install rule and the worktree rule, the full .gitignore,
the `cargo play` alias, and the original wording of eleven source files the export had reworded.
Left out: the decision records (archived with the private repo), the export tooling, the
operator's Discord and triage loop, the lab-laptop scripts and the instruments nothing
references, which live outside the repo now. What read the records is gone from the tooling:
no corpus lint, no records index, no `reserve` verb, no number-collision check.
Three hooks are gone: fmt-at-commit and the file-size nudge were redundant with the gates, and
the assets/ui guard is now a test (`assets_ui_does_not_grow`) that fails for everyone, not only
for a Claude Code session. The remaining pool hooks stand down on a machine without a pool.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>