`GetCurrentCastingInfo` and `GetCastInfo` are the two most-called
nampower functions in grom's corpus — 25 and 20 call sites outside
`NampowerAPI.lua` — and one of them calls with no existence guard at
all: `local _,_,_,_,_,_,autoattack = GetCurrentCastingInfo()`
(SuperCleveRoidMacros/Core.lua:94), in the frame that keeps clearing the
target while an autoattack is queued. On this client that line raised.
The earlier doc row said the client had "a spell id, a deadline and a
guard bit — no start instant, no channel state, no GCD end". That was
true of `PendingCast` and wrong about the tree: `ui_cast_events.rs`
already folds a cast window per caster off the same packets, with a
start, an end, a channel bit and the accumulated pushback, behind
`C_Spell.UnitCastingInfo`. What was actually missing was the caster's
own machinery — the reference's cast globals, which have no descriptor
field anywhere — and the GCD, which `spell/cooldowns.rs` has run
correctly all along and never published.
So `GetCastInfo` folds two halves: its timing columns come from the
player's own window, its identity and GCD columns from a new per-tick
`SelfCast` snapshot. The feed declares itself after the window drain, so
the two halves can never describe different casts. Added:
- `SelfCast` (benilla-ui) — the inflight spell `0xceca88`, the bar's
spell `0xceac48`, the auto-repeat key `0xceac30`, casting/channeling/
onswing/autoattack, the cast counter, the cast type, the target, and
the GCD's start and end on the GetTime clock.
- `ui_self_cast.rs` (benilla-app) — the push, plus `SPELL_CAST_EVENT`.
- `Cooldowns::gcd` — the running global cooldown as one timer. `info`
still answers the per-category question every cooldown swirl asks;
this answers "when does the GCD end", which has no spell to key on
because its consumers divide it (`API.GetGCDRemainingMs`) or compare
it against seconds (`[gcd:<1]`).
- `PendingCast`'s cast counter, in the one place every class of commit
already passes through.
Three decisions where the tempting answer was the wrong one:
- `GetCastInfo` is **nil** when nothing is casting, and an instant's GCD
does not count. Answering a table whenever the GCD is up would make
the consumer's fast path always hit — and `API.IsCasting()` *is*
`GetCastInfo() ~= nil`, so `[casting]` would read true for a second
and a half after every instant. The wrapper's own comment says what to
do instead ("GetSpellIdCooldown … works for instant casts").
- `castId` is two different numbers under one upstream name: a counter
in `GetCastInfo`'s table, beside a separate `spellId`; a **spell id**
in `GetCurrentCastingInfo`'s returns, which is how both of its readers
treat it (Init.lua:89 stores it as castingSpellId, Core.lua:5427 hands
it to C_Spell.GetSpellName). Each is implemented as what its own
consumer reads, and both are spelled out at the binding.
- `SPELL_CAST_EVENT` fires for commits, not refusals. Upstream's
`success` can be 0; this client's local refusals funnel through
`CastErrors`, which carries a spell id and a reason byte and neither a
cast type nor a target — a row from there would invent two of five
fields. Nothing in the corpus reads the failing branch
(Core.lua:5177 is one `if success == 1 then` with no else). Written
down at the type rather than left to be found.
Tests run the consumers' own bodies transcribed, as `cast_info.rs` next
door does: `API.GetCastInfo` with its `GetCurrentCastingInfo` fallback
arm, `API.IsCasting`, `API.GetGCDRemainingMs`, `API.GetCastRemainingMs`
and `CleveRoids.UpdateCastingState`. A positional seven-return shape and
a ten-key table are only correct relative to the code that reads them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
First tranche off the DLL work list. The method that produced it is worth
recording with the code: `SuperCleveRoidMacros`'s `NampowerAPI.lua` is a
capability registry — a `VERSION_REQUIREMENTS` table of every nampower
name with its minimum version, reached through `HasFeature`, which tests
the version **and** `_G[name] ~= nil`. So none of this was broken, and
raising `GetNampowerVersion` would not have helped: the only honest way
to make one of those features live is to define the global.
- `PlayerIsMoving()`, `PlayerIsRooted()`, `PlayerIsSwimming()` — `1` or
nil, as upstream's own doc lines state, never a boolean. All three read
the per-tick posture snapshot `SelfMovement` already carries, which
gains `rooted` (`MOVEFLAG_ROOT`) and `moving` (any of the six movement
bits, the keyboard turns included — they are input to the same word the
client tests, and a turn in place does interrupt a cast).
- `IsAuraHidden(spellId)` — from a new `SpellLookup::hidden_aura`, filled
with exactly `ui_aura::shown_in_aura_ui`'s predicate inverted, so it
cannot contradict the aura list an addon reads beside it. An unknown
spell is nil: "not hidden" is the honest answer about an aura this
client knows nothing about, and the lists show a catalog miss too.
- `GetItemIconTexture(itemId)` — two hops, because an item template
carries a display id and not an icon: the cache row's `displayInfoID`,
then `ItemDisplayInfo`'s `Icon` through a new `ItemIconProbe`. A known
item with no display row is nil (a data gap); an item the cache has
never seen raises and records the ask, as every `item_record` verb here
does. ClassicAPI's `GetItemIcon` wants the same probe, which is why it
is a probe and not a closure.
- `GetTrinkets()` — slots 13 and 14 in `GetEquippedItem`'s own row shape,
an empty slot absent rather than nil-valued, since the consumer walks
it with `if t[i] then`.
Each is in `reference_surface.rs`'s `allowed_beyond_1_12` with why 1.12
has no spelling to prefer — the client reads its own movement word and
tells Lua nothing, applies the aura attribute test inside
`UnitBuff`/`UnitDebuff` without exposing either, and can only reach an
item icon through a bag slot it already holds.
All nine gates green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
grom's stale-minimap report survived the same-interior-skip fix, and his log
says why that fix was the wrong suspect: at the gate the zone-text interior
claim reads `outdoors`, so the skip was never consulted there. The module doc
no longer claims otherwise.
What his run leaves is sharper and stranger. The election cannot have declined
— leaf 1537 → 809 "Gates of Ironforge" takes the zone from 1537 to 1 and the
subzone text with it, so both `ZONE_CHANGED_NEW_AREA` and
`MINIMAP_ZONE_CHANGED` fire — and two separate addons call
`SetMapToCurrentZone()` on those while the map is closed
(`pfQuest/map.lua:1466`, `pfUI/modules/minimap.lua:183`). Zero calls arrived,
with nothing raised anywhere in the session. Offline, against the same addons
and saved variables, both events reach every registered handler and are
answered every time, so the difference is live-only and the engine cannot
currently see which of three things it is.
So it can now. `UiScript::fire_event_reach` answers an `EventReach` — frames
registered when the dispatch began, frames whose handler ran, and the name of
the frame a cut-short walk stopped on (our documented deviation: a handler that
unregisters the walk's saved next ends the dispatch, and every later registrant
silently never hears the event). `UiScript::frame_shown` reads the shown bit
addons branch on, which no amount of engine-side logging could otherwise
explain. `fire_event` is unchanged for its nineteen callers.
`area::feed_zone_events` logs the whole handshake at each fire: what was
elected, reached/registered per event, the `SetMapToCurrentZone` delta, and
`WorldMapFrame`'s shown bit. `0/0` is nothing registered, `reached` below
`registered` is a cut-short walk, and every handler run with `0×` asked is the
guard both answerers share. `addon_harness --fire` reports the reach too.
Gates green (459 s). His corpus offline: 10/10, 8/8 and 4/4 handlers reached.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
grom: "can't see flight master location marks, even tho I have them on."
Nothing was broken — the layer was silently empty. pfQuest-octo has deleted its
shipped pfDB.meta.flight table in favour of this call (db/meta.lua:181 is a stub
whose comment says the data "always comes live from the client (ClassicAPI)"),
and SearchFlightNodes opens with
if not (C_TaxiMap and C_TaxiMap.GetTaxiNodesForMap) then return maps end
so with the namespace absent the option stays ticked, no error is raised, and no
icon is ever drawn. Same silent-by-construction shape as C_Map.GetAreas, same
consumer.
The contract is half read off the bundled ClassicAPI.dll's string pool
(C_TaxiMap, GetTaxiNodesForMap, nodeID, faction, reachable, state and the
FlightPathFaction/FlightPathState enum names) and half off the caller: the
areaID/mapX/mapY triple is not separately visible there, since the pool holds
one copy of each string and C_Map.GetAreaTriggerInfo already uses all three, so
for those SearchFlightNodes itself is the authority. It reads six fields, gates
on `node.areaID and node.mapX and node.mapY`, and feeds the pair into
pfMap:AddNode as meta.x/meta.y, which UpdateNodes divides by 100 — so they are
percentages of the zone rect. Both files say which half is which.
Faction comes from the two MountCreatureID columns, the reference's own gate
(0x4a2650 asks for the mount of the player's half): one column filled is that
faction, both is neutral, and neither means the row is not a flight master at
all — on this install those are exactly the eight Transport/Ferry/zeppelin
endpoints, and passing them on would put "Flight Master" pins on the Menethil
docks.
Attributing a node to a zone needed the file's own names. Geometry over
WorldMapArea boxes proposes — these are axis-aligned boxes around irregular
zones, so a node near a border sits inside two or three of them — but no purely
geometric rule works: "smallest box" misfiled a dozen of ~70 rows (Crossroads
under Mulgore, Aerie Peak under Alterac Mountains), and "most central box",
much better and what zones_at now sorts by, still loses Talonbranch Glade to
Darkshore by about one percent of inset. TaxiNodes.dbc names its rows
"<place>, <zone>" and a city row leads with the city, which is itself a zone, so
trying each comma-separated part against the candidates gets Ironforge over Dun
Morogh *and* Felwood over Darkshore from one rule. zones_at also reaches the
instance-map list, so Alterac Valley's two nodes place on the battleground map
instead of nowhere.
Seeded before the interface loads, after the map catalog it reads the rects
from. That deadline is tighter than file scope: pfQuest replays its saved
tracking selections once on PLAYER_ENTERING_WORLD and then unregisters every
event (database.lua:131-151), so a push one frame later is a push after the only
read.
Verified against the real addon offline, not just by unit test:
SearchFlightNodes now adds 69 nodes through the probe harness. Three tests pin
the rule against the DBC itself — the capitals and the border nodes by hand, and
every named row held to the zone its own name claims, which is the test that
rejected both earlier geometric rules. It also pins the four rows that name no
zone, three of which are this realm's own additions (Northshire Abbey and two
Darkshore canoe routes), so the next custom node shows up as a failing
assertion rather than a pin in the wrong zone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
The GameObject tooltip's skill-lock line took the opener-*unknown* arm
unconditionally — `LOCKED_WITH_SPELL` in red — whatever the player's skill. The
known arm was simply not built, and the code said so where it was missing.
The reference forks on whether any known spell opens the slot's LockType
(`0x52abcb`): known takes `LOCKED_WITH_SPELL_KNOWN`, which carries the
requirement as a `%d` and the colour of the skill-difficulty ramp `0x529fa0`.
Built now, both halves.
grom reported it on a copper vein at Mining 70, which under the ramp is +69 over
the vein's requirement of 1 — the green rung, not the grey one he expected; grey
starts at +100, so a copper vein greys at Mining 101. Either way not red, and
red is what it showed.
The resolve is hoisted out of the `flag_locked` branch, because the "Locked"
line's colour and this line's known arm are two readings of one answer and must
not disagree with the click. `lock::skill_margin` recomputes the two numbers from
the opener the resolver already matched, with the same two functions its own
value test uses (`0x5f850f`), so hover and click cannot diverge on which opener
is in play.
Deviation, recorded at the constants: the three new rung RGBs are the standard
trade-skill palette, not values read out of the binary's table. Only the ramp's
shape is traced — five rungs, bands at +0/25/50/100 — and the rung a line lands
on is what the player reads.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
`WOW_UI_EVENTS=<secs>` (`script/event_prof.rs`): per event, what one fire
costs across every handler it reaches, and which frame took the most of the
worst fire.
The gap it fills is a dispatcher. `WOW_UI_HANDLERS` keys on `(frame, script)`,
which is the right key for an `OnUpdate` and the wrong one for an `OnEvent`,
because every addon library registers *one* frame for *every* event it wants and
fans out in Lua. So the largest identifiable cost in grom's 2026-10-21 session,
eleven separate times, read as
[ui-handlers] 312.5 312.5 0.00 72522.3 AceEvent30Frame:OnEvent
— a true row that names a library's shared frame and no addon, no event and no
cause. `[sys-timing]` saw the same cost as `ui_quest::feed_quest` at 85.7 ms for
one run in 76, because every handler runs synchronously inside whichever system
fired, so the system holding the stack gets the bill.
Two conventions the report sticks to:
- `self` is nest-adjusted, in both columns. A handler's own time has any event it
fired taken back out (`event::fire` differences `event_prof::nested_ns` across
the call), or every event in a cascade reports the cascade and the row naming
the real cost is the one that looks innocent. `total` keeps it, which is what a
`[sys-timing]` span sees.
- A fire is one `fire_event`, however wide it fans out, because that is the unit
a dropped frame is measured in. `calls` is beside it for the width.
Measured on the player's own 92-addon interface (`tick_bench`'s new `[events]`
row, three fires per event from a settled start):
us/fire worst event
5068 14759 QUEST_DETAIL <- HCQuest.lua:139 took 14,097 us
764 1324 QUEST_LOG_UPDATE
662 1564 QUEST_FINISHED
Read `us/fire` against `worst` and that 14.8 ms is **one** fire of three: the
other two cost ~0.1 ms, so it is HCQuest building something once and keeping it,
not a cost paid per quest. With that read correctly, no quest event costs more
than ~1.5 ms recurring offline — so the bench does not reproduce the live 57-73
ms and says so, rather than offering a number that looks like an answer. Hence
the instrument, armed in `./measure.sh`.
Also adds `./measure.sh --frames`, which arms two meters that already existed and
have never been armed: `WOW_FRAME_PHASES=50` and `WOW_RENDER_MS=1`. A frame's
wall time is `max(main app, render app)` since bevy pipelines the render app onto
its own thread, so when the render app is the longer, `wall` climbs while `cpu`,
`main`, `gpu` and every per-system `worst` all read healthy — exactly what that
session's **161 ms frame in a second whose costliest system worst was 2.9 ms over
105 runs** looks like, and what 43 of its 64 spike-seconds look like. Its own
mode because both perturb what they measure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
`[ui-phases]` could say a window paid for 61 whole-graph derivations. It could not say which of the
forty-odd `touch_layout` sites asked for them, and that is the half a fix has to be aimed at.
Measured on the player's interface, one derivation costs ~15.5 ms — from two directions: the
derivation count explains total resolve time at r = +0.977 across twenty windows (solve count:
r = -0.33, so solves are free and derivations are everything), and 26 of 78 worst-single-call
figures over 1 ms land in the 15-16 ms bin across eleven unrelated handlers by six authors, two of
which fired exactly once for their whole window. Eleven handlers cannot each independently be
15.5 ms slow; it is one shared charge landing on whoever reads the graph first, which is also why
the same session shows `resolve` p99 at 14.4 ms and worst frames "mostly tick". One defect, two
accounts.
So `#[track_caller]` down the naming chain and a tally of `file:line`, printed in the report. Not a
backtrace: `WOW_LAYOUT_DERIVE_TRACE` already captures those and has to be rationed to `n` prints
because of what they cost. This is a `(&'static str, u32)` key off `Location::caller()`, cheap
enough to leave armed, and take-and-rearm so a window reports its own rows and a VM replaced by
`/rl` re-arms without anybody noticing it was replaced.
Also a `[churn]` window in tick_bench, which records a negative: a plate roster turning over 4-in-40
every ten frames costs **zero** derivations on the player's own interface. `sync_nameplates` pools
the plate frames, so a new key rewrites an existing plate and mints nothing. With `resolve_bench`'s
hover-sweep, tooltip-content and moving-region tests all asserting zero, four candidate triggers are
now ruled out offline and the live tally is what names the real one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
The startup walk names every addon it loads; `LoadAddOn` named none. So when
the client froze on 10-19 while learning recipes, the session log could not
say whether the trainer window was even open — and `Blizzard_TrainerUI` is
LoadOnDemand, so that was the whole question.
`benilla-ui` has no logger, so the line travels the way its errors and
warnings already do: a `notices` channel on the model, drained to `info!`
beside them in the same pass. Silent for a redundant load, because the stock
`*_LoadUI` glue calls `LoadAddOn` on every open and a line per call would
bury the one that did the work. A refusal carries the reference's own reason
token, and a name that is not installed says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
The load has had an instruction bound since `3b6975fd` — "so a hung load reports
instead of freezing the loading screen" — and the end of the load disarmed it,
"so no handler is killed for being slow". Right about slow, and it leaves a
handler that never returns at all able to freeze the client until the player
kills it, which is what happened to him this morning.
So the bound does not go at the end of the load, it changes shape. Wall time
rather than instructions, because the number has to be obviously beyond anything
legitimate and a count is not: a frame still inside Lua after fifteen seconds is
not slow, it is stuck. `tick` restamps every frame, so no frame is charged the
one before it and a handler reached later in the frame — a click, a fired event —
measures from that mark, which can only make the bound tighter. The raise names
the chunk and the line, so his log says which addon rather than that something
somewhere looped.
It shares the one count hook, which costs 5-13% of pure Lua execution time
(measured: 30M instructions, 235-260 ms bare against 267-274 ms hooked, which
also puts his machine at ~125M VM instructions a second — what sizes the load's
200M at about 1.6 s). Lua is a small part of a play frame, so that is a fraction
of a percent of frame time, and the load was already paying it.
`WOW_LUA_RUNAWAY=<secs>` moves the bound, `0` disarms it.
Two tests: a loop with no exit fails its own call, names its chunk and leaves the
VM usable; and a frame is not charged the wall time that passed before it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
Both lines sat on the world-entry path, so the load the player actually waits
through — the `/reload` — reported nothing at all. His 2026-10-19 session has two
of them, 4,981 ms and 6,276 ms against the 7.5-9 s he started from, and the log
cannot say where either went or whether the bytecode cache was hit, although the
cache directory proves it was: 1,585 entries written at first load, 14 added
across both reloads.
`report_load` now runs on both roots and names which one. The reload's clock
starts before the saved-variable writes, because those are part of the one
blocked frame he sees, which is also what the `frame hitch` warn measures.
`chunk_cache_line` resets its counters on read, so each line is that load's own
work rather than a running total in which a warm reload hides behind the cold
login before it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
The compile is a UI load's biggest removable cost and the only half of it that can
be kept — running the chunks is what an addon *is*. Measured through the addon
harness's `--together` load of the player's own 91 addons, three passes:
```
no cache 4,788 chunks compiled 3,324 ms
cold 1,592 compiled, 3,196 from cache 1,482 ms
warm 0 compiled, 4,788 from cache 471 ms (+24 ms of keys)
```
**About 940 ms off every login and every `/reload`**, and the first load after an
addon changes pays only for the files that changed.
`benilla-config/cache/lua/`, one file per chunk, keyed by a hash of the chunk's
own bytes and its name. Both chunk paths go through it — `run_chunk_named` and the
XML loader's own runner, which is how the biggest files in the corpus arrive
(pfQuest's database is 33 MiB of `<Include>`d Lua).
It cannot go wrong quietly. A key is a property of the bytes, so an edited file is
a different entry and a stale one is never asked for; an entry that is truncated,
corrupt or from another Lua build fails `lua_undump`'s header check and the source
is parsed instead, so a bad entry costs the parse it meant to save. The dump keeps
its debug info — `debugstack` is an addon-facing verb and a stripped chunk would
lie about its own lines — which the test asserts by raising from a cached chunk
and demanding its file and line back. The folder is capped at 512 MiB (one load's
worth is 159 MiB) and emptied wholesale when it is over: a cache that needed an
eviction policy would need bookkeeping that costs more than it saves.
The first build of this looked like it worked and saved nothing, because
`ahash::AHasher::default()` is seeded per process: every session hashed the same
file to a different name, found nothing and rewrote the whole cache (4,776 entries,
477 MiB, a steady 1,592 misses a run). Fixed seeds, and the instrument that caught
it — `benilla_ui::script::chunk_cache_line()`, hits and misses with the time each
spent — stays, because "the cache is installed" and "the cache is working" turned
out to be different claims.
Verified: the harness's verdicts over all 91 addons are **identical** with the
cache and with `WOW_LUA_CACHE=0`, the full gate chain is green, and the
`WOW_DATA` failure sets are unchanged (benilla-app 270, benilla-formats 80).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
`WOW_UI_HANDLERS` names the handler and `WOW_UI_TICK` names the tick phase, and
between them they left the question this answers. A reload of the player's
interface spends **4.47 s inside handler bodies**, and single calls in there cost
hundreds of milliseconds — `pfBag:OnHide` 394 ms in ONE call — which is not Lua
arithmetic. A handler's self time includes the engine work it provokes, so that
time is ours: it is inside the verbs the handler called, and nothing charged it to
them.
`WOW_UI_VERBS=1` wraps every global and every widget method at the end of
`UiScript::new` — before FrameXML or any addon runs, so everything it finds is
ours — and times each call. Off, nothing is wrapped and nothing is paid.
It works offline, which matters more than the live case: the addon harness's
`--together` mode loads the player's whole folder into one VM in load order, which
is the closest thing to a login burst that needs no server. The first run over his
91 addons, three passes:
```
[ui-verbs] 519 verbs, 17,966,414 calls, 9,148 ms in all
4384 6393 685.8 us pcall (addon code, nested)
1498 16071556 0.1 us type (5.4M calls a load, Ace's)
1241 1950 636.2 us frame:GetWidth ← 650 calls a load at 0.6 ms each
548 33093 16.6 us CreateFrame
219 37524 5.8 us fontstring:SetPoint
95 99 960.3 us frame:GetHeight
67 123 545.9 us frame:GetLeft
17 3 5628.6 us frame:GetCenter
```
A geometry read costing 636 µs is the whole finding, and the fix is the next
commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
"then we gotta fix bettercharacterpanel, that one is driving me nuts."
BetterCharacterPanel is built on five nampower getters and had none of them.
`GetEquippedItem` was 8 raises in one session of his log; the other four failed
inside the addon's own `pcall`s, so its item-level column, quality colours,
missing-enchant warning and two-hand test were all dead at once. DoiteAuras
reads the same getter in six places.
The spec is the installed `nampower.dll` itself rather than inference, which is
what unblocked this — the earlier note here said to find the upstream spec
first, and the DLL *is* the spec:
- its usage strings give the signatures,
- "Slot must be a number between 1 and 19" gives the band, so the slots are
1.12's own `GetInventoryItemLink` numbering and `NampowerAPI.lua`'s
`slot - 1` in `IsItemInSlot` is that shim's bug, not the contract,
- its string pool gives the key names in order, and
- "Item not found in DBCache" is a raise, which is exactly why the addon
`pcall`s these three and calls `GetEquippedItem` bare.
`itemLevel` is the one field 1.12 cannot reach at all: `GetItemInfo`'s fourth
value is the *required* level, and later clients are the ones that insert
`itemLevel` there — so the panel's item-level column had no possible source
before this. The cache row now carries it, with `buyPrice` and `displayInfoID`
beside it, and an equipped item carries the raw descriptor numbers the tooltip's
resolved enchant lines could not answer (an id whose `SpellItemEnchantment` row
hides its name is still an id).
Fields left absent rather than guessed are listed in code and in
`docs/ADDON_COMPAT.md`; nothing installed reads any of them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
WIM sets this script on every window it builds — the history frame at load, then
one per correspondent (six whisper windows in one session of his log) — and we
refused the name by raise, which killed the rest of the builder function each
time, not just the handler.
It was on the deliberate "real 1.12 kinds nothing fires raise rather than
silently accept" list. That rule is right and this was the wrong side of it: the
slot now fires, from the ScrollingMessageFrame's own scroll verbs when a scroll
actually moves the view, which is what a chat addon hangs its scroll-down arrow
on. A scroll refused at either end moves nothing and says nothing; whether the
reference fires one there is untraced.
The raise list keeps its rule and its test — `OnInputLanguageChanged` and the
movie frame's are still raises, and a name leaves that list only when something
fires it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
Four benilla-ui tests were red and had been since the commits that changed what
they pin. They were missed because the round's gate was run as `cargo test -p
benilla-app -p benilla-ui` through a grep that only showed benilla-app's results;
scripts/check.sh is the gate that scopes this correctly.
- SetRaidTarget's third argument became a flag rather than the literal word
"local" (7098d642), so the test asserting the word is the only form now
states the contract it replaced: any truthy value is the local mark, which is
what SoloRaidTargetIcons passes.
- GetStringHeight is installed deliberately (75f04b4f), so the regions surface
check that asserted its absence had to flip.
- The LOCALIZED_CLASS_NAMES pair (0a907955) was published only once class data
was fed, leaving the globals absent on a bare VM — which the
allowed-beyond-1.12 list said we exposed and the creature_info test asserted
we did not, while its own comment described the behaviour we wanted. Both
tables are now seeded empty at install, as ClassicAPI publishes them at load:
tInvert(nil) raises where tInvert({}) is an empty map, and pfUI inverts one in
its own file scope.
- The AceGUI label test's Lua used a one-argument SetPoint, which 1.12 correctly
raises Usage: on (LUA_TNONE is not LUA_TNIL — script/tests/anchors.rs pins it,
and the 1.12 AceGUI backport had every one-argument call rewritten for exactly
that reason).
And a correction in docs/ADDON_COMPAT.md that the last of those turned up:
GetStringHeight was credited with the empty VoiceOver options, on the strength of
AceGUIWidget-Label.lua's call site and a probe showing every Label at 624x1.
Both were wrong. AI_VoiceOver ships four copies of AceGUI and its TOC loads
1.12\embeds.xml — and the 1.12 tree reads label:GetHeight(), never
GetStringHeight. The 624x1 was the harness's own missing text measurer, the trap
its notes already warn about. The method stays as ClassicAPI parity surface with
the claim corrected; the real cause of that symptom is the scroll-child anchor,
fixed in 75432f7d. The lesson, written into the row: an addon shipping several
library copies has to be read through its own TOC chain before any call site in
it is evidence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.
Today's work, which is the reason a baseline exists at all:
- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
characters were entering the sun's shadow cascades, so every skinned-rig
shadow specialization compiled live mid-frame; the hitch detector logged
"main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
frame-cost reading no longer pays 2.7 ms a frame for it.
Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
The stock login's Breaking News box (`WOW_GLUE_STOCK=1`):
- At glue init, as `0x46a400` does, the client reads the Lua global `SERVER_ALERT_URL`, which the player's own GlueStrings or a server's patch sets. It fetches the URL once on a background thread: plain HTTP, 5 s timeouts, redirects followed.
- The body builds the reference's buffer (`0x46a580`: 0x7ff cap, appended chunks). A case-insensitive `SERVERALERT:` prefix fires `SHOW_SERVER_ALERT` once from the update (`0x46b939`). Every failure is silent.
- `LaunchURL` opens the browser only for URLs that pass the reference's gate (`0x5abc10`): `http://`, a strict charset and the 10-host whitelist. An over-long host is refused rather than cut.
- No fetch or browser ever runs in a test, capture or unattended run.
Tests run against a local fake server: prefix, 404, redirects, chunked, timeouts, caps, and the frame shown end to end.
Part of #1026🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Part 4b of the stock GlueXML (`WOW_GLUE_STOCK=1`): the stock screens' ModelFFX scenes now go through the glue booth instead of the model-tile path, which drew them black.
- The scene, look and facing come from the glue VM. `SetCharSelectBackground` runs the frame's own SetModel and swaps its fill callback (`0x472128`, `0x472144`), so the select scene fogs until a character sets its stage.
- An ModelFFX pane draws the booth's scene at its own bucket's ARTWORK layer (`0x46fa90`), framed at the pane's aspect with the diagonal law (`0x7ada40`, `0x5c3cc0`). The native box Deviation now holds for the native screens only.
- Each SetModel restarts the stage. The select glow holds through an empty roster until the screen shows again.
- An empty roster shows the Orc scene fogged, and after the last delete the last stage stays, as `CharacterSelect.lua` leaves them.
Part of #856Fixes#978🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Key input follows the client's walks: key-down (`0x765f10`), char (`0x765df0`), key-up to the recorded consumer (`0x765fd0`) and repeat (`0x766070`).
- EditBoxes are in the walk from creation (`0x779ced`), so the stock login's boxes take typing; only a focused box consumes. A box declines PRINTSCREEN and unaltered arrows on an `ignoreArrows` box, so stack-split steps under a focused chat box. A binding's press records the world frame.
- Repeats follow the pressed-key list, so a held key resumes after alt-tab. Chords reach a box only through the walk. Keys the client can't name walk nowhere.
- The stock login reads the agreements before its first screen. Cinematics returns to login (no movie player; Deviation). The Realmlist box has its middle border.
- Alt+F4 closes the window on Windows and Linux (Deviation: the reference relies on the OS).
Fixes#179Fixes#1003Fixes#1004Fixes#1005Fixes#1019🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part 4a of the stock GlueXML (`WOW_GLUE_STOCK=1`): character select's roster.
- The select verbs run on the reference's roster (`0x4736c0`): `GetCharacterInfo` (race and class from `Name_Lang`), `SelectCharacter` with its clamp and event, the facing gate, and `GetCharacterListUpdate`. They are backed by the roster load and teardown (`0x4724d0`, `0x472580`) and screen state 3.
- Under the stock screens the character list is fetched only when the screen asks, as `0x46b880` does. The world connect completes on the auth response (`0x5aab80`), not on a roster.
- Every character-park request goes through one ordered channel. A roster reply completes only the current link's query. A late auth after a cancel is dropped, Warden stays terminal, and a failed list shows `CHAR_LIST_FAILED`.
The native screens are unchanged. Fake-server tests cover the ordering, the cancel, the realm change and the native policy.
Part of #856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Addon loading follows the client's `AddOn_Load` (`0x51f240`) and `.toc` reader (`0x51c9b0`):
- One driver serves the startup walk, `LoadAddOn` and the survey harness. It marks the addon loaded before its dependencies (`0x51f313`), so a requirement cycle loads both and `IsAddOnLoaded` answers during an addon's own load. It loads `## LoadWith` dependents after `ADDON_LOADED` (`0x51f5b2`).
- Every directive is matched by its own literal. `LoadOnDemand`, `Secure` and XML bool attributes parse with the client's string-to-bool (`0x6f1b30`), so `hidden="1"` works. Values are not trimmed, and only an unindented `##` line is a directive.
- `GetAddOnMetadata` reads the metadata map (`0x51d580`): Title defaults to the folder, the last value wins, and locale suffixes apply. The url comes from the server, never the `.toc`.
- `EnableAddOn` keeps a toggle for an addon that isn't installed.
Tests are mutation-checked against the reference order.
Fixes#192Fixes#193Fixes#405Fixes#986Fixes#987Fixes#990🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part 3b of the stock GlueXML (`WOW_GLUE_STOCK=1`): the stock `RealmList` and `RealmWizard` run on the reference's ten realm verbs (`0x46f480`).
- The realm park is now a cancellable operation. Each dial carries the cancel generation it was asked under, so no Cancel is lost, and a status Cancel closes the logon (`0x5b3320`).
- Change Realm with the world up drops the session first (`0x46b22b`).
- The roster arm (`0x4724d0`) sets the chosen realm, or asks for the list when the roster is empty.
- An empty realm list is shown, never dialled at the world port.
- The native type sort puts PvP before RP.
Deviation lines cover the re-dial's full logon, comparator ties, a missing Region row and the native close.
Part of #856 and #834Fixes#833Fixes#974Fixes#982🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- **The layout file** now saves what the reference's writer does (`0x490e60`): `X`, `Y`, `W`, `H` and `FrameLevel` from the frame's held rect, rows in creation order, and only frames that are user-placed and movable or resizable. The restore (`0x4905e0`) pins TOPLEFT to the screen, sets the size and level, and resolves each frame as it goes (`0x490710`). Old anchor-form files still load and are rewritten once.
- **Chat bubbles** re-rank only on a camera move or a new line (`0x4b11ff`), and a repeat speaker moves to the tail, as `0x4b1870` links it.
- **The live re-layout on a window resize** gets its `Deviation:` line. 1.12.1 reloads the UI on an aspect change of 0.001 or more (`0x492f26`).
Tests cover parent/child level restore, the held rect, the hidden-frame resolve, the junk lines, the migration and the bubble tie order.
Fixes#954Fixes#955Fixes#847🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hidden frame resolved and fired `OnSizeChanged` at once, a getter's resize fired at the drain, and `GetWidth`/`GetHeight` answered the rect over scale.
In 1.12.1 hiding sets the deferral flag `0x2` (`0x76ad50`); while set, an invalidate marks the frame pending and passes it to its dependents without a resolve (`0x768178`). Show resolves it first (`0x76ae58`), a getter resolves a pending one and fires `OnSizeChanged` inside the read (`0x7a1980`, `0x76b580`), and an anchor read pulls it for good (`0x7a2fc0`). `GetWidth` answers the set size when non-zero, else the rect span, unscaled (`0x7a1e93`, `0x7a1ee6`).
The deferral is modelled for frames and regions, getters fire inside the read, layout ids mint in handle order (deterministic), and the options window measures its scroll by its edges.
Tests bite on the parent; the verifier is green; stock quads match.
Fixes#948Fixes#971🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under `WOW_GLUE_STOCK=1` the stock login screen now logs in. Separately, realmd's 0x0E answer was refused by the native login too.
In 1.12.1 the glue manager runs the screen states (`0xb41da0`): `DefaultServerLogin` (`0x46afb0`) needs the agreements accepted and opens the status dialog; each tick re-texts it from the logon's state and result (`0x46b140`, tables `0x836b40`, `0x836b78`); the world connect re-texts it every frame (`0x46be8b`). The agreements seed from `readTOS`/`readEULA` (`0x46a400`). Realmd 0x0E is a success (`0x5b2cd4`); a proof's 0x0A changes nothing (`0x5bada2`).
The glue core verbs, the glue manager over the native login, the agreement CVars, `SimpleHTML file=`, case-insensitive loose files and a benilla Realmlist layer file are built; the protocol accepts 0x0E. A recorded `Deviation:` keeps the interface signature checks off.
Tests: fake-realmd refusals and 0x0E; stock-screen login reaching the native submit.
Part of #856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With `WOW_GLUE_STOCK=1` (dev builds), benilla now loads the stock GlueXML off the player's own patch chain into the glue state and draws it; with the switch off nothing changes.
In 1.12.1 the glue builder (`0x46a7b0`) opens `Logs\GlueXML.log` (`0x46a8ce`), runs `GlueXML.toc` through the shared toc runner (`0x6edb90` at `0x46a9b3`), fires `FRAMES_LOADED` (`0x46aa34`) and opens on "login" or "movie" (`0x402b3b`), or "charselect" back from the world (`0x402084`). Glue `GetScreenWidth`/`GetScreenHeight` answer the root unscaled (`0x46dd30`, `0x46dd80`).
Under the switch the glue state replaces the boot state, takes input, and fires `SET_GLUE_SCREEN` on those edges; the native screens are hidden but still drive the network. A data-gated test loads the stock files with only seven unbuilt verbs raising, a list that may only shrink.
Part of #856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first piece of running the stock GlueXML: benilla had no glue Lua state, and no reference data for its surface.
In 1.12.1 the glue state holds the base set (`0x7039e0`) and seven registrars (`0x46abb0`): 119 functions, 105 of them glue-only, with 26 glue events (`0xb41e70`, `0x46a88f`). `RegisterEvent` matches names case-insensitively and drops an unknown one (`0x702140`). The glue builds the 13 base frame types plus `ModelFFX` (`0x46a4fe`), a Model with its own show and paint (`0x46fa00`); the in-game types come only at world entry (`0x495940`).
`reference/` gains the glue event and surface tables, generated and checked against the shapes table. `UiScript::new_glue()` builds that state; a gate holds its globals to the table, with the 108 unbuilt names listed. Nothing in the app changes yet.
Part of #856🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A title-region press refused while another drag held the slot, `SetLayoutMode` stored nothing, and a press on a chat link made the chat frame itself the target, so every link click started a title move of the window.
In 1.12.1 the drag entry takes the slot unread (`0x7652b0`); a mid-drag press goes to the frozen hover (`0x76637f`). With `SetLayoutMode` (`0x488540`) on, a Ctrl or Alt press drags the nearest movable toplevel (`0x765490`) from a 3×3 grip grid (`0x765320`) and is consumed, world included; its release only ends the drag (`0x7664e0`). Each link span is its own Button (`0x7a3240`) routing `OnHyperlinkEnter`/`Leave`/`Click` to the parent (`0x7a3780`–`0x7a37e0`).
These now hold; a release goes to the capture whatever its button (`0x76642b`), and number arguments keep `_ftol`'s low dword.
Tests bite on the parent for each.
Fixes#951Fixes#952Fixes#957🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`SetFont` raised on a `false` flags argument and wrote the height even when the load failed, and only SimpleHTML had `SetSpacing`/`GetSpacing`, so SpellAlert's message frame raised in its OnLoad.
In 1.12.1 `SetFont` (`0x79f210`, the FontString's `0x79d4f0`) reads flags only when `lua_isstring` (`0x79f287`), writes them on every successful load (`0x79f334`), and writes nothing on a failed load or a height ≤ 0 (`0x79f361`, `0x79f2f4`, `0x771571`). Every text table has the spacing pair (`0x79fb40`/`0x79fbe0`; the FontString's clamps at `0x772240`); the line step adds the gap rounded up to a device pixel (`0x5c2070`, `0x5cdc20`), as do the message frames (`0x78899e`, `0x785f06`) and EditBox rows (`0x77d133`).
All seven SetFont bindings share these gates and ask the host's font probe; spacing is stored, inherited, measured and drawn on all six tables.
Tests bite on the parent for each.
Fixes#873Fixes#692Fixes#945🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Region names lived in one first-wins name → id map that also served `GetName`, so a second region given a taken name answered `GetName()` with nil and was `<unnamed>` in its error strings.
In 1.12.1 the name lives on the region: `SetName` (`0x76c650`) keeps the region's own copy at `[+0x98]`, freed by its destructor (`0x76c560`) and read back by `GetName`. Only `_G` is first-come (`0x701bd0`, `0x701c8a`).
Each region now carries its name; `GetName` and the error strings read it directly, and the name map is a first-come lookup only, whose entry a free drops only when it holds that region.
Tests: same name twice, `$parentText` under colliding parents, the second region's error string, a free leaving a same-named sibling's name.
Fixes#939🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four pointer defects: OnEnter and OnLeave set `arg1 = true`; a frame being moved or sized still ran the hover walk; the pointer leaving the window fed `f32::MIN` to a move in flight, flinging the frame off-screen; and the mouseover token read the world pick, not the published mouseover.
In 1.12.1 both scripts run through `0x702690`, which sets no arg global. While `[root+0xcfc]` holds a move or size target, the walk pumps it (`0x7655b0`) and returns before any probe (`0x766164`–`0x7661a0`); a hide clears that slot (`0x76492e`). The pump only sees real cursor events. The token resolver reads the published pair `[0xb4e2c8]` (`0x515970`).
OnEnter and OnLeave fire with no arguments, `mouse_move` pumps a move or size and returns, a hide ends it, the pointer leave has its own call, and the aura feed reads the published mouseover.
Tests bite on the parent for each.
Fixes#932Fixes#933Fixes#936Fixes#938🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A FrameXML template registered under a name already taken replaced the first, nothing was logged, and names differing only in case were stored twice.
In 1.12.1 the registrar `0x6ee500` folds case (`0x64b3f0`, `0x64a4c0`) and keeps the first entry, logging "Virtual object named %s already exists" (`0x871188`), unless the old entry carries a non-zero script context and the new one zero (`0x6ee594`). The stock files load at context 0; `AddOn_Load` (`0x51f240`) loads an addon under its name unless its signature checked SECURE (`0x51dbfb`).
Templates are now keyed case-folded with their context, an addon's load sets the context, an addon counts as secure only from the player's own data with `## Secure: 1`, and the refusal and the unnamed-node line (`0x871020`) reach FrameXML.log.
Tests: duplicates and case variants refused, the context rule, the log lines.
Fixes#556🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moving from the world onto a window, the world mouseover's loss reached the tooltip a frame after the window's OnEnter, so a gate kept the fade off any tooltip the world had not put up.
In 1.12.1 the hover walk runs the old frame's leave before the new frame's enter (`0x766229`, `0x766239`). The WorldFrame's leave (`0x483b10`) runs OnLeave, then publishes the empty mouseover, whose publisher fades GameTooltip whoever holds it (`0x492890` → `0x530ae0`). A nameplate's leave publishes first (`0x7cb8b0`); the Minimap's hides the tooltip (`0x4eb880`), and its probe drops a held blip on a miss (`0x4eb0c0`).
The VM now holds the published mouseover and runs these leaves in the walk, the hide and every mouse disable, freelook's plates included (`0x60f830`); the ownership gate is gone.
Tests: OnEnter no longer sees "mouseover", an addon tooltip fades on a change, blip and Minimap leaves hide it.
Fixes#901🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every resolve stored the rect it computed, however small the move, so float noise in anchor chains moved rects, hit edges and size-change notifications where the reference keeps the old ones.
In the reference, the layout drain compares each of a node's four new edges with the rect it last stored. If every edge moved less than 1e-5 in its units (where the screen diagonal is 1), it keeps the stored rect and skips the store, its dependents and `ApplyRect`. A scale change, a new font or new text clears the resolved bit, so the next resolve stores without the test.
A resolve now keeps a frame's or region's rect under that threshold, scaled by the layout unit, and a scale change or a new measure stores the exact rect.
Tests cover jitter, a move just over, slow drift, a region and the forced stores.
Fixes#919
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chat bubbles were quads drawn straight into an overlay with no widget behind them, so `WorldFrame:GetChildren()` never returned them and bubble-skinning addons such as pfUI's found nothing.
In the reference, a bubble is an anonymous Frame under WorldFrame from a reused pool: eleven regions (the nine backdrop pieces, the tail, the text in the `NAMEPLATE_FONT` global), the text's bottom on the speaker's projected point, its own fade, and an OnShow on each reuse. It fades out and hides when the speaker is out of range or was not drawn last frame.
Bubbles are now built that way, fed by the app and drawn by the frame pass; the overlay path is gone. Item links show their bare names, and a speaker behind the camera fades out.
Tests cover the child, its regions, a pfUI-style skin, reuse and the fades.
Fixes#884
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tick ran the OnUpdate walk without resolving the layout around it, so a resize made in an event handler fired `OnSizeChanged` after the walk, and some walk slots resolved on their own to compensate. Separately, `OnSizeChanged` gated on 2⁻²² in benilla's units, and an emptied FontString kept its old measured box in the incremental layout.
In the reference, the pump drains the layout queue before the walk and again after it, each drain firing `OnSizeChanged`, then re-picks the hover. ApplyRect compares sizes against 2⁻²² in its own units, where the screen diagonal is 1.
The tick now drains before and after the walk and drops the stand-in resolves. Size gates scale 2⁻²² by the layout unit, and a FontString's text going empty or back names it on the layout when its node changes.
Tests pin each.
Fixes#912Fixes#741Fixes#914Fixes#911
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Button state texture's visibility was kept apart from its region's shown bit, so a script `Show()` on a non-current state texture drew nothing. The bar, thumb and ColorSelect setters took a colour form and accepted arguments the reference rejects, the loader built their textures from `file=` or `<Color>` alone, and a bar texture answered nil to `GetTexCoordModifiesRect`.
In the reference, a region has one shown bit that `Show`, `Hide` and the state switch all write. The setters fork on object, path, nil or usage. The loader builds each widget texture as a full Texture element, and installing a bar texture sets its texcoord flag and gives it all points.
All of these now match: one shown bit, the reference's argument forms, full-element textures with their names and texcoords, and no tint without a bar.
Tests pin each.
Fixes#909Fixes#908Fixes#575
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The tooltip fade, a model's animation clock and its OnAnimFinished ran after the whole OnUpdate walk, and OnUpdateModel fired in creation order. A tooltip hidden through its parent also kept its owner and fade.
In the reference, the walk makes one call per frame: its OnUpdate, then its kind's engine update (a tooltip's fade step, a model pane's clock and completion test). OnUpdateModel fires later, from the paint, in draw order. A tooltip's own hide and show run from any caller, its parent's cascade included: the hide un-owns and clears it; the show needs an owner.
The walk now runs a per-kind update in each frame's slot, OnUpdateModel fires per stratum in draw order after the hover re-pick, and show and hide dispatch to a kind's override. CharacterModelBase's override is left for #890.
Tests pin each order and a parent's hide and show.
Fixes#861Fixes#863Fixes#864
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A frame hidden while a button was pressed on it still got OnMouseUp on the release, and its stale capture aimed the next chorded press at it. And showing any frame, even one that takes no mouse input, scheduled a hover re-pick, so a still cursor could fire OnEnter or OnLeave a tick early.
In the reference, the hide's input-index removal clears the capture slot when it holds the hidden frame, and the release goes only to the capture, so it reaches no one. The hover re-pick is armed only when a show inserts a frame into the mouse index.
The hide now drops the capture and the pressed frame; the release then runs nothing, no world click either. A show arms the re-pick only for a frame that takes the mouse.
Tests cover hides while pressed and shows with and without mouse input.
Fixes#844Fixes#845
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
OnUpdate ran in frame-creation order, from a list built before the walk, so a frame hidden by an earlier handler still updated, and strata and level played no part.
The reference's update walk `0x765650` goes through the nine strata from the bottom, each stratum's levels from 0, and each level's list oldest link first. It saves the next frame before each call, so a hide skips a frame before its turn. A frame linked into the level being walked waits a tick; one linked into a level still ahead runs there.
The walk now follows that order, using the link stamps shows and level changes already keep. It checks each frame at its turn and places frames linked or given an `OnUpdate` mid-walk. An EditBox still flushes right after its own `OnUpdate`.
Tests cover the order, mid-walk hides and shows, and a box's flush.
Fixes#742
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Script errors reached the Lua error handler as mlua's display text: a `runtime error: ` word and a `stack traceback:` block, which overflow the stock `ScriptErrors` dialog. And a raise in a `CreateFrame` template's OnLoad was logged as a warning and never reached the handler at all.
In the reference, handlers run under `lua_pcall` with the error function `0x703b40`, which calls the `seterrorhandler` function with the bare message. `CreateFrame` builds its template through the XML instantiator `0x6ee280`, whose OnLoad runs under that same function (`0x704d50`).
Now every caught error records two texts: the decorated one for benilla's log and the bare message for the handler. A load report pairs the two per row, and `CreateFrame` records its template's errors as script errors.
Tests: the handler hears bare messages from handlers, and a template OnLoad raise reaches it.
Fixes#697Fixes#859
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Show and hide propagated a whole subtree first, then fired from a snapshot, parent first: a parent's `OnShow` ran before its children's, and a child hidden by an earlier handler still got its `OnShow`. A frame visible at the end of its load got no `OnShow` at all.
In 1.12.1 show `0x76ae10` and hide `0x76ad50` mark the frame, walk the live child list re-checking each child's gate, and fire the frame's own handler last (`0x76aef5`, `0x76adfd`). The post-load hook fires `OnShow` after `OnLoad` for a visible frame (`0x76a3b3`), from XML and `CreateFrame` alike.
Now handlers run inside a live post-order walk, and the post-load `OnShow` fires. Tests cover the order, a sibling hidden mid-walk, and one or two `OnShow`s after a load; they fail without the fix. Four existing tests' expected orders flip to the reference's.
Fixes#350Fixes#59🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UI scale was folded into the pixel seam, with the root and `UIParent` at scale 1. Frames outside `UIParent` (dropdown menus, need/greed rolls, the world map) shrank with it, `UIParent:GetScale()` answered 1, and `GetCursorPosition()` was off by the scale.
In 1.12.1 the scale is `UIParent`'s own frame scale (`0x494550` → `0x76ac10`, floored at 0.64) over a 768-unit root. `GetScreenWidth`/`GetScreenHeight` divide by it (`0x494590`), and `GetCursorPosition` (`0x48b820`) does not.
Now the seam is `height/768`, and the scale is applied to `UIParent` at the load's end and on a resize. The nameplates' compensation is gone. Backdrops, hit insets and minimap pings now follow a frame's own scale (`0x76b580`, `0x77e8d0`); tooltips at the cursor and `OnCursorChanged` divide by it (`0x530b20`, `0x77db23`). Tests cover each, and a stock-UI comparison shows nothing inside `UIParent` moves.
Fixes#172🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reopening the world map after zooming out showed the old map for a frame before the current zone.
In 1.12.1, every Lua verb that moves the map selection (`SetMapZoom`, `SetMapToCurrentZone`, a `ProcessMapClick` hit) goes through the setter `0x4a67a0`. The setter stores the selection, rebuilds the landmark list, then fires `WORLD_MAP_UPDATE` in place before returning (`0x4a6ce4`). So `WorldMapFrame`'s OnShow repaints the tiles before the first draw.
benilla queued the event to the next tick, so the first frame painted the previous map. The three verbs now run the setter whole. The landmark list is projected from per-row sources inside the setter, so the new tiles never show the old map's markers. The engine's own pushes still queue, as a narrowed deviation.
Tests replay the report's steps on the stock UI and check the event fires inside the verb with landmarks rebuilt first; they fail before the fix.
Fixes#811
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- capture: a name-rank scenario shoots a ranked player's overhead name line
- Overhead names carry the player's PvP title under UnitNamePlayerPVPTitle, as 1.12.1 does (#550)
- review: bundle the rank-title inputs; doc the snapshot and rank-19 limits
- nameplates: read rank strings per VM; test the composed name line
- pvp: correct the rank-title docs; convert the credit team without a cast
- Fix the titles row comment; name-rank reuses WOLF_POS, cites :900
- rank strings: the honor feed reads them off the VM; nameplates only reads the value
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
Several EditBox keys differed from 1.12.1:
- Up/Down did nothing in a multi-line box;
- Home/End went to the text's ends;
- `HighlightText` and Ctrl+A moved the caret;
- focus did not restart the blink;
- a non-digit kept a numeric box's selection;
- Option+Up could not recall chat history on a Mac.
In 1.12.1, Up/Down move a wrapped row, keeping the letter column (`0x77cb20`), or recall history in a single-line box with any modifier but Shift. Home/End stop at a newline (`0x77c980`, `0x77c9f0`). `HighlightText` writes only the selection (`0x77cca0`), and Shift extends by `0x77cd10`. Focus restarts the blink (`0x77dd68`), and Insert deletes the selection before its digit test (`0x77bf41`).
benilla now does each. Down onto the last row lands at the end, and history stays shell-style, both deviations.
Tests cover each key, in the engine and the stock mail and ticket boxes.
Fixes#735Fixes#736Fixes#737Fixes#738Fixes#745Fixes#747🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
EditBox events fired from hoisted drains before any OnUpdate, OnTextChanged before OnCursorChanged, and only on an edit.
In 1.12.1:
- Each box's update `0x77a790` fires its own `OnUpdate` and then its flush `0x77d3e0`, and a key-down or click flushes first (`0x77b1e2`, `0x77b819`).
- The flush runs off dirty bits in `[E+0x31c]`: relayout on bit 0, then `OnCursorChanged` on bit 2, then `OnTextChanged`.
- The ctor sets bit 0, so a box fires at its first show. Every text-region re-seat raises bits 0–2 (`0x77ba7f`), so a resize, new insets, `SetMultiLine` or a font change fires it again.
benilla now keeps the same bits and flushes each box in the walk and at its keys and clicks. So typing DELETE and pressing Enter in one frame enables the popup's button, as in 1.12.1.
Tests cover the order, first show, re-seats, a key's flush and no-op moves.
Fixes#724Fixes#727🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A multi-line EditBox kept its authored height, so a scroll frame around a long mail body or ticket had no range. A box made in Lua with no template never placed its text, so it drew nothing.
In 1.12.1, the flush's relayout sets a multi-line box's height to its insets plus its wrapped text height, or one line when empty (`0x77d4d0` at `0x77d8ad`). Every resize, including the first layout, re-seats the text at the top-left inset (`0x77a8d0` → `0x77b8c0`). `SetTextInsets`, `GetTextInsets`, `SetMultiLine` and a font change re-seat it too.
benilla now does the same. A focused multi-line box's caret and text are clipped only by a scroll frame, as the reference clips neither.
Tests cover growth and shrink, the stock mail and ticket scroll ranges, each re-seat trigger, a Lua box's first layout, and the caret in an empty box and after a trailing newline.
Fixes#722Fixes#726🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`TargetUnit`, `AssistUnit`, `SpellTargetUnit`, `CheckInteractDistance` and `CanInspect` ignored target chains. `party1target`, `pettarget` and `raid3target` named nobody.
The reference's resolver `0x515970` takes a base token, then any number of `target` hops over held units' `UNIT_FIELD_TARGET` (`0x5159d3`-`0x515a2c`). Every one of these verbs resolves through it.
Changes:
- The app and the VM now share one parser, `parse_unit_token`.
- The reach map behind the two range verbs is keyed by guid, filled after the aura feed, and read through the VM's resolver.
- `ObjectStore::is_unit` replaces two private copies of the same test.
- Side effect: the reach feed's new order puts a few feeds after `feed_auras` where their order was undeclared before.
Tests: every chain form, a broken chain, junk after a hop and case folding. `TargetUnit("party1target")` works from Lua.
Fixes#669
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The fourteen 1.12.1 functions #336 lists are defined, each derived from its reference address, and benilla's layer drops the two places it stood in for the stock bag code.
- **Keyring:** `KeyRingButtonIDToInvSlotID` (`0x4c8150`) exists, and the keyring slots answer as inventory slots 82-113. The stock `ContainerFrame.lua:616-619` hover path now runs unchanged, and the layer's keyring `OnEnter` wrapper is deleted.
- **Bags:** the layer no longer replaces `CloseAllBags`. The stock one (`ContainerFrame.lua:702-707`) closes the backpack only when it was not open before the vendor or mailbox opened it (#541). The deviation that opens every bag stays, and its comment now names its only callers.
- **Inventory:** `ShowInventorySellCursor`, `SetInventoryPortaitTexture` and `UpdateInventoryAlertStatus` exist.
- **Character and pet:** `GetDamageBonusStat` (ChrClasses field 2), `GetPetTimeRemaining` (armed from `SMSG_PET_SPELLS`) and `GetActionAutocast` exist.
- **Mail:** `ClearSendMail`, `GetNumPackages`, `GetPackageInfo` and `SelectPackage` exist, over a new `Package.dbc` loader. A selected package rides in the mail send. The compose reset now also clears the stationery, the package and the attachment, as `0x4acdc0` does.
- **Tabard:** `GetTabardInfo` opens the tabard designer through the registrar (`0x5e00e0`).
- **World map:** `GetWorldLocMapPosition` projects a map position onto the displayed map (`0x4a7360`).
- **Loot:** `SetLootPortrait` binds a unit loot source's portrait (`0x4c2b40`).
Verified:
- A test per function for its shapes, raises and effects, failing on main.
- A keyring hover test through the stock path, failing on main.
- A backpack-over-vendor test, failing on main.
- `scripts/gates.sh` and `scripts/smoke.sh` are green.
- A live A/B run: hovering a key asked for `player:82` and showed the key's tooltip. After a vendor, opening the Help panel left the backpack open and closed bag 1; on main the backpack closed.
Built on macOS; no platform seam touched.
Fixes#336Fixes#541🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>