Two follow-ups on the interaction latch:
- The NPC's hello and goodbye come from the latch: a window registering for a different non-player unit plays the hello (0x49318f), a release to nothing plays the goodbye (0x493210), and a swap or a walk-away (0x493305) plays none. Every window greets alike; a game object or a player says nothing.
- Death and shapeshift close the window: the player's death releases with the goodbye (0x5e9a51), the latched unit's death releases without it (0x605950), and a shift into a form where IsShapeShifted holds and the row lacks flag 0x8 releases (0x5de07f).
Tests pin the hello and goodbye per window, the swap and walk-away, and each release, and fail on a goodbye where none plays.
Fixes#1287Fixes#1288🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
FontString text is laid out by 1.12.1's line kernel (0x5c7470) instead of a whitespace-only wrap.
- Breaks: the classifier 0x5c7780, after `-`, `:` and `/`, before whitespace, and between CJK and Hangul characters, with its 25 openers and 57 closers. Whitespace above U+0100 is Windows' C1_SPACE set, as the client asks it.
- A row ends at the last break, else at the glyph that did not fit; the skip after it crosses blank lines and stops at a colour code; a glyph wider than the row ends the layout.
- `nonspacewrap` (off by default, the XML attribute and `SetNonSpaceWrap`) carries a force-cut row into the next (0x5c76ea).
- `maxLines` cuts with "..." at maxLines × the line pitch, also for an auto-height string (0x771ec0, 0x7729b0).
An emulation of the client's draw loop, fit walk and EditBox row table matches on 400k fuzzed strings.
Fixes#217Fixes#218Fixes#219🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The gates memo kept its verdict for a change under `docs/` or in a top-level `*.md` only, so a change to an issue or PR template ran the whole chain again. No gate reads the templates; the workflows under `.github/workflows` stay code.
Scripts only.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NPC windows share 1.12.1's one interaction latch (0xb4e2d0). Every opener closes the current holder through the dispatcher 0x493310, in its order, before it opens (0x4930d0); each close core releases on its own cell, so a direct close while latched fires its `*_CLOSED` twice (quest once: it zeroes its cell first, 0x501201).
All 13 windows are on it: gossip, quest, merchant, taxi, trainer, bank, stable, registrar, tabard designer, mailbox, auction, trade and item text. Opening a vendor during a trade closes the trade and sends one `CMSG_CANCEL_TRADE` (0x4bf527); walking away closes a trade at the interaction leash (0x493230). A second gossip menu from the same NPC closes nothing (0x4e1fb8). A chained quest turn-in leaves the panel up (0x5dc6d7).
Tests pin the event order and count for every pair of windows, and the packets sent.
Fixes#1054Fixes#1099🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CVars and Lua arguments, each against the client's code:
- A CVar row carries the reference's refusing callback, run before the value is stored (`CVar::Set`, 0x63df7d): the video rows, six camera rows, `farclip` (NaN passes, as 0x688d40's unordered compare lets it), `frillDensity`, `gxTripleBuffer` (0 or 1, 0x63afe0) and `gxRefresh` (the 11 rates at 0x8645e0).
- `RegisterCVar` creates at most 30 CVars per process (0x488b25); `GetCVar` and `GetCVarDefault` raise their usage lines.
- `Minimap:SetMaskTexture`, `CloseLoot(1)` (ERR_LOOT_NO_UI, 0x48f27b) and the onSelf of `UseContainerItem`/`UseInventoryItem` follow the client.
- Lua's numeral reader has no hex and takes `d` as an exponent, as the client's strtod (0x4112d1).
Each has a test that fails without it.
Fixes#558Fixes#559Fixes#567Fixes#696Fixes#988Fixes#989Fixes#1025Fixes#302Fixes#366Fixes#1275🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four UI-core fixes, each against the client's code:
- Text breaks on `\r` and `\n` before any flag is read (0x5c286e); `|n` is refused only under flag 0x200, which no stock style sets.
- A texture or sound open with flag 1 or 2 asks the install's loose-file index first (0x647f89), then the archives, so an addon's loose file shadows the archive copy.
- A tiled backdrop divides the frame's own rect by the tile size (0x77f0c0), in FrameXML and glue alike.
- A chat frame releases every live link button before placing any (0x788c00), and a resize in screen units, insets times scale (0x788342), relays the lines and rebuilds them.
Each has a test that fails without it.
Fixes#877Fixes#880Fixes#898Fixes#962🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fresh-clone walk-through as a new contributor found four gaps in the docs:
- No quick test loop: `scripts/check.sh` has nothing to check on a clean clone, and `cargo test -p <crate>` was not mentioned. "The loop" now says both.
- The doc-link lint is a full `cargo doc` of the workspace; METHOD and the script's header now say so.
- What to cite while a benilla-re gap is still open: the client address, with the benilla-re issue or pull request linked.
- When `reference/PIN` moves: only with a change that needs a newer benilla-re row, to a commit on its main. Stated in CONTRIBUTING and `reference/README.md`.
Docs only.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`reference/` held benilla's own copies of the reference's tables, some derived by its own scripts from older inputs: the fire-site table kept one event id where a site fires two, and credited glue-screen sites to in-game events.
Now `reference/` is benilla-re's tables at the commit `reference/PIN` names, byte for byte: bindings, events, glue events, messages, event arguments, verb events, the glue surface, the Lua globals, chat types and groups, and CVars. `scripts/sync-reference.py` copies them, and its `--check` runs in the gates and in CI. Four generators and nine old files go, and every reader moves to the new columns.
New tests hold the chat type, chat group and CVar tables to the copies; the CVar rows already filed are listed as known.
Fixes#245Fixes#960
Part of #244🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Contributors and their agents had no word on how to use the reference beyond a link.
- `AGENTS.md` sends an agent to benilla-re before it builds: clone it beside the repo and grep it, or look an address up on its site.
- `docs/CONTRIBUTING.md` gets "The reference": how to look a fact up (the site, `symbols/`, `tables/`, `systems/`), how to weigh a row's confidence, and that a gap or an error goes to benilla-re as an issue or a finding.
- `docs/METHOD.md`, the pull request template and the issue chooser say the same in a line each.
Docs only; `scripts/check.sh` is green.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In 1.12.1, `TargetByName` (`0x489d60`), `TargetUnit` (`0x4899d0`) and `ClearTarget` (`0x489ff0`) commit through `SetSelection` (`0x493540`) before they return, so the next line of the chunk reads the new target. benilla queued them, so `UnitName("target")` on the next line read the old unit.
The bindings now update the `"target"` view and the resolver's guid at once. They rank over a per-frame candidate feed that shares the app scan's candidate test and rank. A pick `SetSelection` refuses (not selectable, `0x4935ee`) leaves the view alone. The unit feed re-pushes the committed selection the next frame. A full tie goes to the lower guid, written as a deviation.
Tests: the chunk reads the pick; namesake ties, refused picks and the re-push, in benilla-ui and through the app's real feed.
Supersedes #1225, whose branch GitHub would not let us push to.
Co-authored-by: kalistoking <148718830+kalistoking@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- A MessageFrame's link is text; only chat lines take link buttons
- set_spans' doc says only a ScrollingMessageFrame's spans take buttons
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
Every shown EditBox now keeps the client's text window, focused or not. An unfocused box used to cut long text to "prefix..."; it now shows where its caret last left it, re-windowing at 0.25 or 0.75 of the width when the caret leaves (0x77d705–0x77d858, 0x77d320). A password box never re-windows (0x77d769) masks one `*` per byte, and copies as an empty string (0x77e1f8). A window starting inside a coloured run keeps its `|c` code (0x77d8ba).
OnCursorChanged reports the font's line height as `h` (0x77b970), and measures `x` from the window's start (0x77dcd8) by the text's justify (0x77db63).
Paste types the clipboard one character at a time through the typed-character insert (0x77e250 → 0x77c200), so `|` doubles, a tab is four spaces, and OnChar fires per character.
Tests cover each through the Lua surface, plus an app test drawing an unfocused window.
Fixes#728Fixes#739Fixes#740Fixes#744🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
benilla's reference facts come from reading the 1.12.1 client at its bytes. That work is now public as [benilla-re](https://github.com/samwhosung/benilla-re): one row per function, global and field, a doc per system, and the data tables, each cited by client address.
This links it from the README, from METHOD's "The reference first" and from CONTRIBUTING's step on stating the reference fact, so a contributor knows where a client address leads. Docs only; no code changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seven Lua API and event edges departed from 1.12.1:
- SetTradeSkillSubClassFilter and SetTradeSkillInvSlotFilter now raise their errors (0x4ffc70, 0x4ffe60).
- RollOnLoot sends any roll type (0x61bdf0).
- TogglePetAutocast drops a held pet action instead of toggling (0x4be290).
- GetInboxHeaderInfo's hasItem is 1, not the count (0x4aef38).
- UPDATE_BINDINGS fires only from SetBinding and the copy into set 3 (0x4b8056, 0x4b649d).
Mail now follows the result handler 0x4ad050. Take, return and delete edit the rows in place, with no re-list, and each arm fires its own MAIL_INBOX_UPDATE. MAIL_FAILED fires for every result, after the request lock lifts (0x4ad164). Every list result fires MAIL_INBOX_UPDATE. Taking a template mail's item keeps the letter (0x4ad7fd).
Tests cover each.
Fixes#1048Fixes#1049Fixes#1055Fixes#1072Fixes#1071Fixes#1053Fixes#1052Fixes#239🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the stock credits screen the fades drew as solid black boxes over the text, and the black bars covered the borders.
A texture's gradient now tints its four vertices over the texel (0x77f910, TL BL TR BR). A gradient alone draws nothing, and any vertex-colour write collapses it (0x77f750).
A layer batch sorts its quads by texture handle at End (0x7731a0), with an exact port of the client's qsort. Its tie order was checked against the client's own code. The client's handles are heap addresses, so different textures rank by a creation stamp that follows allocation order. That is a narrowed Deviation.
Extract costs what it did before. The options window's ground dim moves to BORDER to keep its look.
Tests cover the gradient, the sort and the stock credits frame.
Fixes#874Fixes#1095Fixes#1092🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Leaving the world (a /reload, logout or worldport) kept loot, trade, mail, the auction house and the other NPC windows open, and a reload kept the target. A loot release for another object, or with result 0, closed the open window.
In 1.12.1, the sweep 0x490a80 closes each window in its legs, in the dying interface: loot with its release, trade cancel, mail, auction, gossip, trainer, taxi, merchant, tabard, stable and the rest. A /reload first drops the target loudly and ends a follow; a logout runs the sweep first and clears the target silently. The release response closes only for the latched guid with a nonzero result (0x5ec090).
benilla now runs those legs in that order on each root, gates the release, and lets each leg drop its own item locks.
Tests cover each leg, each root's order and the release gate.
Fixes#1066Fixes#1044Fixes#209🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failing test gate printed only its log's last 30 lines. Those are the doc-test runs after the failure, so the failing test's name and its panic were missing from both the gate output and the land log.
scripts/gates.sh and scripts/check.sh now print the panic lines, each test's FAILED line and any compiler errors from the log first, then the tail as before.
Checked on a sample failing log; both scripts pass `bash -n`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The glue booth threw its model away where 1.12.1 keeps it. So the create preview restarted its animation on every customization or class change, and the login scene restarted after Credits.
In 1.12.1, the create setters (0x478790 and its siblings) and the re-dress 0x470800 update the same CharacterModel. Only a race or sex change builds a new one (0x470b70). A hidden ModelFFX is unlinked from its strata bucket, so its OnUpdate (0x76d7f0) and its clock stop until it shows again, and the clock steps in whole milliseconds (0x76d846).
Now the booth re-dresses its body in place and keeps a hidden or left scene parked, resuming it on return. Each scene has its own clock (one SceneClock on its root), its own materials, so two scenes on one file stay independent.
Tests cover the keep, the park, the clocks and the cleanup.
Fixes#1083Fixes#1068🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SMSG_SET_FORCED_REACTIONS (0x2A5) was never read, so a forced-reaction aura such as the Gordok Ogre Suit left the ogres hostile.
The client stores the table whole (handler 0x4d59a0) and looks a target faction up in it (0x4d6490). Unit-to-player reactions consult it at 0x6065e6, behind the leading rungs and capped at 6. Player-to-unit reactions consult it at 0x6063e0. On arrival the handler fires UNIT_FACTION for every bound unit. ShutdownGame clears the table, never a worldport.
benilla now decodes the table onto Reputations, answers both reaction directions from it, fires UNIT_FACTION on each arrival, and clears it at the session's end.
Tests cover the decode, both directions against an at-war faction, the caps, the replace and clear, and the target frame recolouring.
Fixes#78
Supersedes #1080
Co-authored-by: kalistoking <148718830+kalistoking@users.noreply.github.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Four stock glue screen defects:
- Credits raised a Lua error: PlayCreditsMusic was unbuilt. It now fades the theme and plays the seven city intro tracks in turn (0x45b000, table 0x836028).
- HideCursor/ShowCursor (0x46db70) only set a flag. The pointer now hides over the credits and the movie on every platform.
- A world status past the 83-row table read RESPONSE_FAILURE's text; GetStatus formats it "(N)" (0x5aa960).
- A plain Model pane (the realm wizard) was lit only by the widget's light. It now takes its M2's own lights, as 0x718960 emits them, under the widget light and fog.
Also writes the Deviation line for the config save: benilla saves a quiet second after a change, where SaveConfig (0x63d980) writes only at world entry, logout and shutdown, so a crash keeps the settings.
Tests cover each.
Fixes#1070Fixes#1069Fixes#1058Fixes#1082🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scroll child drew by its own strata and level. So on the stock create screen the info text drew over the icon frames raised beside it.
In 1.12.1, SetScrollChild (0x786ce0) flags the child's subtree. The flat pass skips flagged frames (0x7659df). The ScrollFrame's HIGHLIGHT callback (0x787220) draws the subtree clipped to its hit rect, frame by frame in link order (0x76b3f0), and draws nothing for an empty clip. Picking keeps each frame's own level, clipped to the first unflagged ancestor (0x76b020).
benilla now draws and picks the same way. A frame shown or hidden inside the subtree keeps the others' order. The options window's scroll trough sits at its ScrollFrame's level, so the page still draws over it. Deviation: a scroll child reparented out draws once, not twice.
Tests cover the order, nesting, clip, pick and the stock create panels.
Fixes#1084🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The glue manager's verbs queued their work for a later frame. In 1.12.1 each verb calls CGlueMgr's function, which writes screenState and signals its dialog before the call returns, then begins its operation. So a dialog that its own OnShow dismisses was drawn for a frame here.
Now each verb runs its synchronous half inside the binding, against the one host GlueMgr the VM shares. Network work stays asynchronous. The Update arms (0x46b930) fire their signals before their later state writes. A failed world connect's "Retrieving realm list" closes on its reply, and a failed logon closes the link but keeps an unattended resubmit.
GetServerName reads the realmName CVar and the selected realm's flags live (0x46d280).
Tests cover each verb's events inside its call, a verb nested in another's fan-out, each arm's order, and the refused login never showing its first dialog.
Fixes#1065🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Five item-use paths differed from 1.12.1's bytes:
- PutItemInBag swapped a worn item instead of storing it (0x4c7d44), and sent a bag into itself (0x4c7cea).
- UseSoulstone skipped the cast cancel before its use (0x48ae6b → 0x6e4940).
- The bar's equip left a held item on the cursor; it now runs 0x5e1840 → SetCursorItem.
- The bind-on-equip confirm emptied the cursor; the item now stays held under the question (0x5e16ce).
- Item use ignored loss of control. The dispatcher's requirement and control gates (0x5d8e0e, 0x5d8eaa), the equip and SetCursorItem gates, the fear/charm exemption (0x6e9d70) and the trade-window refusal (0x1b4) now apply. They read our own character's control only (0x5fa600).
Tests cover each path and gate.
Fixes#1057Fixes#1075Fixes#1078Fixes#1076Fixes#1077🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Verb events fired from app feeds a frame late; 1.12.1 signals inside the call.
Now each verb raises its events in place. Feeds push only what their own server source changed, so a verb's write is never rolled back.
Also, from the bytes:
- The score board waits on pending names (0x4aa216).
- The recruitment cascade runs inside its verb.
- Seven mail verbs share the request lock 0xb6efc8; the mailbox keeps its inbox across a close.
- StartAuction runs its gates, sends the attached guid and empties the slot in the call.
- The tradeskill repeat counts, disarms and resets as 0x4fd7b0 and 0x4fc750 do.
- Quest folds carry across a /reload.
Tests cover each event's timing and the feed rule.
Fixes#1008Fixes#1009Fixes#1010Fixes#1045Fixes#1046Fixes#1047Fixes#1050Fixes#1051Fixes#1059🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stock character create screen had none of its 24 verbs.
They now follow table 0x8380f8:
- the reset draws sex, race, then the five customizations, with class 1 (0x470900);
- a race change redraws and takes the old list's first class (0x470cf0);
- the cycle lands on an end rather than wrapping by modulo (0x470f80);
- GetRandomName builds names from NameGen trigrams (0x475480);
- CreateCharacter checks the name (0x4711e2), then shows CHAR_CREATE_IN_PROGRESS and sends CMSG_CHAR_CREATE in screen state 5.
Success fires SELECT_LAST_CHARACTER and returns to the select; a refusal shows OKAY with its text. The preview stands in the glue booth.
Hair styles and each style's colours now come from CharSections (0x47a3e0, 0x47a400), so an Orc female has 8 styles. A NameGen that fails to load empties only the random name.
Tests cover each verb, the wire and the create flow end to end.
Part of #856Fixes#1073🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Item locks came from pending-op bookkeeping, so unlocks reached the wrong items or none.
Now a lock is a bit per item guid, set by LockItem (0x4953e0) and lifted by UnlockItem (0x495420) at the client's own sites:
- pickups, places, swaps, the delete and the wrap;
- UseContainerItem's arms, taken in the call in 0x4fa0e0's order (bank, deposit, sell, equip, open);
- the watchers: a seat's guid changing, an arrival, a restack;
- the inventory and sell refusals, loot closes, the bind-confirm cancel, the revive.
Readers refuse a locked item where the client does (0x4f9e76, 0x5d8d32, 0x4fa1cd, 0x4fa4bf). Ammo equips by class and unlocks.
A /reload carries the locks and the cursor, minus what the sweep's loot, mail and auction legs unlock. A despawned item loses its lock.
Tests cover each site, reader, watcher and the reload.
Fixes#1033Fixes#242Fixes#228Fixes#354Fixes#227🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stock character select had no EnterWorld.
EnterWorld follows 0x46b500:
- it returns silently with no realm, no record or no world link;
- flag 0x4 opens OKAY CHAR_LOGIN_LOCKED_FOR_TRANSFER and flag 0x4000 fires FORCE_RENAME_CHARACTER, neither sending;
- otherwise it writes and saves lastCharacterIndex, enters state 8 and logs in, the last call of a frame winning.
A refused login (0x5aad70) sets state 9, whose OKAY button re-enumerates on the same link.
The rebuilt glue is now seated before SET_GLUE_SCREEN, so a logout's select asks for its list. A refusal queued in the same drain as the entry now wins, natively too. The login-send doc is a Deviation line stating the reference's load-then-send.
Tests cover each guard and arm, and the entry, refusal and logout flows over the fake world.
Part of #856
Part of #977
Part of #79Fixes#979🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stock character select had no DeleteCharacter or RenameCharacter.
DeleteCharacter opens CHAR_DELETE_IN_PROGRESS and sends the delete (screen state 6). On success it fires SELECT_FIRST_CHARACTER and fetches the list again; a refusal shows OKAY with the status text (0x46c173).
RenameCharacter follows 0x473520 and 0x46b340:
- it needs flag 0x4000;
- the same name, or a failed name check, reopens the dialog with the key;
- otherwise it sends CMSG_CHAR_RENAME (state 7).
The reply (0x46b440) tells apart only result 0x31. Success renames the record, clears the flag, moves the character's local files (all or none, as one folder move) and fires CHARACTER_LIST_UPDATE.
The client's name check (0x5abd50 → 0x6c96c0), with NamesProfanity, ChatProfanity and NamesReserved compiled once, is shared with create.
Tests cover each verb arm, the wire, each name code, and both flows end to end.
Part of #856
Part of #79🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stock glue AddOns screen had no verbs to call, and SMSG_ADDON_INFO was read only for its status.
The glue AddOns verbs now run over the addon registry and an enable store kept in benilla-config, out of the AddOns folder; old enable files move there once. Reading SMSG_ADDON_INFO fires ADDON_LIST_UPDATE. Each record's status, key, revision and url go to the addon sent in the same position (0x51da70). The url is a C string of up to 256 bytes (0x4191b0), and addons past the last record keep its status. A banned addon and those that depend on it read as banned on both screens. IsAddonVersionCheckEnabled tests the CVar's integer (0x51f180). LaunchAddOnURL keeps 0x429ff0's decode. Deviation: it then re-encodes everything outside the URL characters and opens only http(s) with a host.
Tests cover each record shape, the pairing, the decode, the migration and the banned gate.
Part of #856Fixes#1013🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The cursor verbs follow the client's arms, on top of the primitives from #1038:
- **Action bar.** PickupAction places only what the bar can take (`0x4e615d`) and picks up over anything else. Bar spells and macros lift with their own setters, so `CursorHasSpell` sees them. Dropping the slot's own action, or a spell its macro or item casts (`0x4e5a50`), only clears the cursor.
- **Bags and doll.** Clicks pick up over a held spell, macro or coins, and clear a held bar item (`0x4f9de9`, `0x4f9f41`). A misfit doll drop auto-equips (`0x4c78ef`).
- **Trade.** A slot click swaps the old item back (`0x4c0036`). An offered item stays locked until the server closes the trade, and the close fires the unlocks between its two signals.
- `SplitContainerItem` refuses a count outside the stack.
Fixes#1031Fixes#1032Fixes#1034Fixes#1035Fixes#1036Fixes#1037Fixes#156🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stock login's Breaking News box (`WOW_GLUE_STOCK=1`):
- At glue init, as `0x46a400` does, the client reads the Lua global `SERVER_ALERT_URL`, which the player's own GlueStrings or a server's patch sets. It fetches the URL once on a background thread: plain HTTP, 5 s timeouts, redirects followed.
- The body builds the reference's buffer (`0x46a580`: 0x7ff cap, appended chunks). A case-insensitive `SERVERALERT:` prefix fires `SHOW_SERVER_ALERT` once from the update (`0x46b939`). Every failure is silent.
- `LaunchURL` opens the browser only for URLs that pass the reference's gate (`0x5abc10`): `http://`, a strict charset and the 10-host whitelist. An over-long host is refused rather than cut.
- No fetch or browser ever runs in a test, capture or unattended run.
Tests run against a local fake server: prefix, 404, redirects, chunked, timeouts, caps, and the frame shown end to end.
Part of #1026🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Part 4b of the stock GlueXML (`WOW_GLUE_STOCK=1`): the stock screens' ModelFFX scenes now go through the glue booth instead of the model-tile path, which drew them black.
- The scene, look and facing come from the glue VM. `SetCharSelectBackground` runs the frame's own SetModel and swaps its fill callback (`0x472128`, `0x472144`), so the select scene fogs until a character sets its stage.
- An ModelFFX pane draws the booth's scene at its own bucket's ARTWORK layer (`0x46fa90`), framed at the pane's aspect with the diagonal law (`0x7ada40`, `0x5c3cc0`). The native box Deviation now holds for the native screens only.
- Each SetModel restarts the stage. The select glow holds through an empty roster until the screen shows again.
- An empty roster shows the Orc scene fogged, and after the last delete the last stage stays, as `CharacterSelect.lua` leaves them.
Part of #856Fixes#978🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cursor now runs on the client's two primitives. `ClearCursor(a,b)` (`0x495190`) cancels an armed gift wrap, then runs the payload's arm (an item's unlock fires `ITEM_LOCK_CHANGED`; coins fire `PLAYER_MONEY`), then the grid hide and `CURSOR_UPDATE`. Each setter runs that clear first, then its own tail (`0x494b60`, `0x4950f0`, `0x494d20`, `0x494f80`, `0x494e20`, `0x495010`). Every pickup, place, split, sale, trade, mail and auction path goes through them. So a `CURSOR_UPDATE` handler reads the emptied cursor, and `PickupSpell`/`PickupMacro`/`PickupPetAction`/`SplitContainerItem` clear what is held instead of refusing.
An item attached to mail or an auction stays locked until it leaves the slot (`0x4adef0`). Attaching to an occupied slot puts the old item back on the cursor.
Event-order tests cover each setter, the coin clear, and the attach lock and swap.
Fixes#1011Fixes#1012Fixes#1006Fixes#1007
Part of #156🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lua bindings answer the reference's shapes on misses, bad indices and tooltips:
- Indexed getters push their full miss tuple (`GetCraftInfo`, `GetPetActionInfo`, the trainer requirements and others). Indices go through `ftol` and an unsigned compare, so a negative misses instead of raising. Verified slot readers keep their own raises (`IsInventoryItemLocked`, `0x4c8520`).
- The eight GameTooltip setters return the builder's refresh flag, gated as `0x53267c` gates it, so a hovered cooldown tooltip refreshes. `LOCKED` follows the item object (`0x52bdb3`).
- Quality goes through the InventoryType gate (-1), and a pending template shows the question-mark icon.
- `GetLootSlotInfo` returns four values. `SetSpell`, `CreateMacro`, `JoinChannelByName`, `GetQuestReward` and the shapeshift and trade-skill filters raise or answer as the reference does.
- The shape gate probes each getter's miss legs through an argument ladder.
Fixes#145Fixes#993Fixes#994Fixes#995Fixes#996Fixes#997Fixes#998Fixes#1016🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Key input follows the client's walks: key-down (`0x765f10`), char (`0x765df0`), key-up to the recorded consumer (`0x765fd0`) and repeat (`0x766070`).
- EditBoxes are in the walk from creation (`0x779ced`), so the stock login's boxes take typing; only a focused box consumes. A box declines PRINTSCREEN and unaltered arrows on an `ignoreArrows` box, so stack-split steps under a focused chat box. A binding's press records the world frame.
- Repeats follow the pressed-key list, so a held key resumes after alt-tab. Chords reach a box only through the walk. Keys the client can't name walk nowhere.
- The stock login reads the agreements before its first screen. Cinematics returns to login (no movie player; Deviation). The Realmlist box has its middle border.
- Alt+F4 closes the window on Windows and Linux (Deviation: the reference relies on the OS).
Fixes#179Fixes#1003Fixes#1004Fixes#1005Fixes#1019🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1.12.1's MSG_MOVE_TELEPORT_ACK handler (0x603f90) reaches none of the
loading screen's raises: 0x406800 is entered only from SMSG_TRANSFER_PENDING
(0x401900), the glue EnterWorld (0x46b6f1) and 0x407de0. benilla raised the
cover on a same-map snap twice over: the "focus not resident" backstop on the
snap frame, and a >=100 yd relocation raise. The relocation raise goes, and
the backstop stays off while a snap's settle hold is up; the body is held over
the destination as before.
Verified live against the local realm (map 0, Gmagent): .tele ironforge from
Northshire (~3960 yd) and back covered 50 and 93 frames (0-916 ms, 0-1680 ms)
before, 0 and 0 after; cross-map .tele valley and back still raise the cover
("worldport"). New tests in loading_screen.rs fail on the old code at the snap
frame. benilla-app and benilla-world lib tests green, install-reading tests
green with BENILLA_REQUIRE_DATA=1 (the addon corpus is absent on this
machine). Built on macOS (arm64).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In 1.12.1, logging out or a refused character login returns to character select on the same world connection and re-lists there (`0x5aaeb0` → `0x401ee0`, then `0x46b880` → `CMSG_CHAR_ENUM`). There's no realmd logon and no new `CMSG_AUTH_SESSION`. Enter World closes the realmd link (`0x46b70c` → `0x5b3320`), and only the glue's own re-dials reopen it.
- benilla used to drop the socket and log on again from cached credentials. Now the world session splits and rejoins on one connection, and the header cipher carries its state across.
- The realmd link closes at the pick.
- A leftover packet at character select is skipped rather than fatal.
- The write thread applies a pending writer swap before any command.
- A kept-link teardown ends the character without a "server down".
Fake-server tests pin one logon, one auth and one more enumerate. The logout smoke fails without a same-link relist.
Fixes#984Fixes#983🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In 1.12.1 an event a Lua verb raises runs its handlers before the verb returns: `SignalEvent` (`0x703e50`) walks the listeners in place. About 35 benilla binding sites queued theirs for a later drain instead.
- Verbs now raise inside `tick::verb` steps, and the events fire as each step's model borrow drops. A step follows the reference's split wherever it signals before changing state a getter reads: the money pickup, bag pickup and place, merchant grab, repair cursor, item use, action and pet-bar pickup and drop.
- The grid events fire before `CURSOR_UPDATE`, and a clear's unlock before both, as at `0x4952b3`/`0x495455`. `ITEM_LOCK_CHANGED` carries no arguments.
- `pending_events` is host-only now.
Tests pin handler order per family, nesting from a handler, and the order changes.
Fixes#999Fixes#582🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part 4a of the stock GlueXML (`WOW_GLUE_STOCK=1`): character select's roster.
- The select verbs run on the reference's roster (`0x4736c0`): `GetCharacterInfo` (race and class from `Name_Lang`), `SelectCharacter` with its clamp and event, the facing gate, and `GetCharacterListUpdate`. They are backed by the roster load and teardown (`0x4724d0`, `0x472580`) and screen state 3.
- Under the stock screens the character list is fetched only when the screen asks, as `0x46b880` does. The world connect completes on the auth response (`0x5aab80`), not on a roster.
- Every character-park request goes through one ordered channel. A roster reply completes only the current link's query. A late auth after a cancel is dropped, Warden stays terminal, and a failed list shows `CHAR_LIST_FAILED`.
The native screens are unchanged. Fake-server tests cover the ordering, the cancel, the realm change and the native policy.
Part of #856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- `RegisterEvent` and `UnregisterEvent` match a name against the client's event table case-insensitively and drop a name that isn't in it (`0x702140`). A new gate checks that every event name the code fires is in the table.
- A visible XML frame resolves its layout at the post-load hook (`0x76a310` → `0x7680e0(1)`). So its first `OnSizeChanged` fires before its `<Frames>`, `OnLoad` and `OnShow`, as in 1.12.1. A hidden frame still waits for its show.
- `pending_events`' doc now says what the queue is: `SignalEvent` (`0x703e50`) runs handlers in place, and the Lua verbs that still queue are a gap.
- #890 was already fixed by #907; two stale comments around it are corrected.
Fixes#959Fixes#972Fixes#560Closes#890🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lua bindings now return what 1.12.1 pushes, in type and count:
- Flags are 1 or nil, never a Lua boolean. This covers `SetInventoryItem`/`SetBagItem` hasCooldown, `GetQuestLogLeaderBoard` finished, `GetContainerItemInfo` locked/readable, `AtTop`/`AtBottom`, `IsOwned` and `SetAlphaGradient`.
- Eleven verbs whose handlers end `xor eax,eax` return nothing. That covers `PickupAction`, `PlaceAction`, `PickupSpell`, `PickupInventoryItem`, `EquipCursorItem`, `SplitContainerItem`, `PickupMacro`, `DeleteMacro`, `ResetChatColors` and others.
- The miss tuples: `GetInboxHeaderInfo` gives 13 values (`0x4aeff0`), `GetInboxText` four nils, and `GetQuestLogLeaderBoard` three.
- `GetTrainerServiceDescription` is nil with no text (`0x4d9d44`). `GetZonePVPInfo`'s faction is `""`, not nil (`0x48d633`).
The boolean gates now probe every global and widget method.
Fixes#442Fixes#574Fixes#577Fixes#578Fixes#822Fixes#823Fixes#872🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addon loading follows the client's `AddOn_Load` (`0x51f240`) and `.toc` reader (`0x51c9b0`):
- One driver serves the startup walk, `LoadAddOn` and the survey harness. It marks the addon loaded before its dependencies (`0x51f313`), so a requirement cycle loads both and `IsAddOnLoaded` answers during an addon's own load. It loads `## LoadWith` dependents after `ADDON_LOADED` (`0x51f5b2`).
- Every directive is matched by its own literal. `LoadOnDemand`, `Secure` and XML bool attributes parse with the client's string-to-bool (`0x6f1b30`), so `hidden="1"` works. Values are not trimmed, and only an unindented `##` line is a directive.
- `GetAddOnMetadata` reads the metadata map (`0x51d580`): Title defaults to the folder, the last value wins, and locale suffixes apply. The url comes from the server, never the `.toc`.
- `EnableAddOn` keeps a toggle for an addon that isn't installed.
Tests are mutation-checked against the reference order.
Fixes#192Fixes#193Fixes#405Fixes#986Fixes#987Fixes#990🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lua bindings now read their arguments the way the 1.12.1 client does:
- Numeric strings parse as Storm's `SStrToInt` (`0x64ac60`) and `SStrToFloat` (`0x64aaa0`). That covers channel keys, `GetNumber` and `item:` fields.
- Flag arguments go through `GetBoolOrDefault` (`0x6f1c10`) with each site's default. That is about 40 bindings, `SetChecked()` and `ShowHelm()` among them.
- Colour channels take numeric strings (`lua_tonumber`), and alphas are `isnumber`-gated: no raise, 1.0 by default. A three-argument `SetVertexColor` keeps the current alpha (`0x79ae0a`). So action-bar rings dimmed by a drag stay dimmed, as in 1.12.1.
- `SetTexture("1")` takes the colour form. MessageFrame channels follow `0x795590`.
- `LoggingChat`/`LoggingCombat` return the flag.
Binding-level tests use the reference's values.
Fixes#135Fixes#144Fixes#160Fixes#161🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part 3b of the stock GlueXML (`WOW_GLUE_STOCK=1`): the stock `RealmList` and `RealmWizard` run on the reference's ten realm verbs (`0x46f480`).
- The realm park is now a cancellable operation. Each dial carries the cancel generation it was asked under, so no Cancel is lost, and a status Cancel closes the logon (`0x5b3320`).
- Change Realm with the world up drops the session first (`0x46b22b`).
- The roster arm (`0x4724d0`) sets the chosen realm, or asks for the list when the roster is empty.
- An empty realm list is shown, never dialled at the world port.
- The native type sort puts PvP before RP.
Deviation lines cover the re-dial's full logon, comparator ties, a missing Region row and the native close.
Part of #856 and #834Fixes#833Fixes#974Fixes#982🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- **The layout file** now saves what the reference's writer does (`0x490e60`): `X`, `Y`, `W`, `H` and `FrameLevel` from the frame's held rect, rows in creation order, and only frames that are user-placed and movable or resizable. The restore (`0x4905e0`) pins TOPLEFT to the screen, sets the size and level, and resolves each frame as it goes (`0x490710`). Old anchor-form files still load and are rewritten once.
- **Chat bubbles** re-rank only on a camera move or a new line (`0x4b11ff`), and a repeat speaker moves to the tail, as `0x4b1870` links it.
- **The live re-layout on a window resize** gets its `Deviation:` line. 1.12.1 reloads the UI on an aspect change of 0.001 or more (`0x492f26`).
Tests cover parent/child level restore, the held rect, the hidden-frame resolve, the junk lines, the migration and the bubble tie order.
Fixes#954Fixes#955Fixes#847🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unattended run (`WOW_UNATTENDED`, `WOW_RIG`) resolved the same `benilla-config/` as a person. In a dev build, every linked worktree maps that to the primary checkout's folder. So a probe read the player's CVars, bindings and layout, and wrote its realm, settings and window positions back over them.
`local_state::home` now gives an unattended run a fresh folder of its own (`$TMPDIR/benilla-run-<pid>/benilla-config`) and prints it at boot. `BENILLA_HOME` still pins one, and a capture still resolves none. CONTRIBUTING's "Running it unattended" says so.
Test: `an_unattended_run_keeps_its_own_state_folder` checks the rig path, the override and the capture, and that the folder is never the resident one.
Fixes#975🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Player reports and the trace, in one batch.
- SetFactionInactive/SetFactionActive raise `UPDATE_FACTION`, so the list moves at once.
- NPC greetings: the latch, a dead unit, then the reaction pair with the active mover (`0x60c270`); a dead or ghost player hears none. Only a passing click re-tracks the NPC, and a window hello resets it (`0x60c3d1`).
- "You are now <rank> with <faction>." prints on first contact and rank change (`0x62c670`), in packet order, with the real gender.
- A standing packet prints its "increased by" line for the first entry only, never a hidden one (`0x4d57dd`).
- `WOW_MOVE_TRACE` carries each packet's bytes as hex; chat bodies are opt-in, and dot/bang commands and channel passwords are always redacted.
Tests cover each gate, the line order and the redaction.
Fixes#927Fixes#882Fixes#881Fixes#867Fixes#965Fixes#966Fixes#967🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hidden frame resolved and fired `OnSizeChanged` at once, a getter's resize fired at the drain, and `GetWidth`/`GetHeight` answered the rect over scale.
In 1.12.1 hiding sets the deferral flag `0x2` (`0x76ad50`); while set, an invalidate marks the frame pending and passes it to its dependents without a resolve (`0x768178`). Show resolves it first (`0x76ae58`), a getter resolves a pending one and fires `OnSizeChanged` inside the read (`0x7a1980`, `0x76b580`), and an anchor read pulls it for good (`0x7a2fc0`). `GetWidth` answers the set size when non-zero, else the rect span, unscaled (`0x7a1e93`, `0x7a1ee6`).
The deferral is modelled for frames and regions, getters fire inside the read, layout ids mint in handle order (deterministic), and the options window measures its scroll by its edges.
Tests bite on the parent; the verifier is green; stock quads match.
Fixes#948Fixes#971🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under `WOW_GLUE_STOCK=1` the stock login screen now logs in. Separately, realmd's 0x0E answer was refused by the native login too.
In 1.12.1 the glue manager runs the screen states (`0xb41da0`): `DefaultServerLogin` (`0x46afb0`) needs the agreements accepted and opens the status dialog; each tick re-texts it from the logon's state and result (`0x46b140`, tables `0x836b40`, `0x836b78`); the world connect re-texts it every frame (`0x46be8b`). The agreements seed from `readTOS`/`readEULA` (`0x46a400`). Realmd 0x0E is a success (`0x5b2cd4`); a proof's 0x0A changes nothing (`0x5bada2`).
The glue core verbs, the glue manager over the native login, the agreement CVars, `SimpleHTML file=`, case-insensitive loose files and a benilla Realmlist layer file are built; the protocol accepts 0x0E. A recorded `Deviation:` keeps the interface signature checks off.
Tests: fake-realmd refusals and 0x0E; stock-screen login reaching the native submit.
Part of #856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With `WOW_GLUE_STOCK=1` (dev builds), benilla now loads the stock GlueXML off the player's own patch chain into the glue state and draws it; with the switch off nothing changes.
In 1.12.1 the glue builder (`0x46a7b0`) opens `Logs\GlueXML.log` (`0x46a8ce`), runs `GlueXML.toc` through the shared toc runner (`0x6edb90` at `0x46a9b3`), fires `FRAMES_LOADED` (`0x46aa34`) and opens on "login" or "movie" (`0x402b3b`), or "charselect" back from the world (`0x402084`). Glue `GetScreenWidth`/`GetScreenHeight` answer the root unscaled (`0x46dd30`, `0x46dd80`).
Under the switch the glue state replaces the boot state, takes input, and fires `SET_GLUE_SCREEN` on those edges; the native screens are hidden but still drive the network. A data-gated test loads the stock files with only seven unbuilt verbs raising, a list that may only shrink.
Part of #856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>