`BenillaScrollBarTemplate`'s own OnValueChanged was a faux-list handler that both instances replaced with the real scroll, so a new bar that forgot the override wrote `offset`/`updateFunc` on its parent instead of scrolling. The reference's `UIPanelScrollBarTemplate` carries `this:GetParent():SetVerticalScroll(arg1)` itself (`UIPanelTemplates.xml:156-158`).
The template now calls `BenillaScrollBar_OnValueChanged(this, arg1)`; the two instance overrides, the dead faux handler and its header text are gone.
Test: a bare bar from the template scrolls its ScrollFrame parent with no `offset` or `updateFunc` write; it fails with the old template.
Fixes#359🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lazy Peons stood upright under their sleep aura. In 1.12.1 an aura's state kit (kind 2) sets flag 0x1000 (`0x60eecd`) on an effect at the head of the unit's `+0xb4` list (`0x620360`), and every play reaches `0x5fde80` (`0x5feab5`): a still unit (`[+0x9e8] & 0x20000f` clear) plays the first flagged kit's animation above 0 instead of what was asked, on body and mount alike (`0x5fdeee`, `0x5fdf08`). Spell 17743's kit names 71, which OrcMale plays as Sleep.
benilla now keeps those pins per unit, newest slot at the head (`0x604e7b`), and the driver plays the head pin for a still unit's poses, one-shots, jumps and landings, rider and mount both; the clip plays out its window after the aura goes. A still stunned unit now plays Stun, as in 1.12.1.
Tests cover each path; each fails without its part.
Fixes#837🤖 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>
A server that sent `SMSG_WARDEN_DATA` ended the login at character select with a terminal failure. Servers that send it without enforcing it were unplayable, and vmangos serves the roster and the world entry regardless (no handler gates on Warden; `World.cpp:325-338` starts it after AUTH_OK) and only kicks a silent client after `Warden.ClientResponseDelay` (`Warden.cpp:686-691`).
benilla now reads the message past, answers nothing and goes on; the server's policy decides what follows, and a kick is the ordinary lost link. This is a `Deviation:` at the read: the reference hands the message to its loaded module (`0x6ca2f0`, `0x6ca5c0`) and sends its answer (`0x6ca6a0`). `WardenRequired` and the terminal-failure path it alone used are gone.
Tests: a login past a Warden request reaches the world with nothing sent back; a kick after it is a lost link; the handshake reads one past. Each fails with the old refusal.
Fixes#854🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a window narrower than 4:3 the loading screen ran off both sides: its area was always 133.3vh × 100vh. In 1.12.1 the layer `0x406a60` fits the picture through the viewport (`0x58af60`) from `a = 0.75 × aspect` (`[0x832a4c]`): width 1/a, centred, above 4:3; full width and height a, centred, below it; the whole window at a == 1, which `widescreen` off gives. The bar draws in that viewport's [0,1]; the tip is placed in window pixels (`0x5c70b0`, sizes from `0x5c2b50`), its baseline raised by the letterbox band (`0x4066c6`).
The area is now that rect, refitted each frame, and the tip is laid out in window terms. The live window's aspect stands in for `gxResolution` (a `Deviation:`, as for portraits).
Tests: the rect at 1280×1024, 1920×1080, 4:3 and `widescreen` off, a resize, and the tip in each case. The rect tests fail with the old fit.
Fixes#355🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A refused query answer (top-bit entry, empty name) was stored as a record, saved to the cache file and never asked again. In 1.12.1 the refusal leg (`0x556e63`, items `0x55bdf7`, objects `0x558927`, names `0x55f6f0`, guilds `0x561390`, quests `0x563150`) runs the callbacks with status 0 and frees the node; the next lookup with a callback (`0x556aa0`) asks again.
Now a refusal frees the record, is never persisted, and redisplays nothing. Unit, object and slot/crest changes look up at once (`0x60b15b`, `0x60463f`, `0x5f7c40`, `0x5ed764`, `0x5e078f`), and a dropped ask is re-sent. Unit-name reads follow `GetUnitName` (`0x609210`): creatures are read from the unit, never asked. Readers that poll every frame wait 30 s after a refusal (a `Deviation:` until they are split). An empty quest title is a refusal.
Tests cover each path; each fails with its change undone.
Fixes#1674Fixes#393🤖 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 dead player or ghost could use any GameObject: the cursor stayed lit and a right-click opened a mailbox or walked to a chest. In 1.12.1 `usable` (`0x5f3130`) tests `IsDeadOrGhost` (`0x605f30`, at `0x5f315f`) ahead of its lock and range arms and refuses with error 0x7e, `ERR_PLAYER_DEAD` (`0x5f33c6`); the cursor asks the same slot (`0x5f3093`, `0x5f58a8`, `0x5f6858`) and grays.
`cursor_mode::go_player_refusal` now holds the player arms, dead and the existing mounted gate (`0x5f31a8`), and both the cursor and the click use it: dead grays the cursor and the click raises `ERR_PLAYER_DEAD` with nothing sent. Mounted now also grays a lock-less object's cursor, as the same slot does in 1.12.1.
Test: dead, ghost, mounted and live players at a chest and a mailbox, checking the cursor, the use and the error.
Fixes#830🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`SPELLCAST_STOP` fired only at `SMSG_SPELL_GO` and only for a timed cast, so instants, channels and item uses never sent it to addons.
In 1.12.1 it fires only from the cast result: the handler's pop `0x6e741a` → `0x6e4940` (`0x6e4a4b`) while the pending record's caster word `0xceac50` is the player's, filled by the attempt (`0x6e4fb1`) and cleared when the event fires (`0x6e4a5a`); and `0x6e74cb` for a non-failure result with nothing in flight. The go handler fires none. vmangos sends START, RESULT, GO, so a timed cast's bar still ends at completion.
The attempt now fills the caster word, the result fires STOP as those two sites do, local cancels fire only while the word holds, and the go-time raise is gone. Tests replay each cast kind in vmangos order; a live run logged STOP for an instant, a timed cast, a channel and an item use.
Fixes#1604🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Print screen on a glue screen (login, realm list, character select or create) raised a Lua error on a nil `Screenshot` and saved nothing: the stock GlueXML calls it from each screen's `OnKeyDown`, and benilla bound it only in the in-game VM.
1.12.1 binds `Screenshot` in the glue table too (`0x46d3d0`). It arms the same capture (`0x442a10`) and answers with the glue's own events, `GLUE_SCREENSHOT_SUCCEEDED` or `GLUE_SCREENSHOT_FAILED` (`0x46c520`, glue events 14 and 15).
The glue VM now installs the same verb on the same capture ask, and the writer answers with whichever VM is up. Tests cover the glue call arming one capture and the glue events answering it, and the in-game names staying in-game; they fail without the binding.
Fixes#1649🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
In the 17 instances built as one WMO (Wailing Caverns, Blackrock Depths, Molten Core, Dire Maul, the Deeprun Tram and others) benilla had no area: entering by portal kept the outside zone, no `ZONE_CHANGED_NEW_AREA` fired, and logging in inside left the zone texts empty.
On a map whose WDT carries MPHD bit 0 (`0x694904`), 1.12.1's area query `0x6703a0` answers the map's `Map.dbc` field 19 for every entity, ahead of any WMO or terrain read; a 0 there fails the query.
`MapCatalog` now reads field 19, the WDT exposes the bit, and `update_current_area` takes that leg first on such a map. A worldport also drops the old map's held area, which otherwise survived the transfer. Tests cover the leg, the flag's map, a zero field and the worldport; a live portal into Wailing Caverns fired `ZONE_CHANGED_NEW_AREA` with "Wailing Caverns".
Fixes#1636🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A hunter shot the server refused (out of range, line of sight) still sent `CMSG_CAST_SPELL` for Auto Shot: `cast_result` chained `modalNextSpell` on any result whose spell was in flight.
In 1.12.1 a failure's report pops the in-flight slot (`0x6e2201` → `0x6e2218` → `0x6e4ad0(0, 0)`), so the handler's compare at `0x6e7408` misses and the chain at `0x6e74aa` is skipped. It still chains when the report leaves before that pop (reason 0x17; 0x56 with no named form; an uncached 0x5c or 0x78 item, `0x6e1eab`) or when the save held the same spell.
`cast_result` now reads the slot pair up front and chains a failure only in those cases. The test that asserted a failed sting chains is flipped, and each remaining case has its own assertion; each fails with its rule undone.
Fixes#1594🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 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>
Splitting a quest item's stack could flash its objective line ("Wolf Pelt: 4/5"). The toast rode a per-frame diff of the bag count, and a split dips it for a frame: vmangos sends the shrunk source and the new stack's create at once, and the slot naming the new stack later, in the batched update (`Player.cpp:11100`, `10316`–`10327`).
1.12.1 toasts an item objective only from `SMSG_QUESTUPDATE_ADD_ITEM` (`0x5e5ad0`, case 0x19a). It walks the log to the first quest needing the item, and `0x5dd060` shows `ERR_QUEST_ADD_ITEM_SII` with min(held + added, need), then `QUEST_WATCH_UPDATE`. An uncached quest record defers the pair to its arrival (`0x5dd180`).
Item lines now toast from that packet alone; kill and complete toasts are unchanged. Tests replay a split's update sequence (no toast) and a pickup (one toast); both fail without the change.
Fixes#916🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
At character select the IO thread waited only on the app's requests, so a server that dropped the link (vmangos kicks a session idle there after `CharacterScreenMaxIdleTime`) went unnoticed until Enter World started a load.
1.12.1 keeps servicing the connection on that screen, and a drop runs `CGlueMgr::NetDisconnectHandler` (`0x46c540`): `DISCONNECTED_FROM_SERVER` and back to login.
The park now looks at the socket twice a second without blocking. A closed link ends the session as a failed roster ask does; a packet sent unasked is read and handled as `char_enum` handles its strays, in one shared step.
A fake-server test drops the link while the client is parked with no request pending and expects the session lost; it times out without the change.
Fixes#1020🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A shown item tooltip whose line waited on a cache kept missing that line until something re-ran `OnEnter`. Examples are a lock's key, a "made by" name and a set member. Bags re-run it every frame; trade slots, `ItemRefTooltip` and addon tooltips never do.
1.12.1 registers `CGTooltip::OnCacheAnswer` (`0x52e530`) on every record, name, petition and item-name miss, with the tooltip as cookie. On each answer it rebuilds from the inputs the last build kept (`0x52b6cc`–`0x52b70a`), reading the live object, and sizes the plate (`0x52e5ac`). A clear zeroes the kept entry (`0x5300dd`), so a cleared tooltip stays down.
Every item `Set*` now goes through one build that keeps its inputs and records its misses. Each answer rebuilds whatever the tooltip keeps by then; compare asks stack and builder asks deduplicate, as in the reference.
Tests cover each cache and a cleared tooltip.
Fixes#1452🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dev build maps a linked worktree's `benilla-config/` to the main checkout's, so a developer's worktrees share one settings folder. That included a worktree that declares its own account in `.probe-identity`: an attended run from it, or a scripted one without `WOW_UNATTENDED`, read and wrote the main checkout's CVars, bindings, macros and saved variables while logged in as another account. A branch with a wrong settings migration or scripted key presses changed the player's own client, with nothing to show it.
`local_state::shared_root` now keeps such a worktree's state in its own `benilla-config/`. A worktree without `.probe-identity`, and the main checkout, resolve as before. CONTRIBUTING and `.gitignore` say so.
Test: `the_state_root_is_the_folder_or_its_primary_checkout` adds a worktree with a `.probe-identity`, which keeps its own root, and a main checkout with one, which is unchanged; it fails without the change. `the_state_folder_lands_where_the_build_says` follows the rule.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No gate built the shipped player graph outside tests. The player-tests gate compiles with `cfg(test)`, and clippy ran only on the dev feature set, so the player build's own warnings landed unseen: four unused re-exports that only dev probes used.
A new `player-clippy` gate runs `cargo clippy -p benilla -p benilla-app --no-default-features -- -D warnings` in `scripts/gates.sh`, in CI, and in `scripts/check.sh` when it reaches those crates. `docs/METHOD.md` names it.
The four re-exports are deleted: `ground_derived`, `GroundClamped`, `BodyPartsDesc` and `ATTACH_SLOT_NAMES`. Their dev users import them from the defining modules, with no `allow` left.
Test: an unused import added to `net.rs` fails the new gate.
Fixes#1465🤖 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>
Nameplates took a fixed Friz Quadrata path whatever `NAMEPLATE_FONT` held, so an addon or a localized GlobalStrings could not restyle them.
1.12.1's plate constructor reads the global for each FontString it builds: the name at `0x7cb3f9` and the level at `0x7cb49b`. The read is `lua_gettable` on the globals, so `__index` runs (`0x704373`). A number reads as its text; anything else, or a missing global, gives "". `SetFont` refuses "" (`0x44d05c`) and leaves the string with no font.
Plates and chat bubbles now read the global through one lookup, when each is built. The read happens before the UI model is borrowed: an addon's `_G` `__index` that touched the model panicked the bubble's read.
Tests: a plate built after the global is set takes it, an older plate keeps its own font, the non-string cases, and an `__index` that calls `CreateFrame`.
Fixes#921🤖 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>
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>