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>
The solver took any node's recomputed rect whenever it resolved, because about 55 layout writes named a node without invalidating it. So a freed region stayed in its dependents' anchors, `SetTexture` re-resolved a failed texture, and a scroll child kept its authored anchors behind a per-solve override.
In 1.12.1 a node re-resolves only when invalidated or a target stores a new rect (`0x768d20`, slot 0 `0x768cf0`). `SetTexture` only dirties the draw (`0x770200`). A destructor frees its dependents' anchors to it (`0x768e20` → `0x767e70`, no invalidate). A scroll child is re-anchored to one TOPLEFT (`0x787100`).
Every write now invalidates exactly where the reference's does, and the solver's guess, the hold and the art latch are gone. The tooltip invalidates only on its relayout (`0x530640`).
Tests bite on the parent; the verifier is green; the stock UI's quads are byte-identical.
Fixes#947Fixes#949Fixes#958🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first piece of running the stock GlueXML: benilla had no glue Lua state, and no reference data for its surface.
In 1.12.1 the glue state holds the base set (`0x7039e0`) and seven registrars (`0x46abb0`): 119 functions, 105 of them glue-only, with 26 glue events (`0xb41e70`, `0x46a88f`). `RegisterEvent` matches names case-insensitively and drops an unknown one (`0x702140`). The glue builds the 13 base frame types plus `ModelFFX` (`0x46a4fe`), a Model with its own show and paint (`0x46fa00`); the in-game types come only at world entry (`0x495940`).
`reference/` gains the glue event and surface tables, generated and checked against the shapes table. `UiScript::new_glue()` builds that state; a gate holds its globals to the table, with the 108 unbuilt names listed. Nothing in the app changes yet.
Part of #856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A title-region press refused while another drag held the slot, `SetLayoutMode` stored nothing, and a press on a chat link made the chat frame itself the target, so every link click started a title move of the window.
In 1.12.1 the drag entry takes the slot unread (`0x7652b0`); a mid-drag press goes to the frozen hover (`0x76637f`). With `SetLayoutMode` (`0x488540`) on, a Ctrl or Alt press drags the nearest movable toplevel (`0x765490`) from a 3×3 grip grid (`0x765320`) and is consumed, world included; its release only ends the drag (`0x7664e0`). Each link span is its own Button (`0x7a3240`) routing `OnHyperlinkEnter`/`Leave`/`Click` to the parent (`0x7a3780`–`0x7a37e0`).
These now hold; a release goes to the capture whatever its button (`0x76642b`), and number arguments keep `_ftol`'s low dword.
Tests bite on the parent for each.
Fixes#951Fixes#952Fixes#957🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text measured `ceil(sum of steps) + 1`: no kerning, float advances, the last glyph priced by its step, and no outline or shadow lead.
In 1.12.1 the kernel (`0x5c6940`) seeds a line with the outline lead plus the shadow (`0x5c69c0`, `0x5c69a4`), steps each glyph by `ceil(floor(adv) + 1 + min(kern, 0))` on FreeType's 26.6 advances (`0x5ca2d0`, `0x5d1298`), prices the line's last glyph by its ink (`0x5c6b70`, `0x5cb080`) and carries the previous glyph across a break (`0x5c6aa0`). The draw rows use the same law, shadow-seeded, wrapping at `ceil(width)` (`0x5c7470`, `0x5cddb1`).
The measure, the wrap, the justify and the EditBox now follow it, reading the face's own metrics, kerning and composite build (`0x7d40e6`).
Tests pin widths computed outside the code base from FRIZQT and ARIALN at 12–34 px, with and without outline and shadow.
Fixes#427🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A drag kept the frame's authored anchors: `GetPoint()` answered them afterwards, the frame kept following its old target, and a resize grip on a CENTER- or TOPLEFT-anchored frame moved the wrong edges.
In 1.12.1 every drag enters at `0x7652b0`, whose first act pins the frame (`0x768430`): `ClearAllPoints`, one TOPLEFT on the screen root's TOPLEFT at the current rect, and an explicit size. The pump `0x768710` moves that anchor (case 4) or, for each of the eight grips, computes the clamped size and re-pins shifted by the left and top travel only (`0x76898a`). A frame with no rect starts no drag (`0x7652cd`).
Drags now pin and pump that way, with one drag slot; `StartSizing` defaults to BOTTOMRIGHT (`0x776913`).
Tests: the pin and `GetPoint` after a drag, the eight grips on a CENTER-anchored frame, the clamps, the layout cache round trip, and stock chat-window resize and off-screen drop.
Fixes#173🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`ClearAllPoints` invalidated the node, so an anchorless frame lost its rect at once, with everything anchored to it, and any region whose resolve failed stopped drawing.
In 1.12.1 `ClearAllPoints` (`0x7a2b70` → `0x767620`) only frees the anchors, queuing no resolve, so the rect stands. Only the node's own invalidate re-resolves it; a failure (`0x768d20` at `0x768d55`) clears the valid bit and skips slot 0, so its dependents keep their rects. A Texture keeps drawing its last vertices (`0x7706e0` checks only its texture); a FontString stops (`0x773080`, `0x7730b7`). A new string, font or spacing forces a store (`0x771ea4`).
Each node now carries an entry state, and takes a recomputed result only when invalidated or a target stored a new rect; a failed Texture's last quad is kept, and the full derive and the incremental pass read the same state.
Tests bite on the parent; the verifier is green.
Fixes#942Fixes#943🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`SetFont` raised on a `false` flags argument and wrote the height even when the load failed, and only SimpleHTML had `SetSpacing`/`GetSpacing`, so SpellAlert's message frame raised in its OnLoad.
In 1.12.1 `SetFont` (`0x79f210`, the FontString's `0x79d4f0`) reads flags only when `lua_isstring` (`0x79f287`), writes them on every successful load (`0x79f334`), and writes nothing on a failed load or a height ≤ 0 (`0x79f361`, `0x79f2f4`, `0x771571`). Every text table has the spacing pair (`0x79fb40`/`0x79fbe0`; the FontString's clamps at `0x772240`); the line step adds the gap rounded up to a device pixel (`0x5c2070`, `0x5cdc20`), as do the message frames (`0x78899e`, `0x785f06`) and EditBox rows (`0x77d133`).
All seven SetFont bindings share these gates and ask the host's font probe; spacing is stored, inherited, measured and drawn on all six tables.
Tests bite on the parent for each.
Fixes#873Fixes#692Fixes#945🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Region names lived in one first-wins name → id map that also served `GetName`, so a second region given a taken name answered `GetName()` with nil and was `<unnamed>` in its error strings.
In 1.12.1 the name lives on the region: `SetName` (`0x76c650`) keeps the region's own copy at `[+0x98]`, freed by its destructor (`0x76c560`) and read back by `GetName`. Only `_G` is first-come (`0x701bd0`, `0x701c8a`).
Each region now carries its name; `GetName` and the error strings read it directly, and the name map is a first-come lookup only, whose entry a free drops only when it holds that region.
Tests: same name twice, `$parentText` under colliding parents, the second region's error string, a free leaving a same-named sibling's name.
Fixes#939🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four pointer defects: OnEnter and OnLeave set `arg1 = true`; a frame being moved or sized still ran the hover walk; the pointer leaving the window fed `f32::MIN` to a move in flight, flinging the frame off-screen; and the mouseover token read the world pick, not the published mouseover.
In 1.12.1 both scripts run through `0x702690`, which sets no arg global. While `[root+0xcfc]` holds a move or size target, the walk pumps it (`0x7655b0`) and returns before any probe (`0x766164`–`0x7661a0`); a hide clears that slot (`0x76492e`). The pump only sees real cursor events. The token resolver reads the published pair `[0xb4e2c8]` (`0x515970`).
OnEnter and OnLeave fire with no arguments, `mouse_move` pumps a move or size and returns, a hide ends it, the pointer leave has its own call, and the aura feed reads the published mouseover.
Tests bite on the parent for each.
Fixes#932Fixes#933Fixes#936Fixes#938🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An XML state texture on an instance over a template went through the Lua path leg, which reuses the populated region, so the template's TexCoords, size, tint, blend and anchors stayed on the new art.
In 1.12.1 `Button::LoadXML` (`0x7788c0`) builds a fresh region for each state tag through the `<Layers>` adder (`0x6f26f0`) and hands it to the slot store (`0x778fd0`, `0x779110`; the checked pair `0x785330`, `0x785440`), which destroys the region it held. A fresh region has TexCoords 0..1 and BLEND (`0x76fc40`, `0x76fc64`), and an XML highlight keeps its own blend (`0x7789a4`).
The XML leg now builds the region as a `<Layers>` texture and installs it, freeing the old one; the Lua leg is unchanged. An XML highlight with no `alphaMode` now draws BLEND.
Tests: all six tags on a CheckButton template, the name kept, the Lua leg's region kept.
Fixes#875🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A FrameXML template registered under a name already taken replaced the first, nothing was logged, and names differing only in case were stored twice.
In 1.12.1 the registrar `0x6ee500` folds case (`0x64b3f0`, `0x64a4c0`) and keeps the first entry, logging "Virtual object named %s already exists" (`0x871188`), unless the old entry carries a non-zero script context and the new one zero (`0x6ee594`). The stock files load at context 0; `AddOn_Load` (`0x51f240`) loads an addon under its name unless its signature checked SECURE (`0x51dbfb`).
Templates are now keyed case-folded with their context, an addon's load sets the context, an addon counts as secure only from the player's own data with `## Secure: 1`, and the refusal and the unnamed-node line (`0x871020`) reach FrameXML.log.
Tests: duplicates and case variants refused, the context rule, the log lines.
Fixes#556🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moving from the world onto a window, the world mouseover's loss reached the tooltip a frame after the window's OnEnter, so a gate kept the fade off any tooltip the world had not put up.
In 1.12.1 the hover walk runs the old frame's leave before the new frame's enter (`0x766229`, `0x766239`). The WorldFrame's leave (`0x483b10`) runs OnLeave, then publishes the empty mouseover, whose publisher fades GameTooltip whoever holds it (`0x492890` → `0x530ae0`). A nameplate's leave publishes first (`0x7cb8b0`); the Minimap's hides the tooltip (`0x4eb880`), and its probe drops a held blip on a miss (`0x4eb0c0`).
The VM now holds the published mouseover and runs these leaves in the walk, the hide and every mouse disable, freelook's plates included (`0x60f830`); the ownership gate is gone.
Tests: OnEnter no longer sees "mouseover", an addon tooltip fades on a change, blip and Minimap leaves hide it.
Fixes#901🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Anchors were a list: SetPoint re-applied an unchanged anchor, GetPoint counted in the order points were set and answered five nils past the last, and the cursor tooltip replaced every anchor each frame.
In 1.12.1 a frame holds one anchor per point. SetPoint (`0x767c70`) returns at `0x767d5e` with no store and no layout queue when that point's anchor has the same target and relative point and both offsets differ by under 2⁻²² layout units (`[0x8029d4]`). GetPoint (`0x7a2340`) walks the nine slots in point order and pushes nothing past the last (`0x7a240d`). The `ANCHOR_CURSOR` update sets BOTTOM alone (`0x530b20`).
Anchors are now nine slots, and SetPoint, GetPoint and the cursor update follow those rules.
Tests: an unchanged re-set and an in-threshold offset touch no layout, GetPoint order and indexes, a cursor tooltip keeps an addon's second point.
Fixes#926Fixes#131Fixes#879🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A drag started past a fixed 4 pixels from the press. In 1.12.1 the threshold is 0.01 layout units, where the screen diagonal is 1: `0x76b300` stores `0.01f × 0.01f` (`0x81c468`) as an f32, and `0x76b320` starts the drag when the squared distance from the press point is not below it, comparing at x87 double precision. The converters `0x425610`/`0x425710` scale the cursor into those units (`0x41ad80`).
The drag test now divides both points by the layout unit and compares the squared distance in f64 against the f32 threshold. At 1024×768 a drag needs 12.8 px, at 1920×1080 about 22 px; the UI scale does not change it.
Tests: just under and over 1% of the diagonal, 12 and 13 px at 1024×768, the exact f32 boundary, two screen sizes and three UI scales.
Fixes#157🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Any press during a drag abandoned it with `OnDragStop`, so touching the right button while dragging an item or a window with the left stopped the drag.
In the reference, each press on the capture runs the drag arm: a button in the frame's drag mask re-arms it, started back to 0, with no `OnDragStop`, and any other button leaves the drag running. The release of a button in the mask ends a started drag with `OnDragStop`, or disarms an unstarted one. `OnDragStart` names the lowest held button.
Presses now re-arm or leave the drag as the reference does, the release tests the released button against the mask, and `OnDragStart` names the lowest held button. A release the client lost still stops the drag.
Tests cover a right press on a left-only drag, a re-arm, the first release after it, an item drag and the start's button.
Fixes#924
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every resolve stored the rect it computed, however small the move, so float noise in anchor chains moved rects, hit edges and size-change notifications where the reference keeps the old ones.
In the reference, the layout drain compares each of a node's four new edges with the rect it last stored. If every edge moved less than 1e-5 in its units (where the screen diagonal is 1), it keeps the stored rect and skips the store, its dependents and `ApplyRect`. A scale change, a new font or new text clears the resolved bit, so the next resolve stores without the test.
A resolve now keeps a frame's or region's rect under that threshold, scaled by the layout unit, and a scale change or a new measure stores the exact rect.
Tests cover jitter, a move just over, slow drift, a region and the forced stores.
Fixes#919
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A release that ended a started drag fired the frame's `OnMouseUp` before `OnDragStop`, so a frame that both drags and acts on mouse-up ran its mouse-up action at the end of every drag.
In the reference, the mouse-up first checks the frame's drag registration for the released button and its started drag: if both hold it fires `OnDragStop` and the hover frame's `OnReceiveDrag`, and returns before `OnMouseUp`, which only a release without a started drag reaches.
The release now follows that: a started drag fires `OnDragStop` and the drop in `OnMouseUp`'s place, and any other release fires `OnMouseUp` as before.
Tests cover a drag, a plain click, a button the frame is not registered to drag, and a resize grip.
Fixes#920
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Chat bubbles were quads drawn straight into an overlay with no widget behind them, so `WorldFrame:GetChildren()` never returned them and bubble-skinning addons such as pfUI's found nothing.
In the reference, a bubble is an anonymous Frame under WorldFrame from a reused pool: eleven regions (the nine backdrop pieces, the tail, the text in the `NAMEPLATE_FONT` global), the text's bottom on the speaker's projected point, its own fade, and an OnShow on each reuse. It fades out and hides when the speaker is out of range or was not drawn last frame.
Bubbles are now built that way, fed by the app and drawn by the frame pass; the overlay path is gone. Item links show their bare names, and a speaker behind the camera fades out.
Tests cover the child, its regions, a pfUI-style skin, reuse and the fades.
Fixes#884
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Button release un-pushed the button before `OnMouseUp` and `OnClick` ran, so both read NORMAL. A plain click on a floating chat tab then took the pick-up branch and left the window following the cursor.
In the reference, the mouse-up handler fires `OnMouseUp` first, then reads the state: a PUSHED button with the cursor inside it and the Up registration fires `OnDoubleClick` or `OnClick`, and only then is it set NORMAL, unless locked. A failed gate still un-pushes; an `OnMouseUp` that un-pushes cancels the click.
The release now runs that order after `OnMouseUp`, with the started-drag skip kept.
Tests read the state inside each handler and drive the stock chat tab.
Fixes#699
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The tick ran the OnUpdate walk without resolving the layout around it, so a resize made in an event handler fired `OnSizeChanged` after the walk, and some walk slots resolved on their own to compensate. Separately, `OnSizeChanged` gated on 2⁻²² in benilla's units, and an emptied FontString kept its old measured box in the incremental layout.
In the reference, the pump drains the layout queue before the walk and again after it, each drain firing `OnSizeChanged`, then re-picks the hover. ApplyRect compares sizes against 2⁻²² in its own units, where the screen diagonal is 1.
The tick now drains before and after the walk and drops the stand-in resolves. Size gates scale 2⁻²² by the layout unit, and a FontString's text going empty or back names it on the layout when its node changes.
Tests pin each.
Fixes#912Fixes#741Fixes#914Fixes#911
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Button state texture's visibility was kept apart from its region's shown bit, so a script `Show()` on a non-current state texture drew nothing. The bar, thumb and ColorSelect setters took a colour form and accepted arguments the reference rejects, the loader built their textures from `file=` or `<Color>` alone, and a bar texture answered nil to `GetTexCoordModifiesRect`.
In the reference, a region has one shown bit that `Show`, `Hide` and the state switch all write. The setters fork on object, path, nil or usage. The loader builds each widget texture as a full Texture element, and installing a bar texture sets its texcoord flag and gives it all points.
All of these now match: one shown bit, the reference's argument forms, full-element textures with their names and texcoords, and no tint without a bar.
Tests pin each.
Fixes#909Fixes#908Fixes#575
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Any write that changed the layout graph's roster (a frame or region created, freed, re-owned or re-pointed) made the next resolve derive the whole graph: about 1.35 ms for one `CreateTexture` or `CreateFrame` on the stock UI, against 0.05 ms for a no-op.
A write now names the nodes it touched, and the resolve re-derives just those in the cached graph: a joining node is added with its edges, a leaving one is removed and its dependents seeded, a staying one has its edges and inputs re-synced. The full derivation stays the fallback. A region's owner and an art change are now layout inputs that name their region. Verify now also compares the graph itself.
A randomized test checks rects and graph against a scratch solve after every batch of creates, frees, re-owns and re-anchors. `CreateTexture` now costs 0.14 ms, `CreateFrame` 0.15 ms, a backdrop rebuild 0.22 ms.
Fixes#903
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hidden chat frames kept fading their lines, a scroll frame recomputed its range on every read and fired `OnScrollRangeChanged` on every `UpdateScrollChildRect`, hidden or not, and `OnUpdateModel` fired for panes with no size or off screen.
In the reference, each of these runs in the frame's own update slot, only while shown: message frames trim and fade there; a scroll frame recomputes a range its writers marked, stores it and notifies only on a real move. The model paint fires only for a resolved rect whose screen-clamped viewport has size.
Message frames and scroll frames are now walk kinds, the range is stored and marked, and the paint applies the rect and viewport gates. benilla's options scroll measures its body's own overhang.
Tests pin the hidden fades, the stored range and its notify, and the paint gates.
Fixes#182Fixes#175Fixes#892
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A texture object handed to a widget's texture setter stayed on its old frame and layer: Button and CheckButton slots only stored it, and StatusBar, Slider and ColorSelect ignored a texture object altogether.
In the reference, each setter deletes the slot's old texture and re-links the new one into the widget: Button states on ARTWORK, the highlight on HIGHLIGHT with its blend, checked textures, thumbs and markers on OVERLAY, the bar on the given layer. The link writes the texture's shown bit, and the old parent's removal hook runs. Thumbs and markers lose their anchors.
All these setters now go through one install that re-links through the region owner path and writes the shown bit; a non-current state texture answers `IsShown()` nil until its state comes, and the raise texts are the client's.
Tests cover each setter, both shown-bit directions and the backdrop teardown.
Fixes#902
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
GameTooltip's lifecycle took shortcuts the reference does not: a 0.5 s linear fade armed once, a fade cancelled by new lines, cursor-anchored plates that faded instead of hiding, a SetOwner that never hid first, the last anchor kept after a hide, and a health bar shown and hidden without its handlers.
In the reference, FadeOut re-arms a 2 s countdown that holds full alpha for half and then ramps, or hides at once at the cursor anchor. Only the owner core, the show and FadeOut write the fade. SetOwner runs the tooltip's own hide first. The un-own leaves ANCHOR_NONE. The clear hides the bar through its own transition.
Every builder, verb and world or minimap seat now goes through these functions; an uncached item hides and un-owns as in the reference.
Tests pin each of these.
Fixes#889Fixes#134Fixes#896Fixes#893Fixes#895Fixes#843
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A raise's level compaction counted and packed every level any frame of the stratum held, hidden or shown, so a frame given a high level while hidden was pulled down and later showed under a dialog it should cover.
In the reference, each stratum keeps a level bound that grows only when a visible frame links into a level, and the compaction examines and moves only the levels below it, then sets the bound to its count, which the raised frame takes. A frame above the bound keeps its level.
The arena now keeps that bound per stratum, grown on every visible link and set by the compaction, which packs only below it.
Tests cover the hidden frame above the bound and a high show raising the bound for the next raise.
Fixes#885
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A backdrop was data drawn in the frame's own slot, so `GetRegions` and `GetNumRegions` never saw its pieces, and addons that skin by walking a frame's regions missed them.
In the reference, installing a backdrop creates nine Texture regions on the frame: the background on BACKGROUND, then eight border pieces on BORDER, linked into its region list in that order. A resize re-runs their texcoords and colours, and the colour setters tint the background or the border alone.
The pieces are now real regions, drawn through the region path, so edges draw over another frame's BACKGROUND at the same level. `<Layers>` and `<Backdrop>` load in document order. A region leaving its parent empties any Button, CheckButton, StatusBar, Slider or ColorSelect slot holding it, and a dead layout node now seeds only its dependents.
Tests pin the order, colours, resize, teardown and stock quads.
Fixes#883
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fourteen bindings answered Lua `true`/`false` where 1.12.1 pushes the number 1 or nil: the loot slot, resurrect and distance predicates, the talent flags (`isExceptional` read 0, which is true, for every ordinary talent), the cursor predicates, and `GetLootRollItemInfo`'s bind flag, whose count also echoed the stack size where the reference answers 1. `GetRepairAllCost` returned two values with no vendor open, and `GetLootRollItemInfo` a sixth value.
The reference pushes 1 or nil for each and returns exactly the values its catalogue row records.
All of them now answer through `binding_abi::flag`, and `GetRepairAllCost` answers the one value 0 unless a repairing merchant is open. The boolean gate, which missed these, now probes every row whose kinds are nil or number whatever its name or arity confidence, over seeded talent and roll state.
The widened gate fails on the parent for all fourteen.
Fixes#159Fixes#900
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
`EnableMouse(false)` only flipped the bit, so a frame disabled while pressed still got OnMouseUp and OnDragStart, and a hovered one kept its hover. A second mouse button pressed while one was held fired on the frame under the cursor rather than the held one.
In the reference, a mouse disable on a visible frame runs the same removal a hide does: the capture and the press drop, the hover ends with OnLeave at once. The down handler takes the held capture first and fires there, and a Button clicks on release only while PUSHED and with the cursor inside its own rect.
`EnableMouse` now runs that shared removal, the drag arm included. Presses go to the capture when one is held, and the Button release tests pushed and inside rather than topmost.
Tests cover a disable while pressed or hovered, chorded presses and the release gate.
Fixes#886Fixes#887
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The tooltip fade, a model's animation clock and its OnAnimFinished ran after the whole OnUpdate walk, and OnUpdateModel fired in creation order. A tooltip hidden through its parent also kept its owner and fade.
In the reference, the walk makes one call per frame: its OnUpdate, then its kind's engine update (a tooltip's fade step, a model pane's clock and completion test). OnUpdateModel fires later, from the paint, in draw order. A tooltip's own hide and show run from any caller, its parent's cascade included: the hide un-owns and clears it; the show needs an owner.
The walk now runs a per-kind update in each frame's slot, OnUpdateModel fires per stratum in draw order after the hover re-pick, and show and hide dispatch to a kind's override. CharacterModelBase's override is left for #890.
Tests pin each order and a parent's hide and show.
Fixes#861Fixes#863Fixes#864
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A ScrollFrame built its `<ScrollChild>` after its `<Frames>` children and so after their OnLoads: under a scroll frame template, the template's scroll bar loaded before the instance's scroll child, and an OnLoad that read the other frame saw it unbuilt.
In the reference, the ScrollFrame's load runs the base load, then builds the scroll child with its whole lifecycle and its own OnLoad, then stores it as the scroll child, all before the post-load hook builds `<Frames>` and fires the ScrollFrame's OnLoad. A template's scroll child is built at the template's level, ahead of the instance's, which wins.
The loader now builds `<ScrollChild>` in that place, on the one path XML and `CreateFrame` share, and only for a ScrollFrame.
Tests cover the order directly, through a template, and with a scroll child in both template and instance.
Fixes#846
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Raising an overlapped toplevel frame compacts its stratum's levels, and benilla wrote the new levels directly, so the moved frames kept their old link order: which of two frames on one level draws on top, takes the click and runs its OnUpdate first did not change as in 1.12.1.
The reference's compaction moves each frame of a shifted level through the non-propagating level setter, levels from the bottom and each in creation order, and the setter relinks a visible frame at its level's tail. So a shifted level's visible frames end in creation order.
Compaction now moves each frame through `set_frame_level` the same way, in that order; frames carry a creation stamp, since arena order is not creation order once a slot is reused.
Tests cover the relink order, the swapped draw and hit order after a raise, and a mid-walk raise.
Fixes#862
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A frame hidden while a button was pressed on it still got OnMouseUp on the release, and its stale capture aimed the next chorded press at it. And showing any frame, even one that takes no mouse input, scheduled a hover re-pick, so a still cursor could fire OnEnter or OnLeave a tick early.
In the reference, the hide's input-index removal clears the capture slot when it holds the hidden frame, and the release goes only to the capture, so it reaches no one. The hover re-pick is armed only when a show inserts a frame into the mouse index.
The hide now drops the capture and the pressed frame; the release then runs nothing, no world click either. A show arms the re-pick only for a frame that takes the mouse.
Tests cover hides while pressed and shows with and without mouse input.
Fixes#844Fixes#845
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
`cargo test` stops at the first test binary that fails, so a red `scripts/check.sh` or gate run showed one crate's failures and hid the rest: the next run found more. Both scripts now pass `--no-fail-fast`, so one run reports every failing test across the workspace. The exit status is unchanged: any failure still fails the step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OnUpdate ran in frame-creation order, from a list built before the walk, so a frame hidden by an earlier handler still updated, and strata and level played no part.
The reference's update walk `0x765650` goes through the nine strata from the bottom, each stratum's levels from 0, and each level's list oldest link first. It saves the next frame before each call, so a hide skips a frame before its turn. A frame linked into the level being walked waits a tick; one linked into a level still ahead runs there.
The walk now follows that order, using the link stamps shows and level changes already keep. It checks each frame at its turn and places frames linked or given an `OnUpdate` mid-walk. An EditBox still flushes right after its own `OnUpdate`.
Tests cover the order, mid-walk hides and shows, and a box's flush.
Fixes#742
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Script errors reached the Lua error handler as mlua's display text: a `runtime error: ` word and a `stack traceback:` block, which overflow the stock `ScriptErrors` dialog. And a raise in a `CreateFrame` template's OnLoad was logged as a warning and never reached the handler at all.
In the reference, handlers run under `lua_pcall` with the error function `0x703b40`, which calls the `seterrorhandler` function with the bare message. `CreateFrame` builds its template through the XML instantiator `0x6ee280`, whose OnLoad runs under that same function (`0x704d50`).
Now every caught error records two texts: the decorated one for benilla's log and the bare message for the handler. A load report pairs the two per row, and `CreateFrame` records its template's errors as script errors.
Tests: the handler hears bare messages from handlers, and a template OnLoad raise reaches it.
Fixes#697Fixes#859
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An addon's Lua and XML files were read with the exact case its `.toc` or `<Script file>` spelt. On a case-sensitive filesystem, Linux's, a file named in another case never loaded: Questie 3.7.1 lists `Astrolabe\Astrolabe.lua` from a folder shipped as `astrolabe`, so `Astrolabe` stayed nil and its OnUpdate raised every frame.
The reference is a Windows client, and every path it opens matches without regard to case. Addon textures and sounds already resolved that way through `loose_addon_file`; addon scripts, XML and `Bindings.xml` now go through the same resolver, and the second, case-sensitive one is gone. The shared resolver also refuses a component the host reads as more than a plain name (a Windows `C:`), so it still cannot leave the AddOns root.
Test: a file asked for in three spellings resolves to the one on disk; it fails without the change on a case-sensitive volume.
Fixes#851
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>