Commit Graph

86 Commits

Author SHA1 Message Date
Sam Sung ed4ca02a88 A building's pool draws only when its room is visited; type from row one (#2937)
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 #119
Fixes #65
Fixes #2492

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 07:12:21 +07:00
Sam Sung 39e671290e Model textures resolve as TextureCreate does, and BLPs keep their 1x1 (#2904)
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 #610
Fixes #1680

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-31 19:48:17 +07:00
Sam Sung ef2f445f96 Account-data readers run inside their section's settle (#2839)
Fixes #2833
Fixes #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>
2026-10-30 15:48:18 +07:00
Sam Sung 6bc2e31b4b The worn slots, enchants and countdowns read live; the doll's feed goes (#2828)
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>
2026-10-30 09:21:35 +07:00
Sam Sung 3ae988ab17 A capture without the UI enters the world on a boot VM (#2823)
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>
2026-10-30 02:36:09 +07:00
Sam Sung 593a4cce0f cvar_app tests hold the env lock with the window variables cleared (#2821)
`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>
2026-10-30 02:03:07 +07:00
Sam Sung 7e66ef52a0 The bar's state feed goes; macro faces, reload seed and staged buttons (#2817)
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>
2026-10-29 23:26:36 +07:00
Sam Sung 65c83b1883 A follow ends at the packet that ends it, with AUTOFOLLOW_END there (#2803)
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>
2026-10-29 17:39:03 +07:00
Sam Sung 12ec14d6b9 World entry fires as one item at the create, in the cascade's order (#2762)
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>
2026-10-28 16:44:55 +07:00
Sam Sung fcc83ee96b The mouse commands arm and release the world click at their record (#2732)
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>
2026-10-28 04:30:42 +07:00
Sam Sung 09c276fe97 Bag and item reads answer as of their packet; logout reads no player (#2728)
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>
2026-10-28 01:52:03 +07:00
Sam Sung 1343fbbd2c Helm and cloak toggles, account-data reads and the login burst's hold (#2718)
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 #2602
Fixes #2448
Fixes #2715

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:33:25 +07:00
Sam Sung f40fc60c7c Field callbacks run inside the dispatch, in the notify pass's order (#2709)
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>
2026-10-27 16:48:54 +07:00
Sam Sung 6aef15f7e7 Numerals and sscanf convert as 1.12.1's C runtime does (#2675)
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 #2647 Fixes #2648 Fixes #2649 Fixes #2650 Fixes #2651 Fixes #2652

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 08:13:32 +07:00
Sam Sung 721c408d66 Target and npc tokens read live; a released target ends at its release (#2455)
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>
2026-10-26 09:18:40 +07:00
Sam Sung fa947ea2e4 Account data syncs with the server, as 1.12.1 does (#2447)
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 #1337
Fixes #2368

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 07:50:09 +07:00
Sam Sung 0d1cef5207 Window input dispatches record by record, in arrival order (#2429)
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>
2026-10-26 01:56:52 +07:00
Sam Sung 12f4ba34b0 The WMO audit's pin variables no longer share the login PIN's prefix (#2404)
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>
2026-10-25 18:19:25 +07:00
Sam Sung d3b5df3f48 A frame's input runs before its packets, as 1.12.1's does (#2402)
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>
2026-10-25 17:46:31 +07:00
Sam Sung 1befd0404d The opening movie plays on first boot and from Cinematics (#2398)
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>
2026-10-25 15:49:14 +07:00
Sam Sung b4f1eb723c Decode 1.12.1's intro movies: an AVI reader and an MPEG-4 decoder (#2391)
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>
2026-10-25 11:17:04 +07:00
Sam Sung 19af95006b Unattended logins answer a PIN from WOW_PIN or WOW_PIN_TOTP (#2386)
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>
2026-10-25 09:46:06 +07:00
Sam Sung 359eff416a Template caches persist between sessions, as 1.12.1's do (#2352)
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>
2026-10-24 20:34:38 +07:00
Sam Sung bc59b5d17a Transports follow 1.12.1's clock, seating and crossing (#2348)
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 #1220

Fixes #1218
Fixes #1219
Fixes #1222
Fixes #645

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 18:48:05 +07:00
Sam Sung b63b2bb1a0 Console commands and CVars follow 1.12.1; Lua's date and time are local (#2257)
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 #2231
Fixes #2232
Fixes #1628
Fixes #1641
Fixes #1667
Fixes #2098
Fixes #1687
Fixes #1665
Part of #1577

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 23:28:59 +07:00
Sam Sung 7d1c4f5e9c Item, item-push and loot events fire in packet order (#2236)
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>
2026-10-22 18:45:54 +07:00
Sam Sung c18315fdcb UI events fire in packet order from one stamped drain (#2225)
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 #2168
Fixes #2173

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 09:38:05 +07:00
Sam Sung 0689a07471 The mailbox reads, lists and sends mail as 1.12.1 does (#2219)
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 #1241
Fixes #1235
Fixes #247
Fixes #240
Fixes #55
Fixes #140

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 06:21:55 +07:00
Sam Sung 591358af1b Auction lists sort, count down and refuse sells as 1.12.1's do (#2193)
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 #260
Fixes #428
Fixes #253
Fixes #254
Fixes #255
Fixes #256
Fixes #1539
Fixes #258
Fixes #1524
Fixes #2191

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 19:18:37 +07:00
Sam Sung 3387c8225d Unit getters read the unit they name; item events fire for every player (#2176)
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 #1544
Fixes #718
Fixes #1239
Fixes #1232
Fixes #2005
Fixes #169
Fixes #1408
Fixes #1157
Fixes #1409

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 13:41:28 +07:00
Sam Sung fcb42ab40a Script calls apply inside their verb, in one ordered VM pass (#2143)
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>
2026-10-21 03:43:48 +07:00
Sam Sung 74c9430e40 TradeSkill order, selection, folds and the craft button follow 1.12.1 (#2130)
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 #155
Fixes #1260
Fixes #1261
Fixes #876

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 01:44:25 +07:00
Sam Sung 33067c38ac Console commands and CVar defaults follow 1.12.1's registrations (#2104)
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 #1639
Fixes #1663
Fixes #1749
Fixes #1270
Fixes #1279
Fixes #1281

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:51:38 +07:00
Sam Sung c678c196dd Unit, spell and aura tooltips draw 1.12.1's lines (#2072)
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 #220
Fixes #221
Fixes #588
Fixes #590
Fixes #591
Fixes #594
Fixes #599
Fixes #605
Fixes #662
Fixes #663
Fixes #664
Fixes #1184
Fixes #1185
Fixes #1541
Fixes #1542
Fixes #1553
Fixes #1554
Fixes #1558
Fixes #1560
Fixes #1561
Fixes #1566
Fixes #1567
Fixes #1570
Fixes #1573
Fixes #1574
Fixes #1579
Fixes #273
Fixes #682

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:31:34 +07:00
Sam Sung 927f0117fb Test gates run under cargo-nextest, doctests beside them (#2036)
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>
2026-10-20 10:27:18 +07:00
Sam Sung 837a6e1336 Frame sizes and positions follow 1.12.1's screen-unit law (#1946)
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 #1918
Fixes #1321

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:02:31 +07:00
Sam Sung 7940c2bdfe Colours and alphas are bytes, and frame verbs gate as 1.12.1's do (#1907)
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 #1523
Fixes #1536
Fixes #1868
Fixes #1867
Fixes #1247
Fixes #1248
Fixes #1546
Fixes #1557
Fixes #1562
Fixes #1189
Fixes #1509

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:59:24 +07:00
Sam Sung 615198d3bb The Lua library answers as 1.12.1's does (#1862)
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 #1257
Fixes #1258
Fixes #1510
Fixes #1511
Fixes #1503
Fixes #1504

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:35:43 +07:00
Sam Sung 0117b595c8 The loading-screen tip is drawn by the font engine, as in 1.12.1 (#1814)
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>
2026-10-17 23:53:09 +07:00
Sam Sung d8a55240e0 Lua's collector paces and sizes its stack as the client's 5.0 (#1791)
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 #1783
Fixes #1784

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-10-17 17:20:11 +07:00
Sam Sung 167ae46802 The script memory limit warns, then quits, as 1.12.1 does (#1782)
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>
2026-10-17 14:50:32 +07:00
Sam Sung cd46d2ee05 benilla-lua's lexer and compiler behave as the client's Lua 5.0 (#1781)
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 #1755
Fixes #1756
Fixes #1757
Fixes #1758
Fixes #1759
Fixes #1767
Fixes #1434
Fixes #1445
Fixes #1777
Fixes #1778

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:38:19 +07:00
Sam Sung a64b6b8a44 A kit's Stand animation plays as in 1.12, with its two driver rules (#1739)
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>
2026-10-17 09:20:08 +07:00
Sam Sung 65a9016d36 A server's billing minutes give the 1.12 play-time warning (#1718)
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>
2026-10-17 04:08:29 +07:00
Sam Sung 6f65310783 A boot with no readable WoW install is fatal, with an error dialog (#1630)
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>
2026-10-16 23:14:14 +07:00
Sam Sung c0fa656181 The native glue screens are gone; instruments press the stock screens (#1466)
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>
2026-10-16 18:56:52 +07:00
Sam Sung d404e4e9e4 The stock glue screens are the default; native ones are dev-only (#1462)
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 #849
Fixes #850
Fixes #834
Fixes #812
Fixes #360
Fixes #463
Fixes #79
Fixes #977

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:03:33 +07:00
Sam Sung a9c2672b3f A fifth loot roll waits for a frame and opens when one closes (#1458)
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>
2026-10-16 16:28:24 +07:00
Sam Sung 247ae3e631 Nothing of the native glue screens runs under the stock screens (#1456)
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>
2026-10-16 15:43:55 +07:00
Sam Sung 9ff8f6aff4 The login engine moves into glue_session; native login shows its notices (#1451)
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>
2026-10-16 14:04:59 +07:00