Commit Graph
1795 Commits
Author SHA1 Message Date
gromandClaude Opus 5 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
2026-11-04 21:15:49 -07:00
gromandClaude Opus 5 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
2026-11-04 17:00:14 -07:00
gromandClaude Opus 5 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
2026-11-04 14:17:39 -07:00
gromandClaude Opus 5 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
2026-11-04 09:34:00 -07:00
gromandClaude Opus 5 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
2026-11-04 07:53:50 -07:00
gromandClaude Opus 5 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
2026-11-03 20:33:53 -07:00
gromandClaude Opus 5 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
2026-11-03 19:34:52 -07:00
gromandClaude Opus 5 c3a0315985 map: the three capitals with no traced outline drew nothing; feather them
OpenMapClassic declines to stitch a city with no MapCityMask and names
the three it means -- Stormwind, Ironforge, Darnassus -- so zooming into
the Alliance capitals showed the zone's painting of them and never their
own map. The stated worry is a hard box.

PlaceBase takes the outline and the edge feather in the same argument,
and the city branch passes MapCityMask\<id> as the feather, so a city
with no outline can go through the slot its outline would have used. A
feather is the answer to a hard box. Captured at zoom 30: Stormwind's
districts, harbour and Valley of Heroes, blended into Elwynn.

The trace was attempted first, and this is the record so nobody re-runs
it: unlike the zones' land masks there are three shipped city masks to
fit against, so it is a fitting problem. Local-contrast detail,
threshold, close at quarter scale, largest blob, holes filled, dilate,
blur, swept over the three parameters -- best IoU 0.58, and it swallows
the sheet's own torn frame, because a city sheet is busy out to its
border where Orgrimmar's is blank parchment. A 0.58 hull shipped as a
trace would be a guess in an artist's clothes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-03 19:09:22 -07:00
gromandClaude Opus 5 758665d4d8 map: a 1.12 zone sheet is opaque, so the feather belongs on
What was left after the stitch's geometry was hard rectangles at the
zone zooms. Bisected with OpenMapClassic's own /omc layer switches:
baked and gaps draw nothing on this build, zoneart draws them all.

A zone's sheet is masked to its land only for ids in ZoneMaskIDs, the
contiguous Classic range 1411-1452. On this realm that leaves
thirty-four without one -- every city, the dungeons, and every zone the
realm added of its own. Cities already decline to draw unmasked; zones
do not, and Gilneas puts its whole sheet, painted sea and torn parchment
border, over Silverpine.

PlaceBase takes a feather for exactly this and the zone walk passed nil
behind `(not era) and FEATHER`. Sound on Era, where a C_Map zone texture
carries a transparent surround; not true here, where the realm's sheets
are alpha 255 across every texel. The port passes it. PlaceBase prefers
the land mask where there is one, so the forty masked zones are
untouched -- 0.57% of pixels moved in a Dun Morogh view.

scripts/make-map-feather.py generates the three masks as 256x256
uncompressed TGA. A feather is a smoothstep ramp, which upstream's own
NOTICE calls generated rather than cut from art. A per-zone LAND mask
was deliberately not attempted: on this art sea and land do not separate
by colour (a sea patch reads R-B +28, a land patch +34) and the realm's
zone bitmap is 13x13 cells for its largest zone. Either would be a guess
dressed as art.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-03 18:17:14 -07:00
gromandClaude Opus 5 7cb45769c5 docs: the OpenMap zoom ladder end to end, and carrying the tile mode to an existing save
A default only reaches a fresh install, and grom's OpenMapClassic.lua
pins minimapTileMode = "fdid" -- the one value that cannot work on a
client with no FileDataIDs. OpenMapClassic has a migration hook for
exactly this, so the port edit adds a one-shot flip beside its own six,
flagged so an explicit /omc tilemode fdid afterwards is respected.

Measured through the real wheel handler: thirty clicks out reaches
0.0330, the ladder's floor and the whole world; forty back in reaches
2.3734, its ceiling; 27 steps between. Captured at zoom 1, 3, 9 and 40
with his own settings and no setup chunk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-03 17:07:12 -07:00
gromandClaude Opus 5 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
2026-11-03 17:03:21 -07:00
gromandClaude Opus 5 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
2026-11-03 16:54:06 -07:00
gromandClaude Opus 5 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
2026-11-03 16:42:09 -07:00
gromandClaude Opus 5 388db114fc docs: what OpenMapClassic is for, and why the canvas is grey between zoom steps
Two recordings made the target concrete: one continuous stitched world,
wheel from Azeroth through continent, zone, city, dungeon floor, no mode
change and no seams. Ours navigates correctly -- breadcrumb coordinates,
lore panel, Find me, zone crossing -- and draws the continent step, and
is grey everywhere between.

Grey is "the tile files were never asked for": PlaceTiles stores
tex.omFile instead of setting it, and ShowZone only queues it when
inView[mapID] == true, which S.Cull writes from a view box built out of
GetCurrentScrollX/Y, GetCanvasScale, the container's size and
GetMapArtLayers(947). Four of our verbs and one constant art size decide
whether a zone's art is ever loaded.

Also records the third BetterCharacterPanel port edit: the stats column
pinned to the panel's own backdrop, so the overflow is reached by
scrolling inside the window instead of hanging over its edge -- window
359, content 699, needs 340, bar reaches 340.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-03 15:32:50 -07:00
gromandClaude Opus 5 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
2026-11-03 15:05:14 -07:00
gromandClaude Opus 5 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
2026-11-03 14:34:31 -07:00
gromandClaude Opus 5 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
2026-11-03 14:27:51 -07:00
gromandClaude Opus 5 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
2026-11-03 13:03:17 -07:00
gromandClaude Opus 5 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
2026-11-03 10:54:54 -07:00
gromandClaude Opus 5 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
2026-11-03 09:46:32 -07:00
gromandClaude Opus 5 c9756de381 docs: the anchor pfUI's save dropped, the seed that was only a comment, and the fade
Four sections, three of them for changes that cannot be committed because `benilla-config/` is
gitignored (it carries the credentials). Those live on disk and in the mirrors:

  Addon_Ports/pfui_port/pfUI/api/api.lua           AnchorOffsets + SaveMovable/LoadMovable
  Addon_Ports/pfui_port/pfUI/modules/autofade.lua  new module
  Addon_Ports/pfui_port/pfUI/modules/gui.lua       the "Auto Fade" page
  Addon_Ports/pfui_port/pfUI/init/modules.xml      the include
  Addon_Ports/openmap_port/OpenMapClassic/Map/Window.lua   SetClampedToScreen either way

- The map opened with nothing on screen because pfUI's `SaveMovable` kept three of `GetPoint`'s
  five values, dropping `relativeTo` and `relativePoint`, and `LoadMovable` replayed the offsets
  against the frame's own point. OpenMapClassic anchors the map TOPLEFT to UIParent's BOTTOMLEFT,
  so the save moved it a full screen height — measured at 1800 units on a 1440 screen at scale
  0.8. It was latent while OpenMap's own hook kept putting the window back, and surfaced the
  moment that stood down. Fixed by measuring the offsets for the point being named instead of
  copying a pair in another convention, which also fixes an anchor to a third frame; plus an
  off-screen check on restore, so a save already written heals once instead of every login.

- The player's first buff of each session (the commit before this one).

- pfUI: a config section added to `LoadConfig` does not survive a character that already has a
  config, because `pfUI_config` is SavedVariablesPerCharacter and the saved table replaces the one
  LoadConfig filled. `C` is not a stable table either — `GetEnvironment()` rebinds it per module.

- The fade grom actually wanted out of Cinematic, which he has now removed: pfUI's own
  `EnableAutohide`, which only the action bars and four info panels could reach, pointed at the
  minimap, the buff rows, the unit frames and Location Plus, every switch defaulting off and each
  with a row in pfUI's GUI. Buffs and debuffs are separate switches on purpose, and the one real
  cost is stated rather than hidden: with the unit frame switch on, the fade and pfUI's
  out-of-range dimming both write one alpha.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-03 08:22:36 -07:00
gromandClaude Opus 5 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
2026-11-03 08:22:20 -07:00
gromandClaude Opus 5 92237dfb54 docs: the first-open pickers, the pfUI window handover, and why not to bake the map in
Records both fixes with the measurements behind them, the rejected first
attempt and what made its test worthless, and the answer to "should this just
be built into the client": no — the addon's art already draws correctly here,
it is ~26k lines and 67 MB of assets, and Questie and TomTom reach it through
`OpenMapAPI`, which exists only because it is an addon.

The pfUI handover itself is a change to the port's `Map/Window.lua` and
`Map/Compat.lua`, which cannot be committed here: `benilla-config/` is
gitignored because it carries the login credentials, so the addon tree lives
only on disk and in the `Addon_Ports/openmap_port` mirror, verified identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 21:57:23 -07:00
gromandClaude Opus 5 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
2026-11-02 21:57:07 -07:00
gromandClaude Opus 5 d1382f8e01 docs: the nameplate row count is twenty-one, not seventeen
Ten friendly names and five hostile ones are the per-kind switches, plus
`nameplateMaxDistance` — sixteen live — and five more registered and inert.
The previous commit's message carries the same wrong total; this is the
correction, since the doc is the thing anyone reads later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-02 21:17:33 -07:00
gromandClaude Opus 5 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
2026-11-02 21:15:38 -07:00
gromandClaude Opus 5 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
2026-11-02 21:15:38 -07:00
gromandClaude Opus 5 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
2026-11-02 21:15:38 -07:00
gromandClaude Opus 5 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
2026-11-02 21:15:38 -07:00
gromandClaude Opus 5 5308b5a200 docs: a hosted page draws under its host, and art that does not resolve is invisible
Two defects from one play report, both in the ports, both measured rather
than guessed:

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

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

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

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

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

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

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

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

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

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

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

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

Also records WOW_MAP_TRACE and why it exists.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 20:38:22 -07:00