A second profession's opener updated the book instead of reopening it

grom: "nothing happens when I click another tab." Advanced Trade Skill
Window's side tabs cast the other profession's opener and rebuild on
TRADE_SKILL_SHOW alone — its update arm is `UpdateCount = UpdateCount + 1`
and nothing else — so the book swapped underneath it and nothing redrew.

The open is `0x4fc910` and it carries three rules we had one of:

* one event per *open*, not per change: a changed line while a book is up
  is a fresh TRADE_SKILL_SHOW (`0x4fc99f`), never an update. The update is
  a change *within* one open book — a filter, a fold, an item count.
* the opener is the window's toggle (`0x4fc918`): casting the profession
  already open closes it, which is what ATSW2's `this.Name ~= <current>`
  guard exists to avoid tripping.
* an empty rebuilt list signals nothing (`0x4fc997`, the count at
  `0xbde04c`): the line is open and no window appears. Written over the
  list about to be pushed rather than as the literal count read, because
  ours can be a frame late where the reference builds its list inside the
  open call — a book that arrives empty and fills next frame still fires
  exactly one show, on the frame it fills.

`0x4f6000` is the Craft window's same function, keyed on the craft type at
`0xbdcfb8` and with no empty guard; both of its rules land in `feed_craft`
and `open_trade_skill` too, since CRAFT_SHOW is the arm ATSW2 takes for
Enchanting. The filter reset on a line change was already right —
`set_trade_skill` keys it on `trade_skill_last_line`, the `0xbde064` above.

Verified end to end offline with $WOW_CAPTURE_SPELLS seating a real book:
Blacksmithing open with 24 recipes, `ATSWFrameTab3:Click()`, and the window
comes back title=Cooking line=Cooking rows=9 with the check moved to tab 3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-05 16:28:29 -07:00
co-authored by Claude Opus 5
parent a033f13f0f
commit 6bcf937986
3 changed files with 328 additions and 13 deletions
+139
View File
@@ -4360,3 +4360,142 @@ His own session agrees: 11 `frame hitch:` lines in an hour, every one of them at
largest being the loading screens, and **zero** live pipeline compiles. So the thing to pin down
next is which "laggy" it is — a sustained cost, or motion trailing the input, which would be the
zoom ease's 0.29 s half-life rather than a frame cost.
## 2026-11-05, second session: every `<OnLoad>` in the interface read text widths as 0
grom, with ATSW2 finally loading: *"okay atsw2 is loading now, but barely functioning."* Its sort
row was the visible part — `Sort by: ◉ Catego⟨Difficulty⟩ry ◯ Name` — and chasing that one
overlapping label landed on something that had nothing to do with the addon.
`seat_text_measurer`'s own comment says why it exists: *"so `GetStringWidth` right after `SetText`
measures … seated before the manifest loads, **for every `<OnLoad>`**"*. It was not being seated.
`UiFontAtlas` is inserted by an `Update` system through `Commands`, so it appears at the next
command flush — and `load_ingame_ui_on_world_entry` is an exclusive system on the `InWorld` state
transition, which comes **first**. `seat_raster_seam_for_load` took its `let Some(atlas) = … else
{ return }`, and the VM loaded the whole interface with no measurer behind
`measure::ensure_measured`.
So for all of FrameXML and every addon, `FontString:GetWidth()`, `GetHeight()` and
`GetStringWidth()` answered **0** for a string that plainly had text. The reference measures inline
and always knows (`0x79e510` → `0x772890`). ATSW2's line 1642 is the case that showed:
```lua
ATSWNameSortCheckbox:SetPoint('LEFT', ATSWDifficultySortCheckbox, 'RIGHT',
ATSWDifficultySortCheckboxText2:GetWidth()+5, 0)
```
which stored **5.0** instead of 53.8, and drew "Difficulty" across "Category".
**Why it never looks like this.** Nothing errors, nothing logs, and by the time anything asks, the
widths are all correct — the measure is taken on the next read, after the load. The only reads that
are wrong are the ones taken *during* an `<OnLoad>`, and what they leave behind is a stored
geometry nobody recomputes. The three probes that named it, in order, each cheap:
1. Is it the geometry or the drawing? `GetPoint()` on the misplaced frame — a stored offset of 5
where the label measures 48.8 says the arithmetic ran on a zero.
2. Re-run the addon's own `<OnLoad>` from a capture eval (they are globals). Same code, same frame,
right answer ⇒ the difference is *when*, not *what*.
3. One `eprintln!` in `region::measured_wh` when a FontString with text answers 0, printing
`model.measurer.is_some()`. It printed `measurer=false`.
`ensure_font_atlas(&mut World)` is now `init`'s body for a caller holding the world, and a world
that still cannot build one warns instead of loading the interface in silence. The check that it
stays fixed is a walk of `UIParent` in a live capture counting FontStrings that have text and
measure 0: dozens before, **0** after.
The class to watch: **a resource another system inserts through `Commands` is not there for an
exclusive system running on a state transition.** Anything the UI load needs must be built by the
load, not awaited.
### The map's glide is a setting, because two reports bracketed one guess
The same constant was reported from both sides — "slippery" on the 4th, *"its the maps motion that
is laggy"* on the 5th — which is the signal to stop guessing and hand it over. `mapMotionSmoothing`
(Options → Interface, 0–100 in tens, 0 reads "Off") scales the one half-life both the pan and the
zoom ease on, `7 / (pct/100 × 0.30)`. Measured frames for one wheel notch to settle at 60 fps:
| 0% | 10% | **25% (ships)** | 50% | 100% |
|---|---|---|---|---|
| 1 | 17 | **42 (0.70 s)** | 84 | 167 |
What he had played was 167. The top of the slider is still that, deliberately — it is the slowest
anyone could want — and the default is a quarter of it. One rate drives both axes because a zoom
that settles while the pan is still drifting reads as the pan being broken. A drag still writes the
scroll directly and never eases.
### `$WOW_CAPTURE_SPELLS`: seat a real spellbook in a capture
A capture never logs in, so its spellbook is whatever a fixture hardcodes — and a whole class of
addon is driven by what is *in* that book. ATSW2 finds the player's professions by walking tab 1
and matching each spell's icon against its own thirteen-leaf `Professions` list, so with no
profession spell seated it finds nothing, draws no tab and switches nothing, which is
indistinguishable from the defect being chased. `$WOW_CAPTURE_SPELLS=<id>[,<id>…]` adds spells to
whatever book the fixture seats, rather than replacing it. The realm's own ids are in the comment
beside it: Survival 46050, Jewelcrafting 30220, and 1.12's Alchemy 2259, Blacksmithing 2018,
Cooking 2550, Enchanting 7411, Engineering 4036, First Aid 3273, Leatherworking 2108, Tailoring
3908, Smelting 2656. Seat a handful of **recipe** ids with the opener and the window comes up with
a real list, which is the only way to photograph one of these addons offline.
## A second profession's opener updated the book instead of reopening it
grom: *"nothing happens when I click another tab."* Advanced Trade Skill Window's side tabs are the
whole reason it exists, and their `<OnClick>` is two lines:
```lua
if this.Name ~= ATSW_Profession[ATSW_realm][ATSW_player] then
CastSpellByName(this.Name)
```
Everything up to the cast was right — the tab scan builds five correct tabs with their names and
icons, the saved profession key resolves, the opener resolves through the book, and
`send_spell_cast` intercepts it as a profession opener (`0x6e4bce`) and sets the open skill line.
What was wrong was the **event**. `feed_trade_skill` fired one event per *change*: a first open was
`TRADE_SKILL_SHOW` and everything after it `TRADE_SKILL_UPDATE`. ATSW2's update arm is
`UpdateCount = UpdateCount + 1` and nothing else; it rebuilds on `TRADE_SKILL_SHOW` alone, and its
show arm is explicitly written for the window **already being up**:
```lua
elseif event == 'TRADE_SKILL_SHOW' then
if UIParent:IsVisible() then
if ATSWFrame:IsVisible() or ATSWCSFrame:IsVisible() then … CloseCraft() … end
```
so the book swapped underneath it and nothing redrew.
The open is `0x4fc910`, and it reads cleanly:
```
4fc913 call 0x4fc890 ; drop the old list
4fc918 cmp esi, [0xbde040] ; the line already open
4fc91e jne 0x4fc935
4fc92a mov ecx, 0x13b ; → TRADE_SKILL_CLOSE, and return
4fc94c cmp [0xbde064], esi ; the last line shown
4fc95a or eax,-1 … [0x84dd60/64/68] ; → different: reset the three filters
4fc985 call 0x4fca20 ; rebuild
4fc997 jbe 0x4fc9a4 ; the rebuilt count at [0xbde04c] is 0 → no event
4fc999 mov ecx, 0x139 ; → TRADE_SKILL_SHOW
```
Three rules, all of them ours now, in `book_event`:
- **one event per open, not per change** — a changed line while a book is up is a fresh
`TRADE_SKILL_SHOW`; `TRADE_SKILL_UPDATE` is a change *within* one open book (a filter, a fold, an
item count);
- **the opener is the window's toggle** — casting the profession already open closes it, which is
exactly what ATSW2's `this.Name ~=` guard is there to avoid tripping;
- **an empty rebuilt list signals nothing** — the line is open, no window appears. Written over the
list about to be pushed rather than as the literal count read, because ours can be a frame late
where the reference builds its list inside the open call: a book that arrives empty and fills next
frame still fires exactly one show, on the frame it fills.
`0x4f6000` is the Craft window's same function, keyed on the craft type at `0xbdcfb8` instead of a
skill line and with no empty guard; both of its rules are in `feed_craft` and `open_trade_skill`
too, since `CRAFT_SHOW` is the arm ATSW2 uses for Enchanting.
The filter reset on a line change was already right — `set_trade_skill` keys it on
`trade_skill_last_line`, the `0xbde064` above.
Verified end to end offline, which `$WOW_CAPTURE_SPELLS` is what made possible: Blacksmithing open
with 24 recipes, `ATSWFrameTab3:Click()`, and the window comes back `title=Cooking line=Cooking
rows=9` with the check moved to tab 3. Before the fix the same click left the title on
Blacksmithing.