Loot went wrong:
- Takes, closes and master gives queued, then ran in a fixed order.
- The last row closed a frame late.
- Auto-loot opened before it swept, read Shift when the loot arrived, and looted over an open window.
- `GetNumLootItems` counted taken rows, and a response for an object the client lacked opened a window.
The reference:
- `LootSlot` is the confirm form (`0x4c2e70`). A row click takes after its `<OnClick>` (`0x4c1867`). `CloseLoot` zeroes only the source (`0x4c1da0`).
- `SMSG_LOOT_REMOVED` closes an empty window in the packet (`0x4c1f8d`).
- The sweep runs before `LOOT_OPENED` (`0x4c1ed3`, `0x4c2240`). Each interaction stores the byte (`0x5df460`), and `CanLootNow` refuses an open session (`0x5ec1e1`).
- A missing object shows `ERR_LOOT_DIDNT_KILL` (`0x5ebb3f`).
Each fact is tested against its address, with the stock `LootFrame` for the replacing window.
Part of #2202Fixes#2167Fixes#2181Fixes#2180Fixes#2190Fixes#1228Fixes#784Fixes#2189Fixes#72🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chat channels went wrong:
- An eleventh join printed nothing, and only `YOU_LEFT` freed a slot.
- Lookups by number found suspended and pending channels.
- Admin verbs sent a number as the name.
- Defense broadcasts reached unconfirmed channels.
- Trade outside a city took "City", and zone channels answered full names.
- `/chatlist` printed a line of its own.
The reference:
- A join takes its slot when sent (`0x49edf3`), and refusals free it (`0x49c446`). A freed slot keeps its row id, so the walk skips it (`0x49bef0`).
- Number lookups take confirmed slots only. Admin verbs resolve the slot (`0x49e880`).
- The city word needs flag 0x20 and a city (`0x49a2d0`).
Channel verbs, `DoEmote`, `UninviteByName`, `SendAddonMessage` and `ListChannels` now act inside their call. Each fact is tested against its address.
Part of #2202Fixes#276Fixes#2169Fixes#229Fixes#2159Fixes#225Fixes#223Fixes#230Fixes#231🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Targeting went wrong for units the client does not hold:
- A groupmate out of range could not be targeted.
- Assist with nothing to assist was silent.
- `StartDuelUnit`, `SetRaidTarget` and `InviteToParty` ignored most tokens.
The reference:
- `SetSelection` (`0x493540`) stores a guid whose object is not held (`0x4935c8`), and `TargetUnit` selects a group member by guid (`0x489a64`).
- The roster tears that selection down for an offline seat (`0x4e845a`), a departed member (`0x5e6df7`) and a lost pet (`0x5e2719`).
- A guid-only select plays `igCharacterSelect` (`0x4936f7`). A long-range spell at it sends untargeted (`0x6e545b`).
- Assist shows `ERR_GENERIC_NO_TARGET` (`0x489c0e`), and the token verbs resolve any token (`0x515970`).
Every reader of the selection now reads the guid: the target frame, the cast binder, the swing, the plates and the portrait. Each fact is tested against its address.
Fixes#1965Fixes#1234Fixes#322Fixes#174Fixes#199Fixes#2178Fixes#2179🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Auction lists went wrong:
- They kept the server's order until a header click, and re-sorted every frame.
- Time Left, High Bidder, Lvl and Seller sorted on the wrong keys, and names matched case.
- Time left froze at arrival, and expired rows stayed.
- The sell slot took items 1.12.1 refuses.
The reference:
- World entry seeds eight-key sort stacks (`0x4cc236`). A page sorts once, when its last item record lands (`0x4cc9e8`).
- The comparator `0x4cd940` and the CRT `qsort` (`0x73f727`) order the rows, ties included.
- Time left counts down from the deadline, and rows 10 s past it leave the lists (`0x4cdfe0`).
- The sell slot refuses in `0x4ce362`'s order.
benilla now has one CRT `qsort`, which the frame batches, macros, guild roster and addon list share. Each fact is tested against its address.
Fixes#260Fixes#428Fixes#253Fixes#254Fixes#255Fixes#256Fixes#1539Fixes#258Fixes#1524Fixes#2191🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spell packets and cast state went wrong:
- A start or go with no record, or a non-unit's start, drew a cast.
- An enemy casting at you never became your target.
- Cooldown packets for other guids were dropped.
- Esc and move cancels reaped the wrong spell.
- Timers freed the cast slot.
- Spell state outlived a relog.
- Openers were scanned by id.
The reference:
- Both handlers exit first (`0x6e7876`, `0x6e7cd7`).
- A start aimed at you selects its caster (`0x6e799c`).
- Only the pet's guid routes to the pet (`0x6e9696`).
- The cancel reaps the record's spell (`0x6e4a8e`).
- No timer frees the slot; an inventory refusal cancels its item's cast (`0x5e3ab3`).
- World entry zeroes the spell state (`0x6e7150`).
- Openers keep the server's order (`0x5f83d0`).
Hold stops are now keyed by spell (`0x614167`). Each fact is tested against its address.
Fixes#2162Fixes#1302Fixes#1292Fixes#1301Fixes#2163Fixes#2025Fixes#1327Fixes#293🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A verb called from Lua, or a packet's event, applied a frame later, after the call or handler had returned. 1.12.1 applies each verb inside its call and fires each event at its packet.
What changed:
- Verbs apply inside the call: targeting, item picks, refused casts' log lines.
- A packet's chat items and field notices fire in wire order.
- Combat, unghost and death-tutorial events fire at their packets.
- Signals raised while something waits are replayed with the lock and cursor state they had.
- Loot picks and bind confirms are one call-ordered queue. `LOOT_BIND_CONFIRM` fires before its slot is stored (`0x4c290f`, `0x4c2917`).
- A forked script no longer panics on a lend, and two comments now match the reference.
Each order is tested against its address.
Fixes#1993Fixes#2045Fixes#2051Fixes#1419Fixes#2016Fixes#1987Fixes#1237Fixes#2139Fixes#797Fixes#2094🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The party and raid roster answered and sent where 1.12.1 does not:
- Join and leave lines printed for names the cache did not hold.
- A raid joined with no roster never asked for its icons.
- The loot getters needed a group.
- `GetPartyLeaderIndex` answered 0 after a list with no leader.
- The subgroup verbs sent for a held subgroup or an uncached name.
- `UninviteFromParty` and `PromoteToPartyLeader` took only party1-4.
The reference:
- The party leave line asks the cache with no callback (`0x5e6d15`).
- Raid lines wait on theirs (`0x4bacd0`, `0x4bad30`).
- A first raid list sends `MSG_RAID_TARGET_UPDATE` 0xff (`0x4babf4`).
- The getters read the stored cells.
- The leader cell is zeroed by every list (`0x4e85b5`).
- Both verbs resolve any token (`0x515970`).
What changed: the roster follows each of those. Each is tested against its address.
Fixes#2000Fixes#2001Fixes#2003Fixes#2086Fixes#2087Fixes#2088Fixes#2089🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The unit getters answered for the player alone:
- Stat functions gave zeros past player and pet.
- `GetGuildInfo` failed on chained tokens.
- A group member out of view had no race, class or sex.
- `UnitRangedDamage` ignored the weapon's school.
- `UnitIsUnit` resolved equal strings first.
- `UNIT_INVENTORY_CHANGED` fired only for the player.
- Drunk thresholds printed nothing and left `UnitLevel` alone.
- `CanInspect` answered nil.
The reference:
- `UnitIsUnit` compares the strings first.
- The getters read the named unit's fields and the name cache.
- The visible-item watch (`0x51bc3c`) signals for every player holding a token.
- The drunk watcher (`0x5e2a90`) prints and lowers the level.
What changed: each getter reads the unit its token names. The item watches, the ammo callback (`0x4c84f0`) and the inspect view signal where the reference does.
Each fact is tested against its address.
Fixes#1544Fixes#718Fixes#1239Fixes#1232Fixes#2005Fixes#169Fixes#1408Fixes#1157Fixes#1409🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A possessed unit's spell and a pet spell with AttributesEx4 0x20 went out as CMSG_PET_ACTION. `CastSpellByName` never found pet spells and misparsed names. `SendChatMessage` sent `%t` and `%n` unexpanded and passed any `|` escape. `RandomRoll` sent any arguments.
The reference:
- `CastSpellByName` cuts the name and rank at the first `(` and `)` and falls back to the pet's book (0x4b3950).
- A unit caster runs the cast ladder, picking its own target (0x6e4ef4) and ordering Attack after the send (0x6131a0).
- `SendChatMessage` expands the tokens and raises on a bad escape inside its call (0x49f1e0).
- `RandomRoll` checks its arguments first (0x48c7b0).
What changed:
- A `CMSG_PET_CAST_SPELL` writer with byte-exact goldens.
- The unit caster's ladder covers its pick, autoSelfCast and the cursor's binds.
- Both chat verbs now apply inside their calls.
- The channel slots follow every step.
Each fact is tested against its address.
Fixes#639Fixes#168Fixes#406Fixes#1331Fixes#1332Fixes#1531🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cooldown events fired on each record insert. 1.12.1 fires them at its flush sites (`0x4b31b0`). Several inserts also took values the reference doesn't.
- The insert (`0x6e12c0`) and StartCooldown (`0x6e2c60`) now fire nothing. Each packet and cast site flushes once.
- `SMSG_SPELL_COOLDOWN` timers go through op 11, the ranged pad and op 21 (`0x6e9460`).
- An item's game-object cooldown uses its block's category, else the spell's. It waits for the item record and inserts nothing for entry 0 (`0x6e8425`).
- Initial-spells cooldowns are held and back-dated through StartCooldown.
- A NOT_READY with argument 0, and every failed cast, clear the spell's records and flush.
- Bag, worn and tooltip queries ask for a missing item record. Its landing flushes (`0x6e2f70`).
Tests pin each flush site and insert value.
Fixes#2060Fixes#679Fixes#2061Fixes#2063Fixes#2064Fixes#2066Fixes#2076Fixes#2077🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sitting and Z skipped 1.12.1's refusals, and the cast-busy state they test was modelled as a ranged hold.
- The precast (`+0xc8c`, bit `0x400`) is a unit state of its own: set at the send for every spell with a visual (`0x60d020`), ended at its GO, a failure or a move (`0x60d040`), releasing loot as `0x60d0ee` does.
- Sitting (`0x5ed430`) and Z (`0x5eb480`) refuse in the reference's order, mounted through precasting; a sit closes loot and stops the attack (`0x5f0790`).
- A relic class's Z never reaches the ranged state (`0x611cf0`), and a morph keeps the committed sheath.
- Starting an attack aborts a ranged precast, and stops before the swing for Slam (`0x5ecb70`).
- Emotes wait out the busy bit.
Tests pin each refusal, end and tail.
Fixes#1249Fixes#1590Fixes#1591Fixes#1603Fixes#2084Fixes#2137Fixes#770Fixes#2152🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1.12's options declare Show Target of Target as a bare uvar
(UIOptionsFrame.lua:65) and the mode is read on save
(UIOptionsFrame.lua:365); neither calls TargetofTarget_Update. The frame
follows from TargetFrame_OnUpdate and TargetofTarget_OnUpdate on the next
frame (TargetFrame.lua:492-501). Our rows called it as their applyFunc,
with the row as `this`; the function refreshes `this`'s debuffs
(RefreshBuffs(this, 0, "targettarget"), TargetFrame.lua:538), so with a
target's target up the switch raised "BuffFrame.lua:281: attempt to index
local 'debuffBorder' (a nil value)" (seen on a Linux client).
Verified: the_target_of_target_switch_leaves_the_frame_to_its_own_updates
(with a target's target up: no error, the frame shows after one tick on
and hides after one tick off) fails with the applyFunc put back, with
exactly that error, and passes without it; the options and tot_frame
tests pass on a deDE install; cargo test -p benilla-app --lib 5145,
clippy -D warnings, fmt. Built on macOS only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Several of 1.12.1's client-side cast gates were missing, so the server refused casts the client refuses itself:
- The caster validator (`0x6094f0`) now runs its rungs in the bytes' order. Indoors and outdoors are judged by the entity outdoors test (`0x6706f0`).
- The crowd-control exemption checks every aura type (`0x6e9ca0`).
- The binder stops at the first arm that holds, with that arm's guards (`0x6e5b40`).
- TryCast refuses three more cases:
- ranged without ammo, which also greys the button;
- ranged without a ranged weapon (`0x6e4e13`);
- the while-looting effects (`0x6e4ca2`).
- A cast on cooldown with no target says not ready.
Tests pin each rung and the greying through the bar's own site.
The learn and tame rungs remain.
Part of #1202Fixes#1581Fixes#1586Fixes#675Fixes#1199Fixes#1294Fixes#2132🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The taxi map's routes, prices and node list differed from 1.12.1's.
- The search (`0x4dbce0`) weighs each hop by its flight path's points. It runs depth first over the map's slots, and keeps the first route on a tie. Gadgetzan to Sun Rock now flies through Freewind and Crossroads for 10s70c.
- A known node with no route is listed as NONE, with the start fare.
- The other faction's flight points are skipped by race (`0x4dbd2d`).
- `TakeTaxiNode` refuses a mounted player (0xb2), the current node (0xaa) and a node with no path (0xab).
- A closed map keeps its slots (`0x4dc0f0`).
- Out-of-range slots raise or answer INVALID, and the coordinate getters read bare numbers.
Tests replay the issue's routes over the shipped tables and pin each verb's refusal.
Fixes#204Fixes#205Fixes#1236Fixes#1576Fixes#1229🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cast-failure reporter skipped arms of 1.12.1's errorId chain and its pet text. A move also cancelled a cast from the wire word, not from the press.
- The player's chain now runs whole, in the bytes' order (`0x6e1aab`-`0x6e1c5f`). Every id, 0x2c included, formats through its key's live global (`0x4967d7`), and an empty line is still logged.
- Spells aimed at the pet show their `_PET` text (`0x6e1b1b`). An item-target refusal reads `ERR_INVALID_ITEM_TARGET`.
- The full-power line names the player's own power (`0x6e211f`). The pet's handler stays silent when it has no record or finds no name.
- A movement command press cancels the cast whoever drives (`0x515090`). Only a channel needs the player as the mover (`0x515180`).
Tests pin each arm, the pet lines and the cancel through the controller.
Fixes#2135Fixes#1582Fixes#1584Fixes#1637Fixes#1201Fixes#2141Fixes#2142🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In 1.12.1 a verb's effect applies inside the call, whether from a click, `OnUpdate` or an event handler. Here, calls from `OnUpdate` and handlers queued until the next lend, so `TargetByName(name)` then `UnitName("target")` read the old target.
- Every system that runs Lua lends the call host around its body, in one ordered VM pass; the cross-entry queue is gone.
- Seams that changed units through `Commands` queue unit edits, flushed ahead of their readers, so sync points stay at 54 (main: 53).
- Late drains apply before the paint; a CVar or follow ask lands the same frame.
- The actionable ambiguity ceiling falls from 3079 to 1750.
Tests: an `OnUpdate` cast and a handler's cast arm inside the call, three same-frame drains, and a census that each schedule flushes after every edit user.
Lua run inside an engine turn still waits for the turn's end.
Part of #1993🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Five comments stated a wrong reference fact or named code that does not exist:
- `shown_pet_power_type` cited `UnitPowerType`'s member-record read (`0x5179d2`) as its pet leg, which is `0x5179ff`.
- `race_creature_type.rs` named a path that does not exist.
- `drag.rs` cited the unreached copies at `0x425666`/`0x425766`, where the live converter is `0x766f70`, called from `0x766103` and `0x7662f3`.
- `handler_prof` claimed an app-data refusal benilla-mlua does not have.
- `MacroView` cited the save format by file offset, where its address is `0x84cb60`, pushed at `0x4f0b61`.
The NVIDIA present `Deviation:` also gains where the fault is: the driver's own DXGI present, with NVIDIA's present layer disabled too.
Comments only, so no test changes.
Fixes#671Fixes#676Fixes#1106Fixes#1991Fixes#1245🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The update ingest did not do what 1.12.1 does with the same bytes:
- It dropped the header flag.
- It released and destroyed held objects at once.
- It re-asked records on every create.
- It replayed enchant times at an item's write.
- It read character names uncapped.
- It ignored `SMSG_PLAYER_SKINNED`.
Reference: the header byte runs a first pass over guids with top nibble 6 or 8 (`0x4651c1`, `0x4660d8`). A held object is only marked release-pending (`0x464920`) until its hold drops (`0x468410`). Asks come from the create hooks alone (`0x5fb880`, `0x5f7d81`). Enchant times replay when the record applies (`0x5d85b7`). Names read with a 0x30 cap, and an overrun empties the list (`0x5b4523`). `PLAYER_SKINNED` fires with the byte's truth (`0x5e7d52`).
Changed: each follows these, and tests pin each. The faults the passes have on malformed packets are recorded, not copied.
Fixes#2053Fixes#2052Fixes#2054Fixes#2055Fixes#2083Fixes#87🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four TradeSkill and Craft answers differed from 1.12.1:
- Recipes of one colour listed the lowest item level first.
- `SelectTradeSkill` on a header kept the old selection.
- The sub-class folds ignored a non-header row.
- `GetCraftButtonToken` always answered `CREATE`.
Reference: the recipe comparator returns +1 when the first row's ItemLevel is smaller, so the higher level sorts first (`0x4fd4e6`). `0x4fc9b0` stores 0 for a header or an out-of-range index. The folds raise unless `0x4fd650` finds a header, and below 1 they fold every group (`0x5000c0`, `0x500140`). `0x4f6c90` answers the word at the open craft type in the table at `0x84d0f4`: USE, TRAIN, DISGUISE, ENSCRIBE.
Changed: each follows these, and tests pin each.
Fixes#155Fixes#1260Fixes#1261Fixes#876🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NVIDIA's Vulkan driver (610.88 seen) faults inside vkQueuePresentKHR on the first present after a
swapchain resize while the present mode is Fifo, FifoRelaxed or AutoVsync; a maximize is a resize.
Mailbox, AutoNoVsync and DX12 survive. present_choice picks the mode from vsync, the render
adapter's vendor and backend, the OS and $WOW_PRESENT; the pairing above gets AutoNoVsync with the
loop paced to the monitor's refresh rate, everything else keeps Fifo. $WOW_PRESENT=fifo restores
it, $WOW_NOVSYNC is unchanged. The boot window still starts on Fifo; apply_present_mode re-runs
when the adapter first appears and swaps the boot mode alone.
Built and unit-tested on Windows only; the cfg!(windows) arm is a value, not a cfg, so the other
platforms compile the same code.
Three Lua surfaces answered unlike 1.12.1:
- A Font object's `[0]` was a number, so saved variables and `type(v[0])` guards treated fonts unlike every other object.
- `CopyFontObject` pushed the source's values and held them.
- Guild and charter names went to the server with no client name check.
Reference: the saved-variable writer reads `[0]` as a light userdata (`0x704623`). `0x7841d0` merges what the source holds, then holds nothing (`0x7841f4`) and follows the source's font. `0x6c9b70` runs the name validator with a space as its one extra character, the profanity lists on, and a 24-unit cap. `0x4f51ec` maps its codes to the refusal lines.
Changed: Fonts take the object id counter, and the loader's `parent=` tests the Frame type. A font node keeps its held word apart from its values. The guild check runs before the send. Tests pin each.
Fixes#1997Fixes#1983Fixes#1399🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Macros were held by index and kept in creation order:
- A sort or delete moved what a bar slot held.
- `EditMacro` trimmed and capped differently and returned nothing.
- `CastSpellByName` took the wrong quoted span.
- A macro casting a pet spell never bound the pet's book.
Reference: each scope sorts by the CRT `qsort` with `_strnicmp` (`0x73f727`, `0x4f0e10`). Slots hold the macro id, with bit 24 set for a character macro, and `DeleteMacro` clears every slot holding it (`0x4e5d10`). `EditMacro` returns the index after the re-sort. The quoted name ends at the next quote (`0x4efef0`). Names bind the player's book, then the pet's (`0x4b3bc0`).
Changed: the table, the slots, the cursor and the saved files follow these, and tests pin each. Bar slots an earlier build saved for character macros are rewritten once to the new ids.
Fixes#1502Fixes#1505Fixes#1187Fixes#196Fixes#197🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clicks and attack starts passed gates 1.12.1 refuses at:
- A right press with a cursor payload went on to the world.
- StartAttack ran without control or while pacified.
- A charmed player's right-click selected and interacted.
- Talk ignored shapeshift forms.
- A plate right-click while targeting skipped the interact.
- A `/target` miss said nothing.
- `/follow` read the cast slot.
Reference: `0x7662c0` runs the press pre-hook `0x492b50` before any hit-test, which drops a payload and consumes the press. StartAttack returns on `0x20000` or lost control (`0x5ecbaa`, `0x5ecbc0`) for every caller. The router tests `CHARMEDBY` first (`0x481f98`), which plates skip (`0x4949f0`). Talk tests form flag `0x8` (`0x5f0150`). `0x489dbd` picks the miss message, and `/follow` reads `UNIT_CHANNEL_SPELL` (`0x60ffb9`).
Changed: each gate follows these, and tests pin each.
Fixes#321Fixes#381Fixes#785Fixes#1305Fixes#1397Fixes#52Fixes#1326🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The readers differed from 1.12.1:
- A TGA's descriptor bit 4 mirrored it.
- A 32-bit TGA with no alpha bits drew with byte 3 as alpha.
- Spell-text slot digits 0 and 4-9 read a wrong slot and `$g`/`$l` parsed their own way.
- MCNR normals were named `x, z, y`.
- A DBC record of the wrong size was read.
Reference:
- No TGA read path tests bit 4.
- `0x5a37e0` admits truecolour only at 24 bits besides alpha.
- A refused texture draws opaque white (`0x44a260`) at sizes `0x4484ae` admits.
- Only '1'-'9' is a slot digit (`0x507827`), and `0x508180`/`0x5082a0` parse the selectors.
- MCNR is X, Y, Z (`0x6aff10`).
- The loader refuses any other record size (`0x545e36`).
Changed: each reader follows these; three TGA edge cases carry `Deviation:` lines. Three BigWigs ready-check icons now draw white, as in 1.12.1.
Fixes#1679Fixes#1693Fixes#1684Fixes#710Fixes#799Fixes#15Fixes#17Fixes#2102🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seven console commands were not registered: `port`, `worldport`, `setrawpos`, `whois`, `togglehelm`, `togglecloak` and `cameraClip`. Three CVar rows also left the reference: `SoundBufferSize` shipped 100, `WorldDetail` was marked as a 1.12 CVar, and `gamma` shipped `1.000000`.
Reference:
- `0x4049e0` registers the movement and toggle commands, `port` and `setrawpos` splitting with the comma (`0x82ef90`) and `worldport` without it (`0x82ecc8`).
- `whois` sends its name (`0x5adee0`).
- `cameraClip` flips or sets its word (`0x50ddc0`).
- `0x457520` registers `SoundBufferSize` 50 above 1500 MHz, which current x86 parts read, and `0x402d70` registers `gamma` as `1.0`.
Changed: each command is registered with its category and its sends, which have byte-exact goldens. `SoundBufferSize` and the mix-ahead ring run at 50, `WorldDetail` is benilla's own row, and `gamma` registers `1.0`. Tests pin each.
Fixes#1639Fixes#1663Fixes#1749Fixes#1270Fixes#1279Fixes#1281🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The game-speed, game-time update and game-time set messages were dropped. The login time packet also applied a malformed body and kept unset fields as 31 and 63.
Reference: each handler reads its body, refuses one not exactly its length with the "Malformed message" line, and decodes an all-ones field as -1 (`0x642b30`):
- **Set** steps back a minute and advances (`0x6434d0`), so an unset field lands at 00:01.
- **Sync** steps forward or banks a skip count (`0x6436c0`).
- **Frame tick** pays the skip (`0x6435a0`). It runs from world entry to logout (`0x401684`, `0x401f04`).
- **Rate** is clamped to [1/60, 60], NaN kept (`0x643820`).
Changed: the four bodies parse with exact-length goldens, and the clock is a port of the reference's, ticking only in the world. The day number uses UTC civil days, a written deviation from `mktime`. Tests pin each step.
Fixes#2056Fixes#2082🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Combat-log lines read wrong in nine ways:
- Your split damage and extra attacks were damage-typed.
- The XP line ignored the group float.
- An item-name wait dropped its line after 120 frames.
- Game objects and items printed "Unknown".
- An XP kill also printed a death line.
- Templates with unused or positional arguments were refused.
- Open-lock effects wrote nothing.
Reference: split form 2 (`0x62df92`) and the extra-attack line (`0x62b190`) type through the buff table unless the spell is hostile. The XP line compares `ftol(xp / float)` with `xp` (`0x637cd0`). The wait list never expires (`0xc4e208`). `0x6264e0` names by type mask. The XP packet marks the victim (`0x60a0e6`). `vsnoprintf` binds `%N$` and skips unused arguments (`0x669888`). `0x62dc10` writes the open-lock line.
Changed: each follows its formatter; one `fill_exact` serves both fills. Tests pin each.
Fixes#2067Fixes#2073Fixes#2068Fixes#2071Fixes#2039Fixes#2041Fixes#2044Fixes#2043Fixes#2046🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four group behaviours were wrong:
- `IsRaidLeader` and `IsPartyLeader` misread a raid leader outside your subgroup.
- A loot method, master or threshold change printed no line.
- The uninvite menus always kicked by name.
- `SetRaidSubgroup` outside 1-8 did nothing.
Reference:
- Both leader tests compare the leader cell with your guid; only `IsPartyLeader` guards a zero cell (`0x4e9141`).
- The group list stores the loot cells and prints each change (`0x4e86f0`, jump table `0x4e880c`). The master line needs a cached name; the threshold reads `ITEM_QUALITY<n>_DESC`.
- `0x5e9510` kicks a held player by name and anything else with `CMSG_GROUP_UNINVITE_GUID`.
- `SetRaidSubgroup` raises its usage line first (`0x4bb9fe`).
Changed: the leader tests read the guids, the list stores and prints the loot cells, the kicks follow `0x5e9510`, and the subgroup is checked first. Tests pin each.
Fixes#141Fixes#70Fixes#267Fixes#266🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The action bar answered and signalled where 1.12.1 does not:
- `IsActionInRange` measured distance before the target gates.
- `GetActionCooldown` answered enable 1 for slots that reach no cooldown query.
- `IsCurrentAction` held item slots against nothing.
- The cooldown and usable events fired off a button's own state.
Reference: `0x4e56f0` runs `0x6e4440`'s relation and implicit-target gates first. Enable starts 0 (`0x4e6f0d`) and only the query sets it (`0x6e1406`). The cast keeps its item's guid (`0xcecab0`). `BAG_UPDATE_COOLDOWN` comes from `0x4f93d0`'s callers, `ACTIONBAR_UPDATE_USABLE` from `0x4e5c00`'s four, and `SPELL_UPDATE_USABLE` from `0x4b31c0`'s fourteen. Placement signals only 213.
Changed: a usable cache refreshes at those producers. The push half runs before the unit snapshots and the events after them, so handlers read this frame's state, after the control edge. Tests pin each answer and the order.
Fixes#1528Fixes#1532Fixes#1537Fixes#1596Fixes#1597🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four glue-screen behaviours were wrong.
- **Realm names.** The list showed realm names raw.
- **Agreement flags.** The terms-agreement flags were reseeded from the CVars on every glue VM.
- **New characters.** A new character's record kept level 0 and no new-character flag.
- **PreviewRider.** Its comment misstated the sheathe.
Reference: the realm list fill turns each of `"*/:<>?\|` into a space and cuts trailing spaces and dots (`0x5b2450`, `0x5b24b0`). The agreement flags are seeded once per process. A level-0 record stores level 1 and sets `+0x10a` (tested at `0x46c203`), and that character enters at its race's intro eye with no loading tip. The select build holds each weapon in hand (`0x472c8a`, `0x47a15d`).
Changed: names are cleaned at the fill, the flags live in one shared set, and the roster stores the flag and looks up the intro eye at the pick. Tests pin each.
Fixes#1215Fixes#1317Fixes#1318Fixes#1217🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Combat text missed most of the words 1.12.1 raises. A damage shield, an interrupt, extra attacks, a resisted proc, an immunity, a dispel, an aura, environmental damage, a drain and a reputation change about you wrote their log line but no `COMBAT_TEXT_UPDATE`. `SPELL_ACTIVE` never fired, and a swing's `UNIT_COMBAT` skipped the victim-state table.
Reference: each formatter raises its word at its own unit test after the line (`0x62cb5d`, `0x626b53`, `0x62db7d`, `0x628b8a`, `0x62d3ab`, `0x628daa`, `0x62d5ce`, `0x62b74f`, `0x62ac74`, `0x627b94`, `0x62c661`). The usable refresh walks the reactive list `[0xb71150]` (`0x4b31c0`). A swing's action comes from `0x83de28`, environmental damage sends a WOUND (`0x624f83`), and the float resolves its global (`0x703bf0`).
Each word rides the existing line path. The reactive list lives from arrival to arrival. Tests pin each word and gate.
Fixes#2027Fixes#2028Fixes#2029Fixes#2030Fixes#81Fixes#82Fixes#370Fixes#2032Fixes#2040🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`scripts/check.sh` ran the scoped tests with the install only, so a test that reads the install without `wow_data_or_skip!` passed every round and failed first in the gates' `test-no-install` leg, at the land.
`check.sh` now runs that leg on its own scope, right after its tests: `WOW_DATA=` set and empty, `BENILLA_REQUIRE_DATA` unset, the same `scripts/unit-tests.sh` and package flags. It builds nothing new, so it reuses the binaries the test step just built; on the three app and UI crates it ran 8434 tests in under a minute.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unit, spell and aura tooltips draw what 1.12.1's builders draw:
- Unit: ?? for a hostile player ten up, the drunk level, the owner, Beast Lore, a group member's Offline or zone line, Resurrectable, contested PvP, Skinnable by skill, GlobalStrings wording.
- Spell: rank on pet and stance bars, cast speed, per-school cost terms, skill-based level terms, and the totem, use-all and reputation lines.
- Aura: stacks multiply, the caster's level scales, raidFilter applies, and the player's slots are read.
Tests pin each line.
Fixes#220Fixes#221Fixes#588Fixes#590Fixes#591Fixes#594Fixes#599Fixes#605Fixes#662Fixes#663Fixes#664Fixes#1184Fixes#1185Fixes#1541Fixes#1542Fixes#1553Fixes#1554Fixes#1558Fixes#1560Fixes#1561Fixes#1566Fixes#1567Fixes#1570Fixes#1573Fixes#1574Fixes#1579Fixes#273Fixes#682🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The logon and world connections and the session clocks follow the reference:
- A world packet above 0x7fff takes a 3-byte size header, read and sent (`0x5b674d`, `0x5b56da`).
- A realm address with no port dials 9090, cut at its first colon, its port read by `_atol`.
- The logon dials one of the realmlist's addresses at random.
- GetNetStats reports the world link's KB/s.
- A transfer abort shows its TRANSFER_ABORT line.
- The game-time speed is held to 1/60 to 60.
- GetGameTime answers -1, -1 until the server sets the clock.
- The ping sequence carries across reconnects.
The handshake timeout, the 23-bit refusal and a port on a named logon server carry `Deviation:` lines. Byte-exact frame tests cover both header forms.
Fixes#1107Fixes#1657Fixes#1662Fixes#1669Fixes#1193Fixes#1656Fixes#1672Fixes#1677🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The update-object handler now runs 1.12.1's two passes:
- It writes every block of a packet first, then runs the field watches at the packet's end against each object's last write. A packet with two values blocks for one object fires once.
- A re-create notifies every field.
- An object that leaves range is released with its fields and reactivated without its create hook, not destroyed.
- A first create runs no watch, so the flag tutorials fire only on a change, from their watchers.
The guild tag rides the same core:
- `TABARD_CANSAVE_CHANGED` comes only from RefreshGuildTag.
- A `GetGuildInfo` miss waits and signals `PLAYER_GUILD_UPDATE`.
- A new `PLAYER_GUILD_TIMESTAMP` re-asks the guild.
Tests pin each pass, the released table and the guild waiters.
Fixes#1391Fixes#1392Fixes#1499Fixes#1418Fixes#1420Fixes#1416Fixes#1417🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The item tooltip setters (bag, inventory, link, merchant, trade, mail, bank, petition) draw the lines 1.12.1's builder draws:
- spell lines for every trigger, and "Already known" by the taught spell;
- PvP rank and medal, zone and map, the gendered reputation label;
- a recipe's product and reagents, and a charter's signatures;
- a link's enchant, and a trade item's proposed enchant;
- the creator, and a second click on the same link hiding the tooltip;
- the merchant price and repair cost.
Charges and durability come from the object, the block or the template, as `0x52d9bd` and `0x52cca8` choose, so a link, a loot row or a merchant item shows no durability line. Tests pin each line and its gate.
Fixes#153Fixes#154Fixes#283Fixes#1183Fixes#1497Fixes#1498Fixes#1501Fixes#1551Fixes#1552Fixes#1564Fixes#1565Fixes#1569Fixes#1571Fixes#1572Fixes#1838🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The combat log, combat text and UNIT_COMBAT follow the reference's handlers:
- An unresolved unit is named at once as the unit getter's miss, not held and dropped.
- Each spell formatter's `Attributes & 0x180`, unknown-spell and empty-name gates apply.
- SPELL_GO's miss list and a missile's arrival write their miss lines, words and UNIT_COMBAT.
- Melee words follow the victim-state table.
- Hostile, party and pet are classed by the reference's tests.
- Honor and XP lines take their side, gender and gates from the bytes.
- UNIT_COMBAT queues at its place against its line in each handler: a swing's flash goes ahead of its line, a spell hit after it.
Tests pin each order and gate.
Fixes#45Fixes#83Fixes#84Fixes#208Fixes#210Fixes#211Fixes#353Fixes#357Fixes#367Fixes#382Fixes#383Fixes#384Fixes#398Fixes#399Fixes#1172Fixes#1240🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows, `the_ui_quad_shader_composes_and_translates` panicked with `ui_quad.wgsl is embedded`. The test finds the shader by comparing each embedded path's string with `benilla_app/shaders/ui_quad.wgsl`, and Bevy's in-memory reader builds a child path with `PathBuf::join` (bevy_asset 0.18.1, `io/memory.rs:239`), so on Windows the string carries `\` and never matches. The same string became each shader's `embedded://` label.
The test now rebuilds each path from its components with `/`, as an asset path is written on every platform.
Test: the test itself, on the Windows CI job.
Fixes#2020🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test gates ran `cargo test`, which runs the workspace's test binaries one after another. On a warm tree the workspace's tests take 103 s that way, and 59 s under cargo-nextest, which runs every binary's tests in one pool; all 10,411 pass both ways.
`check.sh` and `gates.sh` now run tests through `scripts/unit-tests.sh`: cargo-nextest where it is installed, else `cargo test` as before. nextest runs no doctests, so `scripts/doc-tests.sh` runs those of the library crates in scope. Each gate keeps its environment (`BENILLA_REQUIRE_DATA`, `WOW_DATA=`, the skip log), and a failing run's summary now catches nextest's `FAIL` lines. CI is unchanged.
Test: this land's own gates run under it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cast's refusal line, `SPELLCAST_FAILED`, `CURRENT_SPELL_CAST_CHANGED` and `IsCurrentAction` answered after the verb returned, and in-flight state was cleared from many places, each by its own rule.
1.12.1 keeps one in-flight slot and one saved strike (`0xceca88`, `0xcecaa8`). The attempt pushes it, the cancel (`0x6e4940`) and the cast result (`0x6e741a`) pop it, and the report runs inside the call (`0x6e1a00`).
benilla now has that slot, and every cancel goes through it:
- auto-repeat sends its cast before ATTACKSTOP;
- a right-click skin casts through the attempt;
- a click the cursor cannot bind deselects;
- a second Esc re-sends a strike's cancel, the old deviation removed;
- a channel's button unchecks as it begins.
Tests: each behaviour, failing without it; a live trace shows the cast, its cancel and a refusal's cancel in the reference's order.
Fixes#1985, #1990, #1989, #295, #1325, #1986, #1585, #2012, #2015, #1695, #2014, #2021🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows, `vm_parity`'s cos sweep failed on one item, `cos(1e300)`, whose last digit was the host libm's. 1.12.1 never computes it. `math_sin`, `math_cos` and `math_tan` are a bare `fsin`, `fcos` and `fptan` (`0x7fae1d`, `0x7fae4d`, `0x7fae7d`), and a finite operand of 2^63 or more is outside the x87's range and is left unchanged:
- `math.sin(1e300)` and `math.cos(1e300)` answer `1e300`;
- `math.tan` answers `-1.#IND`: `fptan` pushes nothing, so the pop at `0x7fae84` takes the operand and `0x7fae86` stores from an empty stack.
benilla-lua now answers the same. The sweep's recorded answers for that argument are the reference's, the same on every platform.
Test: the parity sweep's `1e300` items for `math.sin`, `math.cos`, `math.tan` and their degree forms.
Fixes#1999🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows, `benilla-app`'s tests died with an access violation (`0xc0000005`) at the second test that opens the audio device. cpal 0.17.3 keeps one device enumerator for the process, made in the first caller's single-threaded COM apartment; when that thread exits, its `CoUninitialize` unloads the DLLs behind it, and the next call goes through a dead vtable (RustAudio/cpal#1302, open, the same `windows-latest` fault).
Every way into cpal now goes through one `host()`, which on Windows first anchors COM's shared multithreaded apartment for the process (`CoIncrementMTAUsage`, the pattern SDL uses) and joins it to build the enumerator, so no thread's exit can tear it down.
Test: two joined threads each read the device and agree.
Fixes#1984🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unit relations answered from scattered stand-ins: `UnitCanAttack` ignored which unit attacks, `UnitReaction` its second unit, `UnitCanAssist` any pair without the player, and duels, FFA, contested guards and charm read wrong.
One model now follows the reference's functions rung by rung, two units in, argument order kept: `0x606170`, `0x6061e0`, `0x606530`, `0x606980`, `0x6066f0`, `0x605ff0`, `0x606ba0` and `0x6067f0`. Every caller asks it (ring, cursor, TAB, plates, tooltip, cast binder, usable, inspect, follow), and the six Lua verbs answer from it.
Tests: duels both ways and the opponent's pet, FFA, contested guards, a charmed player, cooperation, shipped-DBC reputation rows, and the follow and inspect gates.
Fixes#50Fixes#132Fixes#308Fixes#309Fixes#317Fixes#826Fixes#1323Fixes#1545Fixes#1588Fixes#1602Fixes#2008🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a Windows checkout every text file but Rust took CRLF, since `.gitattributes` pinned LF for `*.rs` alone. A test that matches a shader's source on `\n` (`a_fade_twin_blends_by_the_stage_zero_alpha_op`) failed there, and any `include_str!` of a shader, Lua, XML or TOML file read different bytes than on macOS and Linux.
`* text=auto eol=lf` now pins LF for every text file, keeping the two byte-for-byte exceptions (the vendored benilla-re tables and kira). Every file in the index is LF already, so a renormalize changes nothing stored; only what a Windows checkout writes changes.
Fixes#1998🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The party and raid roster as Lua and its events see it did not follow 1.12.1: our raid row came first, a kick or disband kept the old members, status bits and chain ends read the wrong source, and events fired on edges, merged per frame.
Per `SMSG_GROUP_LIST 0x5e6a40`, `RebuildRaidRoster 0x4ba5f0` and the name callbacks `0x4ba1b0`/`0x4bad90`: our row is last and held only with members; endings clear at once (`0x4e84a0`); views read the descriptor or the record; and every signal is logged where the reference raises it, then fired once, in packet order.
Tests: wire-order signal sequences, solo, party, a raid with us in a later group, kick, disband, worldport, and names that arrive late.
Fixes#263Fixes#265Fixes#395Fixes#670Fixes#684Fixes#701Fixes#704Fixes#705Fixes#712Fixes#715Fixes#717Fixes#2002🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A frame in a saved variable writes as its name, as 1.12.1 writes it
- A saved frame's name is read off the model, not a Lua GetName
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
- The combat log fills deDE's numbered holes, so a German client prints its fights
- The fill's comment cites the client's ordered printf
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
The first Windows run (#1984) died inside `benilla-app`'s lib tests with an access violation, which names no test, and `cargo test` stopped there, so no crate after it ran on Windows.
The Windows job now runs `--no-fail-fast`, so every crate reports, and on a failure reruns single-threaded, where each test's name prints before it runs, so the one a crash takes down is the last name in the log.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Lua cast was queued for a later frame, so on the next line of the same script `SpellIsTargeting()` read the old state and `SpellTargetUnit` found nothing to bind, as FuBar_CorkFu and SmartRes do.
In 1.12.1 `CastSpellByName` reaches `ArmCastTargets 0x6e5250` through direct calls (`0x4b4ab0` → `0x4b3300` → `0x6e5a90` → `0x6e4b60`), and `SpellIsTargeting` (`0x6e6cd0`) reads the word the arm stores.
The input pass and key bindings now lend the engine's call host to the VM around each script. The 20 verbs that queued apply inside the call, each under its own input events, and `CURRENT_SPELL_CAST_CHANGED` fires there. Calls an unhosted entry leaves apply at the next lend's start. The one unsafe lend is in `script/host.rs`.
Tests: CorkFu's and SmartRes's lines arm and bind in the call; a refused cast does not arm; every routed verb runs with a host lent.
Fixes#447🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.3's definition asks for the full test suite green on Windows (#798), and nothing runs it there: the gates run on macOS, the Windows check before a release only compiles the app, and CI runs on Linux.
A `windows` job now runs `cargo test --workspace` on `windows-latest`, with no install, for a fork's pull request and on a dispatch, beside the Linux gates. Its checkout uses the runner's own line-ending setting, as a Windows contributor's does, which is where #798's failures came from.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>