A building's pool drew forever once its room had been visited, and a portal-less building with an area id never drew its pool, Northshire Abbey's wall fountain among them. In 1.12.1 the flood's visit `0x685d70` links a visited group into the liquid list (`0x685f6b`, the only link), and the liquid pass `0x684cd0` unlinks each record as it draws (`0x684e1d`). So a pool draws exactly in the frames its group is visited. The latch is gone, and the fountain draws. Across 184,624 camera poses around the Great Forge, benilla's flood keeps the lava wherever the reference's portal graph does.
A pool's liquid type read every tile row and the group's override. `0x6ba970` scans only the first row and takes the first wet nibble, and its caller never reads the override. benilla now does the same.
`WOW_LIQUID_TRACE=1` prints each pool's per-frame verdict.
Fixes#119Fixes#65Fixes#2492🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model textures loaded by their literal name, and BLP mip chains stopped one level short.
- **#610:** a model texture resolves as `TextureCreate` (`0x449d90`) resolves it. A three-character extension is stripped (`0x449590`), then `.blp` and `.tga` are tried in the reference's order, and a `.tga` the size gate refuses (`0x448450`) falls to the next. This covers M2, WMO, clutter, skybox and baked NPC textures. A `.tga` gets its full chain from `_mipN.tga` files, then box-filtered (`0x4491f0`); one that fails to decode is opaque white (`0x44a2eb`). Found in Adrinalin4ik/manila.
- **#1680:** a BLP keeps its 1x1 level (`0x5a4ac0`), and ground layers take 9 levels.
- Bake, hair and extra-skin textures clamp on both axes (`0x4795ed`, `0x4782a5`, `0x4781dd`).
Tests run the loader and the readers over in-memory sources: the candidate order, the gate's fall-through, the mip chains and the white fallback.
Fixes#610Fixes#1680🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Fixes#2833Fixes#2834
The bindings, layout and macro readers reran at the frame's end off the sections a download stored. 1.12.1 runs each inside the settle its digest or download handler makes (`0x5afaca`, `0x5afd19`): the bindings `0x4b6240`, the layout `0x490380`, the macros `0x4f0600`.
**Changed**
- Each reader runs at its section's settle, ahead of the next packet.
- The interface load registers the readers after VARIABLES_LOADED (`0x4900c2`-`0x4900d1`), so a `/reload` reads the bindings and signals UPDATE_BINDINGS there. File scope sees the fresh, empty binding table.
- A 0.2 macro file's button moves wait for the bar's arrival (`0x5deae2`). The file is rewritten only after them.
- A save queued before the set choice is dropped (`0x4b6506`).
**Tests:** a download between two packets for each reader, the `/reload` order, the macro moves with the bar before and after the settle, a save at file scope. Each fails on main.
Part of #2217.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The character doll's feed pushed the worn slots, weapon enchants and combat stats into the VM once a frame. A listener for an equip packet read the frame's last item, where 1.12.1 reads the item list at the call.
**Changed**
- Worn and bank-bag slots read live (`GetItemInSlot` `0x6228a0`, at `0x4c8cd3`), each slot built only when asked.
- `GetWeaponEnchantInfo` reads live (`0x4c97ca`), with six nils and no player (`0x4c9965`).
- Item countdowns read as of the listener's packet.
- The feed goes, along with its gate, the name cache's counter, and an ammo-hit `UNIT_INVENTORY_CHANGED` that 1.12.1 never raises: a cache hit signals nothing (`0x4c83ce`).
- New guards list the host's reads of handler-written state and its raw store reads.
**Tests:** an equip listener reads its own packet's item, and a line between two writes reads the first charges and enchant time. Each fails on main.
Part of #2168.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #1466 the world entry parks the glue VM. `load_ingame_ui_seated` unparks it and then returns early when a capture runs without the UI. That left the glue VM in the world, so a world capture such as a vista painted the login screen over the world.
**Changed:** in that branch, a glue VM's CVars fold and a boot VM is installed, the VM a reload already holds there. A reload is unchanged.
**Test:** `a_capture_without_the_ui_enters_the_world_on_a_boot_vm` enters the world under `CaptureMode` with the glue VM parked and checks the VM is in-game. It fails without the change (`Glue`).
Fixes#2819.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`cvar_app()` built a client from the window and capture variables without `ENV_LOCK`, while the capture, account-data and state-folder tests set them under it. Under `cargo test`, which runs a crate's tests on threads of one process, `a_locale_default_reaches_its_knob_at_load` saw another test's `WOW_WIN` and failed.
**Changed:** `cvar_app()` now holds `ENV_LOCK` for the client's life, with every variable the build and the load read cleared and a foreground run forced. Tests that set variables of their own build under the same hold.
**Tests:**
- On 16 threads, the cvars, capture, local-state and account-data tests together failed 20 of 21 runs before the change and 0 of 20 after.
- A new test sets `WOW_WIN` on another thread while a client builds. It fails on the old helper.
Fixes#2816.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bar's last per-frame feed is gone, along with four mismatches with 1.12.1 its audit found.
**Reference**
- A macro or item slot's texture runs the attack and ranged face tests on its spell first (`0x4e6ab2`, `0x4e6acb`, `0x4e5a50`).
- A `/reload` keeps the bar in the engine, so the new interface reads it from its first file.
- SMSG_ACTION_BUTTONS only stages its words (`0x5e6680`), and the arrival loads them, skipping unknown spells (`0x5deaf5`).
- The usable walk asks with the bar's callback, behind its player and count gates (`0x4e5b30`, `0x4e5080`, `0x4e50ab`).
**Changed**
- Each of those, and a worldport's book no longer reruns the rank pass over loaded words.
- `feed_action_state` is deleted, and its tests read live.
**Tests:** an Attack macro's weapon icon, the bar during a reload's load, an unknown spell's empty slot, the walk's ask, the worldport's book.
Fixes#2810. Fixes#2811. Fixes#2812. Fixes#2813. Part of #2168.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A follow ended at frame time, in `steer_follow`, and AUTOFOLLOW_END fired there, out of order with the packets that ended it.
**Reference:** `StopAutoMove` (`0x60fb60`) ends a follow inside the packet that ends it: our death, the attack-state swing, the taxi flag, a mover switch or release, a near teleport, a knockback, feign death, a stun, a monster move on us and far sight. It signals AUTOFOLLOW_END (`0x60fb84`), then releases forward and autorun. A near teleport and a knockback also close the loot window (`0x60e990`).
**Changed**
- One stop runs at each of those handlers, with END on the notice lane and the autorun clear behind its listeners.
- The loot window closes on a near teleport and a knockback.
- Each unit holds its client-controlled bit (`0x5fa636`).
- `feed_follow` is gone.
**Tests:** each end fires END between its neighbouring packets' events, and death's END comes before PLAYER_DEAD.
Part of #2168 and #2217.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
World entry was spread over the frame's systems, so `PLAYER_ENTERING_WORLD` fired before every lane item of its frame, and its cascade's resets landed out of order. In 1.12.1 the arrival `0x5dea50` runs inside the create (`PostInit 0x5debe0`) and calls the cascade `0x4908c0` in order.
- The arrival is one item on an `arrival` lane, stamped at the create. It runs its legs in order: the book replay, then each form's UPDATE_SHAPESHIFT_FORMS against the stored list, the slot pass, the cascade with its resets and corpse step, the death state and `LOGOUT_CANCEL`. A /reload pushes the cascade alone.
- System chat is held behind the display latch `0x8435fc` until the cascade releases it (`0x490974`). The leave sweep sets it, and the name and text are composed at the packet.
- The party catch-up fires after PEW.
Each change has a test that fails without it. Part of #2168 and #2217.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The world click was resolved in the look session, frames after the mouse command that made it. In 1.12.1, `CameraOrSelectOrMoveStart` and `TurnOrActionStart` arm it inside their call: `ArmClick 0x514810` picks under the press (`PickAtCursor 0x481190`), and the last primary's release dispatches it (`0x514b70` → `0x481f60`). The VM now keeps the command bits, and the click applies at the record whose binding made it.
Changes:
- The world frame's right-press hook `0x492c20` now runs whole, ahead of the binding (`0x483c62`, `0x483c93`): it cancels targeting, clears a pending wrap, and ends repair mode. It does not run on a press `0x492b50` eats.
- The loading cover, a bindingless right release, and a reload's cascade (`0x49093c` → `0x5144c0`) free the bits.
- The release counts its own frame's travel.
Tests cover each. The click's legs still run after the walk, not inside the call: part of #1988.
Fixes#2729
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bag and item reads now answer as of the listener's own packet, not the frame's end, and the logout reads what 1.12.1 has there.
- The bag getters read the player's bags, the bank and the keyring live. Item and container writes go into the view log, so a listener sees its packet's slots, stacks and bags.
- The loot close's two unlocks fire at its packet (`0x48f2bd`, `0x4c1d36`). A wrong-bag refusal names the bag its packet found (`0x5e3a1e` → `0x5ede00`).
- At logout the object manager goes before PLAYER_LOGOUT fires (`0x46771e`, `0x490c2a`). The inventory getters then answer nothing with no active player (`0x4944a0` → `0x4c8c56`). `UnitName("player")` keeps the character's name.
- The item tooltip feed follows record changes by cursor. The suffix template is copied after the frame's Lua.
Each change has a test that fails without it. flip_readiness falls to 92 rows.
Fixes#2725
Refs #2168
Refs #2217🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three fixes, each with a test that fails without it.
- ShowHelm and ShowCloak send their toggle only while the live PLAYER_FLAGS bit says the opposite (`0x5ee6c4`, `0x5ee783`), and ShowingHelm/ShowingCloak read that bit (`0x48dd8b`). On the native display the call re-dresses the model at once. A display rebuild, a head or back visible-item change, or an item record landing dresses by the flag again (`0x604990`, `0x5e2541`, `0x5ed880`). The mannequin clones what the model shows (`0x476cb0`).
- Account-data readers register through `0x5afe50`. Each section is read when it settles, and its saves are skipped while it is pending. Bindings read set 1 and set 2 apart (`0x4b6240`, `0x4b64b0`), and macros merge by id (`0x4f0600`).
- The login burst is held until the entry UI has loaded, Pong excepted (`0x537b56`), as 1.12.1 loads before it logs in (`0x46c272`).
Fixes#2602Fixes#2448Fixes#2715🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The next piece of the event-core move (#2168, #2217). The update handler's field callbacks now run inside the packet's dispatch, as the reference's notify pass does.
- A new end-of-update handler runs each changed field's callbacks in the order the pass wrote them, inside the dispatch:
- the aura, stack and loyalty lines;
- the death;
- the form release and service closes;
- the pet rename.
- The form release comes right after its own block's signals (`0x5de010`, then `0x5de07f`), so a window closes between UPDATE_BONUS_ACTIONBAR and the next packet's events.
- The stack line reads the live descriptor at notify (`0x62b822`).
- An XP award hides a death line only when it arrived in an earlier packet (`0x62c232`).
Placed-queue readers outside the dispatch go from 5 to 0. A two-packet test fails on main's order.
Part of #2168
Part of #2217🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lua numerals and the sscanf ports used Rust's correctly rounded parse. 1.12.1's C runtime rounds its own way: `__RoundMan` rounds up only when the round bit and some bit two past it are set (`0x40fb43`), so ties truncate and `6e23` reads one ulp low. It flushes results below 2^-1022 to +0 (`0x741ed3`) and keeps 24 digits (`0x41157f`). benilla-re holds these at proof two.
One port of that conversion now lives in benilla-lua (`strtod.rs`, `ld12.rs`). It serves `str2d` and `%f`, which rounds once to float (`0x40feef`) and consumes a digitless exponent marker (`0x7451a5`). `%d` wraps in 32 bits (`0x7456c9`). The time zone is read once per process (`0x414568`), and two client faults are written as Deviations.
A table of 3487 cases generated from benilla-re's transcription, and the exe's fixed cases, pin it.
Fixes#2647Fixes#2648Fixes#2649Fixes#2650Fixes#2651Fixes#2652🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `target` and `npc` tokens read guids pushed into the VM at frame end. A target whose object the server released was torn down by a frame-end system, after later packets' listeners. In 1.12.1 the tokens read the target and npc globals at the call (`0x515a86`, `0x515c00`). The release (`CGUnit_C::Release 0x5fbb60`) ends a target in place (`0x5fbbe5`), except a group member's, whose guid stays (`0x5fbbcd`).
Now both tokens read live through the call host, and the npc is the raw interaction guid. For a game object, `UnitName("npc")` names it (`0x5f8100`) and `UnitExists` is nil. The teardown runs inside the release on a new lane. A reconnect clears the old selection.
Tests: a live target and npc, a game-object quest giver's name, the release firing `PLAYER_TARGET_CHANGED` between two packets' events, and a reconnect. All fail on main.
Part of #2217🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Key bindings, macros, the window layout and the chat setup stayed on the machine and never synced with the server. 1.12.1 keeps eight sections both on disk and on the server, in step by digest. At each world entry the server's digests (`SMSG_ACCOUNT_DATA_MD5`) meet each section's own and the cached one (`0x5afa10`). A section changed here goes up 30 seconds after the interface shutdown that wrote it (`0x5aff40`), and one changed there is asked for and stored.
Now benilla does the same, with the four wire bodies byte-exact. Two recorded deviations keep data the reference would lose: no truncated upload (`0x5afb42`), and no write on a bad download (`0x5afcbc`). `SaveBindings` now writes the set it stored, not the live table at drain.
Tests: wire goldens, the section check and transfer, and the timers. A live round trip against the local server came back byte-equal.
Fixes#1337Fixes#2368🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1.12.1 dispatches each window input record whole and in arrival order (`DrainWindowInput 0x423920` → `IEvtQueueDispatch 0x4245b0`). The record's handler sets the modifiers first (`0x423d06`), and a button record carries its own cursor (`0x42dd00`). Each record runs CSimpleTop's handlers down to WorldFrame's bindings (`0x483c40`, `0x483b70`). benilla read each frame's button and key state, so a key and a click in one frame ran in a fixed order, and a press landed wherever the cursor ended the frame.
Now the UI pass walks ordered records from the window's event stream, each with its own modifiers and cursor. The world mouse and the button binding agree on who owns each press.
Tests cover key-then-click order, a click where the cursor was, and a Shift chord in one frame. The probes now type and click through the window's events, and live runs typed chat and the login account.
Part of #1988🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The WMO portal audit's camera-pin variables (`WOW_PIN_EYE`, `WOW_PIN_MAP`, `WOW_PIN_SITE` and nine more) shared the `WOW_PIN_` prefix with the unattended login's `WOW_PIN` and `WOW_PIN_TOTP`. The map listed an instrument's aim next to the login secret.
The audit's variables are now `WOW_WMO_PIN_*`, in the audit's two files. The login variables keep their documented names. Nothing else set the old ones.
Test: the crate's lints and the audit build; the instrument tests are run by hand, as before.
Fixes#2400🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The frame applied its packets before its input. An `OnClick` reading a unit saw what this frame's packets wrote, and a world click on a unit despawned in the same frame missed it.
1.12.1's tick runs every window input record (`0x423920` at `0x420d23`) before the packets (`0x420d55`). The mouse turn (`0x514400`), right-click interact and cast (`0x514b70` → `0x481f60`) and the press handoff run inside the record. Frame-time work, like the controller's movement and the leash (`0x4818c0`), comes after the packets.
The UI input pass, its bindings, the world mouse and the look sampling, and the world clicks now run ahead of the net drain. Then a new resume point, the feeds, the `OnUpdate` tick and the controller's step run after it.
Tests: a click reads the unit before its frame's packet, a click selects a unit its frame's packet removes, and the pointer edge.
Fixes#2372🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The opening movie never played. `StartMovie` returned nil, so the stock `MovieFrame` hid at once.
1.12.1's movie frame (`0x78ce40`) reads the AVI, decodes, and streams the MP3. Its clock is the audio's play time, or the wall clock without audio. It shows clock × rate frames, decoding skipped ones unseen (`0x78cf30`). It reads `.sbt` captions (`0x78cb00`), and draws 2 or 6 tiles with the picture 24 or 6 rows up (`0x81d400`). On first boot the `movie` CVar flips to "0" and opens the movie screen (`0x402b3b`).
benilla now does all of that on the decoder crate, along with `StopMovie`, `EnableSubtitles`, `OnMovieFinished` (including on teardown), the captions' show and hide handlers, and screenshots waiting while a movie plays. Unattended runs skip the movie.
Tests cover each of these. Live, the logo and the intro played through to login.
Fixes#968🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
benilla could not decode 1.12.1's intro movies, the loose AVIs in `Interface/Cinematics`.
1.12.1 reads the AVI's `hdrl` and `idx1` (`0x78c360`). Index entries whose id's third letter is `d` are video, and `w` entries are audio. It hands the video to a DivX decoder (`0x78c7a0`), which refuses widths other than 1024 and 800.
This adds `benilla-movie`: that AVI reader, plus an MPEG-4 Part 2 decoder written from the published standard. It covers I and P frames, half-pel motion, AC/DC prediction, H.263 quantization and an IEEE 1180 IDCT. It decodes frames one at a time to YUV or RGBA, and returns the MP3 audio bytes. It has no codec dependency.
Tests: the tables, IDCT accuracy, the AVI reader and garbage input. Against the install, all four movies decode every frame. Compared with ffmpeg, the worst frame is 55 dB luma PSNR, and the intro decodes at about 24× realtime.
Part of #968🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unattended login (`WOW_USER` plus `WOW_PASS`) could not answer a PIN-protected account's keypad, so it waited forever.
Two variables now answer it. They go through the same path as the keypad's `PINEntered`, with the same prompt check and the same state report:
- `WOW_PIN`: the digits, as typed.
- `WOW_PIN_TOTP`: a base32 secret. Its RFC 6238 code (HMAC-SHA1, 30 s step, 6 digits) is the PIN, the scheme vmangos verifies (`AuthSocket.cpp:1274`). The code is derived at each prompt, re-dials included.
Setting both is refused, and neither value is ever logged. The variables are documented under "Running it unattended".
Tests: the RFC 6238 vectors, a leading-zero code, each variable answering the prompt, both set refused, and fresh codes on a mocked clock. A live fixed-PIN login against vmangos passed.
Fixes#2371🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Item, object, quest and text records are kept between sessions, as 1.12.1 keeps its template caches.
- **Which stores persist:** each store's constructor sets a persist byte at `+0x39`. The creature, game object, item name, item, NPC text, quest, page text and item text stores are saved. The name, guild, pet-name and petition stores are session-only and cleared at world entry (`0x555740`).
- **The file:** one per store per realm under `benilla-config/cache/`, in the reference's layout, header-checked (build, locale, record size), with a hit sending no query (`0x5586ca`). The records are the server's answer bytes, decoded on load by the live decoder.
- **The item-name store** answers `CMSG_ITEM_NAME_QUERY` (`0x559fe0`), and item-set member names read it.
- **Cleanup:** the old creature-only file is removed on load, and three comments that called the name caches persistent are corrected.
Tests cover every store across a session, header mismatches, eviction and the live packet path.
Fixes#85🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ships and zeppelins follow 1.12.1's clock, seating and crossing.
- **The clock:** only a ship's create sets it, anchored at the packet's arrival (`0x5f7080`). The worldport reseek is gone, and a re-create keeps the anchor (`0x466350`).
- **Worldports:** an off-map ship stays frozen in view (`0x5f5274`), and a worldport keeps only the ridden ship (`0x401bc0`).
- **Crossings:** a crossing draws its route (`0x5f54c2` → `0x407de0`).
- **Seating:** a login seats us in the deck's frame, on a game object only (`0x630a80`). A rider stays aboard while inside the deck's volume (`0x630ba0`), and a gone transport ends the seat (`0x5f4fb0`).
- **Party pet portraits:** an out-of-view party pet's portrait probes the display cache (`0x525cb0`).
Each change has a test that failed before it.
Part of #1220Fixes#1218Fixes#1219Fixes#1222Fixes#645🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The console commands and CVar behaviours 1.12.1 has, and Lua's local time.
- `date` and `time` use the local zone, `time{}` by `_mktime`'s sum (`0x740811`, `0x740836`); screenshots and the logon challenge's bias follow.
- In fullscreen `GetCVar("gxResolution")` answers the screen's own size; the windowed size is what is saved.
- The twelve world commands (`0x66f6c0`), with `showDetailDoodads` gating the clutter and `waterRipples` the wakes.
- `quit`, `ver`, `setmap`, `showplayer` (`0x6400e0`, `0x4049e0`) and `SkySunGlare` (`0x6d1dc0`).
- `ffx`, `ffxGlow`, `ffxDeath` and `ffxRectangle` print enabled or disabled (`0x6cde30`).
- `widescreen` 0 gives the four 4:3 modes and a 4:3 portrait aspect.
Tests pin each address. Built on macOS; Linux and Windows compiled.
Fixes#2231Fixes#2232Fixes#1628Fixes#1641Fixes#1667Fixes#2098Fixes#1687Fixes#1665
Part of #1577🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Item, item-push and loot events now fire in packet order, on the stamped drain of #2225 (Part of #2168).
- Each item watcher's callback fires whole at its packet, in its handler's order (`0x5d94e1`-`0x5d9519`, `0x5d85ce`-`0x5d8606`, `0x5d8798`-`0x5d87c1`).
- One record answer's callbacks run in the order they were asked (`0x5d8321`).
- The loot window's steps and the item push share one lane: a waiting copier ends at its record's answer (`0x4c2afc`), and a waiting push shows there (`0x491ee0`).
- Two responses in one frame give each window its own LOOT_OPENED and LOOT_CLOSED.
- The owed close of #2230 still settles before the window opens.
`BAG_UPDATE` and `ITEM_LOCK_CHANGED` remain in the guard, with their reason. Goldens pin each interleaving against another packet's update. Built on macOS.
Part of #2168🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In 1.12.1 each packet's handler runs whole, and its events fire inside it in arrival order (`0x5384d0`, `0x537aa0`). benilla fired each producer's queue in system order instead. This lays the foundation:
- One stamp clock orders every queued event.
- A single drain fires them by stamp, with nested items placed straight after their cause.
- Chat lines and field notices are its first two lanes.
- A line waiting on a player name, creature record or item fires at its answer's place (`0x49ccc0`, `0x6294b0`).
- A notice's own chat line fires `CHAT_MSG_SYSTEM` in place, as `DisplayError` does (`0x496720` → `0x49a870`).
A guard test lists every handler-filled queue not yet on a lane, so the rest of #2168 can follow. A depth cap on nesting is a written `Deviation:` until #2202 finishes. Goldens pin the order. Built on macOS.
Part of #2168Fixes#2173🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mailbox now follows 1.12.1's inbox and send code, and the mail verbs act inside their call (Part of #2202).
- Expired mail stays listed.
- Mail from an ignored player is dropped on type 0 only.
- Delete and return use the reference's tests, and the postage sum matches.
- Reply follows `0x4aefa5`.
- `GetInboxText` reads the mail's template id, and `CMSG_MAIL_CREATE_TEXT_ITEM` now carries it, pinned byte for byte.
- A row with no paper takes the first usable one, in `0x4ada90`'s order.
- The send slot refuses as `0x4adf38` does.
- The profanity filter masks subjects and bodies.
- The header icons and non-player sender names come from the bytes.
- Keeping the wire id where the reference crashes on a null paper is a written `Deviation:`.
Tests pin each address. Built on macOS.
Fixes#1241Fixes#1235Fixes#247Fixes#240Fixes#55Fixes#140🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Auction lists went wrong:
- They kept the server's order until a header click, and re-sorted every frame.
- Time Left, High Bidder, Lvl and Seller sorted on the wrong keys, and names matched case.
- Time left froze at arrival, and expired rows stayed.
- The sell slot took items 1.12.1 refuses.
The reference:
- World entry seeds eight-key sort stacks (`0x4cc236`). A page sorts once, when its last item record lands (`0x4cc9e8`).
- The comparator `0x4cd940` and the CRT `qsort` (`0x73f727`) order the rows, ties included.
- Time left counts down from the deadline, and rows 10 s past it leave the lists (`0x4cdfe0`).
- The sell slot refuses in `0x4ce362`'s order.
benilla now has one CRT `qsort`, which the frame batches, macros, guild roster and addon list share. Each fact is tested against its address.
Fixes#260Fixes#428Fixes#253Fixes#254Fixes#255Fixes#256Fixes#1539Fixes#258Fixes#1524Fixes#2191🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The unit getters answered for the player alone:
- Stat functions gave zeros past player and pet.
- `GetGuildInfo` failed on chained tokens.
- A group member out of view had no race, class or sex.
- `UnitRangedDamage` ignored the weapon's school.
- `UnitIsUnit` resolved equal strings first.
- `UNIT_INVENTORY_CHANGED` fired only for the player.
- Drunk thresholds printed nothing and left `UnitLevel` alone.
- `CanInspect` answered nil.
The reference:
- `UnitIsUnit` compares the strings first.
- The getters read the named unit's fields and the name cache.
- The visible-item watch (`0x51bc3c`) signals for every player holding a token.
- The drunk watcher (`0x5e2a90`) prints and lowers the level.
What changed: each getter reads the unit its token names. The item watches, the ammo callback (`0x4c84f0`) and the inspect view signal where the reference does.
Each fact is tested against its address.
Fixes#1544Fixes#718Fixes#1239Fixes#1232Fixes#2005Fixes#169Fixes#1408Fixes#1157Fixes#1409🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In 1.12.1 a verb's effect applies inside the call, whether from a click, `OnUpdate` or an event handler. Here, calls from `OnUpdate` and handlers queued until the next lend, so `TargetByName(name)` then `UnitName("target")` read the old target.
- Every system that runs Lua lends the call host around its body, in one ordered VM pass; the cross-entry queue is gone.
- Seams that changed units through `Commands` queue unit edits, flushed ahead of their readers, so sync points stay at 54 (main: 53).
- Late drains apply before the paint; a CVar or follow ask lands the same frame.
- The actionable ambiguity ceiling falls from 3079 to 1750.
Tests: an `OnUpdate` cast and a handler's cast arm inside the call, three same-frame drains, and a census that each schedule flushes after every edit user.
Lua run inside an engine turn still waits for the turn's end.
Part of #1993🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four TradeSkill and Craft answers differed from 1.12.1:
- Recipes of one colour listed the lowest item level first.
- `SelectTradeSkill` on a header kept the old selection.
- The sub-class folds ignored a non-header row.
- `GetCraftButtonToken` always answered `CREATE`.
Reference: the recipe comparator returns +1 when the first row's ItemLevel is smaller, so the higher level sorts first (`0x4fd4e6`). `0x4fc9b0` stores 0 for a header or an out-of-range index. The folds raise unless `0x4fd650` finds a header, and below 1 they fold every group (`0x5000c0`, `0x500140`). `0x4f6c90` answers the word at the open craft type in the table at `0x84d0f4`: USE, TRAIN, DISGUISE, ENSCRIBE.
Changed: each follows these, and tests pin each.
Fixes#155Fixes#1260Fixes#1261Fixes#876🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seven console commands were not registered: `port`, `worldport`, `setrawpos`, `whois`, `togglehelm`, `togglecloak` and `cameraClip`. Three CVar rows also left the reference: `SoundBufferSize` shipped 100, `WorldDetail` was marked as a 1.12 CVar, and `gamma` shipped `1.000000`.
Reference:
- `0x4049e0` registers the movement and toggle commands, `port` and `setrawpos` splitting with the comma (`0x82ef90`) and `worldport` without it (`0x82ecc8`).
- `whois` sends its name (`0x5adee0`).
- `cameraClip` flips or sets its word (`0x50ddc0`).
- `0x457520` registers `SoundBufferSize` 50 above 1500 MHz, which current x86 parts read, and `0x402d70` registers `gamma` as `1.0`.
Changed: each command is registered with its category and its sends, which have byte-exact goldens. `SoundBufferSize` and the mix-ahead ring run at 50, `WorldDetail` is benilla's own row, and `gamma` registers `1.0`. Tests pin each.
Fixes#1639Fixes#1663Fixes#1749Fixes#1270Fixes#1279Fixes#1281🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unit, spell and aura tooltips draw what 1.12.1's builders draw:
- Unit: ?? for a hostile player ten up, the drunk level, the owner, Beast Lore, a group member's Offline or zone line, Resurrectable, contested PvP, Skinnable by skill, GlobalStrings wording.
- Spell: rank on pet and stance bars, cast speed, per-school cost terms, skill-based level terms, and the totem, use-all and reputation lines.
- Aura: stacks multiply, the caster's level scales, raidFilter applies, and the player's slots are read.
Tests pin each line.
Fixes#220Fixes#221Fixes#588Fixes#590Fixes#591Fixes#594Fixes#599Fixes#605Fixes#662Fixes#663Fixes#664Fixes#1184Fixes#1185Fixes#1541Fixes#1542Fixes#1553Fixes#1554Fixes#1558Fixes#1560Fixes#1561Fixes#1566Fixes#1567Fixes#1570Fixes#1573Fixes#1574Fixes#1579Fixes#273Fixes#682🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test gates ran `cargo test`, which runs the workspace's test binaries one after another. On a warm tree the workspace's tests take 103 s that way, and 59 s under cargo-nextest, which runs every binary's tests in one pool; all 10,411 pass both ways.
`check.sh` and `gates.sh` now run tests through `scripts/unit-tests.sh`: cargo-nextest where it is installed, else `cargo test` as before. nextest runs no doctests, so `scripts/doc-tests.sh` runs those of the library crates in scope. Each gate keeps its environment (`BENILLA_REQUIRE_DATA`, `WOW_DATA=`, the skip log), and a failing run's summary now catches nextest's `FAIL` lines. CI is unchanged.
Test: this land's own gates run under it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frame sizes and positions skipped 1.12.1's screen-unit law. The client keeps layout units: a setter stores `S(v) = RN24(K1·RN24(RN24(v)/1024A))` and a getter answers `RN24(1024A·l)/K1`, from the cells `0x41ad10` sets from the aspect (benilla-re `systems/screencoord.md`, two readings).
- The layout resolves in layout units with the reference's four float roundings (`0x767414`, `0x76741f`, `0x767428`, `0x767a30`); the pixel seam converts.
- Size, edge, centre and point getters and setters, the screen-size and cursor globals (glue and game), scroll ranges and scroll events follow the law; OnSizeChanged passes the layout size (`0x76b660`).
- Scroll bars hide on `floor(range) == 0`, as the stock template does.
- The minimap and world-map arrows re-seat when the cells change.
At 4:3, `GetScreenHeight()` is 767.9999427795418. Nothing moves on screen beyond 2e-4 px. A byte audit re-derived the law and the resolver.
Fixes#1918Fixes#1321🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Colours and alphas were kept as floats where 1.12.1 packs them into bytes, and several frame verbs answered bad input unlike the client.
1.12.1's colour setters clamp, round half up into a byte, and read back times `0x81c6b4`; a frame's alpha is a truncated byte (`0x774f32`) whose walk stops at an unchanged byte (`0x76a690`); region and Font `SetAlpha` wrap. `SetScale`, the draw-layer verbs, the scroll range (`0x786e30`) and the binding verbs gate their arguments as the bytes do.
Now one packed `Rgba8` serves every colour, frame alpha is a byte, and each verb raises the reference's usage line. Read-backs shift by at most 1/255; nothing visible changes.
Fixes#1523Fixes#1536Fixes#1868Fixes#1867Fixes#1247Fixes#1248Fixes#1546Fixes#1557Fixes#1562Fixes#1189Fixes#1509🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>