Commit Graph
1766 Commits
Author SHA1 Message Date
gromandClaude Opus 5 5308b5a200 docs: a hosted page draws under its host, and art that does not resolve is invisible
Two defects from one play report, both in the ports, both measured rather
than guessed:

`SetParent` re-levels the frame it is called on and nothing under it
(0x76ab65 pushes propagate = 0, and a script SetFrameLevel does the same),
so Cinematic's pages -- built parentless, widgets at levels 1..20 -- landed
25 levels under the window its toplevel raise had lifted to the top of
DIALOG. The page drew under the window's own backdrop and the backdrop took
every click. The screenshot named it before any code was read: the page's
white read (103,103,103) and its gold (103,84,0), the same 0.404 factor as a
nameplate behind the window, where a fade on top would have composited to
(110,116,121). Multiply, not blend, means underneath.

And the map's resize grips are shown, mouse-enabled at level 9002, and take
the cursor -- but wear a 2.0 texture this chain has not got, and a Lua caller
cannot find that out: SetTexture returns nothing and GetTexture echoes the
path it was handed either way. Twenty pixels of nothing in the map's corners.
The client's own `texture miss:` warning is the only instrument that names
one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 18:42:54 -07:00
gromandClaude Opus 5 204184afb2 ui: the map's own art line, in every session's log
A play report about how the map looks arrives without the diagnostic that
answers it, and the only way to ask for the line again is to wait for the
player to play again. It cost a round trip on 2026-11-02, so the trace is
unconditional now: one line per world-map selection change, with the frame
the detail is drawn into added beside the detail frame's own rect, since a
canvas zoom and an oversized tile change the picture the same way and only
the two together say which one did it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 18:42:45 -07:00
gromandClaude Opus 5 eef0d06852 docs: the map, answered by its own trace
What `WOW_MAP_TRACE` printed, what it meant, and the two smaller faults that came out with it —
including the harness note that `--tick` does not call `resolve`, so `OnSizeChanged` never fires
under it and its silence there is not evidence of an engine gap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 16:36:00 -07:00
gromandClaude Opus 5 7f97777597 ui: the stock map art has to follow the detail frame it is drawn into
`WOW_MAP_TRACE=1` named it on grom's first run, in one line:

    detail 1431x954 @scale 0.70 | 1 256x256 @(10,1122) | 2 256x256 @(266,1122) | ...

1.12's `WorldMapDetailFrame` is always 1002x668, so everything drawn into it is placed in absolute
pixels against that: twelve 256x256 tiles anchor-chained from its TOPLEFT, each exploration overlay
at the offset `WorldMapOverlay.dbc` gives it. The canvas breaks that the moment it sizes the frame
to anything else, and it does — `OnCanvasSizeChanged` falls back to the container's own rect
whenever it has no art layer to read, which is every moment the canvas has no `mapID`, including
the world-view blip the same log shows on every map open.

So: a 1024x768 sheet of art pinned to the top-left of a 1431x954 frame, covering 72% of the width
and 80% of the height, with the rest of the map drawing nothing. That is the grey he reported,
with the art magnified relative to what was left of the frame.

The art is now rescaled to whatever size the frame has. The tiles need only their size set, because
the XML chains them TOPRIGHT-to-TOPLEFT and BOTTOMLEFT-to-TOPLEFT. The overlays carry absolute
offsets, so they are captured from what the stock repaint last wrote — hooked on
`WorldMapFrame_Update`, the only moment those base values are readable — and scaled from that base,
so a second pass cannot compound. Measured: a 1431x954 frame now holds a 1462x1097 grid, the same
1024/1002 and 768/668 proportion the reference frames.

Two smaller faults fell out and are fixed with it: the container resizing was not treated as a
canvas size change at all (the canvas was re-sized only on a map change), and a derived resize
fires no `OnSizeChanged` here, so the rescale takes the size its caller just set rather than
reading the frame back, which would answer the old one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 16:35:53 -07:00
gromandClaude Opus 5 4ea1d97faf docs: the realm's LibStub decides by load order, not version
`Interface\FrameXML\AIO\Dep_LibWindow-1.1\LibStub.lua` loads before every addon and its
`NewLibrary` has the minor-version parse replaced by a literal `minor = 2`. So every library
registers as 2, every later copy is refused, and the FIRST copy of a library to load wins whatever
its version says. Measured: all 22 registered libraries carry minor 2.

What it did to grom: three AceGUI-3.0 copies, a 1.12 backport's core wins by load order, but widget
files never go through LibStub (RegisterWidgetType compares widget versions itself), so the install
runs the retail tree's newer widgets against the older core. `AceGUI.tooltip` is created by the
retail core and neither backport, so TreeGroup's button tooltips raised on every category row the
mouse touched. The `OptionsListButtonTemplate.toggle` gap from the previous round is the same shape
one layer out.

Also records WOW_MAP_TRACE and why it exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 15:55:02 -07:00
gromandClaude Opus 5 e62327424f app: WOW_MAP_TRACE, because no instrument here can see the map
A player's report about how the map *looks* is the one question this side cannot answer. The
offline addon harness seats no renderer, the capture harness seats no UI or addons, and a scripted
login would kick the player off their own account — so a full audit (art present and correct,
catalog right, canvas geometry right, overlay tiling arithmetic right, addon setup complete) still
could not say why grom's Dun Morogh map came up magnified, clipped and grey.

Every number that decides the picture is readable from the VM. `WOW_MAP_TRACE=1` prints them once
per map change: the detail frame's rect and effective scale, then per tile its rect, the texture
path it holds, and the decoded size of that file. The file size is measured through a scratch
texture with no size of its own, because the tile declares 256x256 in `WorldMapFrame.xml` and would
answer that whatever the file holds — and this install's map art is a 512 upscale pack, which is
exactly the disagreement worth seeing.

Off unless the variable is set: it runs a Lua chunk per map change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 15:54:53 -07:00
gromandClaude Opus 5 08689e7d14 docs: the plate token, Cinematic's own settings window, and two more fade rows
Second round of grom's 2026-11-02 session. The nameplate half is engine work and is written up
beside the rest of the compat record; the settings window and the two support rows are in the
ported addons, which live outside this tree, so this is where they are recorded.

The general note worth keeping out of the Cinematic-specific part: an addon that registers its
options with a host and ships no fallback is one stomped global away from having no settings UI
at all, and no engine can stop an addon reassigning a global. Shipping `InterfaceOptions_*` is
still worth doing for the addons that have no window of their own — it cannot be the whole answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 14:51:44 -07:00
gromandClaude Opus 5 c93a0be93f ui: a nameplate now carries the unit token its own event names
`plate.namePlateUnitToken` was set on nothing. It is how every later-era plate addon gets from a
frame back to its unit — `plate.namePlateUnitToken or (plate.UnitFrame and plate.UnitFrame.unit)`,
six sites in `Cinematic/Effects.lua` and `PlateParts.lua` alone — so an addon walking
`C_NamePlate.GetNamePlates()` had a frame's rect and nothing about its unit, and could not tell a
friendly plate from a hostile one.

The measured symptom was grom's friendly nameplates switching themselves on the moment CineMode
started, on a profile whose every friendly-plate box is unticked. That CVar write is the addon's
and it is deliberate: outside instances Cinematic switches every plate kind on so visibility is
decided by *fading* instead, because fades can change in a fight and plate settings cannot. Having
switched them all on, it then faded none of them, because it could not identify one.

The token already existed — `script/nameplate.rs` has had the slot-bound `nameplate<N>` tokens and
the three plate events since the pool was built. All that was missing was publishing the slot's
token on the frame: set before `NAME_PLATE_UNIT_ADDED` and cleared after `NAME_PLATE_UNIT_REMOVED`,
so a handler for either event reads the field that event is about, and a pooled frame reused for
the next unit cannot answer the departed one's token.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 14:51:37 -07:00
gromandClaude Opus 5 607ce45788 docs: six things an Era addon assumes a 1.12 template or verb carries
grom's 2026-11-02 session, written down where the deviations live: the AceGUI TreeGroup toggle that
1.12's OptionsListButtonTemplate has no child for (and why the newest of three installed AceGUI
copies is the one every addon's config window gets), the two templates that reach their children
through the frame's own name so the frame must be named, GetDebugName, the override-binding family,
pfUI's two panel bars, and the rule that a minimap-button drawer only collects the minimap's own
children.

Five of the six are fixes in the two ported addons, which live outside this tree; this section is
where they are recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 14:18:02 -07:00
gromandClaude Opus 5 6e8c4488e1 ui: the override-binding family exists and says what it does not do
`ClearOverrideBindings` was absent outright, so `Cinematic/Camera.lua:470` raised in grom's chat on
every camera shot that ended — and a raise mid-handler costs the rest of the handler, not just the
line.

These are 2.0's secure-binding layer: a second, unsaved binding table a frame owns, outranking the
saved one while that frame wants it and dropped whole by `ClearOverrideBindings(owner)`. This client
models the saved table only (`script/keybind.rs`), so:

- the five setters are no-ops that **warn once each** through the script warning channel. Silence
  was the alternative and it is the worse one — a setter that quietly binds nothing is the same
  class of fault as a hook that quietly arms nothing, and the caller cannot find out.
- `ClearOverrideBindings` is a plain no-op and does not warn. Dropping a table that was never filled
  *is* nothing, and warning there would fire on every `ClearRecast()` while the player does nothing
  unusual.

The module header lists what a real layer needs — an override table keyed by owner in `Model`, a
generation bump for `bindings.rs`'s `sync_dispatch`, an override arm in `BindingDispatch::map` that
wins over the saved chord, and a `Bound::Click` — together with the `SecureHandler*` half in
`secure.rs`, which is the same table seen from inside a snippet and no-op for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 14:17:55 -07:00
gromandClaude Opus 5 bb2e40d1dd ui: GetDebugName, the name an unnamed frame still answers
`Cinematic/Frames.lua:3734` is `entry.name or entry.frame:GetDebugName()` — an addon that keeps a
list of frames reaches for this precisely because some of them have no name, which is the one case
`GetName` cannot serve. With the method absent, `/cine debug frame <name>` raised on the first
unnamed row in the list instead of printing the row the player asked for.

On all three method tables, since the era puts it on ScriptObject. A named frame answers its name,
so a feature test on it learns nothing a 1.12 client would answer differently. The unnamed leg
deviates in its fallback only: it walks the parent's own fields for the key the child is stored
under (frames here are plain tables, so `parent.icon = tex` is visible) and answers
`Parent.<unnamed>` when there is none, where the era answers an index among the parent's children —
an index here would be the order of a list this client does not keep, and a wrong number reads
worse than an honest absence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 14:17:44 -07:00
gromandClaude Opus 5 09aca72c46 ui: hooksecurefunc over a widget method has to read through the metatable
The table form read the field with `raw_get`, on the reasoning that a field
reached only through a metatable is not that table's to replace. A widget
carries none of its own methods — they live on the shared class table its
metatable's `__index` names (`object.rs`'s `frame_meta_for`) — so
`hooksecurefunc(frame, "SetAlpha", cb)` found nothing there and armed nothing,
silently. Measured: **0 fires across 2 `SetAlpha` calls**, while the plain-table
and global forms fired correctly.

That is not a corner of the form, it is what the form is for. 29 files in grom's
corpus hook a widget method, and the one that shows: `Cinematic/Frames.lua:459`
hooks each pfUI frame's own `SetAlpha` so that Cinematic can **follow** pfUI's
out-of-combat fade rather than fight it ("their frames have SetAlpha followed
straight away by ours, taking what it set as the new base"). With the hook
unarmed the two wrote a different alpha to the same frame on alternating frames
— which is grom's "still flashing my unitframe when cinematic is on".

The reference reads `table[name]` plainly and assigns the wrapper back, which
*shadows* the class method for that one object and leaves its siblings alone.
This now does the same: read through `__index`, write raw onto the host, which
is the same thing a plain assignment would do since frame metatables carry no
`__newindex`. A test pins both halves — the chain fires on the hooked frame, and
a sibling of the same kind is untouched, with nothing written to the class table.

Corpus roster byte-identical (87 ok / 1 fail) with all 29 files' hooks armed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 11:10:57 -07:00
gromandClaude Opus 5 18c8cdd762 docs: the MOD blend, SetEnabled, and the label that rode into the canvas
One new section and two amended rows, the three faults behind what grom recorded
on 2026-11-02: the fullscreen tint that drew white, the chat traceback from
picking a profile, and the hovered-zone label scaling with the map's zoom. Each
carries what was measured rather than what was argued, and the general shape —
answering half of an era's call shapes is what makes an addon's `pcall` feature
test lie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 10:00:50 -07:00
gromandClaude Opus 5 aa2ee515aa ui: MOD multiplies, and a gradient is not an alternative to a fill
Cinematic's colour tint painted the whole screen opaque white for as long as its
CineMode ran: `srgb(255,255,255)` across the world with the UI still drawn on top
of it, 16.8 s of it in grom's 2026-11-02 recording, starting the instant he picked
a profile. Three faults on the one path from the addon's call to the pixel, each
of them enough on its own.

`SetBlendMode("MOD")` parsed, answered `GetBlendMode` and then drew as straight
alpha. One pipeline served BLEND and ADD — the shader emitting `(rgb·a, a)` or
`(rgb·a, 0)` into a single `(One, OneMinusSrcAlpha)` state — because no scalar
source alpha can stand in for a per-channel destination factor, and the mode had
no caller worth a second pipeline. It had one: nine of the addon's ten tint
presets are MOD (`Tint.lua:21`), and a MOD colour is built to be multiplied —
`1 − (1 − c)·strength` at **alpha 1**, white at strength 0 — so drawn with alpha
instead it is an opaque near-white quad over UIParent. MOD now keys its own
pipeline, `(Dst, Zero)` with the destination's own alpha kept, off the same
`0x70c190` table the world's Mod batches already read; the shader returns the
multiplier un-premultiplied, since neither alpha appears in that formula, and runs
no longer merge across the flag.

The extract read `fill.or_else(|| gradient.map(midpoint))`, so a region that set
both drew the fill and dropped the gradient. They are not alternatives:
`<Gradient>` writes the four vertex colours (`+0xb8`, `0x77304d`), which
`loader/regions.rs` already reads as "unlike `<Color>` it tints the art". The tint
is exactly that shape — `SetColorTexture(1, 1, 1, 1)` once, re-gradiented a few
times a second — and so is its vignette, whose four edges were drawing as opaque
white bars.

And `SetGradient` took only 1.12's six loose numbers. This client answers
`CreateColor`, so an addon that feature-detects takes retail's colour-object form
and never tries the numbers; the two objects coerced to six zeros through
`as_f32`, the call **succeeded**, and the `pcall` that was the feature test
(`Tint.lua:546`) reported it understood. Both spellings now read a colour object
when the first stop is a table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 10:00:44 -07:00
gromandClaude Opus 5 33d0868009 ui: SetEnabled, the one setter retail put in place of Enable/Disable
A later-era addon writes `SetEnabled` **unguarded on its own controls**, even
where it guards on someone else's. `Cinematic/Options.lua:4176` is a tick's own
`Refresh`, greying it when a rule owns the row; `:305` twelve pages up is the same
file writing `control.SetEnabled and …` about a control it did not make. The raise
at 4176 unwound `RefreshOptions` → `SettingsChanged` → `SetProfile` →
`Welcome.lua:43`, so picking a profile from the addon's welcome window applied the
profile and then abandoned the panel refresh, leaving the traceback in chat and
the window still reading "Nothing runs until you pick one." That is the chat error
grom recorded on 2026-11-02.

Composed from the stock `Enable`/`Disable` pair (`0x780160`, `0x7801b0`) on
Button's table, which CheckButton and LootButton reach down the same chain the
pair travels. EditBox takes it too, written out rather than composed because the
reference gives a box no enabled state at all: it writes the mouse and keyboard
the frame already owns and drops focus, so input is refused and the text is NOT
greyed, which 1.12 has no slot for — `Options.lua:1629` is the call that needs it.

A Slider still answers neither this nor the trio: addons duck-type on them to tell
a control from a bar, which `tests/slider.rs` pins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 10:00:30 -07:00
gromandClaude Opus 5 70e4278c79 ui: the map's hovered-zone label is chrome, not map content
The canvas reparents 1.12's `WorldMapButton` into the zooming child so that every
1.12 addon's icons pan and scale with the map. `WorldMapFrameAreaFrame` hangs off
that button in the client's own XML — it covers the art, so that is where the
label that names what the cursor is over was hung — and it went along with it.

Its label is a 102 px THICKOUTLINE FontString. Inside the canvas it scales with
the zoom, so at zoom 2.5 every letter is a 261 px glyph, and one zone name's worth
of those is most of the 2048² glyph sheet. Measured in grom's 2026-11-02 session:
one `ui_text: a 261.10907 logical-px request … past the 256 ceiling`, then 20
`glyph sheet full … resetting at the frame boundary`, one per hover, each of them
a frame in which every string on screen is missing — the map "flashing the
interface". Frame cost with the map open went 11 ms → 16-20 ms over the same
window.

So it goes back on the window, anchored to the container's top, which is where the
art's top edge is now; at rest that is the pixel it was already at. Measured
through the harness: the biggest text under `WorldMapFrame` was 102 px at zoom 1
and 1632 px at zoom 16, and is now 102 px at every zoom.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 10:00:20 -07:00
gromandClaude Opus 5 c6b19c7926 lua: a size this library derived is a cache, and a # append invalidates it
This is what froze his client. The bound named `Cinematic/Camera.lua:2597`:

    while turnStarts[1] and now - turnStarts[1] > WINDOW do
        table.remove(turnStarts, 1)
    end

`remove` drains the table and remembers a size of 0 — faithfully, that is
`luaL_setn`. The addon then appends with `turnStarts[#turnStarts + 1] = now`,
which is a raw set this layer never sees, so slot 1 is full while the
remembered size is still 0. `remove` reads 0, takes its "nothing to remove"
path, changes nothing, and the caller spins forever. 15 s of Lua in every
frame, for the whole session, in a loop that in the reference cannot fail.

It cannot fail there because 5.0 has no `#` operator: every append goes through
`insert` or `getn`, so a remembered size is always the truth. The collision is
a later-era addon's `#` meeting 5.0's bookkeeping, and it also loses data the
quiet way round — `insert` after a `#` append writes *over* the appended slot
instead of after it, which is a test here now too.

So the two kinds of size are kept apart. One this library **derived** from an
`insert` or a `remove` is a cache: if `t[n + 1]` is not nil the table has grown
since, and the count wins. One an addon **declared** with `table.setn` is
honoured exactly as before, because `setn(t, 0)` as a logical wipe over full
slots is the entire point of it — AceLibrary and Mik's capture tables both turn
on that, and both still pass. An explicit numeric `t.n` field is a declaration
too, and is untouched.

Measured: the corpus roster is byte-identical (87 ok / 1 fail, the failure the
pre-existing AdvancedTradeSkillWindow2), and the drain loop that spun 44 times
in his log now ends in one removal a round.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 08:03:09 -07:00
gromandClaude Opus 5 d1edd74cce ui: a runaway frame writes its own line, and stops costing the whole bound twice
Two of his sessions froze this morning with nothing in the log. The journal
says what happened: every frame 15,0xx ms, `main_ms` 14,970 of it, GPU 3 ms,
the main thread `state R` with 64 s of CPU in 68 s of wall. That dead-constant
15.02 s is `RUNAWAY_FRAME_SECS` — a Lua loop that never returns, re-entered the
next frame and the next.

The bound did its job and said nothing, because its raise unwinds to whatever
`pcall` caught the call. On this corpus that is always an addon's own guard, so
the trip became nobody's script error: `session errors: 0`, no log line, no
file, no line number. `WOW_UI_RAISES` would have caught it and is `eprintln!`
only, which is no use to someone who is in the game.

So the hook writes its own report before raising, and the host logs it at
`error!` beside the ordinary error drain. Each kept report carries a sixteen
frame traceback, because the innermost line is usually inside a library and the
handler that entered the loop is the name worth having.

And the bound tightens to a second once it has tripped. The first one has to be
far beyond any legitimate frame, which makes it far beyond playable: a loop is
re-entered every frame, so the player gets one frame every fifteen seconds —
indistinguishable from the freeze the bound exists to prevent, and that is
exactly what he got. One trip has already proved the call non-terminating; the
ones after it need only enough rope to say so. A second is still some 125M VM
instructions, two orders past the heaviest handler ever measured here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 07:21:48 -07:00
gromandClaude Opus 5 75ba592fba ui: the Era map canvas over 1.12's own world map, and the Line region
What 1.12 withholds here is a framework, not a verb. Its world map is a fixed
1002x668 frame showing one zone — twelve tiles, a hit button, POI buttons, two
dropdowns — with no canvas, no pan, no zoom, no pins and no notion of where one
map sits on another. Every map addon written after 4.0 drives the canvas
framework instead, and there is no stock file to migrate from because there is
no stock feature.

The constraint that shaped every decision: `WorldMapFrame` already has eight
1.12 consumers on this install, all anchoring icons to `WorldMapDetailFrame`
and `WorldMapButton`. So nothing is replaced. The canvas is added, and those
two frames are reparented INTO it — at rest the rect they had, at scale 1 with
no pan, so the stock window is pixel-identical, and the payoff is that the 1.12
art and every icon on it now pan and scale WITH the canvas for free.

Clipping comes from a real ScrollFrame: this client clips a scroll child and
every descendant of it from one source, in the draw pass and the hit-test, so
drawing and hitting agree. The base-art layer a canvas addon hides is a proxy
that switches draw layers rather than the frame, because hiding the frame would
take the POI buttons and every addon's icons with it.

Three regressions, each found by measurement, each pinned: the anchor cycle
(container to detail frame to canvas to container — a cycle resolves to
nothing, and the symptom was `GetCenter()` nil inside the stock hover pass); the
base scale (a minScale below 1 opens the map zoomed out of its own window); and
`dataProviders` being a set rather than an array, because callers iterate it
with `pairs` and bind the key — an array handed that loop the number 1.

`CreateLine` is a Texture carrying two endpoints, drawn through the
explicit-corner quad path the cooldown wedge already uses. Nothing stands in
for it: a line between two moving points is a rotated quad whose angle and
length both change. `C_Texture.GetAtlasInfo` answers nil for every name, and
the nil is the answer — but the getter's presence is what makes the common
atlas guard take its fallback branch instead of raising.

The harness grew a second surface, and the finding is worth keeping: the full
screen map hides `UIParent`, so opening it in the ordinary render probe dropped
the corpus's draw column from 37/86 to 18/86 — nineteen unrelated addons. As
its own pass: no addon lost a quad and nineteen gained, pfQuest 374 -> 911,
pfUI 1063 -> 2259.

OpenMapClassic now reads 1/1 on all five columns, stitching 74 of the realm's
own zones. Corpus roster unchanged at 85 ok / 1 fail; the 12 data-half reds
byte-identical to HEAD's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 22:21:35 -07:00
gromandClaude Opus 5 b49856c5cb ui: texture masks, the later-era texture dials, and the pools a canvas map needs
The mask pair is the one that matters and nothing can stand in for it. 1.12 has
no texture masking at all — its single masked thing is the minimap disc, cut
engine-side — and no idiom reaches the same end, because the point is that a
rectangular texture draws as a non-rectangular shape. A seamless world map is
that operation and nothing else: a zone's art is a rectangle of parchment and
the map wants it cut to the zone's coastline.

It was affordable because the render pass already had the slot. `UiQuad::mask`
is a screen-anchored alpha mask built for the minimap's fixed circle with tiles
panning under it, which is exactly `AddMaskTexture`'s semantics — the mask is
another region with its own anchors, and the subject keeps the fragments its
alpha covers at that place. So this is plumbing, not a new material.

One mask per quad, said rather than hidden: widening it is a second texture
binding and a second uniform on every UI quad in the client. The list is kept
whole, because the order decides the winner — first added, which for a stitched
map is the land mask. Two traps are pinned by tests: a mask region is skipped at
extract rather than marked hidden, since its rect is what it covers; and a mask
whose art is missing drops the mask, not the quad, because zero coverage would
blank the art it was meant to shape.

`Texture:SetRotation` was nearly free — the renderer has carried the angle since
the minimap arrow, so this is the Lua setter for it. `SetDesaturation` is the
fractional form of 1.12's own flag, rounded onto the one flag this renderer has,
with the getter answering the caller's own number. The tile pair is applied: the
host computes the repeat count once the art has loaded.

`SetPropagateKeyboardInput` is stored, not applied: 1.12 already always
propagates, so there is no swallow mode for false to select. `EditBox:HasFocus`
publishes a fact this client already tracks and never exposed.

Corpus: 85 ok / 1 fail, roster and digest unchanged. The 12 data-half reds are
byte-identical to HEAD's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 21:15:55 -07:00
gromandClaude Opus 5 87f93fea46 compat: the uiMapID graph over the realm's own WorldMapArea, and C_Map's other half
1.12 addresses a map by a (continent, zone) pair of dropdown indices — a
position in two lists, which moves when a zone is inserted — and publishes no
map's rectangle on any other map at all, because its window draws one zone at a
time into a fixed 1002x668 frame and never needs to know where that zone sits.
A seamless, pannable map is exactly that missing relation.

`script/uimap.rs` builds the tree as a pure function of the catalog
`ui_world_map.rs` already pushes, rebuilt inside both setters so the graph and
the catalog cannot disagree. No new DBC read, so nothing here can contradict
what the stock map window itself draws.

It is built from OctoWoW's own rows, which is the point: 175 against stock
1.12's 51, with 124 maps added and 49 shipped rects re-tuned. Era's uiMapIDs
are reused where the map is the same map, joined on the WorldMapArea art folder
rather than the zone name, because the folder is the column a realm has no
reason to touch — this realm spells Azshara's `Aszhara` and Hillsbrad's
`Hilsbrad` exactly as 1.12 does. A row Era has no counterpart for is minted
from its own AreaTable id.

One realm quirk is handled with its measurement: a third `areaID == 0` row
whose art folder is `World` is the sheet's own row, which the reference's
catalog walk files beside the two continents — `SetMapZoom(3, 0)` then
`GetMapInfo()` answers `World`. Left in, it is a phantom continent carrying a
second copy of all 39 Eastern Kingdoms zones.

Art is answered as paths, so the chain serves the upscaled pack; the layer
geometry is the logical 1002x668 / 256x256, because normalized fractions are
invariant under a uniform upscale.

The harness now seats the player a world position and the per-frame map feed,
because every verb that answers *where* the player is reads the position, not
the zone name. Corpus: 85 ok / 1 fail, roster and digest unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 20:38:22 -07:00
gromandClaude Opus 5 0886952611 ui: a numeric texture argument is a file id, not a colour channel
With this and the font-object rename below, **OpenMapClassic loads clean, survives a session
start and survives a UI probe** — from dying on its first file, inside Ace3, before a line of
its own code ran.

Later clients gave every shipped file a numeric id and addons pass those instead of paths. Both
of our texture setters read a number as the **colour** arm, and they failed differently, which
is why this is worth a module of its own:

- `region/paint.rs`'s `SetTexture` needs *two* numbers for a colour, so a lone
  `SetTexture(130763)` fell through to the catch-all and drew **nothing**.
- `button.rs`'s slot setters take one number as a colour with green and blue defaulting to 0,
  so `SetNormalTexture(130763)` painted a **solid block** where the button art belongs. That is
  the arm Ace3 calls, and it failed silently: nothing raised, the art was simply wrong, which is
  the worst way for this to go.

**The rule**: a lone numeric argument of 2 or more is a file id. A colour channel is 0..1 and
every id a client ever minted is six figures, so the two cannot collide and `SetTexture(0.5)`
keeps answering exactly as 1.12 does.

**The table is transcription, not invention.** Every numeric literal passed to a texture setter
anywhere in the installed corpus *and* in both `Addon_Ports` addons — 21, all from Ace3/AceGUI
— is annotated by its own author with the path it means:

    button:SetNormalTexture(130763) -- "Interface\Buttons\UI-DialogBox-Button-Up"

So the ids stay the real ones rather than something of ours, and the source is the call site. An
id with no row draws nothing and records a warning naming it, because a wrong texture is harder
to notice than a missing one — and that warning is what says which id to add. `GetFileIDFromPath`
answers only what the table knows and nil otherwise: minting a synthetic id would hand back a
number no other client agrees with.

**`SetNormalFontObject`** is the later-era spelling of 1.12's `SetTextFontObject` (`0x780400`)
— the one name this family left behind, since `SetHighlightFontObject` (`0x780700`) and
`SetDisabledFontObject` (`0x780580`) are 1.12's own. It hands out the same function value, so
the spellings cannot drift. Ace3 calls it on every button it builds
(`AceConfigDialog-3.0.lua:586`), one line above the `SetNormalTexture(130763)` above.

What openmap still cannot do is **draw**: it does not build a map, it re-skins Era's canvas
`WorldMapFrame` (`Map/WorldMap.lua:823`), and that canvas does not exist here. That is the map
stack, not this.

Corpus: 100 addons, 85 ok / 1 fail, roster identical. benilla-ui 2085 passed. Chain green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 18:43:51 -07:00
gromandClaude Opus 5 4d19090259 compat: the cast-info pair as bare globals, and IsMouseButtonDown over a real feed
Cinematic's last two session errors. With these it loads clean, survives a session start,
survives a UI probe and **draws** — all four of the harness's runtime columns.

**`UnitCastingInfo`/`UnitChannelInfo` as bare globals** is an export, not a feature: the
namespaced pair was already built with the whole contract in `cast_info` (the two return
shapes, the millisecond unit, every position this client leaves nil and why), and these hand
out the **same function values** the `C_Spell` table holds, so the two spellings cannot drift.
Six calls of each in `Cinematic/Camera.lua` and its siblings, unguarded. `CastingInfo`/
`ChannelInfo` stay namespace-only: nothing on this install calls them bare, and pfUI reaches
the one it wants through the namespace (`libdebuff.lua:887`).

**`IsMouseButtonDown` needed a feed before it could be built honestly, and the reason it was
declined is why.** `compat/classic_api/input.rs` had said no on the grounds that the only
press state here is `Model::mouse_down_on`, which the pointer layer fills *only* for a press
that landed on a frame — and `Cinematic/Camera.lua:612` asks in order to spot a right-drag **on
the world**, which is exactly what that map cannot see. Answering from it would have been wrong
for the one caller.

So `Model::mouse_buttons` is new: `[Left, Right, Middle, Button4, Button5]`, pushed each frame
from `ButtonInput<MouseButton>` beside the modifiers. Placed there deliberately rather than
with the pointer feed below it, because that feed is skipped for a cursor off the window or a
hidden UI, and "is the right button still held" has to stay true through both — which is the
camera's question. Both spellings (`RightButton` and `Button2`), no argument means any button
as upstream's does, and a button no device has answers not-down rather than raising.

The reference capture of the real client's `_G` has no mouse-button getter at all, and
`ClassicAPI.dll` carries the name, so it is compat's rather than a 1.12 gap.

Corpus: 100 addons, 85 ok / 1 fail, roster identical. benilla-ui 2082 passed. Chain green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 18:43:34 -07:00
gromandClaude Opus 5 482ad0bc5c sound: the later-era Sound_ CVar names, which three installed addons already wanted
`GetCVar("Sound_MusicVolume")` answered nil, so `Cinematic/Core.lua:3621` raised inside a
`format` and took the rest of its `PLAYER_LOGIN` handler — the music restore — with it. 2.x
renamed the sound CVars with a `Sound_` prefix and 1.12's names stayed what they are; Cinematic
is a multi-flavour addon and uses the modern spelling at **all 47** of its music sites, which
is the whole of its music-fade feature.

**This is a fix for the install, not a concession to the port.** Three addons already here ask
with the modern spelling and have been reading nil: DoiteAuras' editor and Tmog
(`Sound_MasterVolume`, `Sound_DialogVolume`, `Sound_EnableDialog`), SoftSpoken
(`Sound_EnableAllSound`, `Sound_SFXVolume`, `Sound_EnableSFX`).

Ten rows, each on the footing the `BackgroundSound` pair in this file already set: two names,
one knob, dispatched together in `sound/mod.rs`'s observer so the spellings cannot disagree
about a setting. Each default is its 1.12 sibling's, since they are the same setting twice.

Two honest compromises, labelled where they live: 1.12 has no dialogue channel, so
`Sound_DialogVolume` rides the SFX volume and `Sound_EnableDialog` the error-speech toggle; and
it has no SFX-only enable, so `Sound_EnableSFX` rides `MasterSoundEffects`, the only sound
enable it has. Riding what the mixer actually has beats inventing tracks it has not got.

**`GetCVarBool`** is absent from 1.12 on this repo's own evidence —
`reference/1.12-globals.tsv` is a capture of the real client's `_G` and carries `GetCVar` and
`GetCVarDefault` and nothing else of the family — and present in `ClassicAPI.dll`'s string
pool, so it belongs to the client this build announces itself as. It answers a real boolean, as
the name says, and **nil for a name with no row**, so "there is no such CVar" stays
distinguishable from "it is off". `Cinematic/Camera.lua:493` reads `ActionButtonUseKeyDown`
through it.

Not built, and deliberately: `IsMouseButtonDown`. `compat/classic_api/input.rs` already
declined it with the right reason — the only press state here is `Model::mouse_down_on`, which
the pointer layer fills *only* for a press that landed on a frame. `Cinematic/Camera.lua:612`
asks in order to spot a right-drag **on the world**, which is exactly what that map cannot see,
so answering from it would be wrong for the one caller. It wants a real mouse-button feed.

benilla-ui 2081 passed. Full chain green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 17:14:50 -07:00
gromandClaude Opus 5 139965f415 ui: PreClick/PostClick, and a comma-separated template list
Two of the `Addon_Ports` pair's load blockers, both measured with `addon_harness` either side.

**`PreClick`/`PostClick`** follow the convention `SCRIPT_KINDS` already set for names that are
not 1.12's — `OnHyperlinkEnter`/`Leave` sit there with the same justification: a later client
added them, addons register them at load, and refusing the name raises and takes the rest of
the file with it. `Cinematic/MenuWheel.lua`'s wheel slots are the callers here. They arrived
with the secure template system so a button could act either side of the protected part;
nothing protected sits between them here, so the order is all they are.

`button::click_button` fires each of the three **separately**, so one that raises is recorded
and the next still runs: the pair exists to bracket the click, and a broken bracket must not
eat it. That is the same reasoning the attribute-click dispatch below it already carries.

**A comma-separated template list**, which 1.12 does not take: its `inherits` is one name, and
`object.rs`'s lookup is a transcription of that (`0x6ee6f0` at `0x7061dd`, no splitter). Era
takes a list and applies them in order, and that is what an addon written for Era passes —
`Cinematic/MenuWheel.lua:312` asks for
`"SecureHandlerShowHideTemplate, SecureHandlerAttributeTemplate"`.

**The evidence is weaker than `compat/`'s other relaxations and the code says so.** The
one-argument `SetPoint` rests on the real `ClassicAPI.dll` writing that form itself; this rests
only on Era doing it and the addon targeting Era — nothing in the DLL's own Lua passes a list,
and **no addon in the installed corpus does either**, which is also why it can break nothing.

Safe by construction: the split is attempted only **after** the verbatim name misses, so every
existing call takes exactly the path it did before and a template whose own name contained a
comma would still win. One unknown name anywhere in the list and the whole call misses,
reporting the literal **unsplit** and leaving no frame behind — so a typo reads as a typo
rather than as a half-built frame.

`a_comma_list_is_one_name_and_a_single_name_still_chains` was a deliberate, traced pin on the
old behaviour, so it is rewritten rather than deleted. It now pins the shape of the relaxation:
the verbatim name tried first, both templates applied in the order written with `$parent`
resolving against the instance, a list with one unknown name still missing unsplit and leaving
nothing behind, and a plain unknown name unchanged. Its doc states the weaker footing, which is
why the miss path is pinned that closely.

benilla-ui 2081 passed. Full chain green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 17:14:32 -07:00
gromandClaude Opus 5 af823dd708 compat: the secure-handler family and the animation surface
Both modules land together because `classic_api/mod.rs` and `widget_surface.rs` carry rows for
each: split, the first commit named a module the second added, and would not have compiled.

## The secure-handler family, because 1.12 has nothing to be secure from

`frame.rs` declined it with a stated reason — "modern restricted execution, which 1.12 has no
equivalent of" — and that is right about the *sandbox* and is the whole of why it is now built.
A secure handler exists so an untainted snippet may call **protected** functions; 1.12 has
neither taint nor protected functions, so the restriction has no counterpart here, while the
plumbing underneath (a named frame store, a snippet on a child's handler) is ordinary
behaviour the snippets need. The body runs as plain Lua with the names the secure environment
would bind.

**The per-header shared table is the part worth getting right.** `MenuWheel.lua:484` writes a
bare `held = false` inside a snippet, and in the real secure environment that lands in a table
per header, not `_G`. `setfenv` onto one table per frame, `__index` to `_G` so the rest of the
API resolves; the test asserts `held` is **not** a global afterwards. My first test asserted
through `_G` and failed — the implementation was right and the test measured the wrong thing.

**`SecureActionButtonTemplate` is not empty**: reading your own attributes on click is plain
dispatch. `type="click"` clicks the frame in `clickbutton` (how the wheels reach the game's
micro buttons), and the lookup follows upstream's order (`type-<button>`, `*type*`, `type`).

**That exposed a real bug of ours**: `SetAttribute` silently dropped a frame.
`attribute_value` kept bool/int/number/string and sent everything else to `ScriptValue::Nil`,
so `SetAttribute("clickbutton", SomeFrame)` stored nothing — and upstream's `clickbutton` is
routinely a frame, which `Cinematic/MenuWheel.lua` relies on. A widget table is now kept as
`ScriptValue::Frame(id)`, the variant `NAME_PLATE_CREATED`'s arg1 already uses, which
round-trips through `object::frame_wrapper`. A non-widget table is still nil, documented.

**Not built, named rather than left to be discovered**: the binding half (`ClearBindings`,
`SetBindingClick`, …) are no-ops — no override-binding layer here to write into — so a wheel
opened with the mouse works and hold-a-key-and-release does not; and the `_onshow`/`_onhide`
attribute snippets are not run, since `SetAttribute` is a plain store with nothing to drive
them.

Templates in `assets/ui/CompatTemplates.xml`, listed on that wall with the reason: no stock
file to migrate from, and XML rather than `compat/` only because `inherits=` is loader-resolved.
A trap caught before shipping: `BackdropTemplateMixin.GetBackdropColor` forwarding to
`self:GetBackdropColor()` calls **itself** once `Mixin()` copies it onto the frame — the stock
getters answer, so there are no getter rows, and a test mixes in and reads back.

## `CreateAnimationGroup`: the surface, with nothing animated

1.12 has no animation system and this UI pass has no interpolation to hang one on. The cost is
the **absent constructor**: `Cinematic/Options.lua:7106` builds a rotating spinner and the nil
method aborted the whole options panel. Measured first — that is the addon's only animation in
55,661 lines, so this is a surface for a decoration.

**A non-looping group finishes the moment it plays**, firing `OnFinished`, because nothing
moves and that is the reading that keeps an addon's flow right; one that chains its next step
off `OnFinished` would otherwise wait forever. A looping group plays until stopped. Stated in
the header, since the name implies motion: a spinner does not spin, a fade appears at its
destination, a sequence lands on its last frame. Setters and getters round-trip.

Corpus: all 100 of grom's addons surveyed before and after — 85 ok / 1 fail, rosters
identical. benilla-ui 2081 passed. Full chain green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 17:12:13 -07:00
gromandClaude Opus 5 64f07d9864 compat: SetPoint("POINT") with nothing after it, because ClassicAPI writes it
**A deviation, and the reasoning matters more than the code.** 1.12 raises `Usage:` on a
one-argument `SetPoint`, and that is traced rather than assumed: the `lua_type(L,3)` tag gate
at `0x7a25e6`..`0x7a2620` accepts four tags and an absent argument is none of them, which
this file's own header has said all along. Later clients read it as the default parent at the
same point with no offset, and that is what every Era-era addon writes —
`Cinematic/CameraWheel.lua:243` is `face:SetPoint("CENTER")`.

Allowed because **the real `ClassicAPI.dll` writes that form itself**: its embedded Lua
carries `f:SetPoint("CENTER");` and `tx:SetPoint(q[4]);`, so on the client this build already
announces itself as (`CLASSIC_API_VERSION` is set) the one-argument form has to work, or
ClassicAPI's own windows would not build. Claiming that client and withholding this is the
inconsistent position `stdlib_backport.rs` settled for the stdlib half.

It costs nothing in the ladder because of how it is spelled: the leg resolves to exactly what
the reference's own **three-argument** shorthand does (`RelTarget::DefaultParent`,
`0x7a270e`) — default parent, `rel_point` defaulted to `point`, offsets falling through to
(0, 0). No anchor shape is added; one spelling is mapped onto another the reference answers.

**It lives in `parse_set_point` and not in `compat/`, and that is load-bearing.** My first
attempt wrapped `SetPoint` per method table and `tests::region_map` caught it: the reference
registers it once in the Region method table, so every widget type runs the same function
object, and three wrapped copies broke an identity `region_map.rs` maintains on purpose (an
addon can pull `WorldFrame.GetHeight` off and apply it to a Texture). The header now says so,
so the wrapper route is not tried again.

Two tests pinned the old behaviour and are rewritten rather than deleted:

- The trailing-nil test gets **stronger**. It existed because `lua_type` tells `LUA_TNIL`
  (the screen root, `0x7a2710`) from `LUA_TNONE`, and the ladder must see the count Lua saw.
  Both forms resolve now, so it resolves a real layout and asserts they land in *different*
  places: the explicit nil at the screen's corner, the absent one on its parent.
- One genuine ordering change, asserted: with no third argument there is no tag to gate, so
  `SetPoint("MIDDLE")` reports "Unknown region point" where it reported `Usage:`. The
  tag-gate-runs-first claim is kept for a *bad* tag, `SetPoint("MIDDLE", true)`.

Movement: Cinematic's load `CameraWheel.lua:243` → `MenuWheel.lua:312` (the secure-template
family, which `compat/classic_api/frame.rs` declines with its own reason).

benilla-ui 2077 passed, 0 failed. Full chain green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 14:46:07 -07:00
gromandClaude Opus 5 164eb7e624 ui: the ClassicAPI-era templates addons inherit by name
`CreateFrame(): Couldn't find inherited node "DialogBorderOpaqueTemplate"` is where
OpenMapClassic stopped — on its first file, inside Ace3, before a line of its own code ran.
It is not that addon's problem: `BackdropTemplate` is named **unguarded** by Ace3 itself
(`AceGUIContainer-Frame.lua:179` and `:206`, `AceGUIContainer-DropDownGroup.lua:128`), so
every Ace3 addon on this install hits it.

`assets/ui` does not grow, so this file carries its reason on that wall's own list: there is
no stock 1.12 file to migrate from, because 1.12 has neither template. They are the same kind
of surface as `script/compat/`'s verbs and live in XML only because `inherits=` is resolved by
the loader and cannot be answered from Lua.

**`BackdropTemplate` is correctly empty.** 9.0 moved the backdrop off the C side and invented
`BackdropTemplateMixin` to put it back; 1.12 still has `SetBackdrop`/`GetBackdrop`/
`SetBackdropColor`/`SetBackdropBorderColor` as real frame methods, so a frame inheriting the
template already answers all four. The mixin table exists for the
`BackdropTemplateMixin and "BackdropTemplate" or nil` idiom and for `Mixin()`, its rows
forwarding to the stock methods. **No getter rows**: a forwarding `GetBackdropColor` would
call itself once `Mixin()` had written it onto the frame, and the stock one already answers.

The dialog border is one `<Backdrop>` over this client's own dialog art — the shape every
stock 1.12 dialog draws, and the same block `GameMenuFrame.xml` uses — rather than a
transcription of Era's nine `SharedUIPanelTemplates.xml` textures. The opaque variant differs
only in background alpha, which is the one thing the two names disagree about. `enableMouse`
stays off: it is a decoration over a frame that handles its own input, and a border that ate
clicks would make an Ace options window unusable.

Movement: openmap `AceConfigDialog:573` → `:586`, now wanting `SetNormalFontObject`.

Full chain green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 14:45:49 -07:00
gromandClaude Opus 5 e731a77a42 compat: RegisterUnitEvent, the fixed-strata pair and SetWordWrap
The next three blockers of the `Addon_Ports` pair, each measured with `addon_harness` before
and after rather than picked off a list.

**`RegisterUnitEvent(event, unit1 [, unit2])`** — Cinematic's load blocker, and
`docs/ADDON_COMPAT.md` already had it on the "plausible and untraced" line. The registration
is the stock one: same `event_to_frames` list, same registration order, and
`Model::frame_unit_events` holds the units per frame per event. `tick.rs`'s walk skips a
frame whose row for this event does not name `arg1`; the map is empty on an install with no
such addon and the walk tests `is_empty()` first, so the common path costs one load.

All three unregister paths drop the row, including the one that is easy to miss: a plain
`RegisterEvent` after a unit register **widens** the registration back, as upstream's does —
the frame keeps its place and loses the filter. A row surviving an unregister/re-register
cycle would swallow that event forever, so the test walks the whole cycle.
`Cinematic/Frames.lua:2611` registers `UNIT_AURA` for `"player"` at file scope, so the nil
method stopped the file dead.

**`SetFixedFrameStrata`/`SetFixedFrameLevel`** are no-ops, and that is the honest answer
rather than a stub: 9.0 added them to tell the *frame manager* to stop re-deciding a frame's
strata and level, and this client has no such manager — both are whatever the setters last
wrote and nothing else rewrites them, so the guarantee the caller asks for already holds.
Ace3 calls both unguarded (`AceConfigDialog-3.0.lua:575-576`).

**`SetWordWrap`/`CanWordWrap`** is 3.x's whether-to-wrap switch; 1.12's `SetNonSpaceWrap` is
a different question (where a wrap may fall, not whether one happens). **Stored, not
applied**, on the footing `SetSpacing` and `non_space_wrap` already set in `types.rs`: the
text layout wraps to the declared width unconditionally and has no one-line-and-clip mode for
the flag to drive, so honouring it would mean inventing a layout mode. Kept because the pair
must round-trip and because the missing *setter* is what costs —
`Cinematic/Options.lua:4130` calls it on a label and aborted the options panel there.

All four are registered in `widget_surface.rs`'s `ALLOWED_BEYOND` with their call sites. The
first `RegisterUnitEvent` row said "Frame" and the wall rejected it: event registration lives
on the one shared frame method table, which is where 1.12 keeps `RegisterEvent` too, so the
two are registered alike on all twenty frame kinds or neither is.

Movement: Cinematic's load `Frames.lua:2611` → `CameraWheel.lua:243`, session
`Options.lua:4130` → clear; openmap `AceConfigDialog:573` → `:575`.

benilla-ui 2077 passed, 0 failed. Full chain green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 14:45:36 -07:00
gromandClaude Opus 5 79e2494fc6 compat: math.huge and SetColorTexture, the two load blockers of the ported pair
Measured through `addon_harness` before anything was written, which is the only reason this
is two small changes rather than a guess at a list. Both `Addon_Ports` addons failed to load
at all; these are their first two stops.

**`math.huge` was nil, deliberately.** `stdlib_backport.rs` restores the 5.1 stdlib members
ClassicAPI backports, scoped to what the corpus calls, and `math.huge` was on the stated
"nothing calls them" list. `Cinematic/Core.lua:1103` is `local lastMovingOff = -math.huge`
at **file scope**, so the nil stopped the addon's main file dead ("attempt to perform
arithmetic on field `?`") and nothing after it ran — one site, but a load-time one, which is
the bar that table sets. The real `ClassicAPI.dll` carries `huge` in its own stdlib name
table beside `fmod`, `modf`, `gmatch`, `match`, `select`, `maxn`, `cosh` and `reverse`, so
this is what the client we already claim to be actually has.

Two tests pinned it absent and are updated rather than deleted: `lua50.rs`'s keeps asserting
this *layer* removes it (the registry stash is the proof), and `stdlib_backport.rs`'s keeps
the narrowness assertion for the four that stay gone. The new assertion is `-math.huge`,
because file-scope arithmetic is the shape that failed.

**`Texture:SetColorTexture`** is the modern spelling of `SetTexture`'s colour arm, which
1.12 already has (`0x770360`'s 8×8 solid surface, and `region/paint.rs` defaults the alpha
to 1 as it does). Retail split the names when `SetTexture` stopped taking a colour, so every
Era-era addon writes the new one. `Cinematic/Options.lua:701` paints its list border with it
and died at session start; OpenMapClassic its map chrome. Four lines of the same composition
chunk `SetShown` and the geometry pair use, and an `ALLOWED_BEYOND` row with both call sites
— `docs/ADDON_COMPAT.md` had it on the same "plausible and untraced" line as
`RegisterUnitEvent`, and this is that line's follow-through.

Movement, same harness both ways:
- Cinematic load: `Core.lua:1103` → `Frames.lua:2611` (~10 files further; wants
  `RegisterUnitEvent` next). Session: `Options.lua:701` → `Options.lua:4130` (3400 lines on,
  wants `SetWordWrap`).
- OpenMapClassic: unchanged at `DialogBorderOpaqueTemplate`, which neither fix touches.

benilla-ui 2076 passed, 0 failed. Full chain green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 14:19:56 -07:00
gromandClaude Opus 5 2a65266837 assets: the shader asset type exists for an app with no render plugins
Every end-to-end asset-pipeline test — the M2, WMO, BLP and ADT loaders, the minimap's
`load_with_settings` path and the Ironforge group grid — panicked with "Cannot allocate an
Asset Handle of type 'bevy_shader::shader::Shader' because the asset type has not been
initialized". `register_shaders`'s `load_shader_library!` calls allocate a `Handle<Shader>`,
and on Bevy 0.18 it is the render plugins that initialise that type; a headless app has
none, so all six went red at once on the upgrade.

They are the only tests that exercise the loaders every zone load depends on, and the gate
could not see it: each one is data-gated, so it skips without the install, and the
no-install gate is the one that runs everywhere. Found by running the workspace suite with
the install mounted — 44 red there against 0 on the gate.

Initialised here when nothing else has, and **guarded**: `init_asset` *inserts*
`Assets<Shader>`, so calling it unconditionally in the real app would drop every shader the
render plugins had already loaded.

benilla-assets: 109 passed, 0 failed (was 103 and 6). Full chain green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 14:01:02 -07:00
gromandClaude Opus 5 e646b5a678 compat: the usable pair tells the truth — 1/0 from nampower's verb, the walk behind C_Spell's
Two defects in one pair of verbs, both found by reading what grom's addons do with what we
answer rather than from a report. A DLL whose source is not to hand still has its contract
written down: in the addons written against it.

**`IsSpellUsable(id|name)` answers two numbers.** Ours gave 1/nil for the first — transcribed
from `IsUsableAction`'s own convention — while already giving 0/1 for the second. Four
readings in the corpus say otherwise, and `AlternativeClassicActionBars/Button.lua:1907` is
the clearest: `elseif usable and usable ~= 0 then` is not a line anyone writes against a
function that answers 1/nil. `SuperCleveRoidMacros/Core.lua:1035` labels its own branch
"IsSpellUsable returned 0 = wrong stance/form, not enough power type", reading the value
straight out of `pcall(IsSpellUsable, …)`. And the one with teeth: DoiteAuras'
`_SafeSpellUsable` keeps a verdict only `if ok and u ~= nil` (`DoiteConditions.lua:970`), so
a nil sent it down a fallback chain ending at `:1022`'s "treat as usable so icon doesn't die
forever" — **every** Usable-mode aura lit on cooldown alone, stance, rage, range and reagents
ignored. Answering the wrong shape and not answering at all land in the same place.

0 is truthy in Lua, so that was checked before changing it: no caller tests the value for
truth unguarded, and the rest compare `== 1`. The DLL's string pool publishes the signature
and the name-lookup error but not the return shape, and the five mirrors of the fork's
`SCRIPTS.md` I could reach are each behind a Gitea login or 404 — the corpus was the better
source. A name it cannot resolve raises there, which is why every caller wraps the call in
`pcall`; ours answers `nil, nil`, and all four callers treat those identically.

**`C_Spell.IsSpellUsable` was computing the wrong thing and raising on its caller's
argument.** It was approximated as "known and not on cooldown" — the one reason upstream does
*not* count (its own list is "hasn't learned it, lacks mana or reagents, reactive conditions
haven't been met") and none of the ones it does. It now reads the same per-spell map the
global does, the reference's usable walk (`0x6e3d60`), which agrees about cooldowns: "a
running cooldown never greys here" (`spell/usable.rs` leg 11), leaving `GetSpellCooldown` the
question about cooldowns.

That makes it the honest answer for a reactive proc, which is exactly what its only caller
asks: Overpower greys on the caster's combo-point byte (leg 5, `PLAYER_FIELD_BYTES` byte 1,
the byte the server sets when the proc banks), so the walk answers it inside the proc window
and nowhere else, while "off cooldown" showed it nearly always. pfUI's reactive indicator
(`modules/nampower.lua:130`) passes a **name** against a `u32` signature, which mlua refuses
for a non-numeric string — a raise once per frame while the indicator was switched on.
`spell_ident` is shared now, so a name resolves through the book as the global's does.

Both returns there stay booleans, unlike the global's: the caller hands the first straight to
`SetShown`, where a numeric 0 would be truthy and show every icon forever. Two conventions,
each its own caller's, written down where they live.

Tests: the global's pair is asserted as `1|0` and `0|1`, with the callers' own accept test
(`pcall` + `u ~= nil`) quoted as an assertion; `C_Spell`'s is asserted over Overpower outside
and inside the proc window with nothing about cooldowns changing, by id and by name, plus the
not-in-book case answering false rather than raising.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 13:11:00 -07:00
gromandClaude Opus 5 315ebe9138 ui: an <IMG> with an empty src names no file
`src=""` was stored verbatim, so the region carried `texture: Some("")`, the resolve asked
the size probe for a nameless file and the renderer searched the patch chain for one and
logged `texture miss: ''` — a defect-shaped warning that can name neither a file nor an
asker. `""` clears on every other texture setter here, as the reference's path arm treats
it like nil (`0x770200`), and `a9ed091eb` closed the three that had stored one; this was
the last route in the UI that still could.

Found by sweeping the rest rather than by a new sighting, and two of the leads this list
carried are now dead: the `CursorPayload` arm it called most likely is unreachable in a
live session (`extract/mod.rs:1020` guards the whole block with `if capture.is_some()`),
and `PortraitSource::File` cannot be empty — `temporary_portrait` always returns a `.blp`.
The M2 and WMO sides filter at parse, and `texture_url("")` would read `'.blp'` rather than
`''`, so the warning's own text says the UI sprite path is where it came from.

Whether this closes the one line in grom's log is unproven: it sat in `benilla-2.log` at
+20.717s, that run may predate `a9ed091eb`, and three measurement launches have since
rotated his logs out. The test is the next sighting: with the filter removed it reports
`texture: Some("")`, and in the fixture the nameless path also reserved the probe's 48 px
of flow under the image — in game the probe answers nothing for a miss, so only the chain
search and the warning were real.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 12:59:29 -07:00
gromandClaude Opus 5 fb0d37e3c8 sound: a stream whose callback stopped is rebuilt, whatever the device layer said
cpal's ALSA loop exits on a `POLLERR` out of `alsa::poll()`, which reaches us as a
`BackendSpecific` error — classified non-fatal there, because an xrun must not tear the
stream down. So `device_died` was never set, nothing rebuilt, the ring filled, the render
thread parked on a full ring and kira's renderer stopped advancing every sound in the mix.
In grom's 2026-10-18 log that was four minutes of silence out of one `soggyplace-zone5.mp3`,
cleared only by a worldport replacing the slot; the `3D sound(s) never played — the
spatial-voice arena was full` count climbing to 267 in the same session is the proof that
nothing at all was completing, music included.

`StallWatch` reads the cycle count instead of the error string: a running stream that
reports no cycle for 2 s is dead however it died, including with nothing raised at all, and
`service` closes and reopens it exactly as it does a lost device. Reclassifying the error
could not have worked — an xrun arrives in the same variant and the string is all we get.

A window with no cycles is not a wall clock with no cycles. `service` runs once a frame on
the main thread and a 4 s load hitch is one window the callback ran right through, so the
span is taken between two `Instant`s and any window that saw a cycle resets it, however
long that window was. That is the same trap as `510dda3d3`, one layer down.

Measured while verifying it, and the reason for the other two pieces here: the rebuild held
the main thread 4.5 s with the audio server down. Attributed with `WOW_SYSTEM_TIMING=1
WOW_STALL_MS=400` to `sound::poll_mix_health`, then split — close 5 ms, finding the default
2 ms, `Stream::open` 4521 ms. Opening a device is a blocking call into the host's audio
stack. `SLOW_OPEN` now says so in the log, and the reopen wait backs off to 8 s while opens
are slow or refused, so an outage costs one blocked frame per attempt instead of one a
second. It did not happen in grom's own failure, where the server stayed up and the reopen
would be instant. Taking the open off the main thread needs a device thread owning every
open and close; not done here.

Verified: four offline tests — a stream calling back at 3 ms frames, the 4.1 s login hitch
arriving as one window of 380 cycles, a real stop named once on the limit, and the backoff's
escalation — plus two live runs on Linux/PipeWire, restarting pipewire under a running client
at the glue screen. Both read the same: position frozen, two genuine starve warns, `output
stream dropped — the stream stopped calling back; reopening`, a reopen in the same frame,
music advancing again. Before this, that was permanent.

Built and run on Linux only; the device layer's own files are untouched apart from the
comment that pointed the wrong way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 12:59:18 -07:00
gromandClaude Opus 5 510dda3d3c sound: the glue-music crackle never happened — a meter that summed frame deltas
`StreamWatch` asked "did this stream play as much audio as wall time passed?" by summing each
frame's `Time<Real>` delta into `expected` and comparing it against the stream's own advanced
position. But a Bevy clock delta describes the interval *ending* where the frame began, while
`window_start` is stamped with `Instant::now()` part-way *through* a frame. So a window opened
inside the 4 s world-entry frame was handed that entire 4 s by the next frame, as audible time
that should have played — while the position, which tracks the speaker honestly, had advanced
only by the window's real span. The difference was logged as injected silence.

The tell was in the log all along: `position advanced` equalled the window `span` to the
centisecond. When those two agree the audio was perfect, whatever `counted` says.

All 101 "glue music stream starved" lines in every log to date are this artifact, plus 104 of
395 zone-music ones. Replaying the new formula over those logs silences all 205 and keeps all
291 real events — streams genuinely frozen with `position advanced 0.00 s`.

Fixed by taking both ends of the window off one clock (`now - window_start`) and deleting the
`dt` parameter outright, so the counted interval and the reported span are the same number by
construction and no caller can supply an inconsistent delta. The old test
`stream_watch_spans_the_window_it_counted` asserted exactly this invariant and passed, because
its fixture fed a delta that agreed with its own wall clock — a fixture that supplies both sides
of a relationship cannot test it. Replaced with the regression that matters: a stream which keeps
up across a 4.1 s main-thread stall must not be reported, and one genuinely frozen through the
same stall must still be named, or the fix has just deleted the meter.

The warn now reports what it measured; "this is what a crackle sounds like" was read as evidence
of a crackle and written into the defect backlog as one.

`examples/stream_probe.rs` decodes a track through the chain exactly as kira's decoder does and
compares the container's declared frame count against frames actually decoded — it killed the
first hypothesis for the real dropout: 43 of 43 zone tracks are clean.

The real dropout is still open and is not a crackle: one `alsa::poll()` POLLERR, which
`output/cpal.rs` classifies as non-fatal and reports as "stream kept", and no sound advances
again for the rest of the session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 11:27:18 -07:00
gromandClaude Opus 5 14d5dc36e9 formats: benilla-formats is green — the world-data goldens, and two tests that
were measuring nothing

The last 11. The realm reships `World\Maps\`, `HumanMale.m2` and
`TaxiPathNode.dbc`, so these tests had lost the *examples* they picked without
losing the properties they were for. Two had quietly stopped testing anything.

**A guard that was a proxy, not a fact.** The lava seam test skipped a vertex
whose UV was `[0.0, 0.0]`, standing in for "this vertex is dry". The realm's
lava leaves non-zero texcoords on undrawn verts, so 121 meaningless pairs were
entering the compare and one of them failed. Gating on the mesh's own `wet` map
instead leaves 277 pairs — the verts both neighbouring blocks actually draw —
and every one of them agrees. The invariant was never in doubt; the guard was.

**A keyless bone read as an empty band.** `empty_band_poses_the_bone` asserted
HumanMale's waist (bone 27) carries exactly one clamp key in Run. In the realm's
model bone 27 has no rotation or translation keys in *any* of its 156 sequences,
so there was nothing to clamp and the assertion was reading a keyless bone. The
subject is the five finger bones 100..=104 now: each appears in every one of the
156 sequences with exactly one clamp key at t = 0, which is the rule — an empty
band poses the bone and never drops it.

**The skeleton was renumbered, and the poses came with it.** 1.12's finger bone
102 is unposed at the HandsClosed frame; bone 47 carries the curl, within 0.004
of 1.12's `[0.2536, -0.016, 0.0577, 0.9654]`. Same for the eyelid: bone 75 ->
bone 24, still scale-only, still a 6633 ms loop that opens at t=0 and shuts
mid-loop. That the *pose* survived is what says renumbering rather than
re-animation, so the window arithmetic is still what is under test. (The model
does have HandsClosed throughout — `parse_m2_animations` not surfacing it is
exactly why the raw reader exists.) Ten new global-sequence bones came with it,
all parks: six 33 ms two-key constant scales and four rotation channels with
empty tables, recorded so a real loop appearing among them reads as a change.

**Tiles chosen instead of inherited.** `examples/liquid_scan.rs` walks a map and
reports which tiles still have each liquid property, so a re-recorded test names
a tile that was picked:
- the river mouth moved to `Azeroth_33_38`, three Still-over-Ocean chunks, all
  disjoint — 1.12's 33_33 has no two-block chunk left. Four tiles *do* overlap
  and are kept out deliberately: Kalimdor 26_54, 41_29, 41_30 and the realm's
  own Azeroth 36_51, where 1.12 had only 26_54.
- the ocean depth divisor moved to `Azeroth_29_54`: 13210 depth bytes inside the
  ramp and not one pinned at 255, so a wrong divisor cannot hide behind
  saturation. 1.12's 41_41 carries no ocean at all now.

**The impassable band was thinned.** `Azeroth_33_44` authors 17 impassable
chunks where 1.12's copy had 48, and the chunk 1.46 yd east of the reported
`.go xyz -6601.98 -531.87 335.60` is among the ones it dropped — so the data no
longer walls the reported spot. The `Some(true)` leg would have stopped being
tested, so it moved to a column inside one of the 17 the realm does flag.

**The transport goldens no longer describe this client.** `TaxiPathNode.dbc` is
re-authored: 11219 rows against 9582, and 7975 rows kept their id while changing
path, index *and* coordinates. Path 436 is gone; the eight survivors miss
vmangos's 1.12 periods by −290815 to +271184 ms, and no transport path crosses
continents any longer. Closing that needs the realm's own transport periods,
which only live observation gives — so the calibration is a report now, with the
deltas printed on the same variant grid a future observation can be compared on.
What is still assertable is asserted: `client_period_ms` and the timetable's own
`period_ms` are one number on all eight. The two map-invariant tests never
needed a server period at all, so they run on taxi path 72 — 37 nodes over maps
{0, 1}, the only shipped path that still spans both — and additionally assert
that every transport golden is single-map, so none of them can silently stand in
for it again.

**benilla-formats: 445 passed, 0 failed.** `cargo fmt --all` and
`clippy -p benilla-formats --all-targets` clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 19:58:18 -06:00
gromandClaude Opus 5 c0685f0273 formats: the realm's tenth race, and what it switched on
Four goldens, and three of them turn out to be the same fact: `ChrRaces.dbc`
has a tenth row — id 10, **High Elf**, on the Blood Elf art (`ClientFileString`
"BloodElf"), Alliance-side.

- **Every talent tab opened to it.** All 27 `TalentTab` rows went raceMask
  511 -> 1023, so race 10 takes every class's tree rather than being gated out
  of any; the test asserted the opposite, that "race bit 10 is outside 511".
  `Talent.dbc` itself was rebuilt too — 476 rows, 317 ids added, 273 removed,
  159 changed — so the trees are the realm's, not 1.12's.

- **Dwarf Mages are real here, and the gate was already written for it.**
  `chain_is_stock` calls a chain stock when no playable race exceeds 8, so this
  install read as stock until race 10 arrived and the `UNUSED_COMBOS`
  Dwarf-Mage skip kept applying; the test's "patched chain or not" passed by
  accident. The skip only ever modelled a compare in the *stock* executable at
  `0x4706b0`, and this install's own `WoW.exe` has none — the whole builder
  holds no immediate compare against 3 or 8 and gates on a `0xFE` sentinel
  (`cmpb $0xfe,0x1(%ecx)` at `0x4706e8`). Re-verified against the binary rather
  than trusted from the comment. `CharBaseInfo` grew 103 -> 139 bytes, which at
  2 bytes a row is 18 new race/class pairs.

- **`CharHairGeosets` was re-authored, and took a test's only coverage with
  it.** 299 rows against 148, 136 of the survivors rewritten, and 1.12's one
  duplicated key — the goblin male's variation 0, listed four times as geosets
  1, 2, 1, 2 — is gone with none replacing it. That key was the *only* thing
  exercising the loader's first-row-wins rule, so re-recording the count alone
  would have quietly dropped it: `or_insert` could have become `insert` and
  nothing would have noticed. The hair decode is now a `hair_geosets_from_bytes`
  seam and a synthetic DBC replays 1.12's goblin block to pin the rule, plus
  the reverse order to prove the later row is discarded and not merged. The
  real-data test asserts the absence instead. Race 9 is where the re-authoring
  shows: the goblin has a full hair set for both sexes now, 14 male variations
  and 17 female, and the male's variation 0 is `GeosetID` 0 where 1.12's
  winning row said 1 — both land on scalp geoset 1, since `visible_geosets`
  takes `max(1, GeosetID)`.

Also:

- **WorldStateUI** 20 -> 34 rows, and the realm rewrote the text and tooltip on
  every one of 1.12's 20. Only 33 ids are distinct — **168 ships twice**, both
  map 0 / area 44, one reading "Banners Controlled: %3809w" and one "Tower
  Score : %3811w / %3813w". Safe, and checked rather than assumed: the catalog
  exposes no id lookup at all, only `rows()`, and the app filters by (map,
  area), so both readouts reach the frame — which is the only thing two rows
  under one id can have been meant to do. Four zones narrow by area where 1.12
  had two, the new three being outdoor PvP: Redridge Mountains, Hillsbrad
  Foothills and Silithus. Live `StateVariable`s run to 3810 where 1.12's three
  all sat under 3000.

- **WMOAreaTable** 21115 -> 26653 rows and 21105 -> 26570 distinct
  (wmo, nameset, group) keys, so the repeats went 10 -> 83; 5633 ids added, 95
  removed, 4080 changed.

benilla-formats: 16 -> 11 failures (and one test more than before). `cargo fmt`
and `clippy -p benilla-formats --all-targets` clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 19:44:37 -06:00
gromandClaude Opus 5 d2414d9f50 stable: the realm sells four pet slots, and our window drew two
Found by the `StableSlotPrices.dbc` golden: the table has four rows where 1.12
had two, and `NUM_STABLE_SLOTS` was 3 (the current pet plus two). Three
independent sources in the install agree on four, so this is not a guess:

- `StableSlotPrices.dbc` ships 4 rows — 500c, 5g, 250g, and a fourth at one
  copper — against 1.12's two.
- the realm's own `PetStable.lua` (patch-4) declares `NUM_PET_STABLE_SLOTS = 4`.
- the realm's `WoW.exe` caps the purchase at four, and `0x4cb0bf` reads straight
  across: `cmp $0x4,%eax; je refuse` on the bought count, then `inc %eax` and
  `cmp 0xc0d728,%eax; jg refuse` against the price table's max id — which is
  exactly `num_stable_slots != NUM_STABLE_SLOTS - 1 && next_slot_cost != 0`.

So the window drew two slots the server would happily sell, and
`GetStablePetInfo(3)`/`(4)` answered nothing: the last two buttons were dead.
`NUM_STABLE_SLOTS` is 5 now, the gate's comment cites the realm's binary rather
than 1.12's 2, and the test fixture helper takes however many leading slots a
case cares about so the existing three-slot fixtures still read.

Also five goldens re-recorded, two of them findings:

- **The spam filter is one line now.** patch-4 ships a 93-byte
  `SpamMessages.dbc` of one row where `patch.MPQ`'s 2161 bytes held 28, and the
  chain takes the whole winning file rather than merging ids — so Blizzard's 28
  gold-seller patterns are gone and what is left matches a rival server's
  address, whitespace-tolerant between every letter.

- **`md5translate.trs` stopped hashing.** 9794 rows against 8401, and an Azeroth
  tile now resolves to the readable path `Azeroth\map32_48.blp` rather than
  `ea283abc0bf9637c3fad5e840a65b38b.blp`. The realm ships the tile at that path
  in patch-4; 1.12's hashed copy is still in `texture.MPQ` at the same 33940
  bytes, so only the table moved and the lookup still has one job.

- ItemSet 172 -> 363 rows, and Defias Leather (161) gained a sixth item, 81007,
  one of the realm's own. Every one of 1.12's 172 sets also gained a name in
  five more locales — the same five-locale fill the realm gave `EmotesText`.
- NPCSounds 156 -> 247, purely additive; the one pre-existing row patch-4
  touched is 90, which gained two kits.

benilla-formats: 21 -> 16 failures. `cargo fmt` and
`clippy -p benilla-formats -p benilla-ui -p benilla-app --all-targets` clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 19:36:30 -06:00
gromandClaude Opus 5 e61976bc0e formats: the world-map goldens, and a nondeterministic continent lookup
The four map tables against the realm's own files. One real bug, and three
findings that are the realm's data rather than ours.

**`WorldMapArea::continent` was a coin flip.** It `find`s the `area_id == 0`
row for a map id, and 5875 had exactly one per map so order never mattered. The
realm ships two for map 0 — 14 "Azeroth" and its own 694 "World" — and the
lookup iterated the `by_id` `HashMap`, whose order is seeded per process. So
`continent(0)` answered Azeroth or World depending on the run. Now it walks
file order, which is the reference's own walk and picks 1.12's row; the test
asserts 14 and passes eight runs in a row where it used to flip.

- **WorldMapArea** 51 -> 132 rows loaded, of 175 in the file. The 43 dropped are
  dropped on purpose: `AreaName` empty, `AreaID` 0, the whole rect zero, one per
  instance map (33 Shadowfang through 533 Naxxramas, then the realm's 800s). An
  instance has no continent art — but `AreaID == 0` is exactly what
  `ui_world_map.rs:146` reads as "this row is a continent", so keeping them
  would put 43 unnamed continents in `GetMapContinents()`. `str_at` folds an
  empty string to `None` and the row falls out; the test now pins that as
  deliberate. The realm's third *named* continent is 694 "World", a second sheet
  over map 0 whose art ships (patch-M) but whose four rect columns are all zero
  — the only named row like that. `map_proj::zone_uv` already refuses a zero
  span and answers 0.0 instead of dividing, so that sheet puts a blip in the
  corner rather than NaN.

- **WorldMapContinent** 2 -> 3 rows. The new row's `MapID` is 42, which the same
  patch *deleted* from `Map.dbc` along with the rest of 1.12's dev test maps
  (13 "Testing", 25 "Scott Test", 29 "CashTest", 37 "Azshara Crater", 42
  "Collin's Test", 44 "<unused> Monastery"). It is inert rather than broken: the
  continent list comes from `WorldMapArea`, and this table is only ever reached
  by `wmc.get(map_id)` for maps 0 and 1, so nothing asks for continent 42.

- **The zone bitmaps are repainted.** `Azeroth.zmp` and `Kalimdor.zmp` have the
  realm's own copies in patch-3 and patch-4, and every id they write is a
  **zone** where 1.12 wrote sub-areas and left the client to roll them up.
  Azeroth: 1874 labelled cells over 34 distinct ids, all zone-level, against
  2630 over 334 of which 310 were sub-areas (Northshire Valley, Crystal Lake,
  Fargodeep Mine). After the one-hop rollup `ui_world_map.rs:193` applies, 1280
  cells agree, 534 name a *different* zone, 816 lost a zone and 60 gained one —
  a redrawn map, not a mechanical flattening. Nothing of ours needed changing:
  the rollup is a no-op on an already-flat grid. The test asserts the structural
  claim behind the number (every labelled cell is a top-level zone), which 1.12's
  own file would fail, so a decode slip cannot hide inside a re-recorded count.

- **WorldMapOverlay** 526 -> 707, and not an extension: 360 ids added, 179
  removed, 347 survivors given a different texture and rect. None of this
  table's numbering is 1.12's, so only the joins are worth asserting.

`dbc_diff` gained `--row=N`, which dumps a row from the winning copy *and* the
baseline — the only way to read a row the realm deleted, which is how the six
test maps above got their names.

benilla-formats: 25 -> 21 failures, no regressions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 19:20:54 -06:00
gromandClaude Opus 5 e58ddc10ab formats: re-record the sound and visual goldens, and a tool to say what moved
Six modules' data goldens against the realm's own files, plus the thing that
made the last three honest rather than guessed: `examples/dbc_diff.rs`.

The tool reads the copy of a DBC the chain *wins* and the copy it shadows and
diffs them row by row. `Chain::copies` is ascending priority and several
`patch-*` archives are mounted, so 1.12's own file is usually still underneath
the realm's; the baseline is the highest copy in a stock archive (`dbc.MPQ`,
`patch.MPQ`, `patch-2`) so the diff reads "what OctoWoW changed about 1.12".
Schema-free — field 0 is the id, a row is its raw 4-byte slots — so an
undecoded column still shows up as "id N differs at slot K". `--row=N` dumps a
row with its text slots as text, which is how three ids below got names.

Two findings, both the shape of a reference into a table that has no such row:

- **The glow band.** The realm rewrote `ItemDisplayInfo.dbc` (patch-5) and left
  `ItemVisuals.dbc` as 1.12's out of base `patch.MPQ` — 34 rows, ids `1..=134`.
  22 ids its new displays name run past that end, where a later expansion's
  table continues, so 61 items wear no glow. The reference drops them exactly
  as we do (`0x479700` skips a visual id past the max id), so this is the
  realm's data, not our decode. Pinned by id with the displays behind each.

- **The silent kits.** `SoundEntries.dbc` grew 4623 -> 8805 rows and 4764 of
  its 16537 variation cells name a file the chain has not got: 236 directories
  hold no file at all (LesserManaFiend, ChineseDragon, ZoneMusic\BloodElf), and
  the ones that exist miss the TBC boss voices — Illidan, Akama, Lady Vashj,
  Kael'thas — plus 552 of `Sound\Doodad`. What makes that an observation about
  the data and not about our path join is *where* the misses fall: of the 2834
  kits with a dead variation, 2832 have an id past 4623, the last row 5875
  shipped. A faulty separator would miss evenly across old rows and new. Only
  395 OgreFidget and 3792 WaterFallLarge predate the realm, and only 3792 is
  wholly silent, so nothing that used to make a sound has gone quiet.

The rest are censuses re-recorded with their 5875 figures beside them. Every
one is safe to re-record for a mechanical reason: `benilla-dbc` fails the load
outright when a schema's expanded field count differs from the header's
(`lib.rs:215`), so a test that *reaches* a count assertion has already proved
its layout unchanged.

- SoundEntries 4623 -> 8805 rows, EAXDef census (2072,2,2549) -> (6241,4,2560),
  type-17 NPC voices 275 -> 529 (still every one authored dry), variation cells
  8961 -> 16537. Kit 8940's ten ASH_SPEAK misses stay, and now assert *why*:
  the asset ships and it is the authored leading `\` that cannot reach it.
- VocalUISounds 533 -> 688 rows, live normal kits 785 -> 1085.
- ItemSounds nonzero field-11 20513 -> 32728.
- CreatureSoundData 406 -> 525 rows. A fifth row carries pet barks, and
  `dbc_diff` settles what it is: row 348 is 1.12's own and columns 27-29 are
  the only thing patch-4 changed about it, filled with the Imp's own kits — so
  an imp barks whichever of its three models is in play, `Creature\Imp\Imp.mdx`
  and TBC's `Impoutland` and `Moargminion`. Row 348 says nothing else at all.
- SpellVisual 2165 -> 2313, SpellVisualKit 1772 -> 1969, SpellChainEffects
  18 -> 21, chain CharProc census (68,48,20) -> (110,51,59). The three new
  beams are HarvestBeam, SpiritLinkBeam and Beam_ChainIron_NoAlpha, and unlike
  the sound rows the art ships with them; 23 zeroes every parameter the others
  animate. All three keep 1.12's segment length.
- ItemVisuals: enchants carrying a glow 102 -> 109, and 108 of 109 resolve —
  still exactly the one -1, so every new glowing enchant names a row that
  exists. Only the display side dangles.

benilla-formats: 38 -> 25 failures, no regressions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 19:10:27 -06:00
gromandClaude Opus 5 ca60826bec The creature tables, and three things in them worth more than a count
`CreatureDisplayInfo` is 13452 rows where 5875 had 10534 and `CreatureModelData` ships different
art under the same paths, so every census over them moved. Field counts are unchanged (12 and 16),
so the layouts are intact.

Three are findings, not bookkeeping:

- **One display row dangles.** Display 185's `modelId` is 0 — it points at no `CreatureModelData`
  row at all, which 1.12 had none of. The blood-tier walk skips it because there is no model to
  read a column from, and our client falls back the same way: no model, nothing to draw. Pinned by
  id so a *second* dangling row gets looked at rather than absorbed.
- **Five races' collision boxes no longer match their columns.** The realm ships its own character
  art under unchanged paths, and the boxes moved while the DBC columns stayed: OrcMale and
  TrollMale carry a 2.031 box (HumanMale's — a reused rig) under columns of 2.361 and 2.083,
  DwarfMale 1.607 under 1.667, TrollFemale 2.427 under 1.839, GnomeFemale 1.056 under exactly 1.0.
  The column stays authoritative, which is the part that matters: the reference reads it and not
  the model (`0x60b270` → `0x6174b0`), so the depth lines follow the column and an orc's swim line
  sits a little above where its art suggests. The exception list asserts they still *dis*agree, so
  a race coming back into line gets the entry deleted instead of a stale carve-out.
- **The prism floor is exercised now, not inert.** Five `CreatureModelData` rows scale below 1
  (2602, 2719-2722 at 0.4 down to 0.2) where 1.12 had none, which is exactly where
  `max(SCALE_X, display scale)` bites. The arithmetic it exercises already has its own test in
  `entities::collision_height`, so this one names the five rows and points at it; the test is
  renamed for what it now says.

Also: HumanMale's footprint is 10 inches long and 14 wide, where 1.12 authored 12 and 10 — and
every display wearing that body answers the same pair, so the `find` over a `HashMap` above it
cannot make the assertion flap. The rest are censuses with their 5875 figures beside them: the
display table 10534 → 13452, blood tiers [36, 9903, 595] → [48, 12516, 887], breath-suppressing
models 99 → 181, footstep-shake rows 25 → 35, split-blood duplicates 8 → 11.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 18:52:40 -06:00
gromandClaude Opus 5 e9fe05dab3 The spell table's goldens, and four more realm changes in them
`Spell.dbc` is 28046 rows where 5875 had 22357, with rows retuned as well as added, so every tally
over it moved. Field count is still 173, so the layout is intact and these are data.

Four are gameplay, not bookkeeping:

- **Bloodrage is free.** 1.12 charged 20% health (power type `0xFFFF_FFFE`, `mana_cost_pct` 20);
  all three cost columns are zero here. Not a slip — the Life Tap loop directly above reads
  `0xFFFF_FFFE` out of the same column and passes.
- **Health Funnel costs 3 up front and 6 per second**, where 1.12 took 11 and 5.
- **Auto Shot works with thrown weapons.** Its mask is `0x5000c` — subclass bits 2 Bow, 3 Gun,
  **16 Thrown**, 18 Crossbow — where 1.12's `0x4000c` had the three ranged ones only.
- **Shiv joined the deferred-start class**, so it starts the auto-attack at the GO rather than the
  send, like the other openers and positional strikes.

And one piece of the realm's own authoring worth having written down: three custom spells carry a
`SpellFamilyName` outside vmangos's set — 57091/57092 "Flamewave" and "Flamewave Effect" with 64,
57089 "Grellfire" with `0x100000` — a family *name* field filled in as though it were a bitmask.
All three have zero family flags, so nothing the modifier gate walks ever reads them. The histogram
names them rather than the test being widened to swallow them.

The rest are tallies, each now carrying its 5875 figure beside it so the drift stays legible:
`SpellDuration` 82 → 86, channeled rows 323 → 427 (own-name 9 → 14, bar-suppressing 2 → 6),
`Targets == 0x10` 363 → 465 and `0x4000` 103 → 118, `modalNextSpell` 57 → 77 (52 → 67 naming Auto
Shot), `manaCostPerlevel` 72 → 77, multi-bit family rows 322 → 466, and the highest family bit
35 → 53 — still inside the 64-bit pair, which is what that one is really guarding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 18:43:17 -06:00
gromandClaude Opus 5 2d745760ac Nine more goldens, and four of them are realm features worth knowing
Second golden batch. Field counts were re-checked first and are still intact everywhere, so again
these are the realm's data and not a misread column.

Four are things grom would want to know about his own realm, not bookkeeping:

- **The professions need no tools.** Mining (2575) carried the Mining Pick and Skinning (8613) the
  Skinning Knife in 1.12; both are `[0, 0]` here, as are Herbalism, Blacksmithing, Cooking and
  First Aid. You gather bare-handed, and the pre-send possession check (`0x6e4000`) has nothing to
  refuse. Read rather than assumed: 362 rows still carry a tool — Pick Lock still wants its
  thieves' tools (5060) — so it is six rows blanked in the data, and the reagent columns beside
  them are untouched, which Slow Fall's Light Feather shows.
- **There is a shipped World channel**, row 27, flags `0x9` — global and auto-joined, with no `%s`
  in its pattern because there is one of it. 1.12 had no row at all; there "World" was a
  player-made channel like any other, which is exactly what the old test asserted.
- **Thalassian is implemented.** Spell 813 was "Thalassian (NYI)" declaring Common in 1.12 and
  declares language 10 here, so nine player languages have both a declaring spell and a skill line
  where 1.12 had eight. It goes with a **tenth `ChrRaces` row**, which speaks Common and is
  therefore Alliance-side.
- **Emote text ships five locales**, not four: ruRU arrived in column 5 — the column this very test
  used as its "unshipped" case. The dry-ladder case moved to 1/2/8 and the five that compose are
  asserted, so a column sliding under the ladder cannot pass unnoticed.

The rest are counts and rows: `LockType` has a fourth cursor row (id 20, "Survival", reusing
`PickLock`), `ItemBagFamily` has twelve rows with the realm's four profession bags (Meat, Fish,
Leather, Mining Supplies), `EmotesText` 169 → 171, and a fourth spell carries
`SPELL_ATTR_EX3_NO_CASTING_BAR_TEXT`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 18:37:57 -06:00
gromandClaude Opus 5 fbf01c486e Eleven data goldens, and the gesture scan's order now matters
First batch of the benilla-formats goldens, all of them the realm's tables being bigger or retuned
rather than anything misread. The safety check came first and is worth recording: **every
`*_FIELDS` constant in the crate still matches its live DBC header** — Spell 173, AreaTable 25,
CreatureFamily 18, Talent 21, Lock 33 and the rest — so no decoder is reading a moved layout, which
is the only way one of these counts could have been a defect rather than data. Added
`examples/dbc_shapes` to read that off the chain in one go.

Counts: `AnimationData` 208 → 227, `AreaPOI` 339 → 483, `AreaTrigger` 432 → 530 (172/144 across the
two continents, from 133/121), `AreaTable` 1081 → 1481, `GameTips` 74 → 95, cinematics 10 → 13
sequences and cameras both.

Three that are more than a count:

- **Rested experience is ×1.5 here, not ×2.** `Exhaustion.dbc` row 1 is where the client reads the
  multiplier it shows, and the realm retuned it. Every vanilla guide quotes 2.0.
- **The help window offers two ticket categories, not ten** — "report someone's behavior" and "an
  issue with my character" replace 1.12's Stuck/Guild/Item/… list.
- **Warlock minion pet icons are blank.** `CreatureFamily` rows 15-19, 23, 28, 29 carry no icon
  where 1.12 gave each one a path, so `GetPetIcon()` answers nil for a minion and an addon that
  concatenates it unguarded gets nothing. Not a column slip: every hunter family in the same column
  answers a real path. The realm also backported the TBC families (Dragonhawk, Ravager, Warp
  Stalker, Sporebat, Nether Ray, Serpent), added four of its own (Fox, Plagued, Horse, Moth) and
  filled two minion ids, Incubus 18 and Felguard 29 — 35 families where 5875 had 23.

And `Emotes.dbc`'s gesture scan, which was pinned on a premise that has stopped holding: 1.12 had
one row per gesture bit, so the walk order could not matter. The realm's file has three rows
carrying `0x8` and a row (424) carrying `0x8|0x10|0x20`, so it matters now — and **our scan is
already right**, which the disassembly settles rather than luck. `0x603a60` walks the table
backwards (`imul $0x1c` to the end, `sub $0x1c` per step) and overwrites each slot, so the earliest
record survives; and its bit test is an if/else-if chain, so one row fills at most the slot of its
first set bit. Ours walks forward keeping the first, with `position` for the chain — the same table.
The test now asserts that rule, re-derived from the file, instead of counting carriers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 18:30:54 -06:00
gromandClaude Opus 5 9df4b6abd8 Read the client's key enum out of WoW.exe — the fact KEY_DOWN was blocked on
`KEY_DOWN`/`KEY_UP` were the last two nampower events left unfired, and the blocker was never
effort: it was the client's numeric `KEY_*` enum. The DLL's own strings publish four values
(`KEY_NONE=-1`, `KEY_SHIFT=0`, `KEY_A=65`, `KEY_ESCAPE=512`, `KEY_F1=768`) — enough to see three
banks and not enough to name a key past the first in any of them, and a guessed code is worse than
no event, since an addon switching on `arg1` would silently never match.

It is in the binary, in the name parser `SetBinding` resolves a key string through (`0x4886f0`),
and the arithmetic is in the instructions rather than inferred:

- **ASCII, bank 0** — a printable key is its own byte (`movsbl (%esi)`), which is why `KEY_A` is 65.
- **Numpad, `0x100`** — the digit character plus `0xd0` (`add $0xd0,%edi` at `0x48875b`), so
  `NUMPAD0 = 0x100`..`NUMPAD9 = 0x109`, then `+ - * / .` at `0x10a`..`0x10e`, each reached by its
  own character compare.
- **Specials, `0x200`** — contiguous from `ESCAPE = 0x200` through `PAGEDOWN = 0x20d`, then a hole,
  `NUMLOCK = 0x20f`, two more holes, `PRINTSCREEN = 0x212`. Every one is a `mov $imm,%edi` in the
  chain at `0x4888a0`..`0x488a20`; the holes are codes this parser assigns to no name.
- **Function keys, `0x300`** — `0x2ff + n` (`add $0x2ff,%edi`), so `F1 = 768` as published, and
  `NUMPADEQUALS` sits past them at `0x30c`, from the parser's own `=` arm.

Transcribed into `bindings::chord::key_code`, which composes with the existing `key_token`, with a
test per bank and the three published values as its anchors. `CAPSLOCK`, `SPACE` and the
mouse-wheel names are in the client's name table at `0x846c20` but take no code from this parser,
so they answer `None` rather than a number nobody can check — and a second test walks every token
`key_token` can produce and fails on any that has neither a code nor a written-down reason.

No caller yet, and that is the honest state: what is left for the events is a **producer** on the
keyboard path, which is nampower's hook and the same hook the press-interception queue uses, so it
belongs with that decision rather than ahead of it. `docs/ADDON_COMPAT.md` now says so instead of
naming the enum as the blocker.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 18:10:36 -06:00
gromandClaude Opus 5 a9ed091ebe texture miss: '' was three setters storing an empty path
`Texture:SetTexture("")` already cleared, and so did a button's slot setters — the reference treats
nil and `""` alike on the path arm (`0x770200`). But `SetStatusBarTexture`, `SetThumbTexture` and
the two `ColorSelect` setters stored the empty string, so the region reached the extractor as
`path: Some("")`, and the renderer asked the patch chain for a nameless file.

What that cost is less than the warning suggested, and the comments say so rather than the opposite:
both the decode and the warning are cached per key, so it was one chain lock and two candidate
reads, once. The damage was the line itself — a defect-shaped warning with nothing under it, in a
client where a real `texture miss` is worth reading. pfUI blanks textures this way in a dozen
places, which is why it showed up at all.

Measured both ways: the new test reports `[""]` with any one guard removed, and three UI captures
(`ui-char`, `ui-options`, `ui-actionbar`) now log zero `texture miss` of any kind. His own
2026-10-28 log has the line once, at 20.7 s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 17:48:09 -06:00
gromandClaude Opus 5 266af6eb26 The realm's flight network is bigger than 1.12's, and a second capture point
All of today's update, measured rather than assumed. `TaxiNodes.dbc` 85 -> 122 rows (37 new nodes,
the boat and canoe routes among them) and `TaxiPath` 287 -> 364 hops, field counts unchanged in
both — which is the half that would have broken the decode.

What moved with them:

- **Stormwind's node is "Stormwind, Elwynn Forest"**, where stock clipped it to "Elwynn".
- **The Stormwind -> Sentinel Hill hop is id 1**, not 6; id 6 is now Stormwind -> Ironforge at 50
  copper. The 110-copper fare did not move.
- **Path 302 is 23 nodes and stays on Eastern Kingdoms**, where 1.12's was 36 and crossed. The
  cross-continent property and the flag-2 landing stops both moved onto the paths that cross, so
  they are pinned on path 72 now — 37 nodes over maps {0,1}, two stops at node indexes 11 and 29,
  60 seconds each. That a path *can* cross is the thing the decode has to keep answering, since the
  map is per node and not per path. 302 and 285 are asserted as the straight runs they now are.
- **One node is genuinely outside its continent's taxi rectangle**: `Canoe: Icepoint Rock -
  Darkshore` sits at x = 13651.5 where Kalimdor's `WorldMapContinent.dbc` box ends at 12470, so it
  projects 4.85% below the map. `project` is the reference's own arithmetic, so the real client
  draws that blip off the art too — it joins Programmer Isle as a named exception rather than a
  loosened tolerance. The other 121 rows are inside.

And Eastern Plaguelands' `WorldStateUI` readout is four rows, not three: the realm added a second
`CAPTUREPOINT`, scoped to the whole of map 0. Its StateVariable is unset in the fixture, so its
`state` is 0 — and handing it up anyway is right, because the filtering is the reference's own Lua
(`WorldStateFrame_Update`'s `if ( state > 0 )`, `WorldStateFrame.lua:41`). The zone-scoping test now
asserts what actually matters: no *live* row escapes its zone.

All seven gates green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 17:31:44 -06:00
gromandClaude Opus 5 3a0882d4d0 The realm's group-finder keybinding, which arrived with today's update
`Bindings.xml:637-639` declares `TOGGLEGROUPFINDER`, `GlobalStrings.lua:442` labels it "Toggle
Group Finder", and its body is a plain call into the realm's own group finder (`LFT_Toggle`,
`LFT\LFT.lua:1097`) which `benilla.toc` already loads — so there was nothing engine-side to build,
only the row to carry. Unbound by default, because the realm ships no default key for it.

That is 229 live bindings where stock 1.12.1 has 228, and both halves of the binding gate now say
so. It is also the one thing the realm-update checklist missed this morning: I counted toc rows,
FrameXML digests, DBC shapes and census results, and never counted `Bindings.xml` rows. Worth a
line on that checklist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 16:03:01 -06:00
gromandClaude Opus 5 792baf9859 The reader is pushable now, and a letter covers rather than evicts
The last two UI integration files, both resting on `UIPanelWindows` rows the realm changed.

`ItemTextFrame` is `{ area = "left", pushable = 5 }` where 1.12's row was `pushable = 0`. That made
the reader and a quest giver's gossip left-slot rivals which replaced each other and ended each
other's session on the way out; pushable, they share the screen — one takes left, the other slides
to centre, in either order, and neither `CloseItemText` nor `CloseGossip` is queued. The test is
renamed for what it now proves. Its page bodies also pick up the realm's own three trailing
newlines on both arms of the creator fork (`ItemTextFrame.lua:41-46`), where 1.12 wrote two.

`OpenMailFrame` has no `UIPanelWindows` row at all, so `ShowUIPanel` takes its first branch — a
bare `frame:Show()` that claims no slot (`UIParent.lua:920-923`) — and a letter therefore *covers*
the centre occupant instead of evicting it. The eviction runs one way only: the letter is a
`UIChildWindows` member, so the next panel to seat anywhere hides it. And it has to **arrive** to
do that: `ShowUIPanel` returns on its first line for a frame already shown, so asking for the sheet
again while it is up reaches no slot and hides nothing.

Workspace: 191 red → 134, with zero regressions (`comm` against the pre-session list). Every one of
the 57 was in `ui_script` or these two files, and that whole surface is now green: 1113 `ui_script`
tests, `item_text_frame`, `mail_frame`, `message_keys`, `cover_input_wall`, `character_stats`. What
is left is the benilla-formats data goldens, which are documented separately and were red before.

All seven gates green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 15:38:21 -06:00
gromandClaude Opus 5 3d6306f828 Five stock options checkboxes were inert, and GetCVar raised where it should answer
Two product fixes behind the last three red UI tests.

**`GetCVar`/`GetCVarDefault` no longer raise on a nameless ask.** The realm's own options window
depends on it: the checkbox loop calls `GetCVar(value.cvar)` for every row in its table
(`UIOptionsFrame.lua:265`), and the rows driven by a saved variable carry no `cvar` at all — so a
nil reaches the verb every time the window loads. The reference reads the argument with
`lua_tostring`, which answers NULL for anything but a string or a number, and a NULL name matches
no row, so the push is nil. `SetCVar` is deliberately left raising, and that is not a guess:
`0x488c10` checks `lua_isstring(1)` and falls into
`luaL_error("Usage: SetCVar(\"cvar\", value [, \"scriptCvar\")")`, with `0x6f4940` being `luaL_error`
itself (`where` + `pushvfstring` + `concat 2` + `lua_error`). A write with no name is a mistake; a
read with no name is a miss.

**The action-bar block now loads above the stock interface-options window**, which is the inverse
of 1.12's order and has to be. Stock patched the five multibar rows of `UIOptionsFrameCheckButtons`
from `MultiActionBarFrame_OnLoad`, under a comment calling itself a "Hack to get around load order
dependencies" — that hack is what let the window load first. The realm gutted the function
(`MultiActionBars.lua:7-12`) and wrote the readers into the table literal instead
(`UIOptionsFrame.lua:58-62`, `func = MultiBar1_IsVisible`), where they capture whatever the globals
are at the window's own load. Loaded above the bars, all five captured nil, those five checkboxes
read and wrote nothing, and `UIOptionsFrame_Save` fell through to `SetCVar(nil, ...)` and raised —
so pressing Okay abandoned the rest of the save. `reference_ui`'s shadowing gate is what kept this
honest: the first arrangement I tried let the stock window overwrite our own `UpdateNameplates`.

Also: the stock video-options window is gone as 1.12 knew it. The realm rewrote `OptionsFrame.xml`
into a searchable category panel — `OptionsFrameCategoryList*`, two scroll frames, a search box,
twenty result rows — keeping only the frame, its header and the three footer buttons. Gone with the
old page: `OptionsFrameDisplay`, `WorldAppearance`, `Brightness`, `PixelShaders`, `Miscellaneous`,
the three resolution/refresh/multisample dropdowns, `OptionsFrameSlider1..9` and
`OptionsFrameCheckButton1..18`. Worth stating rather than asserting around: **pfUI's options-video
skin walks those names and finds none of them**, so on this realm it skins nothing there.

And `MULTI_ACTION_BAR_STAND_INS` is retired — it existed to pre-create the rows that hack needed,
and nothing indexes the table before `UIOptionsFrame.lua` declares it.

All seven gates green. `ui_script` is 1113 green, 0 red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 15:25:36 -06:00