ff2cbdde67b85cd65b51ea8758328b4e4ee8571c
1693
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ff2cbdde67 |
ui: the HIGHLIGHT draw layer is the highlight state, and starts off
A texture an addon made with `CreateTexture(nil, "HIGHLIGHT")` drew all the
time. Only the region `SetHighlightTexture` installs was hover-gated, through
`ButtonState::region_visible`; anything else an addon left in layer 4 was an
ordinary region that nothing ever hid.
In the reference that layer is the highlight *state*, not a place to keep
highlight art, and four things move it:
OnEnter 0x76b6a0 test [frame+0xf8]; push 4; call 0x76a710 EnableDrawLayer
OnLeave 0x76b6f0 test [frame+0xf8]; push 4; call 0x76a730 DisableDrawLayer
Lock 0x76c2e0 writes [frame+0xf8], then enables; on an unlock disables
only when this frame is not the mouse focus ([root+0x7c])
Disable 0x7791bb disables it outright
`0x77983c` settles what the layer means: a Button picks its label's font
instance by reading that layer's own enabled flag at [frame+0x1a8], which is
[frame+0x198] + 4*4, and takes the highlight font object at +0x3b8 when it is
set. So the layer starts off, and every path that can highlight a frame moves
it -- taking whatever the addon put there along.
What this was costing: hand-built column headers and list rows, which are
everywhere. The <Below Average> guild market's Browse page puts
UI-Character-Tab-Highlight in that layer on each of its nine sort headers, so
every header wore a permanent blue glow bar, and its Statistics page showed all
six filter buttons lit at once instead of the two that were selected.
The nameplate glow lives in layer 4 too, and its hover feed drove only the
region's shown flag. Its own OnEnter override chains back into the base notify
(0x7cb8a5 -> 0x76b6a0), so it now moves the layer with it; addons still read
mouseover from glow:IsShown().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
50a229c202 |
The script log called 34 addon packaging bugs "problems" with this client
Every one of the 34 rows a play session reported was read back out of a capture, and not one was ours: 16 are BetterCharacterPanel anchoring to Blizzard_InspectUI frames that LoadOnDemand has not built yet, 7 are .toc and <Script file> lines naming files their own package does not ship, 10 are TitanPanel/InfoBar/Outfitter integrations in addons whose optional dependencies this player does not have, and 4 are CVars 1.12 never had, which `cvars`'s own test says to leave unregistered. The reference shows the player none of that. A missing parent, a missing relativeTo and an unknown inherits are severity-2 lines in FrameXML.log, and SetCVar on an unregistered name raises into the pcall every addon that writes one already wraps it in. Our window was a deliberate superset, which is right for a compatibility report and wrong for a line reading "34 problems": a TitanPanel plugin with no TitanPanel is not a fault here. So `error` (code raised) and `load` (an addon is not running) stay problems and stay listed, and `warn` -- a call accepted that did not do what it said -- becomes a note, counted beside them and listed behind a new Show notes checkbox. Nothing is dropped; the log still carries every one and the window is one click from all of them. Also closes a hunt: the 283 `SetOwner: unknown anchor 'BOTTOMRIGHT'` warnings that three static sweeps of every installed addon could not place are the realm's own FrameXML, Shop_ProcessEntries forcing items into the cache through a hidden scan tooltip. The seven load failures and the two layout defects from the same session are fixed in the player's own addon copies, mirrored to Addon_Ports; both are written up in docs/ADDON_COMPAT.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
15e1ba1fcd |
Firing KEY_DOWN in its own pass ate every key before it reached a binding
A `MessageReader` advances its own cursor, so the first `keyboard.read()` in a
system run takes the messages and every later one answers nothing. The
`KEY_DOWN`/`KEY_UP` producer added in the previous commit read the stream in its
own loop ahead of the binding walk, so the walk saw an empty stream and no key
ever latched, resolved or ran a binding. No error, no log line: the keyboard
simply stops.
It is gated on a listener, so it only bites a player whose addons register one
of those events — which is everyone with SuperCleveRoidMacros installed
(`Core.lua:4911-4912` registers both). grom: *"Keyboard is not working in game
right now for some reason, launched thrice to no avail."*
The whole system now reads the stream exactly once into a buffer and its three
consumers iterate that: the bind-capture seam, the key events, and the binding
walk. That also fixes a latent version of the same thing — the capture seam was
already draining the stream, so the release half its own comment promises
("releases still unlatch below so a key held across the arm cannot stick") could
not run.
The regression test asserts both halves, because either one alone passes with
the other broken: the events fire AND the bound key still runs its binding. It
fails on exactly the second assertion with the bug restored.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
4974326381 |
Bring the nampower, UnitXP_SP3 and ClassicAPI ports up to the installed builds
The three DLLs beside the client were updated; re-measured each against the
thing with evidence behind it rather than against a wiki.
nampower's own string pool is exact — `^NP_[A-Za-z0-9]+$` is the CVar list and
`Usage:` the arity of every function — and both came back identical to this
tree: 49/49 CVars, 63/63 functions. The gap was events. Four built:
BUFF_ADDED_OTHER / BUFF_REMOVED_OTHER ui_aura.rs, AuraEdge::OtherBuff
KEY_DOWN / KEY_UP bindings.rs, key_event_code
The buff pair was argued against here before, on the cost of diffing the
helpful half of every streamed unit for events nothing registered. Answered
rather than overruled: both are gated on `has_listener`, which is the test
upstream now recommends over its own deprecated NP_Enable*Events CVars. The key
pair uses the client's own code table, which `chord::key_code` already held with
a comment saying it was the fact these events were blocked on.
UnitXP_SP3 is packed, so its contract comes from the wiki plus the call sites in
the installed addons — and those call sites exposed a live bug. Every
`UnitXP("target", …)` queued a SelectionRequest, but the mod selects INSIDE the
call and SuperCleveRoidMacros steps the cycle and reads `"target"` back on the
next line, fifty times in one Lua frame. Queued, every pass read the same unit
and the scan reported one enemy however many were there — truthy return, no
error. The pick now resolves in-call from a pool the app publishes
(`feed_unitxp_scan`), exactly the bargain TargetByName already strikes. All nine
target modes are built where four were; `behindThreshold` is real (the reach
feed publishes the angle now, not a verdict); `timer` rides the one timer queue;
`notify` reaches winit's user-attention request; `version`,
`performanceProfile` and a non-raising `debug` answer.
ClassicAPI carries its whole Lua addon gzipped in `.rsrc` — 173 files, a length
header then the bodies, keyed by the paths in its string pool — so the spec for
the installed build is on disk. `scripts/dll-surface.py --unpack` writes it out.
Read against a LIVE VM (not `UiScript::new()`, which is engine-only), the gap is
898 globals, and almost all of it is either a locale string or a subsystem this
client models differently. What is between those is now in
`compat/classic_api/shared_xml.rs`: the pure Blizzard_SharedXML helpers, in
1.12's grammar, from the shipped addon's own source. `tostringall` stays absent
on `tests/stdlib.rs`'s own rule.
The native half is mostly widget methods; intersected with what the corpus
actually calls it is four, of which three are built (`SetResizeBounds` over
1.12's own resize pair, `GetNormalFontObject` — which needed 1.12's missing
`GetTextFontObject` implemented first — and a bare `IsEquippedItem`) and
`SetMotionScriptsWhileDisabled` deliberately is not, because both its call sites
guard on it and the behaviour behind it does not exist here.
Also measured and recorded: every addon-load warning in grom's own logs is a
third-party optional dependency he has not installed or an addon's own packaging
bug, not a defect on this side. docs/ADDON_COMPAT.md has the table.
Ambiguity ceiling 4,956 -> 4,966, measured: nine for `feed_unitxp_scan` against
transform writers on non-networked lanes, one for the attention drain against a
system that also reads WinitWindows and neither writes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
b39651d9b4 |
A <BarTexture name=…> never reached _G, which broke crafting in ATSW2
`apply_statusbar` read `file=` and `<Color>` off `<BarTexture>` and passed them to `SetStatusBarTexture`, which builds the fill region itself. Every other attribute of the element went unread, `name=` among them. In the reference `<BarTexture>` is a Texture element and StatusBar's LoadXML (`0x782ef0`) builds the fill through the shared texture ctor, which registers the name like any other region's. A dropped `name=` is invisible until Lua reaches for the global, and then it raises inside the addon. ATSW2's `SPELLCAST_START` arm shows the progress bar, tints it per ore through `ATSWProgressBarTexture` — the raise — and only then calls `ATSW_StartProcessing`. So every craft left the bar shown and running with nothing able to stop it: `Processing.Active` stayed false, `SPELLCAST_INTERRUPTED` was never registered, `ATSW_StopProcessing` and `ATSW_CompleteTask` were never reached, and the guard `not Processing.Active` let the next craft re-raise. grom's log has seven in a row and his screenshot has the stuck bar. Named now, both ways, as Button state textures already were: the region registry for `GetName()` and `_G` for `getglobal` and a sibling's `relativeTo`. A named `<BarTexture>` with no paint still builds its region. Four of the five in this install declare a name (`ATSWProgressBarTexture`, `ATSWRankFrameBar`, two QuickHeal bars); no stock FrameXML status bar names its fill, so the base interface neither gains a global nor collides with one. Also in the skin: the reagent names under the icons take the same black-to-white `SetTextColor` wrapper as the tools line. They measured 1/1/1 in a capture and read as correct, but a capture has no item links and `ATSW_ShowRecipe` paints the title black only inside `if RLink then` — in play it runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
14a1a7179e |
A tooltip Lua sized itself kept being re-sized out from under it
benilla auto-sizes a GameTooltip in a pre-pass of every resolve, because that is where the text measures are. The reference re-sizes one only from its own content verbs and from Show: 0x530640 has six call sites (0x52ad59, 0x52b029, 0x52e5ae, 0x52f869, 0x530615, 0x530aad), all inside the tooltip code and none in the frame pump. So a SetWidth/SetHeight written after that call stands, and ours was overwritten on the next frame. ATSW2's recipe tooltip is the case grom hit: it Show()s, then sizes the plate from its own line count and a column width floored at 350. Losing that width collapsed the backdrop to the one blank line it pushes through our AddLine and dropped every right column, anchored RIGHT -10 to the plate, on top of its left column -- "Required reagents:" and "Bags Bank Alts" sharing a pixel column in his screenshot. TooltipState::authored_size: set by SetWidth/SetHeight through the existing note_authored_size, cleared by any content change, by SetMinimumWidth/SetPadding, and by Show. layout_tooltips skips a plate that carries it. Clearing it on Show matters: pfUI and pfQuest both SetWidth the shared GameTooltip *before* Show, where the reference overwrites it -- honouring those would pin every later hover at 180 or 100 px. Two tests pin both halves. Measured, driving ATSW2's own OnEnter: plate 350.0 x 81.7, the two columns 232 px apart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
6bcf937986 |
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 |
||
|
|
a033f13f0f |
$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. Advanced Trade Skill Window finds the player's professions by walking spell tab 1 and matching each spell's icon against its own thirteen `Interface\Icons\…` leaves, so with no profession spell seated it finds nothing, draws no tab and switches nothing: indistinguishable from the defect being chased. With it, grom's report reproduces offline. The realm's own ids are in the comment — Survival is **46050**, a profession 1.12 does not have and the one his screenshot is of — and seating 46050,2550,3273,2018,2656 builds five named, shown, correctly-icon'd tabs, which is how the icon match was cleared as the cause of *"I can't switch to other professions."* Added to the fixture's own book rather than replacing it, so every existing scenario still reads as it did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
524e500d43 |
The map's glide is a setting, because two reports bracketed one guess
grom, 2026-11-05: *"its the maps motion that is laggy."* That is `SCALE_RATE`, and it is the same constant he reported from the other side a day earlier — *"scroll is better but feels pretty slippery, like scrolls past where I stop scrolling"* — which is what took it from 10 to 24. At 24 the half-life is 0.29 s, so at 60 fps a wheel notch is still closing 167 frames later: "it overshoots" and "it lags behind" are two readings of one glide, and a third guess would be a third guess. `mapMotionSmoothing` is a percentage of a 0.30 s half-life, 0 is no ease at all, and the slider is on the Options window's Interface page. The top of the range is the 0.29 s he called laggy, because that is the slowest anyone could want; the default is a quarter of it. Measured over a full rung (scale 1 -> 1.6) at 60 fps, frames to settle within 0.001: **0% = 1, 10% = 17, 20% = 34, 25% (shipped) = 42, 50% = 84, 100% = 167** — so the default is four times snappier than what he just played, and still eased. One rate drives both the pan and the zoom ease, read once a frame: the two are one gesture to a player, and 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, as before. Verified in a live session with his addons: `mapMotionSmoothing=25`, one notch settles in 42 frames (0.70 s), map still FULLSCREEN, fade still 80. Options census 141 -> 142 on both counts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
8c6fb4e425 |
Every <OnLoad> in the interface read text widths as 0
`seat_text_measurer`'s own doc says what it is for: *"so `GetStringWidth` right after `SetText`
measures, as the reference answers it inline (`0x79e510` → `0x772890`). Also seated before the
manifest loads, for every `<OnLoad>`."* It was not seated. `UiFontAtlas` is inserted by an
`Update` system through `Commands`, so it appears at the next command flush — and the interface
load runs on the `InWorld` state transition, which comes first. `seat_raster_seam_for_load` then
took its `else { return }` and the VM went through the whole load with **no text measurer at
all**, which `ensure_measured` answers by returning at its first line.
So for the entire UI load — all of FrameXML and every addon — `FontString:GetWidth()`,
`GetHeight()` and `GetStringWidth()` answered **0** for a string that plainly had text. Measured
on grom's install: dozens of them, including every unit-popup entry, every `StaticPopup` button
and the four radio labels this was found through.
What it costs is any `<OnLoad>` that lays something out from a label's width, which is an ordinary
thing for an addon to do. `AdvancedTradeSkillWindow2.lua:1642` puts its Name radio button at
`ATSWDifficultySortCheckboxText2:GetWidth() + 5` to the right of the Difficulty one: the stored
offset came out **5.0** where it should be 53.8, so "Difficulty" was drawn across "Category" 37 px
to its left. grom, 2026-11-05: *"its loading now, but barely functioning."* Re-running
`ATSW_OnLoad()` by hand from a live session gave 53.8 — the same code, the same frame, a measurer
away.
`ensure_font_atlas(&mut World)` is `init`'s body for a caller that holds the world, called from
`seat_raster_seam_for_load`; `init` keeps its own path for every later frame. A world that still
cannot build one (no chain, no window) now warns instead of loading the interface in silence.
Verified: the ATSW2 offset is 53.8 at load, and a walk of the whole loaded interface finds **0**
FontStrings that have text and measure 0 (dozens before). benilla-app 4243 pass / 12 fail, the
recorded baseline with an identical name set.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
6306fed1ca |
A capture seats a player record, because the interface it loads is a player's
A capture never logs in, so `record_from_roster` answers `None` and the record stayed
empty — and the interface a capture loads is the one a player sees, where there is always
a player. That is the harness diverging from the client, not the other way round:
`Turtle_TransmogUI.lua:17` is `strlower(class)` over `local _, class = UnitClass("player")`
at main-chunk scope, so the nil killed the file and raised an error popup over the middle
of **every UI capture** — visible in the Interface options shot, and in nothing a player
has ever seen (his own session logs nine `Turtle_TransmogUI` lines and no such error).
Minimal and presence-only, the same record `manifest.rs` already seats for the offline
loader and for the same stated reason. Where `$WOW_CAPTURE_SAVED` names a character it is
that name, since the saved variables the capture just loaded are keyed on it.
Verified: `WOW_CAPTURE=ui-options` goes from one raised chain file to 0 runtime errors, and
the shot is a clean Interface page.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
082dd2ed0f |
The map's arrow, its stratum, and a fade while you walk
Three of grom's 2026-11-05 reports about the built-in map, all measured. **The player arrow was the one piece of 1.12's map content left outside the canvas.** *"my player arrow goes to the wrong spot at any zoom level, this needs to be accurate all of the time."* `WorldMapFrame_OnLoad` builds it with `CreateWorldMapArrowFrame(WorldMapFrame)` (`WorldMapFrame.lua:19`), so its parent is the WINDOW, while `MapCanvas.lua` reparents everything it is anchored against into the canvas. `SetPoint`'s offsets are in the positioned frame's own units, so the offsets the stock update computes — `playerX * GetWidth()`, times `WorldMapDetailFrame:GetScale()`, which `SeatOnCanvas` pins at 1 because the zoom lives one level up on `ScrollContainer.Child` — are short by exactly the canvas zoom. Measured on a 1600x900 harness with the view centred on the player: exact at zoom 1, 250.5 x 501.0 screen px out at zoom 2. `WorldMapPlayer` — the invisible mouseover rect for the same spot, from the same two FrameXML lines — has always been right, because 1.12's XML nests it under `WorldMapButton`, which IS in the canvas. That makes it the oracle the test uses rather than a recomputed expectation. Not reparented like the detail frame and the button: the arrow is a size-less `Model` whose rect is the file's bounding box, so inside the canvas it would grow with the zoom, and no frame scale fixes the size without undoing the position — both read the same effective scale. The cost of staying outside is clipping, hence an explicit bounds test, which is a hide the stock code never needed because a map that cannot pan cannot put the player off-screen. That re-seat runs every frame the map is open, so nothing in it may force a layout resolve: the art's size is read off `WorldMapButton` (explicitly sized) rather than the detail frame (`SetAllPoints`, so derived), and the bounds test is arithmetic on the controller's own normalized scroll rather than a rect comparison — `GetLeft` on a dirty tree measures 30-370 us against 0.42 us settled, and a pan writes the scroll every frame. While in there: `InstantPanAndZoom` set `Child:SetScale` from its raw argument while `GetCanvasScale` clamps to the zoom ladder, so a scale past the ceiling drew the art at one scale with every extent, pan clamp and pin placement computed for another. It clamps first now, as the ease path already did by construction. **The window fell out of its own stratum every time it opened.** *"the map needs to be on a higher level as I can see my unitframe over it."* `WorldMapFrame.xml:5` declares `frameStrata="FULLSCREEN"`; measured FULLSCREEN with no addons and MEDIUM at level 0 with his, which is the bottom of the bucket pfUI's unit frames live in (`pfUI/api/unitframes.lua:413`). Five visible frames drew over the map rect. No addon does it. The new `$WOW_WATCH_STRATA=<FrameName>` instrument — every `SetFrameStrata` AND every `SetParent` on a named frame, with the Lua caller site — found one FULLSCREEN from the XML loader and then `WorldMapFrame:SetParent(UIParent)`, from the realm's own `WorldMapFrame_Minimize` (`WorldMapFrame.lua:183`) by way of `WorldMapFrame_OnEvent`, on the open path. A reparent forces the moved frame into its new parent's stratum (`0x76ab5a`, pinned off the binary by `script::tests::setparent`), and `UIParent` is MEDIUM. The reparent is right and stays; the window's own declared stratum is re-asserted on `SeatLevels` instead, which already re-seats the frame levels on every show for a cousin reason, plus hooks on Minimize/Maximize since the minimize runs while the window is up. After: FULLSCREEN, 0 frames over the map. **And the fade.** *"It would also be nice if the whole map window went to 80% transparentcy when the player is moving."* `mapFadeWhileMoving`, percent, 0 off, default 80, with a slider on the Options window's Interface page. Written as a FACTOR of whatever alpha the window already has, because that alpha belongs to somebody else — pfUI sets it from its own config and offers shift-wheel over the map — so the base is re-read whenever the live alpha is not the one this last wrote, which is another writer's signature; their setting is what gets faded, and closing the map restores it. `GetSpeed()` is the movement test: ours, always present, and 0 standing still by construction, off the same move flags the engine answers `IsSwimming` and `IsMounted` from. Also `$WOW_CAPTURE_EVAL`/`_SETUP` take a path ending in `.lua` as the chunk. A probe worth writing is longer than a shell argument survives, and a quoting error in a probe reads exactly like the thing being measured not being there. Verified: `the_player_arrow_tracks_the_hit_area_at_every_canvas_zoom` walks zoom 1, 1.5, 2.5 and 4 asserting the arrow and the hit area agree within a screen pixel, and that the canvas applied the zoom asked for. In a real session with his addons: FULLSCREEN and 0 visible frames over the map (was MEDIUM/0 and 5). Options census 140 -> 141 on both counts. benilla-app at its recorded baseline, benilla-ui 2128/0, clippy and the two lints clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
a7a54c6d02 |
Dismount and CancelShapeshiftForm, because our own pfUI port calls both
`pfUI/modules/autoshift.lua:45` answers `ERR_CANT_INTERACT_SHAPESHIFTED` with
if IsMounted() then Dismount() elseif GetShapeshiftFormID() ~= 0 then CancelShapeshiftForm() end
and grom's 2026-11-05 log carries `attempt to call global 'Dismount' (a nil value)` from
exactly there. That is a port written against verbs this client does not have, with a
comment claiming both "do their own engine-side aura scan and send the cancel packet
directly" — which was the design and not the state.
Both were on `compat/classic_api/state.rs`'s not-built list for a reason that turned out to
be answerable. Each needs the SPELL ID behind a state the engine reports as a flag
(`UNIT_FIELD_MOUNTDISPLAYID`) or an index (`SelfMovement::shapeshift_form`), because
`CMSG_CANCEL_AURA` carries an id and nothing else; and the player's aura list carries spell
ids and nothing about what each aura IS. The missing half was one derivation —
`EffectApplyAuraName` 78 (`SPELL_AURA_MOUNTED`) and 36 (`SPELL_AURA_MOD_SHAPESHIFT`), now
`SpellDisplay::mount_aura`/`shapeshift_aura` and two fields on `AuraState` beside
`channeled`, which the feed derives the same way from the same row it already reads.
Each verb is then the aura it names cancelled by id, which is what the reference's own
right-click on the buff does. Neither is gated on `aura::cancel_authorized`, unlike
`CancelPlayerBuff`: that gate is the reference's check inside THAT verb, this is not that
verb, the server makes its own decision about a cancel it is sent, and a mount whose aura
is not flagged `AFLAG_CANCELABLE` would otherwise be undismountable with no way to tell
why.
Allowlisted in `reference_surface` with the reason, as the four taint globals were: 1.12
has no verb for this because 1.12's player has the buff icon under the cursor, and an addon
reacting to an error message has neither an icon nor a cursor.
Verified: `dismount_and_cancel_form_each_cancel_their_own_aura_by_id` seats a plain buff, a
form and a mount in one list and asserts each verb finds its own id past the others, and
that neither fires with nothing to cancel. benilla-ui 2128/0, benilla-app at its recorded
baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
8fcab8517b |
The MasterSoundEffects cut has to ride the play queue, not an observer a frame later
grom, 2026-11-05: *"softspoken was repeating zone readouts over and over unless I manually
removed them from queue."*
1.12 has no verb that stops a playing sound, so every voice-over addon uses the one
primitive there is: `SetCVar("MasterSoundEffects", 0)` then `..., 1)`, whose ENABLING edge
walks the live instance list and stops each node (`0x457500` -> `0x7a6570` -> `0x7a67a0`).
SoftSpoken's `SoundUtils:StopSound` is exactly those two lines, and its `SoundQueue` spends
them twice over: to duck the game's NPC dialogue while it speaks, and — this report — to
stop its OWN clip before replaying one, which `StopKeeping` does and comments on ("it
replays from the start when Advance next reaches it, since the client has no seek").
`bb9eb084` had to exempt `PlaySoundFile` audio from the cut because stopping it silenced
SoftSpoken outright: the addon spends the toggle and THEN plays its own file, so a cut that
reached files took the very line the toggle made room for. Its doc said what that cost —
*"if the overlapping-replay symptom returns, this is why"*. It returned.
The ordering was the thing, not the exemption. `cut_effects_on_reenable` is already
`.after(UiInput).before(drain_ui_sounds)`, which looks like the right gap and is not: the
flag is armed by the `CvarChanged` observer, `CvarChanged` goes out through
`commands.trigger`, and so the observer fires at a command flush a frame AFTER the Lua pass
that wrote it — by which time the drain has started the new clip. The consumer's ordering
cannot fix a producer that is a frame late.
So the cut now rides the sound queue, in call order with the plays:
* `SoundRequest::CutEffects`, pushed from `script::cvars::store` — the one write path
every `SetCVar`, `ConsoleExec` and engine write goes through, and the only place that
holds both the old value and the new, which an edge needs.
* `drain_ui_sounds` walks the queue in order and spends it where the addon wrote it:
stop, then play. Its catalog guard moved inside the loop, because the cut needs neither
a catalog nor assets and a stop dropped for want of one leaves the clip it meant to
stop playing under the next one.
* `cut_effects` is now shared by both paths with a `files` flag. In order with the
addon's calls it stops files, because the stop was written before the play; a frame
late, from the observer, it still spares them, because by then the only file alive is
the one the toggle made room for. Both answers were paid for by a report, and
`stopped_by_reenable`'s doc says which is which.
Verified: `the_master_sound_toggle_queues_its_cut_ahead_of_the_file_that_follows_it` writes
SoftSpoken's own two calls and asserts the queue is `[CutEffects, File(...)]` — the stop
keeping its place in front of the play — with no cut for the disabling edge and none for a
re-assert. `only_the_in_order_cut_reaches_an_addons_own_file` pins both answers.
benilla-ui 2128/0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
fc78e52c25 |
A NAME = nil line in an addon's saved file deletes the default it just set
grom, 2026-11-05: *"I'd also like to get advanced trade skill window2 working … I can't
even get number 2 to load right now."* His log has three runtime errors from it, all the
same shape:
AdvancedTradeSkillWindow2.lua:844: attempt to index global `ATSW_Profession'
AdvancedTradeSkillWindow2.lua:5931: attempt to index global `ATSW_Bags'
AdvancedTradeSkillWindow2.lua:5990: attempt to index global `ATSW_Bank'
All three are assigned `{}` at FILE SCOPE — lines 294, 5929 and 5986 — and the chunk ran:
the traceback carries `ATSW_OnEvent` (2504) calling `ATSW_ShowTrainingPoints` (4269), both
past the first of them. So the tables existed and were taken away, and
`benilla-config/saved/AdvancedTradeSkillWindow2.lua` says by what: 19 of its 24 declared
names stood at `NAME = nil`.
An addon's saved file is executed AFTER its own Lua (`0x51f4b5`, "after the files set
their defaults"), which is what makes a nil line destructive rather than merely useless —
in a fresh VM a global is already nil, so the line carries nothing and can only delete.
And once written it is self-sustaining: the nil it forces is what the next shutdown reads
back and writes again, so one bad write is permanent. The reference does not write them,
which is why no real SavedVariables file has ever had a `= nil` line in it.
Both sides, because the write fix alone still costs a session:
* `saved_variables_bytes_for` skips a nil global. The flat `RegisterForSave` file still
writes them — it is read BEFORE FrameXML, where a nil line and an absent one mean the
same thing — so the two paths split and `serialize_saved` takes the flag.
* `addon::drop_nil_assignments` strips `NAME = nil` lines a previous build already
wrote, matched narrowly against that addon's own `## SavedVariables` set, one
assignment per line, logged once per file. A player's hand-written `X = nil` of a name
the addon never declared is left alone.
It is not one addon: his install carried poisoned files for five — ATSW2 (20 lines),
BigWigs (4), ImprovedErrorFrame (2), MasterTradeSkills (1), SuperCleveRoidMacros (1).
Also the harness gap next door, which is why a capture could not have found this. A
capture answered `GetRealmName()` as "" and `UnitName("player")` as "Benilla" even with
`$WOW_CAPTURE_SAVED=C_Thun-Gromgosh` naming the character whose saved variables it had
just loaded — so `ATSW_Profession[realm][player] = nil` (line 296) raised `table index is
nil` and took 6,000 lines of chunk with it. That is a broken instrument that reads exactly
like the defect being measured, and it hit every addon keyed on `[realm][player]`.
`lifecycle.rs` now seats both from the capture identity, before the addon walk.
Verified: `a_never_assigned_saved_variable_writes_no_line_and_a_nil_line_is_dropped_on_load`
covers both halves. In a real session with his addons, all 24 of ATSW2's variables read as
tables at the first handler and `ATSW_ShowTrainingPoints()` — the function that raised
fifteen times — returns. Addon corpus unchanged at 86/87 clean loads, 86/87 UI probe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
e15a415da9 |
securecallfunction, because modern libraries upvalue it at load time
The stitched map never said which zone you were looking at — from about zoom 3.5 to 10
there is no zone name anywhere on screen, which is the band a player pans around in.
Wiring up a port file that draws them took the addon corpus's "survived a UI probe" column
from 86/87 to 85/87, and the failure was not in the new file:
OpenMapClassic/Libs/CallbackHandler-1.0/CallbackHandler-1.0.lua:19:
attempt to call upvalue `securecallfunction' (a nil value)
Ace3's modern `CallbackHandler` takes `local securecallfunction = securecallfunction` on
its line 10 and calls it for **every handler of every event**, so the first callback any
addon shipping a post-2022 Ace fires raises. Nothing had fired one before, because
OpenMap's whole callback surface was dead for an unrelated reason until tonight.
1.12 has no taint, so there is nothing here to make secure — but the half of the contract
that is not about taint is the half library code leans on: it is a **protected** call, so
one raising handler does not stop the registry walking the rest. `securecallfunction`,
`securecall` (which also takes a global's name), `issecure` and `forceinsecure` go beside
`seterrorhandler` in `stdlib.rs`, with failures reaching `geterrorhandler()` exactly as the
reference's own secure dispatch does, so an addon bug still lands in the log rather than
being swallowed. Back to 86/87.
`the_rest_of_the_bare_globals` asserted these ABSENT, on the reasoning that the restricted
-execution system has no 1.12 counterpart and nothing here implements it. That was half
right; the half it missed is above, so the assertion is inverted with the measurement
written beside it — the same shape `hooksecurefunc` already has one line below.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
65520a4a58 |
The map's darkening was a grid of grey boxes, and a 12 px string was emptying the glyph sheet
grom, after a night's play: "make the map look a lot nicer, it would be really nice if the darkened areas were not just massive squares on the map, and should fit into the actual edges of the terrian if that makes sense, also I shouldn't see this dark squares on the fully zoomed out world map, also make it so the world map can't move out of the frame." **The darkening.** OpenMapClassic washed a never-visited zone through its land mask, and those masks are Voronoi-dilated by construction — "so that the forty of them tile each continent with no gap" — so for any zone that is not on a coast the mask *is* the rectangle. Measured at zoom 2.5 over the Eastern Kingdoms: a grid of flat grey boxes. Nothing could have been softened into a fix; it was the wrong mask. It was also drawn at full strength from every zoom, and at the whole-world view the zones' own art is not drawn at all, so the only thing on the world map was the fog's rectangles. A land mask for every map now comes from the client's own data. `bake_zone_land` unions a map's `WorldMapOverlay` alpha — the pieces the game reveals as you explore, cut to the shape of the ground — into one 256×256 mask; `overlay-land-index.py` writes the list the addon reads, because Lua cannot test for a file and a *missing* mask is dropped by the engine so the whole rectangle draws, which is the defect itself. 53 maps have overlays (the 40 stock zones, Hyjal, Alterac Valley and 11 of the realm's own); 76 have none and keep a wide feather. `clip-zone-masks-to-coast.py` then takes the open sea off the dilated sheet masks — the same dilation was painting parchment out on the ocean — by closing the continent's own land by 500 yards (above the 466-yard inradius of the widest enclosed gap, so seams fill and the sea does not) and keeping a 300-yard coastal margin. Twelve landlocked zones lose 0.0%; Darkshore loses 53%, all of it water. `tga.rs` gains the writer that needed, `script::uimap` and `ui_world_map::build_catalog` become public so an example can reach the uiMapID graph, and `WorldMapUiData` grows the accessor so the VM push and an outside caller cannot build different lists. **The glyph sheet.** His four-minute session reset it 27 times, each dropping every cached glyph. The producer is Atlas-CFM's cursor coordinate readout: a 12 px FontString on `WorldMapDetailFrame`, rewritten every frame, and the canvas zooms by scaling that frame — 140 device px a digit at a canvas scale of 6.9. Nothing about the addon is wrong, so the fix is ours: `raster_ppem` rasterizes exactly up to 64 device px and snaps to a third-octave ladder above it, with `cell()` scaling the cached bitmap and metrics left keyed on the true ppem, so advances and `GetStringWidth` are bit-for-bit unchanged. Same 1,200-frame reproduction: 3 resets and 6,623 rasters before, **0 and 1,424** after, with occupancy climbing once to 30% and stopping. A reset no longer clears the metric cache either, and `note_loud_cell` names the string that asked for an outsized cell. **Four more from the same log.** `SetOwner`'s unknown-anchor line named a forwarding wrapper rather than the addon that chose the anchor (two `debugstack` frames now); the nine reference anchor names are written down with the binary evidence, because `ANCHOR_TOP` is not one of them. The `CreateFrame(kind, …, inherits=<other kind>)` warning fired in both directions and only one drops content — `kind_subsumes` makes it directional, and `type_chain` moves out of `install` so `IsObjectType` and the loader read one table. A string that is not UTF-8 now names the Lua line that cut it, and both glyph warnings quote the string the character came out of. The three world-map CVars stay unregistered, with the argument written down beside `REGISTERED` so it is not re-litigated from the warning. **`World.zmp`.** The realm's `WorldMapArea` carries a third `areaID == 0` row, `694 World`, with an all-zero rect; the reference ships no bitmap for it and neither the graph nor the realm's own `Overrides.lua` lets it reach a player. The miss is a `debug!` line now, and the `warn!` is kept for a continent that *has* a rectangle and is missing its bitmap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
74fd1a52a2 |
Four counts the 2026-11-04 client update moved
The realm rewrote `patch-4.mpq` and `patch-5.mpq` again (19:28 local). Everything
structural held — 152 toc rows with the same six unnamed, `Bindings.xml` still 235/229
live, DBC field counts unchanged everywhere, `WorldMapArea`/`Map`/`AreaTable`/
`WorldMapOverlay` byte-identical where our tests pin them, and the addon census identical
on every column. The content is nine mount spells (`65417-65425`, the Dragonhawks and the
Blood Ring pair) with their ten `CreatureDisplayInfo` rows, and every model and texture
variation they name resolves in the chain.
Four tests pinned counts that moved with them, and all four are examples rather than
rules, so each is re-recorded and the rule beside it left alone:
* `Spell` 28046 → 28055 rows, and the `SpellFamilyName` histogram's family 0 22387 →
22396 — all nine new spells carry family 0, so the eleven-family set, the three junk
keys, the multi-bit count and `max_bit` 53 are all untouched.
* `CreatureDisplayInfo` 13452 → 13462, and the blood-tier array 12516 → 12526. The
companion assertion still describes the data: exactly one display (185, `modelId` 0)
has no `CreatureModelData` row, so `tiers.sum() == len - 1` holds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
ee125519f1 |
The map arrow pointed the wrong way wherever the player was not
`UpdateWorldMapArrowFrames` (`0x4a8d10`, `0x4a7c20`) copies the camera-tracked object's facing to both arrows and leaves them alone when there is no such object. Ours tested "is there a player" with `WorldMapState::player_uv.is_some()` — a different question, because that field is `GetPlayerMapPosition` on the **displayed** map. A view sitting on a continent, on another zone, or on an addon's stitched world answers `None` on every frame, so the arrow kept the heading it had when the window last sat on the player's own zone: the right place, pointing the wrong way. `player_facing_seen` is the real test, set the first time the feed pushes a facing — the feed runs only where there is a player, and the facing it pushes is unconditional. The existing test asserted the old behaviour on its second leg; that leg now asserts the heading following off the displayed map, and a new test holds the other half (before any feed, the login and loading screens, the arrow is left alone). `scripts/soften-zone-masks.py` is for the OpenMapClassic port rather than the client: its per-zone land masks are binary, so every map sheet ends on a crisp line against a differently-lit neighbour. It writes a softened copy — dilate, blur, times a rect ramp that reaches zero inside the sheet's own torn parchment border, which is the half that is not obvious. `docs/ADDON_COMPAT.md` writes up this round's five: the flashing arrow (two writers on one frame, measured `h h S h h S`), why correcting after 1.12 rather than suppressing it cost a full UI conversion every frame, pfQuest's pins being both behind the map and only ever for one zone, the softened masks, and the cities. Gates: fmt 0, clippy 0, benilla-ui 2121 pass / 0 fail, benilla-app 4233 pass / 12 fail (the recorded data-golden baseline). Corpus 86/87 load clean, 86/87 survive a session start and a UI probe, 0 raised on hover, click or drag — unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
841fd1a81f |
Two map verbs disagreed about where the player was
`UnitState::position` was fed from a streamed object's `Transform` read raw, and a
`Transform` is Bevy's frame (`wow_to_bevy` is `(-y, z, -x)`): the two map axes arrived
swapped and negated and the third was the player's height. Three compat verbs answer
from that field — SuperWoW's `UnitPosition`, UnitXP_SP3's `getUnitPosition`, and
`C_Map.GetPlayerMapPosition` — while 1.12's own `GetPlayerMapPosition` reads the
per-frame world-map feed, which converts. So the client sat in a state no real client
can be in: the 1.12 verb placing the arrow and the modern verb saying the player was on
no map at all.
Against a zone's few-thousand-yard rect the height lands outside 0..1, so the modern
verb answered nil — which is OpenMapClassic's "You aren't on the world map here" and
`Map/StockArrow.lua` declining to place anything, leaving 1.12's wrong placement
standing. Against a continent's sixteen thousand it lands inside, and answers a
plausible wrong fraction. Both of grom's reports, one cause. Distance was the one read
this never broke, the transform being rigid.
Measured in a capture with the player at Northshire: `UnitPosition` now answers
(-8980, -160, 110), `C_Map.GetPlayerMapPosition(1415)` and the 1.12 verb agree to seven
places, Elwynn answers a fraction and Dun Morogh correctly answers nil; and with the
view centred on the player's own world point the arrow lands on the container's centre
to a third of a pixel.
A capture could not have shown any of this — its synthetic player carried no position,
so every reader answered nil there. `demo_unit_feed` now publishes the capture's own
parking spot, at the cost of one ambiguous pair written up at the ceiling.
Also here, from the same play test:
* A canvas zoom now tells the two 1.12 frames their scale moved, with their own scale,
which is the only way a 1.12 map addon can hear it. pfQuest counter-scales its pins
inside that hook; the canvas scales its child, so the hook never fired and the pins
kept the size they were built at. Measured 0 -> 24 `pfMap.ResizeNodes` over one
glide from 8x to 30x.
* `CLASSIC_API_VERSION` says 1.18.0, at grom's ask. The DLL beside the real client
reads 1.15.0.0 and nothing in his folder gates above pfUI's 11400 nag threshold, so
the trade is the one that constant has always named and the floor is unchanged.
The addon-side half is in `Addon_Ports/openmap_port`: `unexploredMode` defaults to
`reveal`, with the one-shot migration this addon already uses for the kind. A 1.12 zone
sheet is blank parchment and every scrap of terrain is an overlay the client hands out
only where explored, while a city sheet has no overlays at all — which is the whole of
"it will only paint the actual map art for cities".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
d5beabab42 |
The map's mouse surface shrank with the zoom, and four things fell through the hole
The canvas gave the map no mouse surface of its own, on the reasoning that 1.12's
`WorldMapButton` covers the same rect and sits above it. The button is pinned INSIDE the canvas
child, so it scales with it: measured at the world view with OpenMapClassic driving, at grom's
own 2560x1440, a 23x16 px button inside a 701x468 px window. Every press on the other 99%
reached the next frame that takes the mouse — `WorldMapFrame`, which pfUI makes a
`RegisterForDrag("LeftButton")` handle for the whole window. So dragging the map moved the
window, and a click never reached `ProcessCanvasClickHandlers`, which is the only way a map pin
is ever dropped.
The container takes the mouse and the wheel now and drives the same controller. The frame level
mattered as much: the hit sweep probes one `(strata, level)` plane in link order and a parent is
linked before its children (`0x764aa0`), so a container tied with `WorldMapFrame` still lost
every press to its own parent. It is re-seated to `map:GetFrameLevel() + 1` on every show,
because the window is level 0 while the canvas builds and level 1 once `ShowUIPanel` has raised
it.
With the click arriving, a pin still stored nothing: `Map/Pins.lua:57` trims the note with
`strtrim`, which was deliberately absent. `script/stdlib.rs` still refuses it on the 1.12
surface, for the reason written there; this build claims ClassicAPI, whose client does have it,
so it lands in `compat/classic_api/string_trim.rs` with both spellings. Eight bare call sites in
the corpus, one of them in pfUI's search box on every keystroke.
`SCALE_RATE` 10 -> 24. A 0.7 s half-life was tolerable only while a wheel notch could not
accumulate; now that they add up the glide is what you feel ("scrolls past where I stop
scrolling").
`BenillaMapTextures(uiMapID)` is `GetExploredMapTextures` without the explored filter. Reveal
mode draws unexplored overlays out of a retail FileDataID table this client resolves none of —
captured with the fog default and with reveal, the same view came back pixel-identical, 0 by
`compare -metric AE`. The overlays were never missing, only their names: `Map/RevealFromClient.lua`
in `Addon_Ports/openmap_port` rebuilds the table from this, with paths.
Also in the port, not here: the arrow is placed through OpenMap's own coordinates, because
1.12 measures `GetPlayerMapPosition` against Blizzard's world and continent art and the stitched
map is a different projection.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
c92f078c39 |
A wheel notch stepped from the easing scale, so notches did not accumulate
`ZoomIn`/`ZoomOut` walked the zoom ladder from `GetCanvasScale()` — the live value the smooth zoom is still easing — so the notch after the first re-read a scale that had barely moved, found the same rung still under it, and re-set the target it already had. The ease closes about 63% of the gap a second, which is slow enough that a player never gets a second rung. Driven at grom's own 2560x1440 with his addons loaded, over OpenMapClassic's 27-step ladder (0.0330 .. 2.3734): four wheel-down notches at the map's centre all set target 0.9030 — one rung — and two more at the far right set nothing. That is his "it will only scroll very little out … and will only very little scroll in period". `GetZoomStepScale()` steps from `targetScale` instead, so notches accumulate and either end of a long ladder is reachable. The same measurement rules out the position dependence he saw: the wheel's target over the centre and 19 px from the right edge is `WorldMapButton` both times, because `wheel_target` walks past frames that take no wheel the way `0x7664f0` does. `docs/ADDON_COMPAT.md` also records the two addon-side causes of "I can't see any buttons but I can use them", both fixed in `Addon_Ports/` rather than here: OpenMapClassic's rail tabs are never dressed because `mod.FaceSideTab` ships in a file the release omits, and the OpenMap tab's own icon is absent from this patch chain. And pfQuest's minimap icons now fade with the minimap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
a32963b3f8 |
Find the frame that was eating every map click, and the realm LibStub behind it
A real pointer at the middle of the world map reached nothing: a hook on
`WorldMapButton`'s `OnMouseDown` counted 0 hits from a real click although the
button was shown, mouse-enabled and held the point. A Lua walk of every visible
mouse-enabled frame containing that point answered with exactly one frame, and
it was an unnamed 700x500 `FULLSCREEN_DIALOG` child of `UIParent` — AceGUI's
default container, carrying the title "SoftSpoken Stories" and no content. The
blank login popup grom has reported twice was parked over the centre of the
screen swallowing the map's clicks. One frame, both defects.
`hit_test_name` had been printing `<nothing>` there all along: it answers `None`
for an unnamed frame. Ask what holds the point, not what it is called.
The window is left shown because `AceGUI:Create` shows a container on acquire
and `self.frame:Hide()` is the last line of a `pcall`ed build step, so anything
raising in between leaves it up. The recorded reason was
`AceGUI-3.0.lua:282: attempt to perform arithmetic on local 'argc'`, and the
cause is the realm's own
`Interface\FrameXML\AIO\Dep_LibWindow-1.1\LibStub.lua`, loaded by its
`FrameXML.toc:162` before any addon, which hardcodes `minor = 2` where upstream
parses it — 1.12's Lua 5.0 has no `strmatch`. Every library therefore registers
as minor 2 and the first copy loaded keeps it, while `RegisterWidgetType` still
upgrades individual widgets by their real version: an Ace3v core
(`Fire(self,name,argc,…)`) hosting modern widgets (`Fire(self,name,…)`), whose
first payload value lands in `argc`.
Client side, one real divergence came out of it: `string.split` answered a table,
because BigWigs' `Plugins/SpellRequests.lua` defines `function string:split` and
never calls it, while Ace3's `AceConfigDialog-3.0` and `TreeGroup` both spell
`("\001"):split(uniquevalue)`. `string.split` is now `strsplit` — same function,
same argument order — in the beyond-1.12 string family, with a test pinning the
Ace3 call shape and the arity.
Also: `Step::Drag` moved the cursor eight times without advancing a frame, and a
canvas pans from its `OnUpdate`. It now ticks per move, and the same drag walks
0.5000,0.5000 -> 0.7582,0.3073 -> 0.5080,0.5161 — exactly what driving
`OnMouseDown` by hand produced. The frame between the moves is the gesture.
Four worth recording as dead ends, each of which looked like a client bug: a
verbatim replica of the LibStub file registers correctly; `strfind(34,"(%d+)")`
coerces the number and captures `34` as the reference does; the live function's
`getfenv` is `_G` with every global correct; and a known-good `NewLibrary`
installed on the live table stores the right value. `debug` is absent here, so
the way to ask a Lua function where it came from is to make it raise.
Addon-side edits (gitignored live copies, mirrored to Addon_Ports): the `Fire`
arity shim in both Ace3v AceGUI copies, `issecretvalue` guarded in
OpenMapClassic's vendored Heading widget the way its own Core.lua guards it, and
BigWigs' unused `string:split` removed.
Verified: SoftSpoken `initializationErrors` 0 and `shown=0` at login, nothing
over the map, `--drag` pans it, all three registered options tables open clean
(MasterTradeSkills 4 labels, OpenMapClassic 83, SpokenQuests 6), tabs switch
pages with no raise, session errors 0. fmt and clippy clean; benilla-ui 2119
pass; benilla-app 12 failures, the recorded pre-existing data-golden baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
b7a8e1c406 |
ui: a Slider answers SetEnabled, because OpenMap's options tab called it unguarded
Rendering a panel is not the same as it working. OpenMap's options menu opens 500x500 with 104 frames and 49 placed labels, and its Look tab died on click: Map/Panel.lua:1084, attempt to call method SetEnabled (a nil value) -- so a third of its options was unreachable. SetEnabled is already composed here for Button, CheckButton, EditBox and LootButton, and was deliberately withheld from a Slider, because addons duck-type on the enabled family to tell a control from a bar. What they duck-type on is the TRIO (if widget.IsEnabled then), that is what tests/slider.rs pins, and the trio is still absent; SetEnabled is beyond 1.12 by construction, so a Slider answering it invents no 1.12 behaviour. OpenMapClassic calls it unguarded on every options row with a condition, which is what settles it. On a Slider it writes the mouse -- input refused, art not greyed, which 1.12 has no slot for; the caller dims it itself on the next line. The argument goes through GetBoolOrDefault, not Lua truthiness, because DoiteAuras writes SetEnabled(ok and 1 or 0) and 0 is truthy in Lua. Also records a count that was wrong until checked: grep says 39 calls across five addons, but AdvancedTradeSkillWindow2 and SoftSpoken define their own SetEnabled and SoftSpoken's slider row is fully guarded. The real unguarded widget calls are OpenMap's three and DoiteAuras' two -- and SoftSpoken's blank settings page is NOT this, and is still open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
2281179874 |
capture: a setup slot, for the moment before the scene ages
$WOW_CAPTURE_SETUP runs a chunk at the first aging frame -- early enough to change a setting or create a frame and still have the scene settle around it. $WOW_CAPTURE_EVAL keeps the shutter, where a read and the pixels it explains come from the same frame; anything created there is a frame too late to be in the image. The deep-zoom map shot needed it. OpenMapClassic resolves minimap tiles by FileDataID by default and this client has none, so its own status reports "loaded YES=0 NO=8"; it already carries a path mode for exactly that, asking for textures\Minimap\<folder>\map<col>_<row>, of which this install ships 10,884. Switching to it raised, because S.ResetMinimapTiles reads lodPieces/lodPool sixteen lines before they are declared and so bound them to nil globals -- an upstream bug, and the only two callers are the two switches that reach the mode. Port edits mirrored to Addon_Ports/openmap_port/: the declaration moved above its use, and the tile mode defaulted to the one that can work here. Captured at zoom 40: the minimap's terrain draws through the map. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
62c019e744 |
blp: a short colour map is a sliver tile, not a corrupt file
Seven of the client's own Interface\WorldMap textures never decoded. They are the last column or row of an exploration overlay -- a few pixels wide, a handful of colours -- so they store a SHORT BGRA palette and end: RuinsOfThaurissan4.blp is 404 bytes in all, LakeEverstill6.blp is 276. The parser sliced the nominal 256 entries and refused anything shorter. A refused decode is a quad that draws nothing, and until this week a texture that drew nothing also said nothing, so the symptom was a hole in a zone's revealed art -- indistinguishable from not having explored it. It surfaced only once the stitched world's geometry was right and the render reached those tiles. The palette is now as much of it as the file has, and an index past the end is transparent black, which is what an unused index means. every_world_map_texture_in_the_chain_decodes walks the folder: 3553 textures, 7 failures before, 0 after, and it fails on the old decoder. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
6c01be926d |
map: a map's rect on another map describes art, not a click box
C_Map.GetMapRectOnMap answered, for a continent on the world sheet, the world-level CLICK box -- continent_sheet_rect over the WorldMapContinent tile bounds, which trace the landmass. That is the reference's own 0x4a5d00 and right for what 1.12 uses it for; the two boxes are disjoint so a world-level click can tell the continents apart. It is the wrong answer for art. A continent's map is a 1002x668 sheet with the land inside it and ocean around, so its art rect is 792x528 px on the world where its click box is 300x564. Every 1.12 WorldMapArea rect is 3:2, because every sheet is, and the click box is 0.355 of that. A zone's rect on its continent was already a yard un-lerp and correct, so the error entered once and multiplied through every zone: Dun Morogh landed 42x79 where its art is 1002x668. A stitched world built on that draws each zone a third as wide as it should be, which is why the canvas was grey between the zoom steps. map_proj::continent_art_rect carries the continent's own WorldMapArea rect through world_uv, the projection a position on it already takes. The view keeps both rects; the Era map graph publishes the art one. Measured after: EK, Kalimdor, Dun Morogh and Elwynn all 1.500. Rendered: the zone view stitches its neighbours in, zoom 3 is the continent with its zones, zoom 1 is the world sheet. The stock 1.12 map is unchanged. Three instruments came with it and are kept: $WOW_CAPTURE_EVAL, a chunk run in a capture's VM at the shutter; $WOW_CAPTURE_EXPLORED, so a stitched map's fog can be ruled out in one run; and a named warning for a UI texture that is not in the chain, which is what refuted the other hypothesis -- a grep said the addon's art was missing, a real render asked for zero missing files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
3e8c995e27 |
map: clamp the canvas onto whatever zoom ladder is in force
CreateZoomLevels is the extension point a canvas addon replaces wholesale, and OnLoad seeds a scale (1) before any addon has run. OpenMapClassic's ladder is 27 steps from 0.0125 to 0.899 -- its whole range below this file's own 1, because its stitched world is reached by zooming out past a single zone -- so the canvas sat above its own ceiling, and ZoomIn, which walks the ladder by value, found no step at all above it. ClampZoomToLevels is called from GetCanvasScale rather than from CreateZoomLevels, because CreateZoomLevels is exactly the function the addon will not have. Also records where OpenMapClassic stands, measured: 808 textures in the canvas subtree, 166 placed, spanning ~4600x3270 units of a 1002x668 canvas, with a ladder floor implying a world ~80000 units wide. The Era map tree under it answers correctly; GetMapArtLayers answering one constant for every map is the thread to pull next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
07dd5ba7e1 |
capture: a shot taken of a player's config must not write it back
`$WOW_CAPTURE_SAVED` points the saved-variable READS at a real player's configuration so a capture reproduces their interface. The writes went to the same place, so taking the shot overwrote the configuration it was taken to study -- with a fixture character's state, since a capture logs in as nobody. Three of grom's files came back changed from one run, his pfUI chat scrollback among them. The shutdown tail returns before all four writes under $WOW_CAPTURE. Verified by md5 across a run: zero files touched, zero "wrote" lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
abb4dc1dd3 |
ui: a Slider's thumb had no rect, so no scrollbar could be skinned or seen
"Half of the info on the right is out of the frame" was a scroll frame with 699 of content in a 451 window, a scroll range of 228, and a scrollbar with nothing on it. The thumb's rect was only ever computed inside the draw pass and never published: the region sweep skips anchor-less regions and a thumb is anchor-less by construction, because its slider places it. That is invisible until something anchors to the thumb, and pfUI does -- SkinScrollbar clears the stock knob and hangs its own off the thumb's two corners, so every pfUI-skinned scrollbar in the interface was a bare track, which is a scroll frame that silently cannot be scrolled. The thumb is on the layout roster now, survives the unanchored prune, and carries its slider's (vertical, fraction) in its node hash -- without the last one a named touch moved no hash and seeded nothing, which WOW_LAYOUT_VERIFY caught as an incremental pass disagreeing with a full derivation. The draw pass keeps computing its own rect: extract takes &self and cannot resolve, so a SetValue with no resolve behind it still has to draw where the value now is. map: the window has two layouts and the canvas only knew the maximized one Pressing maximize left the art a 1002x668 patch in a full-screen window and minimize could not put it back, because the container was anchored once at build to WorldMapPositioningGuide TOP -502,-69 -- 1.12's maximized anchor against a fixed 1024x768 guide. Read as insets instead (15/4/33/20 windowed, 10/12/69/31 maximized) the same numbers reproduce 1.12 at 1.12's sizes and fit any other window, which is also the left-edge bleed: -502 against a guide stretched to pfUI's 1017-wide window starts the art 6.5 px in, not 10. The zoom ladder's floor follows: it was the constant 1, true only while the container was the art's own size, and is the measured fit now. A view outside a rebuilt ladder is pulled onto its nearest end, since ZoomIn and ZoomOut walk it by value -- OpenMapClassic's whole ladder sits below ours. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
41512a04ef |
map: 1002x668 is a contract, and the canvas counted the windowed scale twice
Every 1.12 map addon places its art in absolute pixels against WorldMapDetailFrame's 1002x668 — pfUI's reveal does it one SetPoint per tile — so that size is a contract, not a size. 1.12's windowed mode puts WORLDMAP_WINDOWED_SCALE (0.7) on the frame and on WorldMapButton; under the canvas the container already carries the window, so that scale was a second authority on the same rect and, with the frame SetAllPoints to the canvas, came back as unit size: 1002 / 0.7 = 1431. Everything absolute inside it landed at 0.700 of where it belonged, which is grom's screenshot drawing Coldridge Valley twice, and the mouse surface kept its XML 1002x668 beside 1431 units of art, so clicks read 70% of the map. SeatOnCanvas makes the canvas the only authority: both frames to scale 1, the frame filling the canvas, the button given the art's rect — explicitly sized, because pfUI reads WorldMapButton:GetSize() to size the whole map window. Re-run after Minimize/Maximize and on every canvas resize. Rescaling the children this file knows the names of could never have fixed it; the children it does not know the names of are the point. saved-variables.lua now goes through saved_home, so a capture with $WOW_CAPTURE_SAVED opens the player's windowed map — the only mode the bug exists in, and the reason neither harness had ever seen it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
d11b9ac3ae |
map: the art was scaled by the canvas while living in the detail frame
Grom's report was "textures are not where they should be", and still wrong with OpenMapClassic switched off — which is what made it ours rather than the addon's. His own session log had both states 1.4 s apart, same map, same frame: detail 1431x954 with tiles 366x366 (correct, 1431/1002 = 1.428), then the same frame with tiles back at the XML's raw 256x256. `ScaleDetailArt` was being handed the CANVAS's size by `OnCanvasSizeChanged`, on the assumption that the detail frame is `SetAllPoints` to the canvas. 1.12's own windowed mode breaks that: `WorldMapFrame_Minimize` puts `WORLDMAP_WINDOWED_SCALE` (0.7) on `WorldMapDetailFrame` and `WorldMapButton` and insets the frame to TOPLEFT 15,-33, so the same screen area is a different number of units in each — his trace reads `detail 1431x954 scale 0.56` inside `WorldMapCanvas 1002x668 scale 0.80`. Scaled by the canvas's number every tile came out 1.43x too small: the art covered 1024x768 of a 1431x954 frame, the rest bare parchment, and the exploration overlays scaled to match landed nowhere near their terrain. The detail frame's own size is the authority, since the tiles are its children. `GetLeft()` first to settle the layout, because that size is derived and an unforced read answers the size from before the last change; the caller's w,h stay as the fallback for what cannot be measured. Reproduced and controlled in the harness with the windowed scale applied as the stock code applies it: tile 362 where 362 is wanted, against the control's 256 — the number his log recorded. Two earlier attempts are written up in docs/ADDON_COMPAT.md as dead ends, because both looked right: caching the canvas size prefers the wrong quantity, and converting it by the effective-scale ratio gives 366 against a true 362, since Minimize also insets the frame. `$WOW_CAPTURE_SAVED` now reaches the enable-state file too, so a capture reproduces which addons the player has turned off — without which "I turned it off and it is still wrong" cannot be answered offline at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
5e2f1f3ac8 |
ui: SetClipsChildren, the verb an addon asked for by name and was refused in silence
Era's map canvas is built on one verb this client did not have. Its `ScrollContainer` is a plain Frame with `SetClipsChildren(true)`, and everything below it is cropped to that rect. `assets/ui/MapCanvas.lua` substituted a real ScrollFrame, because this client crops a ScrollFrame's scroll child and every descendant of it — which covers the canvas and nothing parented to the container beside it. On Era that distinction does not exist, so an addon has no reason to respect it. OpenMapClassic parents its pin layer exactly there and asks for the verb by name, behind an `if` (`Map/Stitch.lua:1070`), so the absence was not a raise. Measured on grom's own config with the map open: the container sits at L=1151 R=2153 and its seventeen direct children span L=-765 R=2153 — about 1900 units off the left edge, cropped by nothing. Implemented as a frame flag with `SetClipsChildren`/`DoesClipChildren` and the `clipChildren` XML attribute; `effective_clip` now intersects the rect of every ancestor that sets it alongside the ScrollFrame rule, so the two nest and compose. An ancestor rather than the frame itself: the verb clips a frame's children, and stock art overhangs its own frame on purpose. `MapCanvas.lua` sets it on the container too, so a layer parked there is cropped whether or not its addon thought to ask. Not claimed as the whole of grom's map report: the screenshot behind it has not been reproduced offline, because a capture's view is not the view his session was in. This is the part that was provably drawing uncropped. Also here, both from the same play-test: - `C_Spell.CancelSpellByID` answered with `SpellStopCasting` only. pfUI's buff bar cancels with it on a right click where 1.12's calls `CancelPlayerBuff`, so with nothing in flight the click did nothing and said nothing — grom had never been able to right-click a buff off on this client. It now finds the id among the player's own auras and queues it on the same CMSG_CANCEL_AURA path, behind the same `cancel_authorized` gate, and falls through to the cast only when there is no such aura. - `$WOW_CAPTURE_SAVED=<Realm>-<Character>` points a capture at a player's own saved variables. A capture is hermetic by design, but with addons and no saved variables it renders an interface nobody is configured to be looking at — which is why the world map came out correct in a capture while being wrong on his screen. It doubles as the identity because a capture never logs in and `pfUI_config` is SavedVariablesPerCharacter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
afc5379911 |
auras: the player's first buff of a session was swallowed as a first-time diff
DoiteAuras reads the whole aura block once at PLAYER_ENTERING_WORLD and from then on keeps its
table purely from nampower's four self-aura events (`Modules/DoitePlayerAuras.lua:403-490`;
`UpdateAuras` is called nowhere else). The arrival loop here seeds a unit's memory and fires
nothing, which is right for a unit streamed in mid-aura — but it only ever seeded the harmful
half, while the comment above it claimed it seeded the player's helpful half too ("without this
its first real edge would be swallowed as a first-time diff, which is one lost BUFF_ADDED_SELF
per login"). The guid was destructured away as `_guid` and `own_buffs_seeded` was set nowhere but
in the edge loop.
So the player's first buff edge of every session reported nothing. That does not heal: a later
cast of a buff that is still running refreshes its slot with the same spell id, which is not an
edge this watch sees at all, so nothing reports the aura again until it drops completely. A
warrior's Battle Shout, re-cast while it still has a minute to run, never drops — grom's rule for
it read "missing" with the buff plainly up, all session.
Every other link was measured clean first: `GetUnitField("player", "aura")` answers a table, the
arg vector matches what the handler reads (arg3 spell id, arg4 stacks, arg6 raw slot, arg7 state),
and all seven ranks of Battle Shout resolve through both `GetSpellRecField(id, "name")` and
`GetSpellNameAndRankForId`.
The pin asserts both halves — the first buff is an add, and a refresh is not an edge — and its
control is worth noting: with the seed removed the first buff reports `[]`, not a wrong value,
which is why no session log ever named this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
5ed9dded4c |
map: the Continent and Zone pickers were blank on the first map of a session
The realm's `WorldMapFrame.xml` fills them from exactly one place: the Zone dropdown's `<OnEvent>` on `WORLD_MAP_UPDATE` (:321), which re-runs both pickers' initialisers and then the two `WorldMap_Update*DropDownText` functions. That dropdown registers the event in its own `<OnShow>` (:318 — the realm commented the `OnLoad` out and moved it there), while `WorldMapFrame`'s own `<OnShow>` fires it through `SetMapToCurrentZone()` (:711), and a parent's OnShow runs before its children's. So the inline event landed in a gap where its only listener did not exist yet. Measured through the addon harness: the Zone dropdown answers `IsEventRegistered` nil before the open and 1 immediately after, and the pickers come up blank on the first map and correct on the second — which is what grom's recording shows, blank over Dun Morogh and filled over Elwynn later in the same clip. `SetMapToCurrentZone` now queues a second `WORLD_MAP_UPDATE` on the deferred lane as well as firing the inline one. The inline fire stays: it is what repaints the detail tiles inside the OnShow rather than a frame later (the world-map flash). Only this verb doubles up, and only this verb needs to — `SetMapZoom` is a click on a map that is already open. The first attempt at this was reverted and the reason is in the test's own comment: its pin opened the real map on the shipped interface and passed with *and* without the fix, because the stock ordering happens to be fine and the symptom needs the addons. The pin that replaced it asserts the engine half directly — a handler that registers after `SetMapToCurrentZone` returns still receives the event — and answers 0 without the change and 1 with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
ae929e8a81 |
map: the art trace reads the tiles the repaint put up, not the ones it replaced
The line fired in the frame the selection changed, and the repaint that fills the tiles is Lua's own off a queued `WORLD_MAP_UPDATE` — so it read the map that had just left. That is how "the map shows Dun Morogh and the tiles are Azeroth's" got into a log with nothing wrong behind it. It now waits three frames, restarting the wait if the selection moves again. docs/ADDON_COMPAT.md records this round: the cursor position no verb ever wrote, the per-kind nameplate CVars a nil answer greys out, the capture lever above with its two limits measured rather than assumed, and one open item — the map's Continent and Zone pickers, blank on the first open with the addons loaded and fine without them, with the candidate engine fix rejected because the test for it passed either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
ba11fc3315 |
capture: WOW_CAPTURE_ADDONS=1, to put an addon's own drawing in front of the renderer
A map report could be answered from neither harness: `addon_harness` runs the VM with no renderer, so it can say what a region is and never what it looks like, and the capture harness renders for real but is hermetic — `local_state::home` answers None under $WOW_CAPTURE, so no config, no saved variables and no addons, which is what makes a baseline comparable between machines. An addon that draws its own art was therefore unseeable without a server, and the only evidence was a player's screen recording. `WOW_CAPTURE_ADDONS=1` opts the player's addon folder back in for one run — `ui_script::addons::root`, the single narrow exception, with saved variables still out, so an addon runs on its shipped defaults. The world-map fixture will also ask a stitched-map addon for a view through its published API, retrying until the map is stitched (`WOW_CAPTURE_MAP_ZOOM`, `WOW_CAPTURE_MAP_AT`). `scripts/visual.sh` never sets any of it, so no baseline sees an addon. The first shot taken this way answered the open question: with OpenMapClassic loaded this client draws the stitched Dun Morogh map correctly, art, neighbours, labels, lore panel and player dot — so the map report is not the art pipeline. Also corrects a stale comment: the shot has never been Elwynn; continent 2, zone 10 is Dun Morogh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
d39d3ec5cf |
cvars: the later clients' per-kind nameplate switches, so an addon's rows are editable
Cinematic's Nameplates page asks `GetCVar(name) ~= nil` before it will offer a row (`Effects.lua`'s `HasPlateSettings`). A nil answer is not "off" to an addon written for a later client — it is "this client has no such setting" — and the whole row goes grey, which is why grom could not touch Friendly NPCs. The two rows that worked were the two that happen to name `nameplateShowEnemies`/`nameplateShowFriends`, the only nameplate CVars this client had, because 1.12 has none at all. Seventeen rows registered. Sixteen are live: friendly players, friendly NPCs, hostile players, and each side's pets/guardians/totems/minions — eight names on the friendly side and four on the hostile one, each side driving one field, because a 1.12 object update carries an owner and not a kind of owner and an addon writes the whole set together. `nameplateMaxDistance` joins `NameplateRange` and `NP_NameplateDistance` as a third name for one reach. All default "1", the least-restrictive value, for the reason `NameplateRange` gives: the two masters already decide whether plates show at all, and a refinement shipped off would take away plates a player has today. Paired against `VPlateMode::default()` in the defaults test, the pairing the derived-Default trap forced on `occlusion`. Five are registered and inert, each saying so on its own row and in the orphan test's list: `nameplateShowAll` (1.12 has no in-combat-only mode), `nameplateShowSelf` (no plate over the player), `nameplateShowOnlyNameForFriendlyPlayerUnits` and `nameplateForceShowUnitName` (no such plate mode here), and `nameplateShowEnemyMinus` (the plate path has no creature rank). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
36f504820d |
ui: GetCursorPosition answered 0,0, so nothing could drag or size a frame
`Model::cursor_pos` is read by the verb in two places and was written by nothing in the workspace: declared, initialised to the origin, never assigned. So `GetCursorPosition()` answered `0, 0` for every call of every session. That kills the universal drag-and-size idiom — stamp the cursor on `OnMouseDown`, subtract it each `OnUpdate`, size by the difference. Every delta came out zero, the frame was re-anchored to its own current size sixty times a second while the player dragged, and the addon's own "that was a click, not a drag" guard then declined to save. Nothing raises and nothing logs. grom reported it twice as OpenMapClassic's map window refusing to resize; the round before this one painted its invisible grips and left this underneath. Fed in `ui_script/input.rs` beside `set_mouse_buttons`, for the reason that one states: the pointer feed below is skipped for a cursor off the window and for a UI hidden by TOGGLEUI, and the reference answers through both. UI units, y-up from the bottom-left, undivided — UIParent is scale 1 here, so an addon's `x / frame:GetEffectiveScale()` lands in the frame's own units as on the reference. The test pins the whole idiom rather than the getter, because the getter answering *something* was never the question. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
204184afb2 |
ui: the map's own art line, in every session's log
A play report about how the map looks arrives without the diagnostic that answers it, and the only way to ask for the line again is to wait for the player to play again. It cost a round trip on 2026-11-02, so the trace is unconditional now: one line per world-map selection change, with the frame the detail is drawn into added beside the detail frame's own rect, since a canvas zoom and an oversized tile change the picture the same way and only the two together say which one did it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
7f97777597 |
ui: the stock map art has to follow the detail frame it is drawn into
`WOW_MAP_TRACE=1` named it on grom's first run, in one line:
detail 1431x954 @scale 0.70 | 1 256x256 @(10,1122) | 2 256x256 @(266,1122) | ...
1.12's `WorldMapDetailFrame` is always 1002x668, so everything drawn into it is placed in absolute
pixels against that: twelve 256x256 tiles anchor-chained from its TOPLEFT, each exploration overlay
at the offset `WorldMapOverlay.dbc` gives it. The canvas breaks that the moment it sizes the frame
to anything else, and it does — `OnCanvasSizeChanged` falls back to the container's own rect
whenever it has no art layer to read, which is every moment the canvas has no `mapID`, including
the world-view blip the same log shows on every map open.
So: a 1024x768 sheet of art pinned to the top-left of a 1431x954 frame, covering 72% of the width
and 80% of the height, with the rest of the map drawing nothing. That is the grey he reported,
with the art magnified relative to what was left of the frame.
The art is now rescaled to whatever size the frame has. The tiles need only their size set, because
the XML chains them TOPRIGHT-to-TOPLEFT and BOTTOMLEFT-to-TOPLEFT. The overlays carry absolute
offsets, so they are captured from what the stock repaint last wrote — hooked on
`WorldMapFrame_Update`, the only moment those base values are readable — and scaled from that base,
so a second pass cannot compound. Measured: a 1431x954 frame now holds a 1462x1097 grid, the same
1024/1002 and 768/668 proportion the reference frames.
Two smaller faults fell out and are fixed with it: the container resizing was not treated as a
canvas size change at all (the canvas was re-sized only on a map change), and a derived resize
fires no `OnSizeChanged` here, so the rescale takes the size its caller just set rather than
reading the frame back, which would answer the old one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
e62327424f |
app: WOW_MAP_TRACE, because no instrument here can see the map
A player's report about how the map *looks* is the one question this side cannot answer. The offline addon harness seats no renderer, the capture harness seats no UI or addons, and a scripted login would kick the player off their own account — so a full audit (art present and correct, catalog right, canvas geometry right, overlay tiling arithmetic right, addon setup complete) still could not say why grom's Dun Morogh map came up magnified, clipped and grey. Every number that decides the picture is readable from the VM. `WOW_MAP_TRACE=1` prints them once per map change: the detail frame's rect and effective scale, then per tile its rect, the texture path it holds, and the decoded size of that file. The file size is measured through a scratch texture with no size of its own, because the tile declares 256x256 in `WorldMapFrame.xml` and would answer that whatever the file holds — and this install's map art is a 512 upscale pack, which is exactly the disagreement worth seeing. Off unless the variable is set: it runs a Lua chunk per map change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
c93a0be93f |
ui: a nameplate now carries the unit token its own event names
`plate.namePlateUnitToken` was set on nothing. It is how every later-era plate addon gets from a frame back to its unit — `plate.namePlateUnitToken or (plate.UnitFrame and plate.UnitFrame.unit)`, six sites in `Cinematic/Effects.lua` and `PlateParts.lua` alone — so an addon walking `C_NamePlate.GetNamePlates()` had a frame's rect and nothing about its unit, and could not tell a friendly plate from a hostile one. The measured symptom was grom's friendly nameplates switching themselves on the moment CineMode started, on a profile whose every friendly-plate box is unticked. That CVar write is the addon's and it is deliberate: outside instances Cinematic switches every plate kind on so visibility is decided by *fading* instead, because fades can change in a fight and plate settings cannot. Having switched them all on, it then faded none of them, because it could not identify one. The token already existed — `script/nameplate.rs` has had the slot-bound `nameplate<N>` tokens and the three plate events since the pool was built. All that was missing was publishing the slot's token on the frame: set before `NAME_PLATE_UNIT_ADDED` and cleared after `NAME_PLATE_UNIT_REMOVED`, so a handler for either event reads the field that event is about, and a pooled frame reused for the next unit cannot answer the departed one's token. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
6e8c4488e1 |
ui: the override-binding family exists and says what it does not do
`ClearOverrideBindings` was absent outright, so `Cinematic/Camera.lua:470` raised in grom's chat on every camera shot that ended — and a raise mid-handler costs the rest of the handler, not just the line. These are 2.0's secure-binding layer: a second, unsaved binding table a frame owns, outranking the saved one while that frame wants it and dropped whole by `ClearOverrideBindings(owner)`. This client models the saved table only (`script/keybind.rs`), so: - the five setters are no-ops that **warn once each** through the script warning channel. Silence was the alternative and it is the worse one — a setter that quietly binds nothing is the same class of fault as a hook that quietly arms nothing, and the caller cannot find out. - `ClearOverrideBindings` is a plain no-op and does not warn. Dropping a table that was never filled *is* nothing, and warning there would fire on every `ClearRecast()` while the player does nothing unusual. The module header lists what a real layer needs — an override table keyed by owner in `Model`, a generation bump for `bindings.rs`'s `sync_dispatch`, an override arm in `BindingDispatch::map` that wins over the saved chord, and a `Bound::Click` — together with the `SecureHandler*` half in `secure.rs`, which is the same table seen from inside a snippet and no-op for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
bb2e40d1dd |
ui: GetDebugName, the name an unnamed frame still answers
`Cinematic/Frames.lua:3734` is `entry.name or entry.frame:GetDebugName()` — an addon that keeps a list of frames reaches for this precisely because some of them have no name, which is the one case `GetName` cannot serve. With the method absent, `/cine debug frame <name>` raised on the first unnamed row in the list instead of printing the row the player asked for. On all three method tables, since the era puts it on ScriptObject. A named frame answers its name, so a feature test on it learns nothing a 1.12 client would answer differently. The unnamed leg deviates in its fallback only: it walks the parent's own fields for the key the child is stored under (frames here are plain tables, so `parent.icon = tex` is visible) and answers `Parent.<unnamed>` when there is none, where the era answers an index among the parent's children — an index here would be the order of a list this client does not keep, and a wrong number reads worse than an honest absence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
09aca72c46 |
ui: hooksecurefunc over a widget method has to read through the metatable
The table form read the field with `raw_get`, on the reasoning that a field
reached only through a metatable is not that table's to replace. A widget
carries none of its own methods — they live on the shared class table its
metatable's `__index` names (`object.rs`'s `frame_meta_for`) — so
`hooksecurefunc(frame, "SetAlpha", cb)` found nothing there and armed nothing,
silently. Measured: **0 fires across 2 `SetAlpha` calls**, while the plain-table
and global forms fired correctly.
That is not a corner of the form, it is what the form is for. 29 files in grom's
corpus hook a widget method, and the one that shows: `Cinematic/Frames.lua:459`
hooks each pfUI frame's own `SetAlpha` so that Cinematic can **follow** pfUI's
out-of-combat fade rather than fight it ("their frames have SetAlpha followed
straight away by ours, taking what it set as the new base"). With the hook
unarmed the two wrote a different alpha to the same frame on alternating frames
— which is grom's "still flashing my unitframe when cinematic is on".
The reference reads `table[name]` plainly and assigns the wrapper back, which
*shadows* the class method for that one object and leaves its siblings alone.
This now does the same: read through `__index`, write raw onto the host, which
is the same thing a plain assignment would do since frame metatables carry no
`__newindex`. A test pins both halves — the chain fires on the hooked frame, and
a sibling of the same kind is untouched, with nothing written to the class table.
Corpus roster byte-identical (87 ok / 1 fail) with all 29 files' hooks armed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
aa2ee515aa |
ui: MOD multiplies, and a gradient is not an alternative to a fill
Cinematic's colour tint painted the whole screen opaque white for as long as its
CineMode ran: `srgb(255,255,255)` across the world with the UI still drawn on top
of it, 16.8 s of it in grom's 2026-11-02 recording, starting the instant he picked
a profile. Three faults on the one path from the addon's call to the pixel, each
of them enough on its own.
`SetBlendMode("MOD")` parsed, answered `GetBlendMode` and then drew as straight
alpha. One pipeline served BLEND and ADD — the shader emitting `(rgb·a, a)` or
`(rgb·a, 0)` into a single `(One, OneMinusSrcAlpha)` state — because no scalar
source alpha can stand in for a per-channel destination factor, and the mode had
no caller worth a second pipeline. It had one: nine of the addon's ten tint
presets are MOD (`Tint.lua:21`), and a MOD colour is built to be multiplied —
`1 − (1 − c)·strength` at **alpha 1**, white at strength 0 — so drawn with alpha
instead it is an opaque near-white quad over UIParent. MOD now keys its own
pipeline, `(Dst, Zero)` with the destination's own alpha kept, off the same
`0x70c190` table the world's Mod batches already read; the shader returns the
multiplier un-premultiplied, since neither alpha appears in that formula, and runs
no longer merge across the flag.
The extract read `fill.or_else(|| gradient.map(midpoint))`, so a region that set
both drew the fill and dropped the gradient. They are not alternatives:
`<Gradient>` writes the four vertex colours (`+0xb8`, `0x77304d`), which
`loader/regions.rs` already reads as "unlike `<Color>` it tints the art". The tint
is exactly that shape — `SetColorTexture(1, 1, 1, 1)` once, re-gradiented a few
times a second — and so is its vignette, whose four edges were drawing as opaque
white bars.
And `SetGradient` took only 1.12's six loose numbers. This client answers
`CreateColor`, so an addon that feature-detects takes retail's colour-object form
and never tries the numbers; the two objects coerced to six zeros through
`as_f32`, the call **succeeded**, and the `pcall` that was the feature test
(`Tint.lua:546`) reported it understood. Both spellings now read a colour object
when the first stop is a table.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
33d0868009 |
ui: SetEnabled, the one setter retail put in place of Enable/Disable
A later-era addon writes `SetEnabled` **unguarded on its own controls**, even where it guards on someone else's. `Cinematic/Options.lua:4176` is a tick's own `Refresh`, greying it when a rule owns the row; `:305` twelve pages up is the same file writing `control.SetEnabled and …` about a control it did not make. The raise at 4176 unwound `RefreshOptions` → `SettingsChanged` → `SetProfile` → `Welcome.lua:43`, so picking a profile from the addon's welcome window applied the profile and then abandoned the panel refresh, leaving the traceback in chat and the window still reading "Nothing runs until you pick one." That is the chat error grom recorded on 2026-11-02. Composed from the stock `Enable`/`Disable` pair (`0x780160`, `0x7801b0`) on Button's table, which CheckButton and LootButton reach down the same chain the pair travels. EditBox takes it too, written out rather than composed because the reference gives a box no enabled state at all: it writes the mouse and keyboard the frame already owns and drops focus, so input is refused and the text is NOT greyed, which 1.12 has no slot for — `Options.lua:1629` is the call that needs it. A Slider still answers neither this nor the trio: addons duck-type on them to tell a control from a bar, which `tests/slider.rs` pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
70e4278c79 |
ui: the map's hovered-zone label is chrome, not map content
The canvas reparents 1.12's `WorldMapButton` into the zooming child so that every 1.12 addon's icons pan and scale with the map. `WorldMapFrameAreaFrame` hangs off that button in the client's own XML — it covers the art, so that is where the label that names what the cursor is over was hung — and it went along with it. Its label is a 102 px THICKOUTLINE FontString. Inside the canvas it scales with the zoom, so at zoom 2.5 every letter is a 261 px glyph, and one zone name's worth of those is most of the 2048² glyph sheet. Measured in grom's 2026-11-02 session: one `ui_text: a 261.10907 logical-px request … past the 256 ceiling`, then 20 `glyph sheet full … resetting at the frame boundary`, one per hover, each of them a frame in which every string on screen is missing — the map "flashing the interface". Frame cost with the map open went 11 ms → 16-20 ms over the same window. So it goes back on the window, anchored to the container's top, which is where the art's top edge is now; at rest that is the pixel it was already at. Measured through the harness: the biggest text under `WorldMapFrame` was 102 px at zoom 1 and 1632 px at zoom 16, and is now 102 px at every zoom. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
c6b19c7926 |
lua: a size this library derived is a cache, and a # append invalidates it
This is what froze his client. The bound named `Cinematic/Camera.lua:2597`:
while turnStarts[1] and now - turnStarts[1] > WINDOW do
table.remove(turnStarts, 1)
end
`remove` drains the table and remembers a size of 0 — faithfully, that is
`luaL_setn`. The addon then appends with `turnStarts[#turnStarts + 1] = now`,
which is a raw set this layer never sees, so slot 1 is full while the
remembered size is still 0. `remove` reads 0, takes its "nothing to remove"
path, changes nothing, and the caller spins forever. 15 s of Lua in every
frame, for the whole session, in a loop that in the reference cannot fail.
It cannot fail there because 5.0 has no `#` operator: every append goes through
`insert` or `getn`, so a remembered size is always the truth. The collision is
a later-era addon's `#` meeting 5.0's bookkeeping, and it also loses data the
quiet way round — `insert` after a `#` append writes *over* the appended slot
instead of after it, which is a test here now too.
So the two kinds of size are kept apart. One this library **derived** from an
`insert` or a `remove` is a cache: if `t[n + 1]` is not nil the table has grown
since, and the count wins. One an addon **declared** with `table.setn` is
honoured exactly as before, because `setn(t, 0)` as a logical wipe over full
slots is the entire point of it — AceLibrary and Mik's capture tables both turn
on that, and both still pass. An explicit numeric `t.n` field is a declaration
too, and is untouched.
Measured: the corpus roster is byte-identical (87 ok / 1 fail, the failure the
pre-existing AdvancedTradeSkillWindow2), and the drain loop that spun 44 times
in his log now ends in one removal a round.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|