Commit Graph

130 Commits

Author SHA1 Message Date
Sam Sung 31ff8ec00c The XML loader reads each template level, as 1.12.1 does (#1892)
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 #1534
Fixes #1548
Fixes #1547
Fixes #1866
Fixes #1423
Fixes #1858
Fixes #1424
Fixes #1472
Fixes #1861
Fixes #1859
Fixes #1171
Fixes #1860
Fixes #1817

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:35:06 +07:00
Sam Sung 20abc74643 GameTooltip sizes in the show, as 1.12.1 does (#1884)
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 #1206
Fixes #1182
Fixes #1540

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 11:53:25 +07:00
Sam Sung 94fb118517 The UI ticks on the paced frame step the world gets (#1870)
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>
2026-10-18 10:58:30 +07:00
Sam Sung 615198d3bb The Lua library answers as 1.12.1's does (#1862)
Six places where benilla's Lua answered unlike 1.12.1's:

- The lexer took `[=[` long-string levels; 1.12.1's `luaX_lex` tests one byte after `[` (`0x6ff793`) and `--[=[` is a line comment (`0x6ff6c9`).
- `math.random` ran a glibc-style `rand` seeded with 1; now MSVC's (`0x7400e5`, `% 0x7fff` times 1/32767 at `0x7fb310`), clock-seeded as the client is (`0x5d1c8b`), fixed in captures and the addon harness.
- `getfenv`/`setfenv` honour a raw `__fenv` field (`0x702ba0`), and `setfenv` returns nothing.
- An object's text is `type: %08X` in the embedding API too (`0x74301d`).
- `time` converts a date table through MSVC's `_mktime` (`0x74066b`).
- `date` formats with MSVC's C-locale patterns (`0x8323a0`): `%c` is `09/09/01 01:46:40`, `#` drops zeros.

Each has a test that fails without it; the addon corpus parses as before.

Fixes #1257
Fixes #1258
Fixes #1510
Fixes #1511
Fixes #1503
Fixes #1504

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:35:43 +07:00
Sam Sung 81fd090ef2 Saved variables are read as 1.12.1 reads them (#1857)
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 #1166
Fixes #1167

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:24:02 +07:00
Sam Sung 03f192f246 RegisterAllEvents joins every event's list in registration order (#1850)
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>
2026-10-18 09:26:37 +07:00
Sam Sung 30f2955e97 SetCamera on a character pane draws it through the camera it names (#1849)
`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 #1835
Fixes #1847

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:09:02 +07:00
Sam Sung 3e6d9cd1f6 The glue screens box at 16:9 on a window wider than that (#1848)
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>
2026-10-18 08:54:18 +07:00
Sam Sung f199fbd388 The world's edge holds the player, as in 1.12.1 (#1841)
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>
2026-10-18 06:50:27 +07:00
Sam Sung 72c53e1d9c The quest log shows a quest's event and reputation lines (#1840)
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 #1205
Fixes #1837
Fixes #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>
2026-10-18 06:38:04 +07:00
Sam Sung cff056c367 Glyph cells clip to the em and keep 4-bit alpha, as in 1.12.1 (#1829)
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 #1827
Fixes #1828

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 02:20:03 +07:00
Sam Sung f6d8b16219 Text glyphs rasterize unhinted at the client's seat, as in 1.12.1 (#1826)
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 #1813
Fixes #1822

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 01:53:58 +07:00
Sam Sung 8731395bd8 Outrunning terrain streaming stalls the frame, not a loading screen (#1824)
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 #1028
Fixes #1823

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 01:36:39 +07:00
Sam Sung f55bb79b13 Monochrome fonts draw 1-bit with their ring, as in 1.12.1 (#1821)
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 #1811
Fixes #1812

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 00:33:07 +07:00
Sam Sung 560d7ff63e A jump on a moving lift measures its fall in the lift's frame (#1818)
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>
2026-10-18 00:10:37 +07:00
Sam Sung 0117b595c8 The loading-screen tip is drawn by the font engine, as in 1.12.1 (#1814)
The loading screen's "Tip:" could read "Tlp". benilla drew the tip with Bevy's own text raster at a fractional size, which hinted the i's dot onto its stem at some heights, 1080p among them.

1.12.1 draws the tip through its font engine (`0x5c1c30`, from `0x40679c`). The tip now goes through benilla's font engine, the one every FontString uses, at the reference's size, line spacing, bottom-left justification, trimmed line ends and pen step. The shadow takes the body's alpha (`0x5cd65b`), and the shadow offset and the anchor snap to device pixels (`0x5c7010`, `0x5cdf70`). The Bevy text reshape plugin, now unused, is removed.

Tests: the i keeps a gap row at six window heights, the layout matches the engine's for the DBC line, and placement holds at DPI 1 and 2.

Fixes #1089

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 23:53:09 +07:00
Sam Sung 8a6c74036c Sound on Linux skips ALSA's null output when the default fails (#1810)
#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>
2026-10-17 23:41:40 +07:00
Sam Sung 1e28153ae2 Sound plays on another output when ALSA's default will not open (#1808)
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>
2026-10-17 23:02:27 +07:00
Sam Sung b2473e6095 Quest completion and timed-quest failure follow 1.12.1 (#1805)
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 #1793
Fixes #1794

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:01:14 +07:00
Sam Sung 746ad35176 Chat frames fade, hold their view and space messages as 1.12.1's (#1803)
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 #1796
Fixes #1797
Fixes #1798

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:21:03 +07:00
Sam Sung 9eed3d5b7d The quest log reads live and signals as 1.12.1's does (#1801)
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>
2026-10-17 18:52:57 +07:00
Sam Sung 93098a0646 Chat frames lay out, page and scroll as 1.12.1's; panes aim as they open (#1799)
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 #1786
Fixes #1787

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:21:58 +07:00
Sam Sung c97fd03a57 Lua tables, ipairs and globals behave as the client's 5.0 (#1792)
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 #1487
Fixes #1789
Fixes #1790
Fixes #1169

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:49:05 +07:00
Sam Sung d8a55240e0 Lua's collector paces and sizes its stack as the client's 5.0 (#1791)
The collector now paces as the client's Lua 5.0 does.

The threshold:
- The state opens at 4× its count (`0x6f6ec8`), and InitLua then sets 256 KB (`0x7039f2`).
- After a collection it is the count plus the larger of the old headroom and a quarter of the count (`0x6fae00`). The headroom is capped at the free space in the client's six block pools, which a new book models (`0x6fae90`).
- The memory watch collects from a threshold of 0 (`0x703bd6`), and varargs never collect.

The stack and call records start at 45 and 8 slots and grow by 5.0's rule (`0x6f5f60`). They are checked once per collection, during mark, and halved under a quarter's use (`0x6f7949`). A tail call checks the stack where the call is made (`0x6f922c`).

A collection of a 10 MB heap takes about 1 ms; each change fails its test without it.

Fixes #1783
Fixes #1784

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-10-17 17:20:11 +07:00
Sam Sung 80f751ffe5 Lua memory counts as the client's 5.0; the old shim's overrides are gone (#1788)
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 #1779
Fixes #1780

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:01:55 +07:00
Sam Sung d93c231d8e Linux audio plays: cpal 0.17.3 fixes ALSA's stalled callback (#1785)
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>
2026-10-17 15:30:35 +07:00
Sam Sung 167ae46802 The script memory limit warns, then quits, as 1.12.1 does (#1782)
The AddOns screen's script memory setting did nothing.

In 1.12.1, `scriptMemory` (KB, default 49152, `0x402e42`) is one limit for the whole Lua state. It is checked each frame in the world by `0x495590`:
- Over the limit, the client does a full collection and counts again (`0x4955d5`).
- Still over, it fires `MEMORY_EXHAUSTED` with the limit in MB (`0x49563d`), so the stock popup shows, and starts a 15 s deadline.
- Still over at the deadline, it quits (`0x495602`).
- Back under, it fires `MEMORY_RECOVERED` (`0x495656`).
- A limit of 0 or less turns the watch off.

benilla now does the same. Its count still uses 64-bit object sizes (#1779), recorded as a deviation.

Tests: the state machine, plus two full stock-UI logins. A 1 MB limit shows the exact popup and freeing memory hides it. The default shows nothing (the stock UI is 13 MB).

Fixes #1081

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:50:32 +07:00
Sam Sung cd46d2ee05 benilla-lua's lexer and compiler behave as the client's Lua 5.0 (#1781)
benilla-lua's lexer and compiler now behave as the client's 5.0:
- A control byte is "invalid control char" (`0x6ffabb`).
- Every chunk lexes as source, binary ones too (`0x6f6a15`).
- Lines end at `\n` alone (`0x6ffb94`); a dot is never a NUL (`0x6ff98a`).
- A `while` condition over 100 instructions is too complex (`0x6fd03b`), counted as 5.0's code generator counts it.
- Upvalues get no name in errors (`0x6fc5e0`).
- `string.dump` refuses closures with upvalues, and dumps the stack top (`0x6f438a`, `0x6f4373`).

The client installs no Lua hook (`lua_sethook` has no caller), so the load's runaway guard has no client mechanism to follow. It is now an abort no `pcall` can catch, recorded as a deviation.

An audit re-derived every commit from the bytes. Each fix fails its test without it.

Fixes #1755
Fixes #1756
Fixes #1757
Fixes #1758
Fixes #1759
Fixes #1767
Fixes #1434
Fixes #1445
Fixes #1777
Fixes #1778

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:38:19 +07:00
Sam Sung 1764e9b063 Every chosen difference from 1.12.1 carries its Deviation: line (#1775)
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 #821
Fixes #1599

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:41:00 +07:00
Sam Sung d5e43dd3e1 Lua's table, bit and error paths behave as the client's 5.0 (#1774)
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 #1435
Fixes #1448
Fixes #1449
Fixes #1480
Fixes #1482
Fixes #1483
Fixes #1484
Fixes #1485
Fixes #1760
Fixes #1761
Fixes #1762
Fixes #1763
Fixes #1764
Fixes #1765
Fixes #1766
Fixes #1771

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:26:34 +07:00
Sam Sung 826194e2ac benilla-lua's strings and numbers read and print as the client's (#1773)
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 #1367
Fixes #1368
Fixes #1373
Fixes #1375
Fixes #1376
Fixes #1450
Fixes #1481
Fixes #1486
Fixes #1770

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:51:19 +07:00
Sam Sung b3bfae15d8 Cache readers ask at the client's trigger or only read; no timed re-ask (#1772)
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>
2026-10-17 12:35:19 +07:00
Sam Sung a64b6b8a44 A kit's Stand animation plays as in 1.12, with its two driver rules (#1739)
A spell visual kit naming animation 0 (Stand) was read as no animation, so 41 kits lost their play. In 1.12.1 the field is skipped only when negative (`0x60f366 jl`): a state kit naming 0 recomputes the base (`0x60f383`-`0x60f399`) and never pins (`0x5fdeb5 jle`); a cast or impact kit plays it through `0x5fe2f0` (`0x60f3c5`); the holds and the missile release need above 0 (`0x612a0a`, `0x6e8295`).

Playing it needed two driver rules benilla lacked: PlayTrack drops a request for the id a track already asks for (`0x5fdcab`), so a recompute no longer restarts an unchanged gait; and a Stand request over a swing, ready stance or cast is dropped (`0x5feb00`-`0x5feb56`).

Tests: the parse, the recompute without a pin, the holds and release, the dedup, each demotion arm, and Stand ending at its window. Each fails without its part.

Fixes #1719

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:20:08 +07:00
Mathih13 8100fe9820 Run the script host on benilla-lua; remove the vendored C Lua and mlua (#1255)
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>
2026-10-17 08:51:37 +07:00
Sam Sung f5992d0041 A character its font lacks draws nothing, not a box (#1736)
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>
2026-10-17 07:11:20 +07:00
Sam Sung 251364dbbf A fading opaque M2 batch fades evenly, not by its texture's alpha (#1735)
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>
2026-10-17 07:00:10 +07:00
Sam Sung 5f96e65d94 Two-texture M2 batches draw their env-mapped second stage (#1734)
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>
2026-10-17 06:30:23 +07:00
Sam Sung 142ed718d3 The Benilla scroll bar template scrolls its parent itself (#1730)
`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>
2026-10-17 05:40:07 +07:00
Sam Sung 1c6d4a717e A still unit plays its aura state kit's animation, as Lazy Peons sleep (#1721)
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>
2026-10-17 04:49:20 +07:00
Sam Sung 65a9016d36 A server's billing minutes give the 1.12 play-time warning (#1718)
A server that sent BillingTimeRemaining got no warning: benilla dropped the auth response's billing minutes and plan flags. In 1.12.1 the handler (`0x5b41b0`, gate `0x5b41f9`) keeps the group from AUTH_OK or the first AUTH_WAIT_QUEUE; `InitializeGame` arms a deadline at each world entry for 1-4320 minutes on a non-recurring plan (`0x48f636`-`0x48f679`); the game UI's per-frame poller (`0x495679`-`0x495826`) fires `BILLING_NAG_DIALOG` once at 30 minutes or fewer and prints `BILLING_NAG_WARNING` each 5 minutes, then each minute from 5 (`IGR_BILLING_NAG_DIALOG` at 600 for an internet-cafe plan).

The protocol keeps the group across the queue; `ui_billing` transcribes the arm and the poller. `GetBillingTimeRested` was set on the glue VM; it is now seeded into every in-game VM.

Tests: the four vmangos body shapes byte-exact, the queue's group outlasting a bare AUTH_OK, the cadence, the stock popup and chat line, and the arm at entry only.

Fixes #813

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 04:08:29 +07:00
Sam Sung 52cc825e60 A server's Warden request is read past and the login goes on (#1717)
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>
2026-10-17 03:33:17 +07:00
Sam Sung 112601c2a9 The loading screen fits its picture inside the window both ways (#1716)
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>
2026-10-17 03:22:51 +07:00
Sam Sung a8ad08a391 A refused cache answer is freed, never saved, and asked again (#1713)
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 #1674
Fixes #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>
2026-10-17 02:31:20 +07:00
Sam Sung 71321043ff A dead player or ghost cannot use an object; its cursor grays (#1709)
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>
2026-10-17 01:57:41 +07:00
Sam Sung ec97169554 SPELLCAST_STOP fires from the cast result, for every kind of cast (#1704)
`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>
2026-10-17 01:26:52 +07:00
Sam Sung fd642f79c0 Print screen on the glue screens takes a screenshot (#1673)
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>
2026-10-17 00:34:52 +07:00
Sam Sung cda3a91f7b Instances built as one WMO take their zone from Map.dbc (#1670)
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>
2026-10-17 00:14:47 +07:00
Sam Sung 87f3b24f68 A refused shot no longer chains Auto Shot (#1638)
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>
2026-10-16 23:44:52 +07:00
Sam Sung 6f65310783 A boot with no readable WoW install is fatal, with an error dialog (#1630)
With no WoW install found, or a `Data` folder whose archives will not open, benilla booted into a window of textless buttons, since fonts, art and the stock interface all come from the install.

It now ends before any window exists. A native error dialog (`rfd`: AppKit, Win32, `zenity` on Linux) names every folder searched, or the folder and the archive error, and says where to put the install; then it exits 1. An unattended run prints the same text to stderr and exits without a dialog.

1.12.1 ends at a fatal dialog for a present but corrupt base archive (`0x403b6f`) and only logs a missing one (`0x403b00`); ending at no install at all is recorded as a `Deviation:`.

Tests cover both messages and the failing exit; both cases were run live unattended. Built on macOS, Linux and Windows.

Fixes #836

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:14:14 +07:00
Sam Sung f438c19f9a Quest item toasts come from their packet, not from bag-count changes (#1606)
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>
2026-10-16 22:53:18 +07:00