`GetWidth`/`GetHeight` answer against current layout, so they resolve first — and
a resolve that follows any `CreateFrame` re-derives the whole layout graph, because
minting a frame id drops the cached one (`Model::frame_id` → `touch_layout`). A UI
load is nothing but creation interleaved with reads, so the player's interface paid
**839 whole-roster derivations in one load, 2.84 ms each: 2.4 s**, against 650
`GetWidth` calls. `WOW_LAYOUT_DERIVE_TRACE` named the giver-up: every sampled one
was `create_frame`.
Most of those reads cannot learn anything from a resolve. An edge takes its anchor
scan first and only then `combineEdge(center, opposite, size · scale)`
(`resolve_left`, `resolve_right`), so an authored size IS the span unless the
anchors pin **both** of that axis's edges. `LayoutInput::authored_span` is that
rule, written beside the solver whose precedence it mirrors, and it refuses the two
cases where only the solver knows: both edges anchored, and a clamped frame, whose
rect the screen can squeeze.
Measured on the player's own 91 addons through the harness's `--together` load,
three passes each:
```
derives per load 839 → 235
preamble per load 2,414 → 560 ms
frame:GetWidth 686 µs → 60 µs per call (1,241 ms → 118 ms over three loads)
```
**1.85 seconds off every login and every `/reload`**, and the steady state gains
the same way: an addon reading a size it set in an `OnUpdate` no longer walks the
roster for it.
Proven as well as measured: six UI captures (character, bag, merchant, quest, quest
log, tooltip) are **pixel-identical** before and after, MAE 0.000, max 0; the
layout suite passes under `WOW_LAYOUT_VERIFY`, which re-derives every frame from
scratch and demands the same rects; and `resolve_bench` now asserts both halves —
an authored, singly-anchored span answers with no gate walk, while one whose
anchors pin both edges still forces the resolve and reports the anchors' distance,
not the authored number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
`SetVerticalScroll`'s own note here already said it: `0x786db0` stores the offset,
**re-anchors the child (0x787100)** and fires its event, "all only on a change".
0x787100 passes the offsets to `SetPoint` — so the engine writes the child's
TOPLEFT once, when the scroll state changes. This applied it instead as a layout
override, recomputed every frame, which is a different thing: it made the
engine's anchor permanent and unoverridable.
That is why AI_VoiceOver's options could be opened and read but not scrolled.
AceGUI-3.0 scrolls by moving the child **itself** — `SetScroll` is
`content:ClearAllPoints()` then `content:SetPoint("TOPLEFT", 0, offset)`
(AceGUIContainer-ScrollFrame.lua:62-77) and it never calls `SetVerticalScroll` at
all — so the override put the content back at the top on every frame. Measured
through the harness: `SetScroll(1000)` computes offset 170 (content 549 − view
379) and the content's top now moves 563 → 733 where before it did not move.
`SetScrollChild` and both offset setters now write the merged TOPLEFT through
`layout_methods::write_anchor`, which is the commit `SetPoint` itself uses, so
the merge rule is shared rather than reimplemented and a child authored
TOPLEFT/TOPRIGHT keeps the TOPRIGHT that gives it its width. The whole override
path goes away with it: no third element on the layout plan, no override in the
node hash, no special case in the edge walk or in `retarget`, and
`LayoutSolver::set_frame_anchored` is gone.
One behaviour genuinely changes and the test says so now: `SetScrollChild(nil)`
no longer restores the child's authored anchor, because the authored TOPLEFT was
replaced the first time the frame took the child. Nothing was ever saved to
restore — that expectation was an artefact of the override model, not a trace.
The child keeps where the last scroll left it, and its own `SetPoint` wins from
there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
A live ScrollFrame re-points its child's TOPLEFT to the frame's own TOPLEFT at
the scroll offsets, which the reference does by calling SetPoint on the child
(0x787100). This replaced the child's whole anchor list instead — and 1.12's
SetPoint merges a point into the list, it does not clear it.
The difference is not academic. AceGUI's scroll child is authored TOPLEFT *and*
TOPRIGHT to its parent with an explicit height and no width
(AceGUIContainer-ScrollFrame.lua:192-195), so the corner pair is where its width
comes from. Dropping TOPRIGHT left it with one corner and no width, so it
resolved to no rect at all — and with no rect for the child, nothing below it
resolved either.
That is what drew AI_VoiceOver's options window as chrome and tabs over an empty
content area. The whole widget tree was there the entire time: probed through
addon_harness, the panel builds TabGroup → ScrollFrame → four InlineGroups of
CheckBoxes, Dropdowns, Sliders and Buttons, every one sized and IsVisible() true
— and every one from the scroll child down answering GetLeft() == nil. After
this, the same probe reads the scroll child at L=190 T=563 and its first
checkbox at L=200 T=530 B=506, inside the viewport.
The override's edges now include the authored anchors' targets as well, since the
merge means the child reads both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.
Today's work, which is the reason a baseline exists at all:
- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
characters were entering the sun's shadow cascades, so every skinned-rig
shadow specialization compiled live mid-frame; the hitch detector logged
"main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
frame-cost reading no longer pays 2.7 ms a frame for it.
Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
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>
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>
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>
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>
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>
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>
Part of #41: the comments of `benilla-ui` (the Lua UI engine, its FrameXML loader and its widgets) are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 43,140 comment lines become 15,161.
- **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal.
- **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them:
- `0x64a480` is the case-sensitive compare (a byte `repz cmpsb`), and `0x64a4c0` the case-insensitive one.
- A frame's id sits at `+0xb0`, not `+0xb4`. `EnableKeyboard`/`IsKeyboardEnabled` are `0x776ec0`/`0x776f90`.
- A region's unpinned edge has no fallback to its owner's rect: the owner supplies only the scale.
- `$parent` expands to the nearest named ancestor, or "Top" when none is named (`0x76c5b0`), not simply to the parent.
- A StatusBar fills from its internal normalised fraction, not from `GetValue()`.
- The addresses of `StartDuel` and `StartDuelUnit` were swapped.
- Many "untraced" notes now give the traced reference fact: the drag threshold, the per-grip sizing edges, the empty-`autoFocus` skip, and `SetTexCoordModifiesRect`'s default.
- Stock FrameXML line citations are checked against the stock 1.12 files and corrected. `ref-` file names become the stock file names.
- **Deviation labels.** "Deviation:" now marks only a choice with a reason. A surface that exists only for the retired Era addon target says so plainly: it is not 1.12.
- **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Nine second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the code, the stock files, vmangos and the reference.
Behaviour differences the rewrite surfaced are filed as their own issues (#131–#142), and more are being validated. The comments here state what the code does. String literals are untouched; they are #92.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comments that pointed into the maintainer's private reverse-engineering notes (a note by name, a
path, a section or finding id, the analysis's own name for a client function, a process word) now
carry the fact itself, cited by the reference client's address, by vmangos file and line, or as
"the reference". Nobody outside could follow those pointers.
Comment text only, plus the test-assert and log strings that carried the same pointers. No claim
changes: a comment the notes have since corrected keeps its wording here, and correcting it is
separate work.
The two reference generators, `scripts/gen-reference-globals.py` and
`scripts/gen-reference-verb-events.py`, named the private notes' layout to find their inputs. They
now take the input files as arguments (`--capture`, `--framexml`, `--names`, `--extents`,
`--disasm`). Run on the same inputs, both write the committed tables row for row; only the two
header lines that named the old environment variable change.
Checked per file:
- the code is identical: each file with its comments stripped compares equal before and after; a
changed string literal is compared under a mask that proves only literal text moved and every
literal kept its line count;
- no client address the old text cited was dropped;
- a second reviewer checked every added address against its derivation;
- a detector for the notes' names, paths, ids, function names and process words finds none left;
- `scripts/gates.sh` is green.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- UI layout solves what moved, not what exists — hovering a full bag no longer re-solves the whole interface
- world albedo textures stop keeping a second copy of themselves in RAM
- collision gets a coarse grid for oversized slabs (32x fewer triangles tested), idle units stop re-asking the ground every frame, and anchors spawn on first consumer instead of by the thousand
- aura and loot watchers scan when the store changes rather than on the clock, and static-transform tracking and plate reaping join the sweep
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Other players walk the world. A watched player's motion between server updates now runs through
the same grounded movement resolve as your own character — same capsule, same colliders, same
step-up and ground election — so they follow terrain every frame instead of every packet, and
stop at a wall instead of walking into it and being snapped back. Their jumps are swept against
the world too, and a standstill jump steered mid-air reports that steer, the one mid-air press
the reference client broadcasts rather than defers.
- The move relay rebuilt: relayed moves ride a per-unit chain on real time, a due move never jumps
ahead of its unit's queue, and SMSG_COMPRESSED_MOVES — the batched carrier the server sends under
load — is decoded rather than skipped. Our own send path now matches the reference client's:
facing streams while moving, Nagle is off so each packet is one write, and the strafe axis nets so
left and right can never ride the same packet.
- NPC and quest text speaks the whole $-macro grammar, carved from the reference bytes — your name,
class and race, the gendered forms, and the server's live world states — and the same
substitution feeds gossip, quest text, the quest log and unit frames. Greetings are a weighted
draw, rolled per NPC per open, and a quest giver with no quest to give is no longer clickable,
where it used to open a dialog holding nothing but a placeholder greeting.
- Spell tooltips finish the reference's line law: cast time and cooldown behind the real passive
gate, the required tool or form, the reagents block, the chance-to-X line, a multi-bit
equipped-item requirement named properly instead of by raw bits, and a buff's dispel class.
- The UI layout engine got a dense, reusable anchor solver, and a change gate that skips the resolve
entirely on frames where no layout input moved — in practice, almost every idle frame. Pets are
named by their pet number rather than a creature-template entry, so the client asks the query the
server can actually answer. And the asset extractor grew a stray-geometry census for tracking
down geometry that renders where it should not.
This snapshot replaces the six dated `sync` commits that stood here before it, whose subjects said
nothing but the date. If you cloned before now: `git fetch && git reset --hard origin/main`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>