Commit Graph

308 Commits

Author SHA1 Message Date
Sam Sung f060e0e1ee Bank, gossip, purse and repair costs read live; events at packets (#2542)
The bank, gossip, purse and repair-cost getters read frame-final copies, and their events fired from feeds after the drain.

Now they read live as of each packet:
- the purse, as GetMoney does (`0x518160`)
- the repair costs per slot (`0x4faf30`)
- GetNumBankSlots and GetBankSlotCost, from the player's own field with no banker test (`0x4f85b0`, `0x4f8630`)
- gossip's slots and text (`0x4e28d0`, `0x4e28c0`)

A bank lane carries OPENED (`0x4f8522`) and the refusal lines. PLAYERBANKBAGSLOTS_CHANGED fires at its field (`0x4f8460`). A gossip lane pushes SHOW at the draw (`0x4e22b5`), and asks the NPC's name there. A gossip draw with no player or no NPC held writes nothing and fires nothing (`0x4e20bb`), so a late greeting no longer reopens a closed window.

Four feeds are gone. Tests in `bank_live_tests.rs` and `gossip_live_tests.rs` fail on main. READS_FRAME_FINAL 56 → 52.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 23:00:30 +07:00
Sam Sung 15dd7b524c Item requirements, lock openers and sets read live as of their packet (#2496)
The item requirement state, lock openers and set blocks were read from frame-final copies pushed by feeds, so a listener saw the frame's last level, skills and bags, not its own packet's.

Now the requirement checks (`0x5eaae0`, `0x5ea930`), the known-spell test (`0x60c740`), the lock openers (`0x5da5e0`) and the set blocks (`0x52d8a0`) read the player and bags live, as of each packet. With no player held, the getters answer the reference's fixed values: GetQuestGreenRange 0 (`0x4e17f8`), GetComboPoints 0 (`0x51a261`), and the item tooltip hidden (`0x52b699`). The tooltip build asks for its creator, charter and set-member names inside the build (`0x52e204`, `0x52b93b`, `0x52df4b`). The merchant's buyback and repair reads are as of the packet too.

Four feeds are gone, including feed_player_req. Tests in `lanes/requirement_live_tests.rs` fail on main. READS_FRAME_FINAL 60 → 56.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 22:04:49 +07:00
Sam Sung 03a90574d5 Stairs climb: the floor is classified by the face, not the contact (#2473)
Walking into some stairs stalled the player until they jumped, such as the Stormwind auction house's street door. The movement code classified the floor by parry's contact normal. At a stair nosing that normal blends the faces meeting there (ny 0.38), so the tread read as steep.

1.12.1 classifies by the hit face record's own normal against cos 50° (`0x80dffc`). It does so in the step probe (`0x636592`), the ground classify (`0x636ecf`) and the step trigger (`0x6334e6`). The casts now return the hit triangle's face normal (`collision::BodyHit`), and all four sites read it. A mover with no direction or fall bit runs no resolver (`0x634100`), so a body stopped on a nosing stands there and sends no FallLand.

Tests on the real Stormwind WMO cover the climb, short taps, and a stop at every frame of it.

Fixes #755

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:59:38 +07:00
Sam Sung d3b5a4a569 The pet bar, menu, stats and book read live; presses signal in call (#2467)
The pet bar, menu, stats and book getters read frame-final copies. A reaction or Stay/Follow press raised PET_BAR_UPDATE from the VM's copy, even when the press was refused.

Now the pet's getters read live through the call host: PetHasActionBar (`0x4bdc20`), GetPetActionsUsable (`0x4bcf70`), GetPetActionInfo (`0x4bdc50`), HasPetUI (`0x4be670`), PetCanBeAbandoned/Renamed (`0x4be500`/`0x4be580`), HasPetSpells (`0x4b4410`) and the rest. A press writes the state word and signals inside the call, after the gate (`0x4bd1f2`) and before the send (`0x4bd444`). A stunned pet's press stays silent. The pet-teach "already known" line and CreateMacro/EditMacro read the pet's book live. `GameTooltip:SetPetAction(0)` answers nil (`0x532812`).

The bar, menu and book feeds are gone. Tests in `lanes/pet_live_tests.rs` fail on main. READS_FRAME_FINAL 63 → 60.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:09:35 +07:00
Sam Sung f9489c6578 A DXT or raw BLP shorter than the palette window decodes (#2466)
`benilla-blp`'s `parse_header` refused any BLP shorter than the 256-entry palette window, whatever its compression. So small DXT or raw textures failed to load, such as AtlasLoot's 516-byte `gold.blp` highlight.

1.12.1's parse (`0x5a83b0`) copies the 1172-byte header with no length check (`0x5a83da`). It fails only on the magic and the type, and only the paletted path reads the palette.

Now the palette window is required for compression 1 only. Test: `dxt_without_a_palette_decodes`, a 4x4 DXT5 with no palette.

From chairmanpow's benilla-octo `660cf760`, carried on Thorsten42/benilla@78f7362.

Fixes #2464

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 18:53:08 +07:00
Sam Sung b7eb120325 The trainer, stable, tabard and charter read live; events at packets (#2465)
The trainer, stable, tabard and charter getters read frame-final copies, and their events fired from feeds after the drain.

Now they read live through the call host, matching their reference reads: the trainer's rows (`0x4d9096`, `0x4d9d07`, `0x4d97c8`), the stable's records (`0xb72058`) and display (`0x4cb99e`), and the charter's five getters (`0x4f43d0`…`0x4f5230`). Each family's events fire on its own lane at its handlers. TRAINER_UPDATE fires at each `0x4d7d40` caller: twice for a superseded rank (`0x5e63a4`, `0x5e64f1`), and between the skill line and the profession rebuild (`0x5de42d`). A close in the same frame no longer swallows a queued SHOW. That also fixes the merchant, whose CLOSED could fire before its SHOW. A /reload keeps the trainer's rows.

The four feeds are gone. Each family's tests fail on main. READS_FRAME_FINAL 67 → 63.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 18:33:51 +07:00
Sam Sung af5bfa582f The dialogs read live; their events fire on a lane at their packets (#2463)
The dialog getters read frame-final copies, and their events fired from frame-final feeds after the drain, so a listener saw them out of packet order.

Now CheckBinderDist (`0x5dff50`), CheckPetUntrainerDist (`0x5dfd30`), the boot, spirit healer, meeting stone, summon and instance getters read live through the call host. Their events fire on a dialogs lane stamped at each handler. The duel, binder, dialog verb, meeting stone, summon and instance feeds are gone.

Fixes on the way:
- The binder and pet unlearn questions pass the range gate first (`0x5dfe3c`, `0x5dfc2d`), the innkeeper click included.
- The boot timer's delay is signed (`0x496340`).
- The duel checks the player's own challenge before ignore, and its timer runs before packets (`0x420d0c`).
- A deleted ticket signals UPDATE_TICKET (`0x5e461d`).

Tests in `lanes/dialog_live_tests.rs` fail on main. READS_FRAME_FINAL 75 → 67.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 17:32:11 +07:00
Sam Sung fe4b12bc2d The merchant and flight map read live; their events fire at packets (#2461)
The merchant and the flight map answered from VM copies pushed at frame end, and their events fired from frame-end feeds. In 1.12.1 the merchant getters read the vendor's rows at the call (`0x4fb130`, `0x4fb580`), and a close leaves no rows (`0x4fae22`). The flight map's slots are built inside `SMSG_SHOWTAXINODES` (`0x4dc380`), with each fare read at the call.

Now both read live through the call host. `MERCHANT_SHOW`, `MERCHANT_UPDATE`, the buy and sell lines, `TAXIMAP_OPENED` with its tutorial, and the taxi replies fire on lanes at their handlers. A vendor row's item query names the vendor and a buy-back row's names 0 (`0x4faf05`, `0x4fb3f0`). A new flight path closes the gossip (`0x5e3e68`). Buy-back names carry their suffix (`0x5d8bc0`).

Tests: live rows, fares and repair costs; each event's place; byte-exact query goldens. All fail on main.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 14:58:32 +07:00
Sam Sung 1f75ed73ee Inspect and honor read live; the honor reply fires at its packet (#2460)
Inspect and honor answered from VM copies pushed at frame end. In 1.12.1 another player's slot getters read its visible-item block at the call (`0x4c8328` → `0x5f0d60`). The PvP getters read the active player's honor block (`0x51a4b0`–`0x51aa90`). The inspect reply handler stores a reply only for the target (`0x4c6e58`), then signals `INSPECT_HONOR_UPDATE` (`0x4c6f56`).

Now these getters read live through the call host, and the reply's event fires on the notice lane at its packet. The inspect honor slot is built as the reference's: target, has-data and pending, with one ask per new target. The recorded deviation that clears it on a re-target stays, with its reason restated: a server that never answers would leave the last player's figures shown.

Tests: an inspected link at the call, the honor event between two packets' events, the tooltip's maker line, and a re-target clearing the slot. All fail on main.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 14:31:14 +07:00
Sam Sung 18a99340b5 The stance bar reads live; the mirror timers fire at their packets (#2459)
The stance bar answered from a VM copy pushed at frame end, and the mirror timers' events fired from a frame-end feed. In 1.12.1 the stance getters read the form list at the call: `GetShapeshiftFormInfo` (`0x4b45c0`) gives four nils past the list or with no player, and castable comes from the usability walk (`0x6e3d60`). The mirror handler (`0x5e7990`) signals in place, then the tutorial. Releasing the active player stops all three timers (`0x5e9b90`).

Now the stance getters, `CastShapeshiftForm` and `GameTooltip:SetShapeshift` read live through the call host. The mirror events fire on the notice lane at their handler, and the player's release sends `MIRROR_TIMER_STOP` for each type. Both frame-end feeds are deleted.

Tests: live list, active, castable, cooldown and hover; four nils with no player; mirror events in packet order; the release's stops; `SetShapeshift(0)` past the list. All fail on main.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 12:58:19 +07:00
Sam Sung 19b67feec6 Friends, ignore and who read live; their events fire at their packets (#2458)
The friends, ignore and who lists answered from VM copies pushed at frame end, and their events fired from a frame-end feed. In 1.12.1 the getters read the lists at the call (`GetFriendInfo 0x5ad060`, `GetIgnoreName 0x5ad460`, `GetWhoInfo 0x5ad6e0`). The handlers raise `FRIENDLIST_UPDATE`, `IGNORELIST_UPDATE` and the who answer in place, which goes to the frame or to chat by the flag at that moment (`0x5adf9c`).

Now the getters read live through the call host, and the events fire on a social lane at their handlers. Each item seats the lists as its packet left them, so a listener's `SetSelectedFriend(1)` or `RemoveFriend(1)` acts on that packet's list.

Tests: getters read lists the handlers wrote with no frame between, the events interleave in packet order, a listener's verbs act on its signal's list, and the who route follows the flag at its stamp. All fail on main.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 12:08:42 +07:00
Sam Sung 6354cd983d The battlefield reads live; its events fire at their packets (#2457)
The battlefield's status, list, scoreboard and positions answered from VM copies pushed at frame end, and their events fired from frame-end feeds. In 1.12.1 the getters read the battlefield state at the call (`0x4ab4a0`–`0x4ab820`, `0x4ab9d0`, `0x4abf90`). The status handler fires `UPDATE_BATTLEFIELD_SCORE` before `UPDATE_BATTLEFIELD_STATUS` (`0x4aaa5a`, `0x4aab05`).

Now every getter reads live through the call host, and the events fire on a battlefield lane at their handlers. The board sorts with the reference comparator through the CRT qsort (`0x4aa350`, `0x73f727`). A row with no name record counts as team 0, and a world entry clears the stat columns (`0x4a9e67`).

Tests: live getters against a written world, a frame of mixed packets firing in order, name callbacks at the name packet, and a 40-row sort. All fail on main.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:20:27 +07:00
Sam Sung 807067ba66 Skills, talents and spellbook read live; their events fire at packets (#2456)
The skill lines, talents and spellbook answered from VM copies pushed at frame end, and their events fired from frame-end feeds. A listener therefore saw them as the frame's last packet left them.

Now their getters read live through the call host: skills through `0x5ea3f0`, talents through `0x4f3200` with character points at `+0xe48`, the book through `0x4b3fe0`. `SPELLS_CHANGED` and `LEARNED_SPELL_IN_TAB` fire at the learn, unlearn and supersede handlers, after `UPDATE_SHAPESHIFT_FORMS` (`0x4b2904`), and also at arrival and level-up (`0x5edb7b`). `CHARACTER_POINTS_CHANGED` fires from its field watch (`0x4c7098`), its one source; the page-turn source was spurious. A talent counts a known higher rank (`0x60c8d0`). The respec question drops with no trainer in range (`0x5df9be`).

Tests: eight full-client tests cover the live reads and each event's place. Five fail on main.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:56:09 +07:00
Sam Sung 721c408d66 Target and npc tokens read live; a released target ends at its release (#2455)
The `target` and `npc` tokens read guids pushed into the VM at frame end. A target whose object the server released was torn down by a frame-end system, after later packets' listeners. In 1.12.1 the tokens read the target and npc globals at the call (`0x515a86`, `0x515c00`). The release (`CGUnit_C::Release 0x5fbb60`) ends a target in place (`0x5fbbe5`), except a group member's, whose guid stays (`0x5fbbcd`).

Now both tokens read live through the call host, and the npc is the raw interaction guid. For a game object, `UnitName("npc")` names it (`0x5f8100`) and `UnitExists` is nil. The teardown runs inside the release on a new lane. A reconnect clears the old selection.

Tests: a live target and npc, a game-object quest giver's name, the release firing `PLAYER_TARGET_CHANGED` between two packets' events, and a reconnect. All fail on main.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:18:40 +07:00
Sam Sung 5de4942fde The party colour and the meeting stone read the party slots (#2454)
Two party tests read the wire's whole member list, which holds every raid subgroup and is not emptied when the party ends. In 1.12.1 both read the four party slots at `0xbc6f48`. The selection colour asks `CGPartyInfo::IsMember` (`0x4e7f70`): us, or a guid in one of the slots. The meeting stone tests slot 1 (`0x5f6a05`), then counts the slots (`0x4e86d0`, `0x5f6a45`).

Now the target ring and nameplates use the relations' party test, and the meeting stone gates on the party slots. A raid member of another subgroup no longer shows the party colour. A raid of four or more gets NO_RAID_GROUP rather than GROUP_FULL, and a player whose party ended can join.

Test: the stone gate with a raid whose other members sit in another subgroup, and with an ended party. Both fail on main.

Fixes #2450
Fixes #2451

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:05:18 +07:00
Sam Sung 73c5ff8887 widescreen 0 lays the interface out 4:3, stretched across the window (#2453)
With `widescreen` at 0, the in-world interface kept the window's aspect. In 1.12.1 the game's aspect answers exactly 4/3 then (`0x4901e0`). It is stored at `0x48fe1e` for the screen basis `0x41ad10`, re-read on a resize (`0x492f13`) and for the automatic UI scale (`0x492fb4`). The interface lays out 4:3 and is drawn stretched across the window.

Now a `UiScreen` built from the window and the CVar carries the interface's size, its exact aspect and its stretch. Every window-to-UI conversion goes through it: input records, hover, the UI pass, tooltips, the minimap, the world map, nameplates, bubbles, combat text, the cursor and the probe click. With `widescreen` on the stretch is 1, so nothing changes.

Tests: a click round trip at 1920×1080, the cells' aspect at exactly 4/3, and the automatic scale at 700 and 766 tall. All fail on main.

Fixes #1577

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 08:47:50 +07:00
Sam Sung f304e3a10a A drunk player's honor title takes their real gender, a deviation (#2452)
The honor line's rank title took the active player's gender from `PLAYER_BYTES_3` byte 0, while every other gendered text read the real gender. 1.12.1's `CGPlayer_C::GetGender` (`0x5ed5b0`) does read that byte whole for the active player. But the byte also carries the drunk bits (`gender | drunk & 0xFFFE` on vmangos), so a heavily drunk female reads as male.

Now the honor line reads the real gender (`UNIT_FIELD_BYTES_0` byte 2) like the rest. One `Deviation:` line on `unit_gender` names the reference's read and why benilla differs. The byte-0 getter is gone.

Test: a female with byte 0 full of drunk bits gets the female rank title. It fails on main.

Fixes #2103

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 08:29:18 +07:00
Sam Sung e8c5b60650 The quest log reads live at the call; its events fire at their packets (#2449)
The quest log's getters read copies pushed into the VM at frame end, and its events fired from a frame-end feed. A listener therefore saw the log as the frame's last packet left it, not as the reference's slot reads (`0x4dfa41`, `0x4e01df`) find it at the call.

Now the slots, held counts (`0x622130`), standing (`0x4d6370`), coinage and the timers' clock read live through the call host. Template landings and refusals, toasts with `QUEST_WATCH_UPDATE`, rank moves and time syncs fire on a quest events lane at their handlers' stamps. The LFG quest list reads the live slots too. The frame-end clock feed and most of the log feed are deleted.

Tests: five full-client and five VM tests cover the live reads and the event order at the packets, including a refused template. They fail with the live reads off or the events stamped late.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 08:14:09 +07:00
Sam Sung fa947ea2e4 Account data syncs with the server, as 1.12.1 does (#2447)
Key bindings, macros, the window layout and the chat setup stayed on the machine and never synced with the server. 1.12.1 keeps eight sections both on disk and on the server, in step by digest. At each world entry the server's digests (`SMSG_ACCOUNT_DATA_MD5`) meet each section's own and the cached one (`0x5afa10`). A section changed here goes up 30 seconds after the interface shutdown that wrote it (`0x5aff40`), and one changed there is asked for and stored.

Now benilla does the same, with the four wire bodies byte-exact. Two recorded deviations keep data the reference would lose: no truncated upload (`0x5afb42`), and no write on a bad download (`0x5afcbc`). `SaveBindings` now writes the set it stored, not the live table at drain.

Tests: wire goldens, the section check and transfer, and the timers. A live round trip against the local server came back byte-equal.

Fixes #1337
Fixes #2368

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 07:50:09 +07:00
Sam Sung a1f2f36c46 Reloading dead keeps the DEATH popup, now a recorded deviation (#2445)
Reloading or reconnecting while dead and unreleased shows the DEATH popup, which 1.12.1 does not. Every UI start in the reference zeroes the release timer (`0x490131`), so `GetReleaseTimeRemaining` answers 0 (`0x4962a0`). A `/reload` also signals no `PLAYER_DEAD` (`0x490168`), so `UIParent.lua` shows no popup and the player is left with no Release button.

benilla keeps the popup on purpose. This change writes that down: two `Deviation:` lines name the reference facts and the reason. It also corrects two comments that said the reference keeps the timer across a reload. Behaviour is unchanged.

Fixes #2300

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 07:26:55 +07:00
Sam Sung 016b18a972 Group gates read the live group; raid rows are stored at the rebuild (#2444)
The group verbs' gates read the frame-final party snapshot. The raid rows were derived from the party slots, so the two could never disagree. In 1.12.1 a group list seats the party slots, zeroing the leader first (`0x4e84a0`), then fires `PARTY_MEMBERS_CHANGED` (`0x5e6c66`). Only after that does it store the leader (`0x5e6c7e`) and the loot method, and rebuild the raid rows (`0x4ba5f0`).

Now the gates read the live group through the call host: membership, loot method, convert to raid, ready check, the by-name verbs, and the chat and addon-message group checks. The raid rows are their own state, written at the rebuild. Loot placement, the map blips, battlefield counts, the meeting stone and targeting now read those rows.

Tests: a gated verb decides on the world's group at the call, and the raid rows stay as stored until the rebuild. Both fail on main.

Fixes #2437
Fixes #2436

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 07:07:41 +07:00
Sam Sung f57549d585 An outline at a 1.5 display scale rings one texel a pass, as 1.12.1's (#2443)
At a 1.5 display scale, outlined text drew two dilation passes where 1.12.1 draws one, so letter edges came out thinner and partly covered. The outline's pass count was `r × round(dpi)`, and round(1.5) is 2. 1.12.1 rings an outlined glyph one texel per pass: NORMAL dilates once and THICK twice (`0x5cea30`, `0x5ce440`).

Now the count is `r × floor(dpi)`. A fractional scale gets the reference's one texel a pass, and a whole-number high-DPI raster keeps its logical ring weight. The recorded deviation now says floor, with its reason.

Test: at dpi 1.5 a NORMAL cell grows one texel on each side, both plain and mono. It fails with `round`.

Fixes #2408

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 06:45:00 +07:00
Sam Sung 24327a022a The video lists answer during the UI load, built on their first call (#2442)
During the in-game UI load, `GetScreenResolutions()` and `GetMultisampleFormats()` answered nothing. Their lists were filled only by per-frame systems that first run after the load, so an addon reading them at file scope, like Turtle WoW's options panel, got empty menus. In 1.12.1 the resolution list is built on the first call to any of the resolution verbs, from the display's modes (`0x48bc90`, guarded by `0xb4b3d4`). The multisample list is built the same way on the first multisample verb (`0x48c3e0`, guarded by `0xb4b440`).

Now the resolution and multisample verbs read both lists live through the call host, which is lent during the load. They fall back to the model with no host, and the per-frame feeds still keep the model current.

Test: an addon reads both lists at file scope during the world-entry load in the headless client, and gets the full lists. It fails on main.

Fixes #2439

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 05:47:10 +07:00
Sam Sung a49ed51568 Buff timers read their expiry stamps live, as 1.12.1's do (#2441)
Buff timers read live through the call host seam, for #2168. `GetPlayerBuffTimeLeft` (`0x4e4930`) and the buff tooltip's time line (`0x532be3`) read the expiry stamps (`0xbc5f68`, written by `0x4e4390`, read by `0x4e4450`). They answer the expiry minus now, 0 once lapsed or unstamped, in seconds. A new `PlayerBuffExpiry` query answers from the world's `AuraDurations`, and the feed no longer joins durations into the pushed list. With no host, the pushed expiry stands.

A buff with no catalog row keeps its flag unresolved, as `0x4e44b0` skips the write on a miss. The call resolves it from the expiry, so a server-sent timer still shows.

Tests: stamps written in the world read through a lent host, with no push between, and so does the tooltip's time line. They fail on main.

Part of #2217

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 05:19:17 +07:00
Sam Sung 095e1f4507 Name cache and creature record getters read live, as 1.12.1's do (#2440)
The name cache family, read live through the call host seam, for #2168. A new `names/live.rs` answers two queries:
- the player-name read (`0x55f080`), which replaces `CallHost::player_name`, for the auction seller and the drunk watcher (`0x609210`);
- the creature-record read, for `UnitCreatureType` (`0x605570`), `UnitClassification` (`0x605620`), `UnitLevel`'s hidden -1 and `UnitIsCivilian` (`0x612550`).

`UnitPVPName` (`0x5172b0`) takes its name and civilian leg live. Each read falls back to its snapshot with no host lent. The name events already sat on their lanes at their handlers.

On the way, `UnitPVPName` for a unit with no cached name returns the `UNKNOWNOBJECT` string, as `0x609210` does on a miss (`0x609324`), where benilla returned nil.

Tests: the type word, the rank and the world boss's -1, the cached name, the civilian flag and the unknown name, each read live. They fail on main.

Part of #2217

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 04:45:56 +07:00
Sam Sung f81dfe0a1a Reputation getters read at each packet's place, as 1.12.1's do (#2438)
The reputation family, read at each packet's place, for #2168. The faction getters and the slot reads of the three faction verbs ask the call host live (`0x4d65ac`, `0x4d65ea`, `0x4d6613`, `0x4d6660`). A reputation lane applies each message's list edit at its stamp, and `feed_reputation` is gone.

The producers follow the handlers' order. A standing message stores the standing (`0x4d634a`) before its "increased by" line, so `CHAT_MSG_COMBAT_FACTION_CHANGE` listeners read the new standing. Then come the adder (`0x4d5826`), the flag write, the rank line (`0x4d5896`) and the sort. Flag writes replay as set/clear masks, so a listener's `SetFactionInactive` survives a later at-war packet. A reload seats the list and standings at the head of the world-enter cascade.

The tests cover per-packet standings, at-war, visibility, watched, `UnitReaction`, the chat line, the verb bit and the reload. They fail on main.

Part of #2217

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 04:22:30 +07:00
Sam Sung 42db0b49d2 Party and raid getters read at each packet's place, as 1.12.1's do (#2435)
The party and raid family, read at each packet's place, for #2168. The group getters read live through the call host seam: count, leader, loot method, the `GetRaidRosterInfo` family, the `UnitIn*` tests and the `partyN`/`raidN`/pet token rows (`0x4e86d0`, `0x4bb560`, `0x4bb8c0`, `0x4e9130`, `0x4bb910`). The group tokens are rebuilt over the roster as of each packet. `READY_CHECK`, its answers and `UPDATE_INSTANCE_INFO` go on the party lane at their handlers.

On the way, it matches the bytes. The party slots are compact: a zero GUID leaves no gap (`0x4e82eb`), and our own entry is skipped (`0x5e6b88`). `UnitPlayerOrPetInParty`/`Raid` follow `0x4e8fa0` and `0x4bafd0`, matching members and their pets, never the player.

Tests: two group lists in one frame each read their own roster; each event reads the cells its reference store left; the zero-GUID and pet cases. They fail on main.

Part of #2217

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 03:48:07 +07:00
Sam Sung 579844828c Guild notes and a letter's text are profanity-masked, as in 1.12.1 (#2434)
Two profanity-mask sites the reference has and benilla lacked.

- **Guild notes:** the note accessors `0x4d09b0` and `0x4d09c0` call `MaskGuildMemberNotes` (`0x4d09d0`), which masks both notes with the cached form (`dl = 1`) once and clears the member's mark. The note sort, `GetGuildRosterInfo` and the note setters' compare all read through it. benilla now masks both notes once per roster list, so the sort, the getters and the setters see the masked text.
- **Letter text:** `ShowItemTextPage` masks a single item text, a letter body, with `dl = 0` (`0x4e366b`, `0x4e3672`) after its `$` expansion. Its page-chain arm never masks, so a book's pages stay as written.

Tests: a guild member's notes are masked once by their first read, a letter's text is masked with the filter on, and a book's page is not. The first two fail without the change.

Fixes #2431
Fixes #2432

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 03:25:00 +07:00
Sam Sung cef40a824b Guild getters and events read at each packet's place, as 1.12.1's do (#2433)
The guild family, read at each packet's place, for #2168. Every guild getter now reads live through the call host seam, with the reference's nil cases (`0x4d1190`, `0x4d1200`, `0x4d14a0`, `0x4d1890`, `0x4d1e60`, `0x4d1e90`, `0x4d1fe0`, `0x516de0`, `0x4c9330`). Guild events go on a "guild" lane at their handler, and `feed_guild` is gone.

On the way, it matches the bytes: the stale roster signal fires for every guild event but 12, 13 and a failed 10 (`0x5e74dc`). GE_MOTD no longer stores the MOTD (only `0x4d0b6d` writes it), and leaving no longer clears the roster. One roster update fires per missed lookup. Rank names and the MOTD go through the masker (`0x4a1a60`). Two roster updates the reference never raises are dropped.

Tests: two roster packets in one frame each read their own roster, and each event arm and write is pinned. They fail on main.

Part of #2217

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 02:56:14 +07:00
Sam Sung 0d1cef5207 Window input dispatches record by record, in arrival order (#2429)
1.12.1 dispatches each window input record whole and in arrival order (`DrainWindowInput 0x423920` → `IEvtQueueDispatch 0x4245b0`). The record's handler sets the modifiers first (`0x423d06`), and a button record carries its own cursor (`0x42dd00`). Each record runs CSimpleTop's handlers down to WorldFrame's bindings (`0x483c40`, `0x483b70`). benilla read each frame's button and key state, so a key and a click in one frame ran in a fixed order, and a press landed wherever the cursor ended the frame.

Now the UI pass walks ordered records from the window's event stream, each with its own modifiers and cursor. The world mouse and the button binding agree on who owns each press.

Tests cover key-then-click order, a click where the cursor was, and a Shift chord in one frame. The probes now type and click through the window's events, and live runs typed chat and the login account.

Part of #1988

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 01:56:52 +07:00
Sam Sung 73114e22df One seam for live getter reads, and a guard on frame-final reads (#2428)
Groundwork for firing each packet's events at its place (#2168). That needs every getter a listener can call to read live state through the call host, and about twenty families still have to get there in parallel.

**The seam.** The host gains one generic hook, `read_live`. A getter asks `live::ask(lua, &query)` and falls back to its snapshot when no host is lent. Every live read sits in one set, `LiveReads` (`script_calls/live.rs`). A family adds its query and its answer in its own files, plus one registration line. The cooldown and worn-item reads move onto it.

**The guard.** `lanes/host_read_tests.rs` caps `READS_FRAME_FINAL` (93 feeds) and lists the 23 systems that may write a live-read input between the dispatch and the lane drain. The flip needs both empty.

No event moves. Test: a getter reads a value written earlier in the same turn.

Part of #2168

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 00:59:48 +07:00
Sam Sung 13226190dd A swim pitch key starts inside its press, before the frame's packets (#2423)
In 1.12.1 the press tick runs the pitch arm after turn (`0x514777`), so a swim pitch key starts inside its press, before the frame's packets. benilla latched the pitch keys in the frame-time pass, after the packet drain. A pitch press in a frame whose packet rooted the mover sent no pitch start at all.

`press_tick` now runs the pitch emitter after the turn arm, and its start or stop goes out after the turn's message. `stop_pitch_keys` and the mouse-look hand-off are unchanged. The latch facts already match: the stop is sent only while swimming (`0x5147e9`), and a stun clears the latch either way (`0x5147fa`).

Test: a swimming pitch press in a rooting frame sends the pitch start, the stop, then the root ack. It fails with the arm in frame time.

Fixes #2412

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 23:08:15 +07:00
Sam Sung d814179f7c Pet cooldowns flush at each packet's place, as 1.12.1's do (#2421)
In 1.12.1 the pet's cooldown list (list 1) flushes at each packet's own place. `PET_BAR_UPDATE_COOLDOWN` fires from five sites: `0x6e2e8e`, `0x6e3080`, `0x6e8601`, `0x6e95b9` and `0x6e971c`. The pet getters query that list live (`GetPetActionCooldown` at `0x4be03c`, plus the pet book and bar at `0x4b40dd` and `0x4e6cf7`). benilla flushed the pet's list once a frame and read it from the frame's final state.

Now the pet's list logs and replays like the player's (#2414), on its own lane in packet order, and the three getters read it live. A failed `CAST_RESULT` fires its GCD-clear pair (`0x6e167d`) before the pop (`0x6e2218`), so the cast slot and the targeting word change only at the pop.

The tests cover two pet packets in one frame, a player packet then a pet packet, and the GCD-clear pair seeing the cast still held. Each fails without its piece.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:50:51 +07:00
Sam Sung aadbf8823b Game objects use their type's interact range, as 1.12.1's do (#2419)
A door lit its interact cursor out to 5.5556 yd, where 1.12.1 stops at 5.0. benilla used the NPC-service reach for every game object type.

In 1.12.1, `0x5f3130` tests `(dx² + dz²) + dy² <= r²` against the object's position. `r` is the type handler's `+0xc`: 5.0 by default (`0x5f2f67`), 5.5555553 for types 2, 9, 24, 26 and 27, 10 for type 4, 100 for chairs and fishing nodes, and 0 for type 12. When a known spell opens the lock, its maximum range is used instead (`0x5f3328`). Without one, state 2 refuses with error 0xe5. A chair then seats strictly within 3 yd.

Now the cursor, the click and the walk all use those ranges. The walk stops at 0.9 × the handler range.

Tests cover the door at 5.2 and 5.0 yd, a spell opener, a destroyed object, the chair and click-to-move; each fails without its piece.

Fixes #2410

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 22:28:37 +07:00
Sam Sung 92c701c90d UI text seats on the device-pixel ceiling, as 1.12.1's anchor snaps (#2415)
The reference snaps a string's anchor with `y = floor(H · y)` in y-up device pixels (`0x5cdf70`). In screen rows that is a ceiling. benilla rounded the block top to the nearest row on the logical grid, and rounded the ascender in logical pixels. So a row with a fraction in (0, 0.5] landed one row high, and the error moved with the display scale. The login screen's "Account Name" sat 1 px above 1.12.1's.

UI text now seats with the device-pixel ceiling, with the ascender from the font's pixel size and the seat nudge read as device pixels. World text is unchanged.

Test: `the_login_text_sits_on_the_reference_rows` checks the login labels, the version line and the copyright at the D3D shot's rows. It fails before the fix. Captures at 1× and 2× match the reference rows on every login-screen label.

Fixes #2407

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 21:11:21 +07:00
Sam Sung b584e833eb Cooldown and cast events fire at each packet's place, as 1.12.1's do (#2414)
In 1.12.1 the cooldown and cast handlers write state, then fire their events, at each packet's own place. `SPELL_COOLDOWN` (`0x6e9460`) adds its records and then fires the cooldown pair. The cast result's pop (`0x6e741a` → `0x6e4940`) runs before `SPELLCAST_STOP`. The getters (`0x6e2ea0`, `0x6e2ed0`) read the live list. benilla raised cooldown events once a frame, from counter edges, and its cast getters read a frame-end view.

Now the player's cooldown list logs each write and signal per packet, and a lane replays them in order, with the call host lent. The four cooldown getters read the list live. Cast views are logged per packet, and a listener's own cast stays visible through later packets' views.

The tests cover two cooldown packets in one frame, cast start and stop in one frame, an early-stopped drain, a worn item's cooldown and a failed cast. Each fails without the change.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:50:15 +07:00
Sam Sung a2208c2c50 A movement press runs its tick before the frame's packets (#2411)
In 1.12.1 a movement key press runs the controller's tick inside the press, before the frame's packets. `SetCommands` (`0x515090`) sets the bit, sends any cast cancel, then runs the tick at its tail (`0x5151a0` → `0x514640`), which reads root and stun at that moment. benilla ran its movement after the packet drain, so a root arriving in the same frame reached the wire before the start.

A new press pass in the input stage now ends follow and click-to-move (`0x60e990`), stands the player up, sends the cast cancel, then emits the start or stop. `control` keeps the frame-time integration.

Tests pin each wire order (start before a same-frame root ack, stand then start, cancel then start, the follow edges); each fails with its piece undone. A live trace showed the press ahead of the drain in all 80 presses.

Fixes #2399

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:45:00 +07:00
Sam Sung c5c3b3d146 An unattended PIN login presses the stock keypad, which then hides (#2409)
An unattended login on a PIN account answered the keypad from `WOW_PIN` by calling `PINEntered`'s tail directly, which skipped the stock Lua. The stock `VirtualKeypadOkayButton_OnClick` (GlueXML `AccountLogin.lua`) calls `PINEntered` and then hides `VirtualKeypadFrame`, so the keypad stayed on the login screen.

The environment's PIN now presses the stock keypad: `Click` on the button for each digit's position (button `p + 1` carries id `p`), then on Okay. The stock handlers run as they do for a mouse press, including the hide. A login with no keypad on screen still submits directly.

Test: `wow_pin_presses_the_stock_keypad_and_it_hides` runs the real GlueXML with `WOW_PIN=4821`. It checks the positions reach the logon, the keypad is hidden and its PIN cleared. It fails before the fix.

Fixes #2405

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 19:23:36 +07:00
Sam Sung 12f4ba34b0 The WMO audit's pin variables no longer share the login PIN's prefix (#2404)
The WMO portal audit's camera-pin variables (`WOW_PIN_EYE`, `WOW_PIN_MAP`, `WOW_PIN_SITE` and nine more) shared the `WOW_PIN_` prefix with the unattended login's `WOW_PIN` and `WOW_PIN_TOTP`. The map listed an instrument's aim next to the login secret.

The audit's variables are now `WOW_WMO_PIN_*`, in the audit's two files. The login variables keep their documented names. Nothing else set the old ones.

Test: the crate's lints and the audit build; the instrument tests are run by hand, as before.

Fixes #2400

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:19:25 +07:00
Sam Sung c4236fb4c1 Action usability follows each packet, as 1.12.1's cache does (#2403)
`IsUsableAction` answered from a list pushed per frame. So a `UNIT_MANA` or `ACTIONBAR_UPDATE_USABLE` listener read the frame's last power, and after drinking the last potion, the slot's change listener still saw it usable.

1.12.1 answers `IsUsableAction` from a usable cache (`0x4e5230`). The slot write `0x4e58e0` fills it, and eight field watchers refresh it inside each packet (`0x4b31c0` → `0x4e5c00`). `IsCurrentAction`'s form arm reads the live form byte (`0x4e558b`), and `IsEquippedAction` walks the worn slots (`0x622270`).

benilla now keeps that cache, refreshed at the same eight watchers and stored by the recount. The form and equipped reads go live through the call host, and items resolve through the per-packet views.

Tests: 14 getters agree live against snapshot across six slots, plus the usable, form, equipped and last-potion cases that fail on main. A refresh costs about 1% of a raid storm frame.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:03:34 +07:00
Sam Sung d3b5df3f48 A frame's input runs before its packets, as 1.12.1's does (#2402)
The frame applied its packets before its input. An `OnClick` reading a unit saw what this frame's packets wrote, and a world click on a unit despawned in the same frame missed it.

1.12.1's tick runs every window input record (`0x423920` at `0x420d23`) before the packets (`0x420d55`). The mouse turn (`0x514400`), right-click interact and cast (`0x514b70` → `0x481f60`) and the press handoff run inside the record. Frame-time work, like the controller's movement and the leash (`0x4818c0`), comes after the packets.

The UI input pass, its bindings, the world mouse and the look sampling, and the world clicks now run ahead of the net drain. Then a new resume point, the feeds, the `OnUpdate` tick and the controller's step run after it.

Tests: a click reads the unit before its frame's packet, a click selects a unit its frame's packet removes, and the pointer edge.

Fixes #2372

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 17:46:31 +07:00
Sam Sung 9619157d47 Unit tokens resolve against the live objects, as 1.12.1's do (#2401)
Unit tokens resolved through a target map pushed per frame. So a listener of an earlier packet resolved `targettarget` as the frame ended. A pet's events fired under the frame's final pet tokens. A held unit that no token covered answered "Unknown Being".

1.12.1 resolves a token inside the call (`0x515970`): player, pet (our charm, else summon), every `target` hop from the held unit's `UNIT_FIELD_TARGET` (`0x515a1c`), and party and raid pets from the member's descriptor. `SignalUnitEvent` lists the tokens when it signals (`0x515e50`, `0x515c50`). `UnitName` names any held unit (`0x609210`).

These now read live through the call host, through one derivation shared with the snapshot. The target, mouseover, npc and roster globals stay client state. The event fan-out uses the same derivation.

Tests: 29 token forms agree live against snapshot, plus the three cases that fail on main.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 17:27:39 +07:00
Sam Sung 1befd0404d The opening movie plays on first boot and from Cinematics (#2398)
The opening movie never played. `StartMovie` returned nil, so the stock `MovieFrame` hid at once.

1.12.1's movie frame (`0x78ce40`) reads the AVI, decodes, and streams the MP3. Its clock is the audio's play time, or the wall clock without audio. It shows clock × rate frames, decoding skipped ones unseen (`0x78cf30`). It reads `.sbt` captions (`0x78cb00`), and draws 2 or 6 tiles with the picture 24 or 6 rows up (`0x81d400`). On first boot the `movie` CVar flips to "0" and opens the movie screen (`0x402b3b`).

benilla now does all of that on the decoder crate, along with `StopMovie`, `EnableSubtitles`, `OnMovieFinished` (including on teardown), the captions' show and hide handlers, and screenshots waiting while a movie plays. Unattended runs skip the movie.

Tests cover each of these. Live, the logo and the intro played through to login.

Fixes #968

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:49:14 +07:00
Sam Sung 96356c7984 Aura getters read the live unit, as 1.12.1's do (#2397)
`UnitBuff`, `UnitDebuff` and the tooltip's `SetUnitBuff`/`SetUnitDebuff` answered from a list pushed per frame. So a chunk that changed its target and then read `UnitDebuff("target")` saw the old target's auras. A listener whose later packet charmed us also saw a stale gate.

1.12.1 reads the unit object inside the call (`0x519500`, `0x5198f0`). That covers the aura slots, the flags nibble, applications and level; the gate is unit flags bit 27, `CanAssist` and our charmer (`0x5195f1`). A member out of view answers from the roster record, read by guid at the call.

These getters now read live through the call host, through one derivation shared with the snapshot. `GetPlayerBuff*` stays as it is: it reads client state rebuilt per packet.

Tests: live against snapshot across seven tokens with stacked, Magic, Curse and hidden auras, and the three cases that fail on main. Cost: about 50 ns per call.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:12:55 +07:00
Sam Sung 4f1a904ba0 UnitCharacterPoints and UnitOnTaxi answer per unit, as 1.12.1's do (#2396)
`UnitCharacterPoints` answered our own unspent points for every unit. `UnitOnTaxi` answered only for `"player"`, from our own taxi state, so a party or raid frame never saw a member on a flight path.

1.12.1 resolves the token in both:
- `UnitCharacterPoints` (`0x519420`) returns the counters at `+0xe48`/`+0xe4c` for the active player, and 0, 0 for anyone else.
- `UnitOnTaxi` (`0x517a40`) returns 1 when bit 20 of the resolved unit's flags is set (`0x517a86`).

Both now resolve the token. `UnitOnTaxi` reads the unit's flags live through the call host, falling back to the snapshot. `UnitCharacterPoints` raises its reference usage text for a bad argument.

Tests: points for the player and for another unit, the usage text, a party member on and off a taxi, and live against snapshot.

Fixes #2381
Fixes #2382

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:54:22 +07:00
Sam Sung 2d367f4190 Entering the world clears the battleground winner and scores (#2394)
After a battleground ended and the player left, the VM kept the match's winner and scores. The board was reset only on a disconnect, so `WorldStateFrame`'s end-of-match panel could come back outside the battleground.

1.12.1's world-enter cascade calls `0x4a9db0` (`0x4909fb`), which zeroes the score caches: the request stamp `0xb6ebc8`, the ended flag and the winner (`0xb6ebcc`, `0xb6ebd0`), and the row and filtered counts (`0xb6ebc0`, `0xb6ebc4`).

Every world enter now clears the board and its request stamp, and the feed pushes the empty board to the VM. As in the reference, no event fires.

Test: after a finished match, a world enter leaves `GetBattlefieldWinner()` nil and zero scores, and the next score request is not throttled.

Fixes #2376

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:02:27 +07:00
Sam Sung a1a28e45cc A far cinematic shot waits at black for its world, as 1.12.1's does (#2392)
A cinematic shot that moved the camera far faded in as soon as its path started, so its terrain and models popped in on screen.

1.12.1's shot arm (`0x48edf0`, shared by start and advance) reloads the world in place when the camera moves more than 30 yards (`0x48eed3` → `0x66fc80` → `0x694920`). That happens at full black. It sends `CMSG_NEXT_CINEMATIC_CAMERA` only afterwards (`0x48ef11`).

Now a far shot holds its black, with its clock at 0, until the spot is resident or the ceiling is reached. Then it announces, starts its narration and fades in. The gate, the residency release and the ceiling are the teleport hold's, shared. The shot, the teleport and the end's far reload bump one reload mark that re-arms the stream's load.

Tests: far and near shots, the ceiling, a skip, the announce and narration timing, and the re-arm.

Fixes #2370

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:31:48 +07:00
Sam Sung b4f1eb723c Decode 1.12.1's intro movies: an AVI reader and an MPEG-4 decoder (#2391)
benilla could not decode 1.12.1's intro movies, the loose AVIs in `Interface/Cinematics`.

1.12.1 reads the AVI's `hdrl` and `idx1` (`0x78c360`). Index entries whose id's third letter is `d` are video, and `w` entries are audio. It hands the video to a DivX decoder (`0x78c7a0`), which refuses widths other than 1024 and 800.

This adds `benilla-movie`: that AVI reader, plus an MPEG-4 Part 2 decoder written from the published standard. It covers I and P frames, half-pel motion, AC/DC prediction, H.263 quantization and an IEEE 1180 IDCT. It decodes frames one at a time to YUV or RGBA, and returns the MP3 audio bytes. It has no codec dependency.

Tests: the tables, IDCT accuracy, the AVI reader and garbage input. Against the install, all four movies decode every frame. Compared with ffmpeg, the worst frame is 55 dB luma PSNR, and the intro decodes at about 24× realtime.

Part of #968

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:17:04 +07:00
Sam Sung 1943788122 Stat, money and XP getters read the live object, as 1.12.1's do (#2389)
The stat and player-field getters answered from a copy pushed at the frame's end. So a `UNIT_STATS`, `PLAYER_XP_UPDATE` or `PLAYER_MONEY` listener read the frame's last packet, not its own.

1.12.1's getters read the object inside the call: for example `UnitStat` at `0x518600`, `GetMoney` at `+0xf70` (`0x518160`), and dodge, block and parry at `+0xe5c`/`+0xe58`/`+0xe60`.

Twenty-five getters now read live through the lent call host, among them `UnitStat`, resistances, armor, damage, attack power, defense, the avoidance chances, money, XP and rest. The snapshot and the live read share one derivation. Worn weapons resolve through the per-packet views. With no player held, the getters answer 0 or nil, as the reference does.

Tests: live against snapshot for every getter across four tokens, gear and buffs included; each listener reading its own packet's value. Cost: `UnitStat('player')` takes 344 ns live, against 261 ns from the snapshot.

Part of #2217

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 10:39:11 +07:00
Sam Sung fbc90b9bbf Test builds open no audio device (#2388)
Every headless test app built `GamePlugins`, whose `SoundPlugin` opened the default output device unless `$WOW_NOSOUND` or `$WOW_CAPTURE` was set, and no test run sets either. Under nextest that is one CoreAudio client and speaker stream per test process. With a few runs going at once, `coreaudiod` sat near 300% CPU, every sound on the machine crackled, and each test blocked on the audio server's connect.

A test build now takes the existing silent path, so its mixer is `None`, as with no device.

Test: `a_test_client_opens_no_audio_device`. With the change undone it fails on a machine with a speaker, taking 96 s; with it, the test passes in 0.06 s.

Fixes #2380

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 10:22:17 +07:00