Commit Graph

120 Commits

Author SHA1 Message Date
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
Sam Sung 7063549659 Character select notices a server that drops the link (#1563)
At character select the IO thread waited only on the app's requests, so a server that dropped the link (vmangos kicks a session idle there after `CharacterScreenMaxIdleTime`) went unnoticed until Enter World started a load.

1.12.1 keeps servicing the connection on that screen, and a drop runs `CGlueMgr::NetDisconnectHandler` (`0x46c540`): `DISCONNECTED_FROM_SERVER` and back to login.

The park now looks at the socket twice a second without blocking. A closed link ends the session as a failed roster ask does; a packet sent unasked is read and handled as `char_enum` handles its strays, in one shared step.

A fake-server test drops the link while the client is parked with no request pending and expects the session lost; it times out without the change.

Fixes #1020

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 22:04:18 +07:00
Sam Sung 91803130c6 Item tooltips rebuild in place when a cache they asked answers (#1515)
A shown item tooltip whose line waited on a cache kept missing that line until something re-ran `OnEnter`. Examples are a lock's key, a "made by" name and a set member. Bags re-run it every frame; trade slots, `ItemRefTooltip` and addon tooltips never do.

1.12.1 registers `CGTooltip::OnCacheAnswer` (`0x52e530`) on every record, name, petition and item-name miss, with the tooltip as cookie. On each answer it rebuilds from the inputs the last build kept (`0x52b6cc`–`0x52b70a`), reading the live object, and sizes the plate (`0x52e5ac`). A clear zeroes the kept entry (`0x5300dd`), so a cleared tooltip stays down.

Every item `Set*` now goes through one build that keeps its inputs and records its misses. Each answer rebuilds whatever the tooltip keeps by then; compare asks stack and builder asks deduplicate, as in the reference.

Tests cover each cache and a cleared tooltip.

Fixes #1452

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:21:02 +07:00
Sam Sung d8720510be A worktree with its own probe account keeps its own state folder (#1489)
A dev build maps a linked worktree's `benilla-config/` to the main checkout's, so a developer's worktrees share one settings folder. That included a worktree that declares its own account in `.probe-identity`: an attended run from it, or a scripted one without `WOW_UNATTENDED`, read and wrote the main checkout's CVars, bindings, macros and saved variables while logged in as another account. A branch with a wrong settings migration or scripted key presses changed the player's own client, with nothing to show it.

`local_state::shared_root` now keeps such a worktree's state in its own `benilla-config/`. A worktree without `.probe-identity`, and the main checkout, resolve as before. CONTRIBUTING and `.gitignore` say so.

Test: `the_state_root_is_the_folder_or_its_primary_checkout` adds a worktree with a `.probe-identity`, which keeps its own root, and a main checkout with one, which is unchanged; it fails without the change. `the_state_folder_lands_where_the_build_says` follows the rule.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:26:14 +07:00
Sam Sung c50db9bfc0 A player-clippy gate lints the shipped player build (#1471)
No gate built the shipped player graph outside tests. The player-tests gate compiles with `cfg(test)`, and clippy ran only on the dev feature set, so the player build's own warnings landed unseen: four unused re-exports that only dev probes used.

A new `player-clippy` gate runs `cargo clippy -p benilla -p benilla-app --no-default-features -- -D warnings` in `scripts/gates.sh`, in CI, and in `scripts/check.sh` when it reaches those crates. `docs/METHOD.md` names it.

The four re-exports are deleted: `ground_derived`, `GroundClamped`, `BodyPartsDesc` and `ATTACH_SLOT_NAMES`. Their dev users import them from the defining modules, with no `allow` left.

Test: an unused import added to `net.rs` fails the new gate.

Fixes #1465

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:03:00 +07:00
Sam Sung c0fa656181 The native glue screens are gone; instruments press the stock screens (#1466)
The stock 1.12 glue screens are now the only glue screens, so benilla's native login, realm list, character select and create screens are deleted (−17k lines). So are the native switch, its census tests, the art-extent module and its extract command.

What still drove the native screens now presses the stock ones through their own Lua verbs (`capture/glue_press.rs`):
- the rig;
- `WOW_PROBE_CHARCREATE`;
- the `glue-login`, `glue-charcreate` and `glue-realmlist` captures.

The three world-entry tests run on the glue VM. The schedule's ambiguity ceiling drops from 3939 to 3348.

Live: smoke, a glue round trip, a stock pick, a rig teleport, a create and delete, all three captures, and the player build's boot.

Fixes #856

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:56:52 +07:00
Sam Sung baf729cee6 Nameplates read NAMEPLATE_FONT when built, as 1.12.1 does (#1464)
Nameplates took a fixed Friz Quadrata path whatever `NAMEPLATE_FONT` held, so an addon or a localized GlobalStrings could not restyle them.

1.12.1's plate constructor reads the global for each FontString it builds: the name at `0x7cb3f9` and the level at `0x7cb49b`. The read is `lua_gettable` on the globals, so `__index` runs (`0x704373`). A number reads as its text; anything else, or a missing global, gives "". `SetFont` refuses "" (`0x44d05c`) and leaves the string with no font.

Plates and chat bubbles now read the global through one lookup, when each is built. The read happens before the UI model is borrowed: an addon's `_G` `__index` that touched the model panicked the bubble's read.

Tests: a plate built after the global is set takes it, an older plate keeps its own font, the non-string cases, and an `__index` that calls `CreateFrame`.

Fixes #921

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:17:27 +07:00
Sam Sung d404e4e9e4 The stock glue screens are the default; native ones are dev-only (#1462)
The stock 1.12 GlueXML screens are now the default in every build: login, realm list, character select and create. benilla's native screens run only in a dev build with `WOW_GLUE_NATIVE=1`, or under the rig and capture runs, which still drive them, until the native screens are deleted. `WOW_GLUE_STOCK` is gone.

On the way:
- An unattended reconnect re-dials the kept realm and lists the roster. Under the stock screens it stalled at the realm list.
- `WOW_CHARSELECT_PICK` selects on the stock screen.
- `smoke.sh` counts the stock run's VMs and fonts.

The stock screens fix the native screens' defects listed below.

Tests cover the switch, the pick and the reconnect. Live: smoke, a kick and re-entry, the player build's boot.

Part of #856.

Fixes #849
Fixes #850
Fixes #834
Fixes #812
Fixes #360
Fixes #463
Fixes #79
Fixes #977

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:03:33 +07:00
Sam Sung a9c2672b3f A fifth loot roll waits for a frame and opens when one closes (#1458)
A fifth open loot roll was announced at once. The stock frames hold four, so its frame never opened and it could not be voted.

1.12.1 starts a roll through `0x61b430`: below four open it counts up and fires `START_LOOT_ROLL` with the wire countdown. Otherwise it defers the roll. Every close (own vote, WON, ALL_PASSED) fires `CANCEL_LOOT_ROLL` and then promotes the oldest deferred roll (`0x61b760`). An uncached item takes its turn when its record arrives (`0x61b460`).

benilla now does the same. Each roll waits, defers, starts or is open; at most four are open, and every close promotes oldest first. The client's server-close paths decrement the count even for a deferred roll that never counted (`0x61b6f8`, `0x61bc17`), so a fifth frame can open there. benilla keeps the count exact, as a `Deviation:`.

Tests: five announce four; each close path promotes; the wire countdown; a deferred close.

Fixes #1061

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 16:28:24 +07:00
Sam Sung 247ae3e631 Nothing of the native glue screens runs under the stock screens (#1456)
With `WOW_GLUE_STOCK=1`, character select, realm list, create and the glue strings still ran native systems. They spawned hidden trees and could write the hidden `GlueDialog`. A refused scripted login showed nothing on the stock screens.

Every native glue system is now gated on `native_glue`. The cross-switch instruments (round trip, login shot, realm smoke) move into `glue_session`. Under stock, `WOW_CHAR` enters by the stock Enter World. The account guard's refusal opens the stock `OKAY` dialog, the path `CGlueMgr::Login` takes for an empty account (`0x46b090`). The schedule's ambiguity ceilings drop to 3939 and 372.

Test: a census fails on any system that touches the native screens' components or resources without the gate; it lists 44 on main. Live: native smoke, a stock login to create and back, a stock logout round trip, the stock realm smoke, and a guard refusal.

Part of #856.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:43:55 +07:00
Sam Sung bc52e8a7f4 Item tooltips show the lock's key or skill line and a gift's giver (#1454)
Item tooltips lacked the lock's sub-line and a wrapped gift's giver.

The reference: the lock check (`0x52bd57`) takes the Lock row's slots in place. The first slot picks the arm: a key names its item in white (`0x52bf30`), and a skill names the LockType and the required value, coloured by the walk at `0x5da5e0`. A blank LockType name prints blank; only a missing row reads `UNKNOWN` (`0x52bede`). A wrapped item (flag `0x8`) prints `ITEM_WRAPPED_BY` with `GIFTCREATOR`, from the object or, for the trade partner's slots, from the block (`0x534559`, `0x534574`). Real lockboxes use locks whose first slot is empty, so they show no sub-line, as in 1.12.

The rows keep all eight slots, and both trade sides carry the wrapped word and the giver. Tests cover each arm, a blank name, and gifts in bags and trade.

Fixes #1440
Fixes #1441

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:26:56 +07:00