GameTooltip re-sized itself on every resolve from its lines, at 10/40/260 units, counting a taller right text. So `GetWidth` after `Show` read stale, a script's `SetHeight` after `AddLine` was overwritten, and descriptions wrapped wide.
1.12.1 sizes only in the show (`0x530aad`), `AppendText` (`0x530615`) and the cache answers, never in `AddLine` (`0x531630`). `GameTooltipSize` (`0x530640`) takes the widest unwrapped line plus a 38.4 gap, wraps the rest at the running widest (capped at 230.4), sums shown left heights with 2.0 gaps, pads 10.24 a side, then re-anchors without clearing.
Now the sizer is that function, called from those sites, and the engine no longer anchors line cells; the stock templates declare them. A measure memo keeps a steady hover from measuring again. Tests pin each loop, constant and caller.
Found in Thorsten42/manila@50452d3fa5.
Fixes#1206Fixes#1182Fixes#1540🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UI ticked on raw `Time<Real>`, which under pipelined rendering mis-splits shown frames (26.4 and 8.0 ms for two 16.7 ms frames), so a held paperdoll arrow turned in uneven steps while the bones, on paced `Time<Virtual>`, did not.
1.12.1 hands `OnUpdate` the loop's own frame time, unclamped (`0x420f70` → `0x885608` → `0x76b2c0`).
Now the pacer publishes one step per frame (`FrameStep`) that `Time<Virtual>` and the UI both read: `OnUpdate`'s elapsed, `GetTime` (a `Deviation:`, since 1.12.1 reads its clock live at `0x515ea0`), the cooldown anchor, aura expiries and `GetFramerate`. A stall reaches the UI whole. The pacer keeps every millisecond, drains its carry, and no longer jumps after a pause or splits a long hitch. Tests replay a stream measured on this machine.
Fixes#1846🤖 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 flat saved-variables file ran as Lua, and an add-on's saved file was skipped when its toc declared no scope.
1.12.1 reads `SavedVariables.lua` as lines (`0x4913b0`): a name, `=`, then a quoted string (raw, empty is nil) or a number read by `SStrToFloat` (`0x64aaa0`) and rounded to f32; anything else is skipped and no Lua runs. An add-on's account and character files run whenever they exist (`0x51f4a2`), and at save a scope that declares nothing removes its file (`0x51f859`). `RegisterForSave` is refused outside Blizzard code (`0x4884e0`).
Now the reader parses lines, unescaping the writer's escapes (a `Deviation:`). An old-format file with lines the reader drops is copied once to `.bak`. An undeclared scope's file read cleanly this session is renamed to `.bak` instead of deleted (a `Deviation:`), and `RegisterForSave` raises 1.12.1's two errors.
Fixes#1166Fixes#1167🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `RegisterAllEvents` frame sat on a list of its own: it heard every event after all named listeners, even untabled names, and `UnregisterEvent` could not take one event off it.
In 1.12.1 there is no all-events flag. `RegisterAllEvents` (`0x774c20` → `0x7023e0`) appends the frame to each tabled event's list where it is absent, as one `RegisterEvent` per event would, so dispatch (`0x703e50`) runs in registration order and `UnregisterEvent` (`0x702280`) removes that one event. The walk sets `event` once and restores it after (`0x703ea4`, `0x703f04`); `this` and the args stay per listener (`0x704f10`).
Now registration goes through one per-event list and the second pass is gone. Tests cover the interleaving, the per-event unregister, a re-register going last, a mid-dispatch registration, a frame registered both ways, and the `event` global.
Fixes#1165🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Model:SetCamera(n)` on a PlayerModel did nothing: the character pane always drew through its fixed pane rig.
1.12.1's PlayerModel registers no SetCamera of its own (`0x506230`), so the Model verb (`0x76e0e0` → `0x76cec0`) installs camera n of the copy; one past the model's count drops to the orthographic leg (`0x76ce80`). Every new copy gets camera 1 back at the ready hook (`0x505890` → `0x76cec0(1)` at `0x50589c`), and is armed with Stand (`0x505a2f`).
Now the pane draws through the installed camera, live or frozen as the reference does, with the ortho leg's particles at its unit (`0x7b2a50`). A camera or arm held for a pending copy dies when a new build is asked. The API-wall scanner now follows `#[path]` test modules. Tests cover each leg, the reset and both held-state sequences.
Fixes#1835Fixes#1847🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Widening the window on a glue screen stretched the login, select and create scenes past 16:9.
1.12.1 lays the glue out at `gxResolution`'s aspect (`0x41ad10` at `0x46a828`), and `GlueParent_OnLoad` (GlueParent.lua:74-84) boxes GlueParent to a centred 16:9 rect on a wider screen. The size callback `0x46abe0` only re-scales and re-lays GlueParent, and its window keeps gxResolution's shape while dragged. benilla's glue root is the window, but GlueParent's box was set only at build.
Now a resize of the glue's screen re-runs the chain's own `GlueParent_OnLoad` from its declared `setAllPoints`, so the box follows the window without a rebuild, which would reset the create screen and the typed login (a `Deviation:` line). A new test resizes a built glue through 21:9, 2:1, 16:10 and 4:3, and checks the box, the scenes and that the screen stands.
Fixes#1844🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A player could walk or swim past the edge of the world and keep going. 1.12.1 stops every movement step whose swept box touches terrain that isn't loaded. The query `0x6aa8b0` fails on a null tile or chunk (`0x6aadfa`, `0x6aae1f`), and the substep loop then runs no resolver and reports the time skipped (`0x634154` → `0x615ae0`).
benilla now builds the reference's walk, swim and fall boxes and holds the body when its box touches a tile that isn't resident. Position, fall clock and FALLING are kept, and `CMSG_MOVE_TIME_SKIPPED` goes out for the held time. A tile whose terrain file fails to load counts as resident, so it is never a wall.
Tests: a walk stops at the edge and walks back, and a jump hangs. Live, a teleport past the edge held in place.
Fixes#1830🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The quest log left out two kinds of objective line. A quest with an end text, such as an escort or event quest, showed no objective and couldn't be watched. A quest that requires a standing with a faction lacked its reputation line.
1.12.1 counts both (`0x4e0000`). `GetQuestLogLeaderBoard` (`0x4e0110`) answers line 1 with the raw end text, typed "event" and finished by the slot's complete bit (`0x4e02a2`), and the other lines move down one. It ends with a "reputation" line: `QUEST_FACTION_NEEDED` with the faction's name and the current and required standing labels (gendered), finished at the required standing (`0x4e0787`). Both are now built in that order.
Tests: an event-only quest, an event plus kill quest, and a reputation-gated quest's lines, text and finished state.
Fixes#1205Fixes#1837Fixes#1834
Co-authored-by: Thorsten42 <3400178+Thorsten42@users.noreply.github.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two glyph-cell differences from 1.12.1.
Rows: the client keeps at most the font's pixel em of a glyph's rows. `0x5d1120` stores `min(em, rows)` (`0x5d1195`-`0x5d11a2`), and every bake reads that many rows from the top. benilla packed the full raster, so unhinted glyphs one row taller than the em (FRIZQT `ý þ` at 14 px, Arial Narrow `Ò Ó Ù Ü` at 14 px) drew a faint extra row. Cells are now clipped to their first `ppem` rows before the ring is built.
Alpha: the client's glyph texels are ARGB4444. The plain bake keeps coverage's top 4 bits (`0x5cf298`-`0x5cf2a1`), and the outlined fill's gray is `(cov·0xff) >> 12` (`0x5cf1b8`). benilla packed 8-bit coverage. Coverage cells and outlined fills now pack 4-bit values, so coverage under 16 draws nothing, as in 1.12.1.
Tests: an over-tall `Ò` keeps its top 14 rows, and every packed alpha is 4-bit exact.
Fixes#1827Fixes#1828🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text glyphs were rasterized hinted and seated 1 to 3 px left of 1.12.1's. The client loads every glyph with `FT_LOAD_NO_HINTING` (`0x5d1324`, flags `0x208a`/`0x20c2`). It places each quad at the pen plus `ceil(horiBearingX)` (`0x5cd099` → `0x5cb080`), with the ink 0, 1 or 2 columns into the cell for plain, NORMAL and THICK (`0x5ce481`, `0x5ce48a`). benilla hinted the anti-aliased cells and seated them at `floor(xMin)` less the outline padding.
Anti-aliased cells now come from the face's unhinted 26.6 outline, the one the measure already reads, through `zeno`, swash's own scan converter. Every bake is seated at the rounded-up bearing with its ink column. The drawn ink now meets the measure.
Tests: Arial Narrow `e` at 18 px matches FreeType's unhinted raster byte for byte, and the five bakes seat at the bearing. Fitted to a 1.12.1 capture, a chat line lands on the client's columns.
Fixes#1813Fixes#1822🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moving fast raised the loading screen whenever the body outran terrain streaming. 1.12.1 never shows a screen there. Its tile pass (`0x6985d0`) blocks on each missing inner-window tile (`0x698689` → `0x443bd0`), so the frame hitches.
benilla now does the same. It requests every missing inner-window tile at once, waits for them in the frame, and builds them and their colliders untimed, during play and cinematics. The loading-screen backstop is gone. A read still hung after 10 s is waited on once, then streams on. A second defect fed this: a settled stream re-walked its window only at a tile crossing, so tiles ahead were requested late. It now re-walks on every chunk step.
Tests: outrunning the stream raises no screen, and a hung read costs one capped wait. Live, at speed ×100 across Kalimdor, no screen came up.
Fixes#1028Fixes#1823🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Action-bar hotkey digits sat on black boxes. Their font inherits `monochrome="true"`, and benilla had no monochrome mode, so it baked them anti-aliased with a THICK outline that filled the whole small cell.
1.12.1 rasterizes a monochrome font 1-bit (`0x5d1300`, render mode 1). `0x5ce440` then draws it with a one-pixel black ring, plus a 7/15-alpha second ring under THICK. benilla now carries the flag from Font and FontString XML and from every `SetFont`, `Button:SetFont` included. It rasterizes such glyphs with FreeType's mono rules (drop-out mode 2, span jitter) and bakes the ring.
Also fixed: a FontString's XML `outline="NORMAL"` or `"THICK"` with a font file now outlines (`0x771214`). It had been parsed with the Lua spellings.
Tests: the hotkey numerals match a 1.12.1 capture texel for texel, a THICK mono cell is ringed rather than filled, and the flags round-trip.
Fixes#1811Fixes#1812🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A jump on a descending lift (Undercity, Gnomeregan) played the hard-landing grunt and dust. While airborne, benilla carried the rider with the deck but kept the jump's start height in world coordinates. So the lift's drop during the arc, about 24 yd on Undercity's 29 yd/s cars, counted as a fall, and FALLINGFAR latched early.
1.12.1 measures the fall height (`0x7c60c0`) in the mover's own frame: transport-local while aboard, re-mapped when the mover boards or leaves (`0x7c6620`). `Player::carry_to` now moves the start height with the deck. A remote mover's arc is measured in the deck's frame when it takes off and lands on the same transport.
Tests: a 60 fps jump on a deck dropping 28.95 yd/s lands with no fall height and no FALLINGFAR, for us (23.64 yd before) and through packets for a remote (23.88 yd before).
Fixes#1250🤖 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>
#1808's stand-in search ranked ALSA devices only when the host's name read "ALSA", but cpal names it "Alsa". So the ranking never ran, and the first device ALSA lists won. On the reported machine that was `null`, which discards every sample, so sound stayed silent behind a warning that it was playing.
The check now compares the host's `HostId::Alsa` variant, so the ranking applies: `pipewire`, then `pulse`, then each card's own outputs, and never `null`.
The Linux crosscheck compiles it. The ranking's tests are #1808's.
Fixes#1806🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Linux, sound stayed silent for the whole session when ALSA's `default` would not open. On the reported machine, PipeWire holds the card and `default` is left as the stock `dmix`, which cannot open it.
benilla opened only the host default and gave up when that failed. Now, when the default will not open, the first other output that does stands in: `pipewire`, then `pulse`, then each card's `sysdefault` and `plughw`, with HDMI cards last. It never picks `null` or the rate and mix plugins. The device watch now compares against the default the device was chosen against, so a stand-in is not torn down every poll. A `Deviation:` line records that 1.12.1 runs silent when FMOD's driver 0 fails (`0x7a445f`).
Tests: the ranking over three `aplay -L` lists, including the reported one. The Linux crosscheck is green.
Fixes#1806🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quest completion and timed-quest failure now follow 1.12.1.
- **isComplete runs the client's turn-in test (`0x4df580`).** The complete bit counts only when the template's flags ask for it, and a quest with nothing to check is never complete. Counters, held items, both standing bounds and money are read live. A pass fires the "Quest Completion" tutorial (`0x4df792`).
- **A timed quest that runs out latches failed.** The latch is set by the rebuild, by GetQuestTimers and by `SMSG_QUESTUPDATE_FAILEDTIMER` (`0x4df4f0`, `0x4df550`). GetQuestTimers prints the failure line and then fires QUEST_LOG_UPDATE inside the call (`0x4e1529`, `0x4e1554`).
- **Timers count by the local clock until the first sync,** and a zero-difference sync reads a second more (`0x4def00`).
- **Quest-giver failures wait for the quest record** and skip a latched quest (`0x5dc898`). A plain update failure no longer re-asks the givers.
Each change fails its test without it.
Fixes#1793Fixes#1794🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A ScrollingMessageFrame now fades, keeps its view and spaces its messages as the client's does.
- Only the shown messages fade (`0x7884a5`-`0x788582`). A relayout off the bottom re-arms what it walks, including the first older message, before the fit test (`0x788af0` at `0x7888be`). That covers a scroll, a resize, a font change, a show and UpdateColorByID.
- A scrolled-up view stays on its messages as they arrive. It moves down a line only when a full ring overwrites the topmost shown message (`0x78819e`-`0x7881bb`).
- The gap between messages is the snapped unscaled spacing times the scale (`0x788954`-`0x788975`). Rows within one message snap their scaled gap (`0x5c2143`-`0x5c215c`). Each message must fit whole.
- ScrollToTop and ScrollToBottom do nothing on an empty frame (`0x788690`, `0x7886d0`).
Each change fails its test without it.
Fixes#1796Fixes#1797Fixes#1798🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The quest log getters now read live state at the call, and QUEST_LOG_UPDATE fires where the client fires it. Before, every log quest's reward items were asked up front, and a per-frame snapshot rebuilt the log.
- World entry registers the log watcher and rebuilds (`0x4de430`). The rebuild holds while quest records are pending, and the last landing rebuilds it (`0x4de510`, `0x4de8b0`).
- The getters ask on a miss (`0x4e0461`, `0x4e05e0`, `0x4e0b9e`). One update fires per missed record that lands, a recorded deviation from the client's per-call callbacks.
- An item a listed quest needs fires one update (`0x4df7b0`). The time response and a faction rank change fire one each (`0x4def1b`, `0x4d58f3`).
- Cached names and templates seed each new VM.
On an idle frame with 20 quests: 46.5 µs before, 1.0 µs after. Each change fails its test without it.
Fixes#1209🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A ScrollingMessageFrame now lays out, pages and scrolls as the client's relayout does.
- A message that does not fit whole, past the anchored one, ends the layout (`0x7889b3`-`0x7889c5`). A half-fitting top line is hidden, not clipped, and the shown count that paging reads agrees.
- PageUp and PageDown take one line step per shown message, then one step back, and stop on a refusal (`0x7885b0`, `0x7885e0`). One shown message nets no move.
- ScrollUp is refused once the oldest message shows, and AtTop reads that flag (`0x78861c`, `0x7931b5`). It is nil on an empty frame (`0x787775`).
A character pane's camera takes its aimed pose with its spawn, so a pane is framed on the frame it opens.
Each change fails its test without it.
Fixes#1786Fixes#1787🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
benilla-lua's tables and globals now follow the client's Lua 5.0, not 5.1.
Tables:
- They size, fill and rehash as 5.0's ltable.c does: the hash part doubles (`0x6fab90`), and a log size of 0 is the shared dummy (`0x6fa601`).
- A hash part past 2^24 nodes raises `table overflow`.
- Constructors store every 32 items.
- A weak table's clearing removes the key, so `next` from it raises (`0x6f79d0`).
- A weak-keyed table keeps its string keys (`0x6f79f0`).
Globals and libraries:
- `ipairs` returns itself, and a second argument steps once.
- `setglobal` assigns the top of the stack.
- The state opens in the client's order, with no `_G`.
Each new test fails without its fix.
Fixes#1487Fixes#1789Fixes#1790Fixes#1169🤖 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)
Lua memory now counts as the client's does:
- Every object is charged at its 32-bit Lua 5.0 size: a string is 17 bytes plus its length (`0x6f9da1`), a table header 32 (`0x6fa500`), a node 40, and a closure 36 plus 4 per upvalue (`0x6f9ebf`).
- Prototypes count 5.0's instructions and constants (`0x6fcb2b`).
- The string table and the concat buffer are counted as 5.0 sizes them.
The stock UI reads 10,618 KB, against 13,294 KB before.
The host's old C-host shim overrode benilla-lua's natives. It is gone:
- `collectgarbage` now moves the real collector, taking `__ftol`'s low word (`0x6f4400`);
- `gcinfo` reports that collector;
- `^` is the client's `math_pow` (`0x7fb4bd`);
- `loadstring` returns one value (`0x7032ba`).
An audit re-derived every size from the bytes. Each fix fails its test without it.
Fixes#1779Fixes#1780🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On some Linux setups the game was silent. The device opened, but its data callback ran once and stopped, with no error. The music position never moved.
This is cpal 0.17.1's ALSA bug (RustAudio/cpal#1099, "Data callback called only once on ALSA"), fixed in 0.17.3 by setting playback's `avail_min` to one period. benilla's stream starts only at the start threshold, which is exactly that case. cpal goes to 0.17.3.
alsa 0.11 and bevy_audio's unbuilt cpal 0.15 cannot share a lock (`links = "alsa"`). A lock-only stand-in for alsa 0.9 settles that, documented in `third_party/alsa-0.9`.
The realtime-fallback log line now says what happens on each platform. Following the default device gets its `Deviation:` line: 1.12.1 initialises sound once (`0x7a4330`).
Built on macOS, with Linux and Windows linted.
Fixes#820🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Seven places where benilla chose to differ from 1.12.1 said so without a `Deviation:` line, so `docs/DEVIATIONS.md` missed them. Each now has its line, with the client address and the reason:
- targeting stays armed after a range failure (`0x6e6089`);
- a kit's first shake reads its node before it is filled (`0x620e0e`);
- screenshots are PNG and can report a failure (`0x4426f0`);
- UnitPVPName of a guid-0 token is nil (`0x517343`);
- an aura's visual holds while any slot keeps the spell (`0x5ff2d2`);
- text input is Unicode, not the ANSI code page (`0x42d185`);
- benilla runs no code a server sends: no patch, no survey and no Warden module (`0x474610`, `0x5bad53`).
The login queue now follows the client: a leading blank line (`0x837230`) and a 32-bit estimate that wraps (`0x46aed3`), with tests. The environment-map comment cites the shader the client picks (`0x71de50`).
Fixes#821Fixes#1599🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The table library and `unpack` now run as the client's 5.0 natives (`0x822d40`). benilla's closure layer over them is gone. `unpack` refuses past 2,048 slots (`0x6f2f43`) and reads its table alone. `getn` converts a numeric-string `n` (`0x6f4f80`). `next` says "for" (`0x871aac`).
The bit library answers unsigned except `arshift` and `mod` (`0x7faa6d`). Its folds take every argument, and `math.pow`, `atan2` and `mod` check argument 1 first.
A Rust callback's error now carries the caller's `file:line`, as `luaL_error` does (`0x6f4940`); only the sound verbs raise bare. Error text keeps its bytes, from `assert` to the error handler.
An audit re-derived every commit from the bytes. Each fix fails its test without it.
Fixes#1435Fixes#1448Fixes#1449Fixes#1480Fixes#1482Fixes#1483Fixes#1484Fixes#1485Fixes#1760Fixes#1761Fixes#1762Fixes#1763Fixes#1764Fixes#1765Fixes#1766Fixes#1771🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
benilla-lua's string and number library now answers as the client's:
- `string.byte` returns one byte (`0x7fc210`).
- `gsub` takes only a string or a function (`0x7fd163`), and `%0` raises (`0x7fce20`).
- `lower` and `upper` case-map UTF-8 (`0x7fbeb0`).
- `tonumber` with a base is MSVC's 32-bit `strtoul` (`0x40db09`), on every platform.
- Chunk names cut at a NUL and at `\n` (`0x835144`).
- `string.format` is the client's native one (`0x7fd3d0`): `__ftol` conversions, `%q`'s table, one-digit `n$`, and the client's errors.
- Number text is the MSVC CRT's, ported down to `$I10_OUTPUT`: `1.#INF`, `-1.#IND`, `1e+020`.
- A NaN made from numbers is the x87 indefinite, and the math library returns the runtime's own answers at infinities.
A fuzz against the client's own `sprintf`, run in emulation, matches 1.5M cases. Every fix fails its test without it. `tostring` and `%g` got faster.
Fixes#1367Fixes#1368Fixes#1373Fixes#1375Fixes#1376Fixes#1450Fixes#1481Fixes#1486Fixes#1770🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the client, a QueryCache lookup (`0x55ba30`) either passes a callback, which queries on a miss, or passes none, which only reads (`0x55baac`). A refusal frees the key, so the next callback lookup asks again (`0x556f35`). benilla re-asked every miss on a timer, through `get_or_ask`.
Now every reader says how it reads, as `Ask::Read` or `Ask::Event` at the client's trigger. The trigger is a packet, a window show, an item's create (`0x5d8300`) or a getter the stock UI polls. The timer, its `Deviation:` and the duplicate lookups are gone. Key 0 is never asked (`0x55ba42`). A recipe cooldown no longer fires `TRADE_SKILL_UPDATE` each second; the client's senders are only the list rebuild and the filters (`0x4fd164`).
Each reader family has a test: one ask at its trigger, none between, a refused key asked again next. An audit re-derived the core and 29 classifications from the bytes.
Fixes#1714🤖 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>
The script host now runs on benilla-lua alone. The vendored C Lua 5.1.5 (`third_party/lua-src`) and mlua are removed, about 17k lines. `vm_parity` checks benilla-lua against the answers the C build recorded on Linux and macOS, and `front_end` checks every corpus chunk's compiled shape against the C build's digest. `GRAMMAR.md` records each grammar change from 5.1, by client address.
A handler's `this`, `event` and `arg1`… are bound in one VM call. An event fire saves and restores `event`, while the handler runner saves only `this` (`0x704d50`).
Ours on the branch: VM fixes from review (hook dispatch counts, registers and frames around a collection, protected-call handlers, library errors) and `vm_parity`'s two-VM leftovers removed.
Tests: the full gate chain on the merged tree, `vm_parity` and `front_end` among them. Live: smoke and a stock-glue login. Load time is level with C Lua, and memory is within 7%.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
Cyrillic in a chat bubble drew as boxes: Friz Quadrata, the bubble's font, has no Cyrillic, and benilla drew the face's `.notdef` glyph for each missing character. In 1.12.1 the glyph load asks the face's Unicode charmap first (`FT_Get_Char_Index`, `0x7ce960` at `0x5d1159`) and returns with nothing loaded on index 0 (`0x5d1162`); the line builder then draws no quad and adds no advance (`0x5cced6` → `0x5cd280`), and kerning spans the gap (`0x5cd27a`). The charmap is the first record FreeType calls Unicode (`0x5d015d`, `0x81eb70`).
Each face now carries that charmap, and a code point it lacks is an empty cell on every surface. The chat frame keeps Cyrillic, which Arial Narrow carries.
Tests: a missing code point in Friz takes no room and draws nothing; the charmap pick skips (3,10) and takes the first Unicode record. Each fails without the change.
Fixes#832🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fading opaque M2 batch blended by `texel alpha × fade`, so where its texture carries alpha (helms, shoulders, the Wind Rider's skin, the Blackfathom pots) it thinned or vanished while fading. In 1.12.1 a blend-0 batch takes combine preset 0 (`0x811f8c`), whose alpha op is SELECTARG2 on the current (`0x80a274`; ALPHAARG is never set), and the diffuse alpha is the element alpha (`0x70c73a`, c28.w): the fade alone, blended SRC_ALPHA/INV_SRC_ALPHA (`0x811fe0`).
Each batch now carries its stage-0 combine (a 0x101 model's from its array), and the fade twin applies that preset's alpha op to the fade. Unfaded batches, cutouts and other blends are unchanged.
Tests: the shader's alpha op for presets 0 and 1, the batch's stage-0 combine, its uniform. The shader test fails with the old line.
Fixes#1732🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An M2 batch with two textures drew only its first stage, so Cupid's bow, the two Stratholme maces and the obsidian mining node lost their reflection. In 1.12.1 the element binds one stage per texture (`0x70b740`-`0x70b823`); a version 0x101 model takes each stage's combine from its header array (`0x70b7c2`, `[md+0x148][k]`), else from the blend table `0x811f8c`; a coordinate lookup above 2 generates the view-space reflection (`0x70b8bd`, the same map as stage 0's env path). These five are the only 0x101 models, each stage 1 preset 1, modulate on the default Direct3D device (`0x80a25c`).
benilla-m2 reads the combiners; the second stage is parsed, loaded and multiplied over the lit first stage before fog, with all six presets on D3D's ops.
Tests: the combine source, a synthetic two-texture parse, the obsidian node's stages off the install, the shader's coordinate and presets. Each fails without the change.
Fixes#1722🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`BenillaScrollBarTemplate`'s own OnValueChanged was a faux-list handler that both instances replaced with the real scroll, so a new bar that forgot the override wrote `offset`/`updateFunc` on its parent instead of scrolling. The reference's `UIPanelScrollBarTemplate` carries `this:GetParent():SetVerticalScroll(arg1)` itself (`UIPanelTemplates.xml:156-158`).
The template now calls `BenillaScrollBar_OnValueChanged(this, arg1)`; the two instance overrides, the dead faux handler and its header text are gone.
Test: a bare bar from the template scrolls its ScrollFrame parent with no `offset` or `updateFunc` write; it fails with the old template.
Fixes#359🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lazy Peons stood upright under their sleep aura. In 1.12.1 an aura's state kit (kind 2) sets flag 0x1000 (`0x60eecd`) on an effect at the head of the unit's `+0xb4` list (`0x620360`), and every play reaches `0x5fde80` (`0x5feab5`): a still unit (`[+0x9e8] & 0x20000f` clear) plays the first flagged kit's animation above 0 instead of what was asked, on body and mount alike (`0x5fdeee`, `0x5fdf08`). Spell 17743's kit names 71, which OrcMale plays as Sleep.
benilla now keeps those pins per unit, newest slot at the head (`0x604e7b`), and the driver plays the head pin for a still unit's poses, one-shots, jumps and landings, rider and mount both; the clip plays out its window after the aura goes. A still stunned unit now plays Stun, as in 1.12.1.
Tests cover each path; each fails without its part.
Fixes#837🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A server that sent BillingTimeRemaining got no warning: benilla dropped the auth response's billing minutes and plan flags. In 1.12.1 the handler (`0x5b41b0`, gate `0x5b41f9`) keeps the group from AUTH_OK or the first AUTH_WAIT_QUEUE; `InitializeGame` arms a deadline at each world entry for 1-4320 minutes on a non-recurring plan (`0x48f636`-`0x48f679`); the game UI's per-frame poller (`0x495679`-`0x495826`) fires `BILLING_NAG_DIALOG` once at 30 minutes or fewer and prints `BILLING_NAG_WARNING` each 5 minutes, then each minute from 5 (`IGR_BILLING_NAG_DIALOG` at 600 for an internet-cafe plan).
The protocol keeps the group across the queue; `ui_billing` transcribes the arm and the poller. `GetBillingTimeRested` was set on the glue VM; it is now seeded into every in-game VM.
Tests: the four vmangos body shapes byte-exact, the queue's group outlasting a bare AUTH_OK, the cadence, the stock popup and chat line, and the arm at entry only.
Fixes#813🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A server that sent `SMSG_WARDEN_DATA` ended the login at character select with a terminal failure. Servers that send it without enforcing it were unplayable, and vmangos serves the roster and the world entry regardless (no handler gates on Warden; `World.cpp:325-338` starts it after AUTH_OK) and only kicks a silent client after `Warden.ClientResponseDelay` (`Warden.cpp:686-691`).
benilla now reads the message past, answers nothing and goes on; the server's policy decides what follows, and a kick is the ordinary lost link. This is a `Deviation:` at the read: the reference hands the message to its loaded module (`0x6ca2f0`, `0x6ca5c0`) and sends its answer (`0x6ca6a0`). `WardenRequired` and the terminal-failure path it alone used are gone.
Tests: a login past a Warden request reaches the world with nothing sent back; a kick after it is a lost link; the handshake reads one past. Each fails with the old refusal.
Fixes#854🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a window narrower than 4:3 the loading screen ran off both sides: its area was always 133.3vh × 100vh. In 1.12.1 the layer `0x406a60` fits the picture through the viewport (`0x58af60`) from `a = 0.75 × aspect` (`[0x832a4c]`): width 1/a, centred, above 4:3; full width and height a, centred, below it; the whole window at a == 1, which `widescreen` off gives. The bar draws in that viewport's [0,1]; the tip is placed in window pixels (`0x5c70b0`, sizes from `0x5c2b50`), its baseline raised by the letterbox band (`0x4066c6`).
The area is now that rect, refitted each frame, and the tip is laid out in window terms. The live window's aspect stands in for `gxResolution` (a `Deviation:`, as for portraits).
Tests: the rect at 1280×1024, 1920×1080, 4:3 and `widescreen` off, a resize, and the tip in each case. The rect tests fail with the old fit.
Fixes#355🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A refused query answer (top-bit entry, empty name) was stored as a record, saved to the cache file and never asked again. In 1.12.1 the refusal leg (`0x556e63`, items `0x55bdf7`, objects `0x558927`, names `0x55f6f0`, guilds `0x561390`, quests `0x563150`) runs the callbacks with status 0 and frees the node; the next lookup with a callback (`0x556aa0`) asks again.
Now a refusal frees the record, is never persisted, and redisplays nothing. Unit, object and slot/crest changes look up at once (`0x60b15b`, `0x60463f`, `0x5f7c40`, `0x5ed764`, `0x5e078f`), and a dropped ask is re-sent. Unit-name reads follow `GetUnitName` (`0x609210`): creatures are read from the unit, never asked. Readers that poll every frame wait 30 s after a refusal (a `Deviation:` until they are split). An empty quest title is a refusal.
Tests cover each path; each fails with its change undone.
Fixes#1674Fixes#393🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A dead player or ghost could use any GameObject: the cursor stayed lit and a right-click opened a mailbox or walked to a chest. In 1.12.1 `usable` (`0x5f3130`) tests `IsDeadOrGhost` (`0x605f30`, at `0x5f315f`) ahead of its lock and range arms and refuses with error 0x7e, `ERR_PLAYER_DEAD` (`0x5f33c6`); the cursor asks the same slot (`0x5f3093`, `0x5f58a8`, `0x5f6858`) and grays.
`cursor_mode::go_player_refusal` now holds the player arms, dead and the existing mounted gate (`0x5f31a8`), and both the cursor and the click use it: dead grays the cursor and the click raises `ERR_PLAYER_DEAD` with nothing sent. Mounted now also grays a lock-less object's cursor, as the same slot does in 1.12.1.
Test: dead, ghost, mounted and live players at a chest and a mailbox, checking the cursor, the use and the error.
Fixes#830🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`SPELLCAST_STOP` fired only at `SMSG_SPELL_GO` and only for a timed cast, so instants, channels and item uses never sent it to addons.
In 1.12.1 it fires only from the cast result: the handler's pop `0x6e741a` → `0x6e4940` (`0x6e4a4b`) while the pending record's caster word `0xceac50` is the player's, filled by the attempt (`0x6e4fb1`) and cleared when the event fires (`0x6e4a5a`); and `0x6e74cb` for a non-failure result with nothing in flight. The go handler fires none. vmangos sends START, RESULT, GO, so a timed cast's bar still ends at completion.
The attempt now fills the caster word, the result fires STOP as those two sites do, local cancels fire only while the word holds, and the go-time raise is gone. Tests replay each cast kind in vmangos order; a live run logged STOP for an instant, a timed cast, a channel and an item use.
Fixes#1604🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Print screen on a glue screen (login, realm list, character select or create) raised a Lua error on a nil `Screenshot` and saved nothing: the stock GlueXML calls it from each screen's `OnKeyDown`, and benilla bound it only in the in-game VM.
1.12.1 binds `Screenshot` in the glue table too (`0x46d3d0`). It arms the same capture (`0x442a10`) and answers with the glue's own events, `GLUE_SCREENSHOT_SUCCEEDED` or `GLUE_SCREENSHOT_FAILED` (`0x46c520`, glue events 14 and 15).
The glue VM now installs the same verb on the same capture ask, and the writer answers with whichever VM is up. Tests cover the glue call arming one capture and the glue events answering it, and the in-game names staying in-game; they fail without the binding.
Fixes#1649🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
In the 17 instances built as one WMO (Wailing Caverns, Blackrock Depths, Molten Core, Dire Maul, the Deeprun Tram and others) benilla had no area: entering by portal kept the outside zone, no `ZONE_CHANGED_NEW_AREA` fired, and logging in inside left the zone texts empty.
On a map whose WDT carries MPHD bit 0 (`0x694904`), 1.12.1's area query `0x6703a0` answers the map's `Map.dbc` field 19 for every entity, ahead of any WMO or terrain read; a 0 there fails the query.
`MapCatalog` now reads field 19, the WDT exposes the bit, and `update_current_area` takes that leg first on such a map. A worldport also drops the old map's held area, which otherwise survived the transfer. Tests cover the leg, the flag's map, a zero field and the worldport; a live portal into Wailing Caverns fired `ZONE_CHANGED_NEW_AREA` with "Wailing Caverns".
Fixes#1636🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A hunter shot the server refused (out of range, line of sight) still sent `CMSG_CAST_SPELL` for Auto Shot: `cast_result` chained `modalNextSpell` on any result whose spell was in flight.
In 1.12.1 a failure's report pops the in-flight slot (`0x6e2201` → `0x6e2218` → `0x6e4ad0(0, 0)`), so the handler's compare at `0x6e7408` misses and the chain at `0x6e74aa` is skipped. It still chains when the report leaves before that pop (reason 0x17; 0x56 with no named form; an uncached 0x5c or 0x78 item, `0x6e1eab`) or when the save held the same spell.
`cast_result` now reads the slot pair up front and chains a failure only in those cases. The test that asserted a failed sting chains is flipped, and each remaining case has its own assertion; each fails with its rule undone.
Fixes#1594🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With no WoW install found, or a `Data` folder whose archives will not open, benilla booted into a window of textless buttons, since fonts, art and the stock interface all come from the install.
It now ends before any window exists. A native error dialog (`rfd`: AppKit, Win32, `zenity` on Linux) names every folder searched, or the folder and the archive error, and says where to put the install; then it exits 1. An unattended run prints the same text to stderr and exits without a dialog.
1.12.1 ends at a fatal dialog for a present but corrupt base archive (`0x403b6f`) and only logs a missing one (`0x403b00`); ending at no install at all is recorded as a `Deviation:`.
Tests cover both messages and the failing exit; both cases were run live unattended. Built on macOS, Linux and Windows.
Fixes#836🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Splitting a quest item's stack could flash its objective line ("Wolf Pelt: 4/5"). The toast rode a per-frame diff of the bag count, and a split dips it for a frame: vmangos sends the shrunk source and the new stack's create at once, and the slot naming the new stack later, in the batched update (`Player.cpp:11100`, `10316`–`10327`).
1.12.1 toasts an item objective only from `SMSG_QUESTUPDATE_ADD_ITEM` (`0x5e5ad0`, case 0x19a). It walks the log to the first quest needing the item, and `0x5dd060` shows `ERR_QUEST_ADD_ITEM_SII` with min(held + added, need), then `QUEST_WATCH_UPDATE`. An uncached quest record defers the pair to its arrival (`0x5dd180`).
Item lines now toast from that packet alone; kill and complete toasts are unchanged. Tests replay a split's update sequence (no toast) and a pickup (one toast); both fail without the change.
Fixes#916🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
At character select the IO thread waited only on the app's requests, so a server that dropped the link (vmangos kicks a session idle there after `CharacterScreenMaxIdleTime`) went unnoticed until Enter World started a load.
1.12.1 keeps servicing the connection on that screen, and a drop runs `CGlueMgr::NetDisconnectHandler` (`0x46c540`): `DISCONNECTED_FROM_SERVER` and back to login.
The park now looks at the socket twice a second without blocking. A closed link ends the session as a failed roster ask does; a packet sent unasked is read and handled as `char_enum` handles its strays, in one shared step.
A fake-server test drops the link while the client is parked with no request pending and expects the session lost; it times out without the change.
Fixes#1020🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>