Texture paint answered and drew unlike 1.12.1: `SetTexture("")` and non-string names answered nil, a missing file cleared desaturation, `SetTexCoord()` and an unknown `SetBlendMode` passed quietly, a failing bar path still installed a bar, and DISABLE, ALPHAKEY and MOD drew as BLEND.
1.12.1's load (`0x770200`) writes the shader slot only for a file that loads (`0x7702c0`); the binding answers 1 for a null name (`0x79bdae`). The submit draws a coloured DISABLE or ALPHAKEY texture as BLEND (`0x76fb82`); ALPHAKEY alpha-tests at 224/255 and MOD multiplies the destination (DESTCOLOR, ZERO, `0x80a1fc`).
Now one texture load serves every setter, the usage lines raise, the shader draws each mode with a MOD pipeline of its own, and a new test validates the UI shader with naga. The stock UI draws as before.
Fixes#1425Fixes#1864Fixes#1863Fixes#1521Fixes#1556Fixes#1550Fixes#1559Fixes#1555🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It was a nil stub, so an assistant got no raid target icons in the unit
popup (UnitPopup.lua offers RAID_TARGET_ICON to the leader or to
IsRaidOfficer) and the raid frame's add-member button stayed off for
them. 0x4bb910 walks the raid roster (0xb712a8, count [0xb713e0]) for the
row whose guid is the local player's and pushes 1 when that row's rank
([row+0xc], GetRaidRosterInfo's return 2) is above 0, so for an
assistant and for the leader; outside a raid, without a player or
without our row it answers nil.
Verified: cargo test -p benilla-ui --lib
(is_raid_officer_reads_our_own_roster_rank), clippy -D warnings.
Built on macOS only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fonts were copied at load: a later change to a font object never reached the fonts and strings built from it, and a Button's state fonts were names overlaid at draw, so a label measured in one font and drew in another.
In 1.12.1 a font follows one parent (`0x770c60`) under a five-bit inherit mask; each setter clears its own bit and pushes only what changed to its followers (`0x784180`, `0x770910`). A Button holds three embedded fonts and relinks its label on each real state change, after the script (`0x779810`).
Now fonts link live, case-blind by name (`0x64b3f0`), `Font:SetFontObject` links and refuses loops, a Button's fonts are real linked fonts with their getters, and `<NormalColor>`-style elements load (a `Deviation:` for two). Tests cover the push, the masks and each button state; the stock UI draws as before.
Fixes#1204Fixes#1517Fixes#1403Fixes#1855Fixes#1856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CVar set path differed from 1.12.1's: values compared case-sensitively, text on a numeric CVar was refused, a read-only CVar still took console and host writes, `ConsoleExec` ran commands a frame late, and console `set` kept the whole line.
1.12.1's InternalSet (`0x63e0b0`) returns for a read-only record (`0x63e0ba`), compares with `SStrCmpI` (`0x63e0ec`) and stores any text, with the record's int and float by `SStrToInt`/`SStrToFloat` (`0x63e127`). `ConsoleExec` runs the line inside the call (`0x515f43`), and `set` tokenizes with `SStrTokenize` (`0x64ae50`).
Now there is one set path: the registered default strings, one `SStrToInt`, read-only refusing every writer, CVar commands run inside `ConsoleExec`, and the host copies are gone. Saved values match 1.12.1: a slider at its default writes "1" over "1.0", and hand-edited text reads 0.
Fixes#1478Fixes#1853Fixes#1278Fixes#1319Fixes#1274Fixes#1640Fixes#1851Fixes#1475🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four Button behaviours differed from 1.12.1: `Click()` on a disabled check box did not toggle it, a checked texture made from a path drew with BLEND, `Click(name)` passed its argument raw, and a pressed button's label never shifted.
1.12.1's CheckButton click (`0x785550`) toggles before the base click's enabled gate (`0x779552`); the create leg of the checked setters stores ADD (`0x7852f4`); `Click` maps its argument case-blind to a button name or `UNKNOWN` (`0x7826c0`); and the label relink (`0x779810`) offsets the text by `PushedTextOffset` while PUSHED.
Now each follows the reference: a disabled box also ignores a press, `Set/GetPushedTextOffset` and `<PushedTextOffset>` are built, and held stock buttons shift their label about 1.3 units down-right. `Set*FontObject` raises on an unknown font name (`0x87c5fc`). Each has a test.
Fixes#1175Fixes#1176Fixes#1179Fixes#1516Fixes#1854🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The XML loader merged a template chain into one element, losing each attribute's level: `setAllPoints` lost to a template's `<Anchors>`, an empty `text=` wiped a template's text, the template's `<TexCoords>` won.
1.12.1 never merges: each loader re-enters on `inherits` (`0x76fe36`, `0x770f9e`, `0x76985c`), then reads its own node's attributes and first children (`0x6f2c50`).
Now regions load per level in `0x770f40`'s order; frames read geometry, `frameLevel`, StatusBar attributes and button text per level. A failed texture fills green (`0x878d24`). Level tests cover each rule; the stock load is unchanged but for the AddOns window's crops.
Fixes#1534Fixes#1548Fixes#1547Fixes#1866Fixes#1423Fixes#1858Fixes#1424Fixes#1472Fixes#1861Fixes#1859Fixes#1171Fixes#1860Fixes#1817🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
A held paperdoll turn re-arms a shuffle once per cycle. When it fell inside Stand's half-blend, the pane reused the one track it held for that clip, so the fading shuffle jumped back about 3 frames every 0.53 s, and the fade's end restarted the primary.
1.12.1's arm (`0x7121a0`) copies the outgoing primary into the blend slot with its own clock (`0x7125d6`). A refused arm (`0x7125c9`/`0x7125d4`) keeps that slot, the latched shortcut (`0x712549`) leaves it alone, and the slot ends on its own (`0x7147b1`), weighted by the smoothstep (`0x714880`).
Now each sequence has a second graph node, and the pane's blend slot runs on whichever node the primary is not on. A refused arm leaves it untouched, and the window's end stops only the secondary. Headless player tests cover the held turn, the fade end and the shortcut.
Fixes#1845🤖 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>
The red "GM mode is ON" line did not show on a session's first login, only on a relog.
In 1.12.1 the interface and the world load before `CMSG_PLAYER_LOGIN` goes out (`0x46c272`), and the loading screen drops when the player is created (`0x5debbd`). vmangos sends that create ahead of the login notices (`Handlers/CharacterHandler.cpp:675`), so the line is added after the reveal and gets its full time on screen. benilla's cover stays up through its own warm-up, about 6 s on a cold login, and the line faded underneath it. A relog is warm, so the line survived.
Server notice lines (`SMSG_NOTIFICATION`, area-trigger messages) now wait while the cover is up and fire on the frame it drops.
Test: a notice under the cover fires once, at the reveal. A live cold login shows the line the frame after the cover clears.
Fixes#1252🤖 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>
The ESC menu's Support rung was greyed with nothing behind it. The 1.15 era menu that the layer's ladder follows opens the help window from Support. In 1.12 that window is the stock `HelpFrame`, opened by the "?" micro button's `ToggleHelpFrame()` (`MainMenuBarMicroButtons.xml:217`).
Support is now live: it plays the menu click, closes the menu as the Macros rung does, and toggles the help window. Edit Mode stays greyed.
Test: `support_closes_the_menu_and_toggles_the_help_window` clicks the rung and checks the toggle and the closed menu; the live-entries test now lists Support.
Fixes#1800🤖 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>
`leave_drops_the_world_session_and_keeps_the_logon` failed now and then at its first `assert_eq!(world.open(), 1)`.
The fake world server counted a session as open only after it had sent `SMSG_AUTH_RESPONSE`. A client that read the reply and reported `WorldAuthenticated` could therefore reach the assertion before the count rose.
The count now rises where the link is held, before any reply goes out, so `open()` never trails what the client has seen. Only the test fixture changes.
🤖 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>
Dev builds get three tools, all behind the `dev` feature:
- **Hot reload:** an edited addon `.lua` is swapped into the closures already running, keeping state and event registrations. Edited XML handlers and benilla's own `assets/ui` layer reload too.
- **An addon profiler:** Lua time and heap per addon.
- **A Lua inspector:** tables, frame scripts and upvalues, with scalars editable in place.
benilla-mlua's `unsafe raw_vm` becomes a safe lent `with_vm`.
Ours on the branch:
- the top level is compared off the compile path, in a scratch heap;
- watched files are polled in slices, and each addon's XML is walked once per session;
- no lend while a callback of the state runs;
- profiler owners are cached per source epoch;
- key-ups reach the UI while an egui field has focus.
The player build carries none of it. Tests: the full gate chain on the merged tree.
🤖 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>
Three tests failed on a Windows checkout:
- one split a source path on `/`;
- one split its own `include_str!` on `"\n}\n"`, which a CRLF checkout lacks;
- one captured a traceback's addon folder with a greedy `.*`.
Each now normalises the separator or line end, or bounds the capture. A sweep found the same path forms in four more source-walking tests (`fire_scan`, `world_api_wall`, `reference_strings` and `front_end`), now normalised too.
`.gitattributes` pins Rust sources to LF and leaves `third_party/kira`'s CRLF files as they are.
Built and tested on macOS. The Windows cases are reasoned from the path and line-ending forms, not run.
Fixes#798🤖 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>
benilla-lua now matches the client's Lua 5.0 wherever an addon can see the difference:
- Number keys hash as `hashnum` does (`0x6fa260`), so `pairs` walks them in the client's order.
- Calls nest at most 4096 deep (`0x6f61c0`).
- `^` calls the global `__pow` (`0x6f9b5e`).
- The parser's limits and messages are the client's (`0x6ff440`); the client has no assignment limit.
- A numeric for's variable is its counter (`0x6fe380`, `0x6f9262`).
- A generic for uses `forlist`'s layout (`0x6fe540`): the first name is the control, and closures share the variables.
- Only calls name a callee (`0x6fc499`), and a stray token reports `<eof> expected` (`0x871cf4`).
Each fix has a test that fails without it. Two audits re-derived every address from the bytes. The bench is faster or level.
Fixes#1369Fixes#1370Fixes#1371Fixes#1372Fixes#1374Fixes#1377Fixes#1752Fixes#1753Fixes#1754🤖 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>
The README asked for a server with Warden off, which stopped being true when a Warden request began to be read past (#1717): benilla now plays on a server that sends Warden without enforcing it, and one that enforces it disconnects the session after its own timeout. The requirement now says that.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>