Commit Graph
179 Commits
Author SHA1 Message Date
gromandClaude Opus 5 400457ff82 superwow: TrackUnit/UntrackUnit, and the four chat-bubble CVars
TrackUnit is the one with a live consumer and a real feature behind it:
1.12 draws party members on the minimap and raid members NOWHERE, and
pfUI puts the whole raid on it with these two — for every party and raid
token on PARTY_MEMBERS_CHANGED/RAID_ROSTER_UPDATE/PLAYER_ENTERING_WORLD,
each inside a pcall, so on a client without the function the feature is
simply absent, which is what it has been here.

The set is guids, not tokens, so a tracked raid member keeps its dot
while the roster shuffles its token out from under it. The dot itself is
drawn by a fourth always-show clause beside the reference's own three
(`shows_tracking_dot`) — `tracked_creature` stays exactly `0x5ed210`, so
the predicate that file documents is still the client's, and a tracked
unit gets the ordinary cell, hover name and distance cull with nothing
new drawn. The set is read off the VM handle the blip system already
holds rather than behind a resource of its own: `BlipInputs` is at the
16-param ceiling and its last element is already a nested pair for that
very reason.

The bubble four come straight off the DLL's string pool. Three are
nampower's `NP_ChatBubbles*` under SuperWoW's unprefixed names, writing
the same fields — two mods, one knob, last writer wins, as the nameplate
reach already is — each keeping 0 because the reference bubbles none of
those kinds. `ChatBubblesCreatures` is SuperWoW's alone and the only
behaviour of the four: it NARROWS `ChatBubbles` rather than standing
beside it, because the mod hooks the reference's own gate and a switch
added at that site can only subtract; the other reading would bubble
creatures with the master switch off. It ships 1, so an untouched client
bubbles monster say and yell exactly as 1.12 does. All four are welded to
their struct literals in the defaults-mirror test.

CREATE_CHATBUBBLE is deliberately NOT fired, and the row says why: its
payload can only be the bubble, and this client's bubbles are not Lua
frames — the same wall `C_ChatBubbles.GetAllChatBubbles` already answers
an empty list for. Argless would be worse than silence, since an addon
written against upstream reaches for arg1's regions and raises once per
bubble.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-25 12:58:31 -06:00
gromandClaude Opus 5 ac857ada36 superwow: CanLootUnit, GetSpeed and UnitNameplate
Three of the DLL's own manifest, and two of them needed no new feed at
all — which is the point worth recording, because the row that called
this family "todo, blocked: each needs new app-side state" was wrong
about two of the three.

`CanLootUnit` is the loot sparkle's test read through the token map:
`UnitState` already carries `dead` and raw `dynamic_flags`, so a corpse
that sparkles answers 1 and there is one source of truth for "has loot
for me". The bit is per-viewer on the server, so "can" means can.

`UnitNameplate` goes through the SAME `plate_frame` wrapper as
ClassicAPI's `C_NamePlate.GetNamePlateForUnit`, welded by a test that
asserts both names hand back one object — an addon may feature-detect
either, and two different frames for one plate is the kind of bug that
only shows up as an anchor in the wrong place.

`GetSpeed` is the one that needed a field: the posture snapshot gained
`speed`, filled from `net::current_speed`, the reference's own
`CMovement::GetCurrentSpeed 0x7c4c90`. What could not be settled is
written down rather than guessed over — upstream publishes no prose and
nothing in the corpus calls it, so "current" is a reading, taken because
it is the more informative of the two and because it is the only function
in the client named after speed that a hook would return. Standing still
it is 0, as that getter is.

`SelfMovement` loses its `Eq` (an f32 field); nothing compared two
snapshots — the feed assigns a fresh one each tick.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-25 12:43:08 -06:00
gromandClaude Opus 5 3b6e91be35 superwow: RAW_COMBATLOG and CombatLogAdd, the only two names his addons call
Of SuperWoW's whole surface these were the two names grom's corpus calls
that this client lacked (`scripts/dll-surface.py --wanted`), and the first
is load-bearing out of proportion to its size: `SuperCleveRoidMacros`
funnels FIVE subsystems through one `RAW_COMBATLOG` frame — immunity and
resist learning, hidden-CC detection, debuff-duration learning, and the
reactive-proc window that lights Overpower and Revenge. Its own comment
says why the last of those can come from nowhere else: "SPELL_GO only
provides hit/miss COUNTS, not miss TYPES".

`route` is the fire site: the one funnel every combat line already passes
on its way to a window and to WoWCombatLog.txt. `arg1` is the printed
sentence; `arg2` is the same sentence with each unit name spelled as its
GUID, composed a second time through the same `compose_line` so the two
can only differ in the name slots. Fired before the `CHAT_MSG_*`, which is
what "raw" means upstream. Both the fire and the `arg2` composition are
gated on `has_listener` — a raid's combat log is tens of lines a second.

`CombatLogAdd` is also BigWigs' SuperWoW detection (`if CombatLogAdd
then`), so defining it turns that module's path on.

Three corrections came out of reading the DLL's own usage strings, which
it publishes beside the names — the arity and argument meaning of every
remaining SuperWoW function is now measured rather than inferred, and
`docs/ADDON_COMPAT.md` carries the manifest:

- `CombatLogAdd("text" [, addToRawLog])` has a SECOND argument. Nothing
  in the corpus passes it; it would have been missed.
- `SUPERWOW_COMBAT_LOG_EVENT` does not exist. The DLL registers exactly
  three events, and that name is in no part of its string pool.
- `IsIndoors` is capital-I beside `IsSwimming`/`IsMounted`, not the
  lowercase `isIndoors` two files guessed at — all three are already live
  off one feed, so that clause of the "blocked" row was already answered.
- The detection pair is `SUPERWOW_STRING="SuperWoW 2.2 by Balake"` and
  `SUPERWOW_VERSION="2.2"`, both quoted. Ours had the string without its
  author clause and the version as a number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-25 12:30:55 -06:00
gromandClaude Opus 5 a9bb9624e9 app+ui: nampower's press-interception queue — the half that changes how keys feel
The half of upstream's queue that `queue.rs` has said for three rounds it
deliberately left out. A press the client would refuse for a **timing**
reason is now held in the same slot an explicit `QueueSpellByName` uses,
and the drain sends it the moment the guard clears: no packet, no global
cooldown, no error line while it waits. One queue, because upstream has
one — a second set of slots beside the first would be a different feature
wearing the same name.

**The guard reopens early, and that is the headline.** `PendingCast` kept
one timestamp and now keeps two. The guard reopens at the cast's own
already-known length plus `NP_MinBufferTimeMs`; the record still lives for
the cast plus two seconds, so a resolution arriving a round trip later is
still recognised as the answer to that cast. That split is not tidiness —
the `modalNextSpell` chain that starts a hunter's Auto Shot reads the
record, and a record that died with the guard would break shooting.

The buffer is not a latency figure, which is why upstream's default is
55 ms and not a ping: our packet reaches the server half a trip after the
send, the server finishes its cast half a trip after our own cast time
elapses, so a next cast sent at exactly `cast_time` arrives just as the
server frees up and the latency cancels out of the arithmetic. The buffer
is jitter margin on top. `NP_NonGcdBufferTimeMs` is wider because a
non-GCD spell has nothing *but* the guard between two presses.

**`NP_RetryServerRejectedSpells` is the mitigation for that**, and that is
why upstream defaults it on. If the margin is ever wrong the server refuses
the next cast and the press is simply gone — the dropped cast the early
guard buys its speed with. It goes back once, held a buffer's worth, on
exactly two codes: 0x3c not-ready and 0x61 another-action. Every other
refusal is the server telling the player something true, and re-sending one
would be a packet a second that never succeeds.

**What a window measures, and how that was decided.** Upstream publishes
four and no sentence saying. Two readings fit: how long an entry survives,
or how close to the guard's end a press must land. This takes the first,
because `queue.rs` already reads `NP_SpellQueueWindowMs` that way and one
number meaning two things in one client is a trap nobody finds twice;
because `Replacing queued nonGcd spell X with Y` means the last press wins,
so holding every press costs nothing in staleness; and because the other
reading would make the 250 ms cooldown window refuse almost every press
made during a global cooldown, which is the complaint nampower exists to
answer.

Two rows do read a window as remaining time, and both say so themselves.
`NP_InterruptChannelsOutsideQueueWindow`'s name states one. And
`NP_CooldownQueueWindowMs` has to be one: a cast ends in a second, a
Hearthstone cooldown is half an hour, and holding that press means either
firing it half an hour later or swallowing it with no error line. A quarter
of a second is an allowance for pressing a touch early, not a queue.

**Where the rung sits.** Five rungs stay above it — the profession opener,
the auto-repeat re-press, the dead caster, the targeting cursor's clear and
the on-swing re-press — because each is what the press *means* whatever is
in flight. Everything below is a real refusal a player should read: no
reagent, out of range, no power, stunned, moving, wrong form. Upstream
hooks the cast entry and holds those too, so its error arrives a second
later about something the player stopped doing. Refusing them now is the
deviation and the better half of the trade.

Fourteen CVar rows went live. What stays stored is named with its cost: the
dynamic buffer optimizer and its ceiling (off in upstream's own defaults),
`NP_OnSwingBufferCooldownMs` (nothing to rate-limit — a re-press of the
queued strike already takes the reference's silent bail), the channel
latency trim, and the three quickcast rows, all off by default and all
needing the world point under the mouse in the cast path.

`CastSpellNoQueue`/`CastSpellByNameNoQueue` became meaningful: they take
`CastLadder::send_now`, which skips the rung. The queue's own pop takes it
too — a pop that could be re-held is a loop that never casts anything.

The test worlds now seed the **registered** defaults rather than
`Default`'s all-zeroes, which is a configuration nobody runs: three ladder
tests were passing only because the queue was off in them. Each now asserts
the queue behaviour and keeps the reference's 0x61 in a world where a
player unticked the queue. And one test drives the whole cycle through the
real drain — press held, nothing on the wire, guard cleared, one packet out
— because the failure this guards against is a keypress that produced no
packet, no error and no cast, which is the one a player cannot describe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-24 23:46:42 -06:00
gromandClaude Opus 5 702eb475a4 app+ui: the queue, the disenchant sequencer — 62/62
QueueSpellByName, QueueScript, ChannelStopCastingNextTick,
DisenchantAll. Every one of nampower's 62 registered Lua functions is
now defined; the `comm -23` in the doc prints nothing.

**Upstream's queue has two halves and one is deliberately not built.**
The Lua surface — remember this spell, send it when the client will
accept it — is `spell/queue.rs`. The other half intercepts *every
ordinary press*, so a keypress during a cast is held rather than
refused, and it is governed by NP_QueueCastTimeSpells,
NP_QueueInstantSpells, the buffer rows and the quickcast pair. That half
is not a binding: it changes how every key in the game feels, its bugs
are double-casts and dropped casts, and it is not something to switch on
as a side effect of porting three function names. Those twenty CVars
stay stored-only, each row saying so; the machinery they would sit on is
now here, and turning it on is its own piece of work with its own live
test.

Two of the twenty-two went live, because they are the explicit queue's
own knobs: NP_SpellQueueWindowMs and NP_QueueSpellsOnCooldown.

Three slots, because upstream has three and its own event says so:
SPELL_QUEUE_EVENT's code is 0/1 on-swing, 2/3 normal, 4/5 non-GCD, so
the queue is keyed by what kind of cast a spell is — and the kind comes
off its own Spell.dbc row through the same three tests the send path
makes. Each pops on its own condition: non-GCD and on-swing as soon as
no cast guards (neither uses the global cooldown), normal when the GCD
has run out too.

Three decisions at the queue, each pinned:

- A replacement raises no popped event. A second queue of one kind
  replaces the first — upstream's own log line is `Replacing queued
  nonGcd spell X with Y` — and the replaced entry was never sent, so a
  consumer counting pops against queues would see one too many.
- An expiry raises nothing either. `*_POPPED` means "this was sent".
- The expiry is measured from the end of the cast in flight, not from
  the moment of queueing, so queueing early during a long cast is not
  punished. A lapsed deadline cannot shorten the window.

**`DisenchantAll` is specified completely by the DLL's own strings**,
which is why it is built this way rather than designed. `0x93087`
onward carries the argument note, the precondition, the three quality
keywords, every stop reason and the three chat lines — one of them as a
Lua chunk that builds the item link itself.

"move during cast to cancel" is the abort, and it needed nothing new:
moving already cancels a cast here, the cast then fails, and a failed
cast stops the run. The one piece of machinery a sequencer usually
needs is the client's own, and the player's way out is the one upstream
documents.

How a step's outcome is read: the item is gone, or it is not. There is
no "disenchant succeeded" packet — the item leaves the bag. Each step
remembers the guid it cast at and waits for it to vanish with a
ten-second deadline. An observable test with no new wire plumbing,
right for the same reason the cast bar's own edges are.

The queue's drain and the sequencer both live inside
`drain_spell_casts`, which that file's own note explains: only one
system may hold `CastLadder`, because a second would share its
resources with every other cast-send system in an order the ambiguity
gate cannot know is safe.

`spell/queue.rs` names no VM — the first draft mentioned `UiScript` in
one comment and `ui_script_wall.rs` counted it as gameplay code naming
the VM, which is the gate working as intended: that module really is
gameplay state and not a feed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-24 13:25:27 -06:00
gromandClaude Opus 5 b652c90a4b app+ui+protocol: the four measured-out names, and the unit descriptor by name
Each of these had a reason for being out. Each reason turned out to be
answerable, and two of them were simply wrong.

`GetSpellPower(school)` is **delegated to `GetSpellBonusDamage`**, which
this client already answered — one number under two mods' names, as
SuperCleveRoidMacros says itself ("Reads the same client field
nampower's GetSpellPower does"). Delegating is the only way to guarantee
they cannot drift, and the test asserts equality across all seven
schools and the off-range values rather than a literal. The earlier
"needs a descriptor band fed" was wrong: it was already fed.

`GetRaidTargets()` is the 8-slot **board**, now pushed beside the party
roster. The earlier reason was right about the scan it rejected and
wrong about the alternative: `GroupState::raid_targets` is the
reference's own table and copying it costs eight words a push. A mark
with nothing on it is absent, because a zero GUID is a string that
passes `if t[4]`.

`LearnTalentRank` is one `LearnTalent` press per missing rank, which is
what a player clicking the button does. The earlier reason — the burst
cannot be checked before it is sent — is true and is not a reason to
withhold it: the check that matters is the server's and the client has
never had another. What can be checked locally is, so a rank already
held queues nothing and one past the maximum is clamped.

`GetUnitData` is the whole descriptor, from a new raw block on
`UnitState`. The earlier reason — "a decision about which fields" — is
now a table of seventy columns under the DLL's own names, read off its
string pool in UnitFields order; `GetUnitField`'s scalar half reads the
same block, so the two cannot answer differently for one name, and a
test walks the table asserting exactly that.

**The indices are derived and the derivation is checked.** Only thirteen
`FIELD_UNIT_*` constants existed here; the rest come from 1.12's block
sizes. Those thirteen span index 22 to 147 and every one lands where the
layout puts it — thirteen independent confirmations, run as a test
rather than asserted in a comment, with two more that no columns overlap
and that nothing falls inside the aura blocks' 47..=124.

Three decisions at that mapping:

- An unstreamed column is absent, not zero. `ObjectFields::at` is
  `get_raw`'s strictness rather than `get_u32`'s absent-is-zero
  convenience: `health = 0` for a unit whose health has not arrived is a
  number a caller divides by.
- `aura`/`auraApplications` come from the filtered per-unit aura list,
  not the raw slots — the same place `GetUnitField` has always answered
  them from. An unfiltered second answer would be a different unit by
  the same name.
- `status` and `zoneId` are left out of nampower's seven computed
  columns: nothing says what `status` is a status *of*, and a zone is
  only knowable for the player. The other five are filled by the caller,
  which knows them.

`SpellRecValue` is now `RecValue`: it serves both record readers, and a
second copy of the same four shapes would drift.

58 of 62 registered names. The four left are `DisenchantAll` and the
queue trio.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-24 12:08:46 -06:00
gromandClaude Opus 5 e716709bca app+ui: the nine file and credential verbs, fenced and keyed
Built on grom's explicit say-so, after a round where they were withheld
and the reason recorded. Both halves of that decision stay in the doc,
because the reasoning is what makes the second half a decision rather
than a drift.

ReadCustomFile/ImportFile (one operation, two upstream names),
WriteCustomFile/ExportFile, CustomFileExists, ExecuteCustomLuaFile,
StartItemExport, EncryptPassword, EncryptedServerLogin.

**The fence is app-side and it is not a `..` scan.**
`local_state::custom_file_path` admits one relative name whose every
component is an ordinary name, inside `benilla-config/CustomFiles/`, and
refuses a leaf that is already a symlink. Each escape a string check
misses has its own rule and its own test row: an absolute path (`join`
*replaces* the base rather than appending), a Windows prefix, a `..`
nested inside a longer path, and a planted symlink — the one no amount
of sanitising the string will catch. A subdirectory rather than the
config root, because a verb that could create `config.toml` or
`autologin.toml` by name would let an addon rewrite the client's own
settings; that is a different power from "keep a file of your own".

The VM side holds three closures and no paths, so no spelling of a
`filename` reaches outside that directory.

**The credential pair's deviation is forced, not chosen.** Both are
registered by nampower under "Registering *Glue* Lua functions" and this
client has no glue Lua VM at all — login, character-select and
character-create are Rust. `EncryptPassword` is pure and works the same
anywhere. `EncryptedServerLogin` cannot mean "submit the login form" in
the only VM there is, so it stores the credential for the next login:
`enabled` on, the plaintext field cleared, the blob kept, and
`AutoLogin::effective_password` reads it back at the refill.

What that protects is an `autologin.toml` or a SavedVariables file read
by someone else or pasted into a forum: the blob is authenticated and
keyed on 32 random bytes in a different file, mode 0600. What it does
not protect is anything running as this user, which can read the key as
easily as the blob — there is no user secret in the chain and there
cannot be, because the point is logging in without typing one. Both
halves are at the module.

Encrypt-then-MAC with HMAC-SHA1 as the PRF for both halves, from the two
primitives the tree already had (sha1 moves from a dev-dependency to a
real one). The HMAC is tested against RFC 2202's own vectors, because a
hand-written one still round-trips with itself when it is wrong — and a
first draft of the key reader did exactly that kind of silent wrong: it
returned raw bytes on creation and hex text on read, and the caller
guessed between them with "32 bytes and not all hex digits". It guessed
wrong the moment a random key came out all-hex. Returning one thing is
the fix, and the note is at the function.

**The existing plaintext path is untouched.** Storing the password in
plaintext was grom's own explicit choice when the alternatives were a
keyring or nothing, so this is additive and nothing migrates by itself.
The machinery to make the Remember Password tick-box write a blob
instead is now all here — one line — and that is his call.

Base64 was missing from the whole tree; it is in `benilla-ui`'s
`script/encoding.rs` with RFC 4648's own vectors, and
`C_EncodingUtil.EncodeBase64` will want the same one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-24 11:30:49 -06:00
gromandClaude Opus 5 f87ef148b8 app+ui: the trinket cooldown — and a stock verb that always said "cold"
Built: GetTrinketCooldown, UseTrinket, GetSpellRangeData,
SetLocalRaidTargetIndex, GetQuestLogQuestIds, GetQuestDialogQuestId,
CombatLogFlush.

The find on the way through is not a nampower verb.
**`GetInventoryItemCooldown` is a stock 1.12 verb and it answered
`(0, 0, 1)` unconditionally** — right shape, always "cold", so nothing
errored and no worn trinket ever drew its cooldown swirl on the
character pane. Stock PaperDollFrame.lua:691 calls it on every slot
update. Feeding an equipped slot's cooldown for GetTrinketCooldown fixes
that one off the same column, and the test runs
`CooldownFrame_SetTimer`'s own arithmetic against it.

The feed keys its query on the (use spell, item entry) pair exactly as
the bag feed does, so one trinket reads the same cooldown from the doll
and from a bag.

**The risk was the gate, and my first test for it was vacuous.**
`feed_char` sits behind a change gate and no descriptor moves when a
cooldown is armed, so `Cooldowns::feed_epoch` had to become a gate
input. The first test passed with that input deleted: `run_system_once`
builds a fresh system each call, so the gate's `Local` memo resets and
`vm_reset` opens the gate every time. A test for a gate input has to run
the system the way the schedule does. Rewritten with `add_systems` +
`update`, it fails with the input removed — verified by probe, and the
reason is written at the test.

`feed_char` also hit Bevy's sixteen-parameter ceiling, which was a fair
signal its parameters wanted grouping by what they are for:
`ItemCooldownInputs` and `RepairInputs` replace six.

Two decisions recorded at the code:

- `GetSpellRangeData(rangeIndex)` answers three values, not a table.
  Every table-returning verb in this DLL has its key names in the string
  pool and this row's are not there. `flags` bit 0 is the melee family,
  where the engine takes the combat-reach sum instead of the authored
  pair — a caller ignoring the bit reads 0..5 for every melee ability.

- `CombatLogFlush()` is a deliberate no-op. Upstream buffers its
  combat-log writes and this empties the buffer, so what an addon asks
  for is "my lines are on disk now" — and `ui_chat/logging.rs` writes
  each line to an unbuffered File as the window shows it, so that holds
  at all times. Defining it is the honest answer: the capability is
  present, and a missing global makes `HasFeature` report it absent.

Also fed, both one wire column each: `QuestState::quest_id` (the wire
names the quest on all three giver panels and 1.12's Lua names it
nowhere — a title is not a key) and `SpellDisplay::stack_amount`'s
neighbour work from the previous commit.

Five names stay out, each now with the field list that makes it a
decision rather than a shrug — `GetUnitData` (80 raw descriptor columns,
read off the pool, zero call sites, and a partial answer is worse than
none), `GetRaidTargets` (the reference keeps a board; this client feeds
the mark per unit, and a scan over named tokens would silently omit a
marked unit), `DisenchantAll` (a server-paced sequencer, which is the
queue), `GetSpellPower`, `StartItemExport` (writes a file — it belongs
with the withheld seven).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-24 10:21:31 -06:00
gromandClaude Opus 5 f06caa9968 ui+app+formats: the Spell.dbc row, and seven columns that answered nil
`GetSpellRecField` knew three names. Counting what the corpus actually
asks it for: `name` (4 sites), `school` (3), `stackAmount` (2),
`spellIconID` (2), then one each of `rank`, `rangeIndex`, `mechanic`,
`manaCost`, `effectMechanic`, `effectApplyAuraName`. Seven of those ten
answered nil, and each was a feature quietly off rather than an error —
DoiteAuras caps its stack display on `stackAmount`
(DoitePlayerAuras.lua:539) and SuperCleveRoidMacros classifies a
crowd-control effect off the two arrays (Utility.lua:7776-7794).

The eighty field names are the DLL's own, read off its pool in Spell.dbc
column order. The mapping to `SpellDisplay` lives in
`ui_script/spell_rec.rs`, beside the struct it reads — a copy on the VM
side would be free to drift — and `GetSpellRec(spellId)` is that same
field list walked through that same per-field reader, so the two verbs
cannot answer differently for one name. A test asserts exactly that,
key by key, over one row.

The array columns are a 1-based table of three, which is measured, not
guessed: SuperCleveRoidMacros walks `effectMechanic` as
`for i = 1, 3 do local em = effectMechanics[i]`. `totem`, `reagent` and
`reagentCount` take the same shape, two and eight long.

One new DBC column — `StackAmount`, 39, bracketed by two constants this
tree already had (38 ModalNextSpell, 40 Totem[0]). It is the only one
added: fifteen further names upstream answers are left nil rather than
putting fifteen unused u32s on a struct the spell catalog holds one of
per row. They are listed at `FIELDS` with that reason, so the gap is a
decision rather than an omission.

`GetSpellRecField`'s three string columns keep the spellbook fallback
the verb had before the probe, so a VM with a book and no catalog — a
capture, or the boot VM before the catalog loads — still answers a
learnt spell's name and rank. Every other column is a DBC row and has
nowhere else to come from, which is why the fallback is exactly three
names wide and says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-24 09:28:05 -06:00
gromandClaude Opus 5 cf0e157f34 ui+app: the bag walk, the modifier fold, and two shapes the DLL's pool corrected
Built: `GetBagItems`, `GetSpellModifiers`, `GetSpellIdCooldown` — plus
fixes to two verbs that already existed and had the wrong *shape*,
found the same way `GetItemIconTexture`'s wrong argument was: by
comparing what we answer against nampower's own string pool.

`GetBagItems()` hands back every occupied bag slot as
`{ [bagIndex] = { [slot] = row } }`. DoiteAuras asks seven times over
and walks both levels with `pairs`, reading `itemId` and `stackCount` —
it is how that addon answers "do I have this item" without 100
`GetContainerItemInfo` calls per condition evaluation. Empty slots and
empty bags are absent, because a nil value in a Lua table *is* absence
and `pairs` is how both levels are read.

`GetSpellModifiers(spellId, modifierType)` answers
`flat, percent, hasModifier`, where percent is the final multiplier (90
means 90% of the original) — the consumer says so in its own words and
multiplies by `percent / 100`. The 26 modifier types come from the
DLL's usage string. The fold is `spell/mods.rs`' existing reader,
reached through a new `spell_modifier_probe` the lifecycle reinstalls
whenever the wire moves the tables: a VM-side reimplementation of a
four-gate 64-bit fold would be free to drift from the mana cost the
action bar greys a button on. The third return is the point — the
consumer tests it before touching either number, and the duration path
multiplies, so `0, 0` for an unmodified spell would zero every debuff
timer that asks.

`GetSpellIdCooldown(spellId)` answers the twenty-key table, from a new
`Cooldowns::legs`: `info`'s three legs kept apart, same walk and same
arms, so the folded number a swirl draws and the legs an addon reads
cannot describe different timers.

**`GetBagItem`'s row had the wrong key names** — `bag`, `slot`,
`count`, `quality`, `link`, `locked`, this client's own
`GetContainerItemInfo` spellings. nampower's row is contiguous at
`0xa8a68` and is the block `GetEquippedItem` already used, so a caller
reading `.stackCount` got nil and no error. One `bag_slot_table` now
serves both, with three columns absent rather than zeroed: this client
resolves a bag slot's enchants to names at the app boundary and keeps
no random-properties id for a container slot, and a zeroed enchant id
would be a claim.

**`GetItemIdCooldown` returned the wrong kind of value** — the stock
`start, duration, enable` triple. At the version we report it is a
table, and the addon layer settles it: `API.GetItemCooldownInfo` and
`API.GetSpellCooldownInfo` return these calls' results *directly* and
their callers index them. `cd.gcdCategoryRemainingMs` is the primary
path of `API.GetGCDRemainingMs`, which the `[gcd]`/`[nogcd]` macro
conditionals divide by 1000 — against a triple that is
`(0.0).gcdCategoryRemainingMs`, which raises. Latent rather than live
(nothing calls those wrappers today), still wrong.

Two things the schedule gates caught and improved:

- The probe refresher started as an exclusive system, which is
  ambiguous against everything in Update it is not ordered against:
  +274 pairs over the ceiling. `NonSendMut` is all it needs.
- Its memo started as a bare `Local`, which `session.rs`' own gate
  refuses — a memo about what this VM has been told must die with the
  VM. As a `VmMemo` it now reinstalls on the first frame after a
  `/reload`, which is better than the reasoning I had written for the
  `Local`: a fresh VM has no closure at all until something installs
  one.

One count the earlier sweep got wrong, corrected in the doc:
`GetSpellPower`'s "7 call sites" are BetterCharacterStats' own method
`BCS:GetSpellPower(school)`, not nampower's global. Nothing in the
corpus calls the global at all. A bare name match is not a call site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-24 09:00:17 -06:00
gromandClaude Opus 5 ecee994ab6 app+ui: the player's own cast state, and SPELL_CAST_EVENT off the commit
`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
2026-10-24 07:16:30 -06:00
gromandClaude Opus 5 f120e8003d ui: six of nampower's reads, and the item-icon probe behind two of them
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
2026-10-23 21:06:41 -06:00
gromandClaude Opus 5 ce8992bc3e Draw flight masters: C_TaxiMap.GetTaxiNodesForMap over TaxiNodes.dbc
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
2026-10-22 07:42:22 -06:00
gromandClaude Opus 5 754d577eaf ui: a trade-skill list mutator signals inside its own call
`ExpandTradeSkillSubClass`, `CollapseTradeSkillSubClass`,
`SetTradeSkillSubClassFilter` and `SetTradeSkillInvSlotFilter` fire
`TRADE_SKILL_UPDATE` from inside the call now, as the one helper all four
tail-call in the reference does (`0x4fd710`, `0x4fd730`, `0x4fd750`;
`reference/1.12-verb-events.tsv`). It used to be a `trade_skill_touched` flag
drained by `ui_tradeskill::drain_trade_skill` and answered the following frame.

Inline is load-bearing. These four verbs are called *from* a
`TRADE_SKILL_UPDATE` handler: a profession library reads the recipe list by
unfolding every header, walking it and folding back what was folded. The signal
is therefore re-entrant by construction, and every addon that does it carries
its own guard — LibCraftingProfessions has `trade_skill_scan_in_progress`, ATSW
an `atsw_scans < 2` counter. Those guards are written against an inline signal
and only an inline signal lands inside the window they cover.

Deferred to the next frame it landed after the guard had cleared: the library
rescanned, the rescan called the verb, the verb re-armed the flag, and the two
kept each other alive at one fire a frame for as long as the window was open.
grom's 2026-10-21 session shows it directly — `[ui-events]` reporting
`TRADE_SKILL_UPDATE · 1.01 fires/f · ~900 call(s)` across 37 consecutive
windows, three unbroken minutes, at 0.58-0.84 ms a frame in handlers over four
addons rebuilding their lists for nothing. It is the 118-second and 61-second
stretches at 55-70 fps in a session whose in-play median was 86.

Two tests: each mutator's fire is read with its own `run` still on the stack, so
a deferred signal fails; and a library-shaped in-progress guard turns one real
update into exactly one scan, with nothing owed to a later tick.

The bench's `[events]` block also fires the five non-quest events the live
session named. They cost ~0.2-0.5 ms there against 12-72 ms live, because this
harness has no NPC in front of it, no trainer list and no open map — recorded as
a gap, not an answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-21 14:47:57 -06:00
gromandClaude Opus 5 0c862838da ui: a SetScale names its subtree instead of giving up on the graph
`[ui-phases]`'s new write-site tally answered the combat-fps question on its
first armed session: 463 whole-graph layout derivations across 34 windows, and
**232 of them** at `object/frame_state.rs:427` — the `touch_layout()` inside
`SetScale`. Splitting the per-window count by site and regressing against
resolve time over 43 in-world windows, resolve correlates with `SetScale`
derivations at r = +0.996 and with every other derivation at r = -0.041. The
four costliest windows of the run are 36-64 `SetScale` derivations each and
almost nothing else.

That also corrects the flat "~15.5 ms per derivation" of the round before: a
mint-triggered derivation costs ~1.1 ms, a `SetScale`-triggered one 12-13 ms.
The difference is that `SetScale` gave up *two* ledgers, not one.

The caller is pfUI's action-button press animation
(`pfUI/modules/actionbar.lua:182`, `:207`, `:233`, `:259`): four styles that all
run `this:SetScale(this:GetScale() + fade)` on `OnUpdate` for the ~60 frames a
button's fade lasts. Pressing an ability starts it, which is why a settled
interface never reproduced this and a fight always did.

Both give-ups had precise versions already, written for `SetParent`:

- `touch_layout()` -> `touch_layout_subtree_scale(h)`. A scale write moves the
  subtree's `effective_scale` and nothing else: anchor targets are ids fixed at
  `SetPoint`, so no edge moves and the roster is unchanged.
- `touch_measure_all()` -> the same walk now names the subtree's regions on the
  measure ledger. This was the bigger half. `measure_dirty = None` makes the
  next `fontstrings_needing_measure` walk all ~43,000 regions of a real
  interface to issue, typically, zero requests; `stale_measure_key` reads only a
  region's own data plus *its owner frame's* `effective_scale`, so the subtree's
  regions are exactly the ones that can go stale.

Measured on the player's own 92-addon interface, 43,195 regions, one `SetScale`
per frame for 240 frames (`tick_bench`'s new `[scale]` row):

    before   changing  resolve 0.601 ms/f · 0.996 derivations/f (239/240)
    after    changing  resolve 0.005 ms/f · 0.000 derivations/f (  0/240)
    control  same      resolve 0.005 ms/f · 0.000 derivations/f (  0/240)

`changing` is now indistinguishable from a control that writes the value it
already holds. The harness understates the live cost (its measure answers are a
6px-per-char stub), so the live saving is to be confirmed in a play-test, not
predicted from that row.

Correctness is left to `WOW_LAYOUT_VERIFY`, which `cfg!(test)` forces on for all
1975 benilla-ui tests: it re-derives the whole graph after every `resolve()` and
panics on any rect the incremental pass got wrong, and runs the full measure
roster beside the ledger drain and panics on any stale region the ledger missed.
So the two new tests assert only that the cheap path was taken, on a graph three
frames deep with a FontString on the leaf. The real-interface verify run fails
identically to its documented pre-existing red (`frames 13379 -> 13379, regions
29597 -> 29645`), so this change adds nothing to it.

Also repairs an assert message in `layout_gate.rs` whose line continuations had
been eaten by a heredoc, leaving runs of spaces mid-sentence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-20 20:57:15 -06:00
gromandClaude Opus 5 c1a56f98ab layout: name the write that asks for a whole-graph derivation
`[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
2026-10-20 19:17:44 -06:00
gromandClaude Opus 5 497570a68c addons: an on-demand load says what it loaded
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
2026-10-19 10:54:56 -06:00
gromandClaude Opus 5 0807144f38 modelframe: a unit pane holds the unit it was given
`SetUnit` resolved nothing: it stored the token and the extract re-read it
every paint. unitscan's alert is the live consumer — it `SetUnit'target'`s the
rare it just found by name, and the addon restores the player's own target on
the very next line — so the card showed the rare for one frame and then went
empty. It only looked right while our by-name targeting was leaving the target
on the rare, which is the accident the previous commit removed.

The reference resolves the token in `SetUnit`'s own rebuild worker (`0x505b50`,
the one `RefreshUnit` also runs) and nothing re-resolves it per paint. Three
things say so independently: FrameXML re-issues `SetUnit` from
`PaperDollFrame_OnShow` and on `UNIT_MODEL_CHANGED`, which would be pointless
against a live token; `RefreshUnit` exists at all; and pfUI detects a portrait
change with *no event* by pinning a scratch pane and comparing `GetModel`
against the shown one (`api/unitframes.lua:2069`), a test that says nothing
unless a pane holds what it was given.

`ModelState::unit_guid` is that pin, read through one helper
(`Model::unit_pane_subject`) so the paint gate and the extract cannot disagree
about whether a pane has a subject. A pane seated *before* its unit streamed in
keeps re-reading its token — ours arrives asynchronously where the reference's
is already in the object manager — and `RefreshUnit` re-pins, as its worker
does.

`modelframe.rs`'s own extract test passed only because its `SetUnit` ran with no
target; its comment claimed the general rule, so it now states the real one and
pins both halves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-17 13:41:35 -06:00
gromandClaude Opus 5 bbe2e3aaa9 target: TargetByName selects inside the call, as the reference's does
unitscan probes for a watched rare by targeting its name, reading the name back,
and calling TargetLastTarget to put the old target back — but only
`if prevTarget ~= foundTarget` (unitscan.lua:112-130). Two things had to be true
inside that one Lua frame and neither was, so the restore never fired and every
scan kept your target. TargetLastTarget itself was fine and was never even
reached.

**The read after the call.** `TargetByName 0x489d60` runs the resolver and
selects before it returns, so the next line sees the pick. Ours queued a request
the app drained later in the frame, and the token snapshots had been pushed
*before* Lua ran — so the probing frame saw no change at all, and by the frame
that did see the mob, `prevTarget` had already been re-read as the mob.

The app now keeps each query a script has asked for resolved a frame ahead
(`by_name::feed_named_targets`, one candidate walk for the whole asked set, and
nothing walked at all when no script uses the verb). With the unit already in
hand the binding answers `"target"` for the rest of the Lua frame and queues the
selection *by GUID*, so the optimistic answer and the committed selection cannot
name different units. The override is dropped at the top of the next tick. A
query asked for the very first time keeps the old one-frame-late path, and that
ask is what starts it being resolved.

**The order of two selection verbs in one frame.** `TargetLastTarget`'s memory
was sampled once a frame, so mid-frame it still named whatever preceded the
*previous* selection: the restore would have landed on the wrong unit even with a
synchronous read. The reference stamps inside the selection itself, so its memory
is always the thing being left behind; `selection_requests` now calls
`LastTarget::note` per request, and the per-frame sampler stays as the catch-all
for selection changes that are not requests (a stream-out, a death clear, a
click-off). The two compose: the test walks the whole probe-and-restore sequence
and checks both the mid-frame pick and the memory the frame ends with.

Fixes the `/target X` then `UnitName("target")` macro idiom generally, not just
unitscan — SuperCleveRoidMacros is built on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-17 08:40:59 -06:00
gromandClaude Opus 5 751cac062b compat: C_Spell.GetSpellEffectMechanics, the per-effect sibling
`SuperCleveRoidMacros/ClassicAPI.lua:267` calls it unguarded, so its absence was a raise a
session. It is not a second spelling of `GetSpellMechanicByID`, which this tree already answers:
that one reads `Spell.dbc`'s spell-level `Mechanic`, and vanilla stores some mechanics on an
*effect* instead. The consumer's own comment names the case — Rake's bleed is effect-level, so
the spell-level column says 0 while this answers `{0, 15, 0}` — and a macro conditional asking
whether its target is bleeding gets the wrong answer from the spell-level field alone.

`SpellDisplay::effect_mechanic` was already parsed for the cast-immunity scan, so this is a field
on the probe payload and a reader, not a new lookup. The three-way answer is its sibling's, for
the sibling's reason: nothing at all for an id no spell owns, three zeros for a real spell that
carries no mechanic anywhere, the ids otherwise — collapsing those two would reclassify crowd
control as ordinary damage, which is the hand-kept table this function exists to replace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-16 18:45:25 -06:00
gromandClaude Opus 5 81d96af35d Everwood: the HD/OctoWoW line of benilla, imported onto upstream
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
2026-10-15 15:01:14 -06:00
Sam SungandClaude Opus 5.5 5c7eb5fc75 Stock glue screens draw their 3-D scenes through the glue booth (#1039)
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 #856

Fixes #978

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:13:47 +07:00
Sam SungandClaude Opus 5.5 eb63c8861b Cursor runs on ClearCursor and the setters' own event tails (#1038)
The cursor now runs on the client's two primitives. `ClearCursor(a,b)` (`0x495190`) cancels an armed gift wrap, then runs the payload's arm (an item's unlock fires `ITEM_LOCK_CHANGED`; coins fire `PLAYER_MONEY`), then the grid hide and `CURSOR_UPDATE`. Each setter runs that clear first, then its own tail (`0x494b60`, `0x4950f0`, `0x494d20`, `0x494f80`, `0x494e20`, `0x495010`). Every pickup, place, split, sale, trade, mail and auction path goes through them. So a `CURSOR_UPDATE` handler reads the emptied cursor, and `PickupSpell`/`PickupMacro`/`PickupPetAction`/`SplitContainerItem` clear what is held instead of refusing.

An item attached to mail or an auction stays locked until it leaves the slot (`0x4adef0`). Attaching to an occupied slot puts the old item back on the cursor.

Event-order tests cover each setter, the coin clear, and the attach lock and swap.

Fixes #1011
Fixes #1012
Fixes #1006
Fixes #1007
Part of #156

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:47:20 +07:00
Sam SungandClaude Opus 5.5 781bf36ab3 Key input follows the client's walks; stock login takes typing (#1027)
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 #179
Fixes #1003
Fixes #1004
Fixes #1005
Fixes #1019

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:07:34 +07:00
Sam SungandClaude Opus 5.5 aba120cdcb Lua verbs fire their events before returning, in the reference's order (#1015)
In 1.12.1 an event a Lua verb raises runs its handlers before the verb returns: `SignalEvent` (`0x703e50`) walks the listeners in place. About 35 benilla binding sites queued theirs for a later drain instead.

- Verbs now raise inside `tick::verb` steps, and the events fire as each step's model borrow drops. A step follows the reference's split wherever it signals before changing state a getter reads: the money pickup, bag pickup and place, merchant grab, repair cursor, item use, action and pet-bar pickup and drop.
- The grid events fire before `CURSOR_UPDATE`, and a clear's unlock before both, as at `0x4952b3`/`0x495455`. `ITEM_LOCK_CHANGED` carries no arguments.
- `pending_events` is host-only now.

Tests pin handler order per family, nesting from a handler, and the order changes.

Fixes #999
Fixes #582

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:14:10 +07:00
Sam SungandClaude Opus 5.5 0f505a2ebf RegisterEvent ignores case; a frame's first resize precedes OnLoad (#1001)
- `RegisterEvent` and `UnregisterEvent` match a name against the client's event table case-insensitively and drop a name that isn't in it (`0x702140`). A new gate checks that every event name the code fires is in the table.
- A visible XML frame resolves its layout at the post-load hook (`0x76a310` → `0x7680e0(1)`). So its first `OnSizeChanged` fires before its `<Frames>`, `OnLoad` and `OnShow`, as in 1.12.1. A hidden frame still waits for its show.
- `pending_events`' doc now says what the queue is: `SignalEvent` (`0x703e50`) runs handlers in place, and the Lua verbs that still queue are a gap.
- #890 was already fixed by #907; two stale comments around it are corrected.

Fixes #959
Fixes #972
Fixes #560
Closes #890

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:08:07 +07:00
Sam SungandClaude Opus 5.5 7158c58575 Hidden frames defer their layout; GetWidth answers the set size (#973)
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 #948
Fixes #971

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:06:04 +07:00
Sam SungandClaude Opus 5.5 45b7914c9e The stock login screen logs in; realmd 0x0E is a success (#970)
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>
2026-10-12 10:28:08 +07:00
Sam SungandClaude Opus 5.5 51a2ce5586 Layout writes invalidate as 1.12.1 does; only a reached node re-resolves (#964)
The solver took any node's recomputed rect whenever it resolved, because about 55 layout writes named a node without invalidating it. So a freed region stayed in its dependents' anchors, `SetTexture` re-resolved a failed texture, and a scroll child kept its authored anchors behind a per-solve override.

In 1.12.1 a node re-resolves only when invalidated or a target stores a new rect (`0x768d20`, slot 0 `0x768cf0`). `SetTexture` only dirties the draw (`0x770200`). A destructor frees its dependents' anchors to it (`0x768e20` → `0x767e70`, no invalidate). A scroll child is re-anchored to one TOPLEFT (`0x787100`).

Every write now invalidates exactly where the reference's does, and the solver's guess, the hold and the art latch are gone. The tooltip invalidates only on its relayout (`0x530640`).

Tests bite on the parent; the verifier is green; the stock UI's quads are byte-identical.

Fixes #947
Fixes #949
Fixes #958

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:46:24 +07:00
Sam SungandClaude Opus 5.5 a9dd325bb7 A glue Lua state with the reference glue surface, events and ModelFFX (#963)
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>
2026-10-12 07:38:02 +07:00
Sam SungandClaude Opus 5.5 985dabff44 Layout-mode drag, title press takes the slot, links are their own button (#961)
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 #951
Fixes #952
Fixes #957

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:22:54 +07:00
Sam SungandClaude Opus 5.5 5af22799d0 A drag pins one TOPLEFT anchor at the frame rect, as in 1.12.1 (#953)
A drag kept the frame's authored anchors: `GetPoint()` answered them afterwards, the frame kept following its old target, and a resize grip on a CENTER- or TOPLEFT-anchored frame moved the wrong edges.

In 1.12.1 every drag enters at `0x7652b0`, whose first act pins the frame (`0x768430`): `ClearAllPoints`, one TOPLEFT on the screen root's TOPLEFT at the current rect, and an explicit size. The pump `0x768710` moves that anchor (case 4) or, for each of the eight grips, computes the clamped size and re-pins shifted by the left and top travel only (`0x76898a`). A frame with no rect starts no drag (`0x7652cd`).

Drags now pin and pump that way, with one drag slot; `StartSizing` defaults to BOTTOMRIGHT (`0x776913`).

Tests: the pin and `GetPoint` after a drag, the eight grips on a CENTER-anchored frame, the clamps, the layout cache round trip, and stock chat-window resize and off-screen drop.

Fixes #173

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:57:59 +07:00
Sam SungandClaude Opus 5.5 aff59fd55b ClearAllPoints keeps the rect; a failed resolve reaches nothing (#950)
`ClearAllPoints` invalidated the node, so an anchorless frame lost its rect at once, with everything anchored to it, and any region whose resolve failed stopped drawing.

In 1.12.1 `ClearAllPoints` (`0x7a2b70` → `0x767620`) only frees the anchors, queuing no resolve, so the rect stands. Only the node's own invalidate re-resolves it; a failure (`0x768d20` at `0x768d55`) clears the valid bit and skips slot 0, so its dependents keep their rects. A Texture keeps drawing its last vertices (`0x7706e0` checks only its texture); a FontString stops (`0x773080`, `0x7730b7`). A new string, font or spacing forces a store (`0x771ea4`).

Each node now carries an entry state, and takes a recomputed result only when invalidated or a target stored a new rect; a failed Texture's last quad is kept, and the full derive and the incremental pass read the same state.

Tests bite on the parent; the verifier is green.

Fixes #942
Fixes #943

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:36:21 +07:00
Sam SungandClaude Opus 5.5 87bf5904ff A region keeps its own name; only the name lookup is first-come (#944)
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>
2026-10-12 04:17:03 +07:00
Sam SungandClaude Opus 5.5 3bfec89085 A template name registers once, case-folded, as in 1.12.1 (#937)
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>
2026-10-12 03:36:57 +07:00
Sam SungandClaude Opus 5.5 6fff0fa7a1 Hover leaves publish the world mouseover loss before the next OnEnter (#935)
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>
2026-10-12 03:28:42 +07:00
Sam SungandClaude Opus 5.5 56f7fa003a A resolve keeps a rect that moved under 1.12.1's 1e-5 threshold (#929)
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>
2026-10-12 02:26:23 +07:00
Sam SungandClaude Opus 5.5 eec1804d37 Chat bubbles are WorldFrame child frames, as in 1.12.1 (#925)
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>
2026-10-12 02:06:36 +07:00
Sam SungandClaude Opus 5.5 ff3d15b132 The tick drains the layout around the walk; size gates in layout units (#922)
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 #912
Fixes #741
Fixes #914
Fixes #911

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 01:46:42 +07:00
Sam SungandClaude Opus 5.5 fa5568dcdc Creating or freeing a frame or region re-derives only what it touches (#915)
Any write that changed the layout graph's roster (a frame or region created, freed, re-owned or re-pointed) made the next resolve derive the whole graph: about 1.35 ms for one `CreateTexture` or `CreateFrame` on the stock UI, against 0.05 ms for a no-op.

A write now names the nodes it touched, and the resolve re-derives just those in the cached graph: a joining node is added with its edges, a leaving one is removed and its dependents seeded, a staying one has its edges and inputs re-synced. The full derivation stays the fallback. A region's owner and an art change are now layout inputs that name their region. Verify now also compares the graph itself.

A randomized test checks rects and graph against a scratch solve after every batch of creates, frees, re-owns and re-anchors. `CreateTexture` now costs 0.14 ms, `CreateFrame` 0.15 ms, a backdrop rebuild 0.22 ms.

Fixes #903

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 01:05:23 +07:00
Sam SungandClaude Opus 5.5 4e0011bdd7 Backdrop pieces are nine Texture regions on the frame, as in 1.12.1 (#905)
A backdrop was data drawn in the frame's own slot, so `GetRegions` and `GetNumRegions` never saw its pieces, and addons that skin by walking a frame's regions missed them.

In the reference, installing a backdrop creates nine Texture regions on the frame: the background on BACKGROUND, then eight border pieces on BORDER, linked into its region list in that order. A resize re-runs their texcoords and colours, and the colour setters tint the background or the border alone.

The pieces are now real regions, drawn through the region path, so edges draw over another frame's BACKGROUND at the same level. `<Layers>` and `<Backdrop>` load in document order. A region leaving its parent empties any Button, CheckButton, StatusBar, Slider or ColorSelect slot holding it, and a dead layout node now seeds only its dependents.

Tests pin the order, colours, resize, teardown and stock quads.

Fixes #883

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:10:33 +07:00
Sam SungandClaude Sonnet 5.5 a1130c693a Mouse disable drops the press; a chorded press goes to the held frame (#899)
`EnableMouse(false)` only flipped the bit, so a frame disabled while pressed still got OnMouseUp and OnDragStart, and a hovered one kept its hover. A second mouse button pressed while one was held fired on the frame under the cursor rather than the held one.

In the reference, a mouse disable on a visible frame runs the same removal a hide does: the capture and the press drop, the hover ends with OnLeave at once. The down handler takes the held capture first and fires there, and a Button clicks on release only while PUSHED and with the cursor inside its own rect.

`EnableMouse` now runs that shared removal, the drag arm included. Presses go to the capture when one is held, and the Button release tests pushed and inside rather than topmost.

Tests cover a disable while pressed or hovered, chorded presses and the release gate.

Fixes #886
Fixes #887

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 22:37:01 +07:00
Sam SungandClaude Sonnet 5.5 481cc8870e A hide drops the press and capture; only a mouse frame's show re-picks (#888)
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 #844
Fixes #845

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 21:50:57 +07:00
Sam SungandClaude Opus 5.5 16affa8d45 OnUpdate walks strata, level and link order, as 1.12.1 does (#868)
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>
2026-10-11 19:46:15 +07:00
Sam SungandClaude Opus 5.5 c9ddd80342 Script errors reach the error handler bare, as 1.12.1 hands them (#865)
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 #697
Fixes #859

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:12:21 +07:00
Sam SungandClaude Opus 5.5 dc63a06178 The UI scale is UIParent's own scale over a 768-unit root (#841)
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>
2026-10-09 13:17:23 +07:00
Sam SungandClaude Opus 5.5 f0dfa86148 PLAYER_LOGIN fires at world entry, once the player exists (#840)
`PLAYER_LOGIN` fired at the end of the UI load, before the player existed: its handlers saw `UnitExists("player")` nil and level, health and XP 0, at every login and `/reload`. Ace2's `OnEnable` runs there.

In 1.12.1 the load only arms a one-shot (`0x49011d`). The world-enter cascade fires `PLAYER_LOGIN` if armed (`0x49094b`), then `PLAYER_ENTERING_WORLD` (`0x49096a`), inside a sound-suppression bracket (`0x4908d5`–`0x490a56`). It runs from the player's own create (`0x5deb60`), or on `/reload` from the load (`0x490168`) with the player still in the world. A worldport re-arms nothing.

Now the load arms it, and the unit feed fires both, silenced, once the player is seated. A `/reload` loads under the live player and no longer plays the target-select sound. Tests cover login, worldport and reload; they fail without the fix.

Fixes #189
Fixes #191

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:38:06 +07:00
Sam SungandClaude Opus 5.5 e9f0e89a0b EditBox events fire from each box's flush, as 1.12.1 orders them (#734)
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 #724
Fixes #727

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 07:38:38 +07:00
Sam SungandClaude Opus 5.5 010e41e600 unit getters: chained tokens like raid3target answer their unit (#716)
The `Unit*` getters and `SpellCanTargetUnit` answered nil for chained tokens like `raid3target` or `party1target`, so a raid-assist addon's main-tank frame stayed empty.

Every getter and `SpellCanTargetUnit` resolves its token through `0x515970` and then reads the guid's object (`0x468460`); see `UnitHealth 0x517511`/`0x51752d` and `SpellCanTargetUnit 0x6e6d3a`.

A token with target hops now resolves to a guid in the VM and reads a per-guid snapshot. A new feed pushes one for each held unit a chain ends on. `SpellCanTargetUnit` keeps its verdicts by guid. The three target, target-of-target and NPC snapshot builders are folded into one.

Tests: `UnitName`/`UnitHealth("raid3target")` and `UnitExists("pettarget")` answer the chained unit, `SpellCanTargetUnit("party1target")` answers its verdict, and plain tokens are unchanged.

Fixes #693
Fixes #694

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 13:19:48 +07:00
Sam SungandClaude Opus 5.5 0a920b52fe unit tokens: party1target and other target chains resolve (#706)
`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>
2026-09-30 10:20:16 +07:00