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
This commit is contained in:
@@ -3589,3 +3589,188 @@ trade is the one that constant's own comment has always named: an addon that gat
|
||||
rather than on a function takes the newer branch and may reach for a name this layer has not
|
||||
built, where it gets a clean `nil`-call error naming the function. The floor is unchanged and
|
||||
still load-bearing — below `11304`, pfUI does not load at all.
|
||||
|
||||
## The player arrow was flashing because two things were placing it
|
||||
|
||||
> "my player arrow is still all messed up, its like flashing between a position and where I
|
||||
> actually am" — grom
|
||||
|
||||
1.12 places the world map's arrow from `WorldMapButton_OnUpdate`, **every frame the map is
|
||||
shown** (`WorldMapFrame.lua`, the `-- Position player` block): `PositionWorldMapArrowFrame`
|
||||
against `WorldMapDetailFrame` when `GetPlayerMapPosition("player")` answers, and
|
||||
`ShowWorldMapArrowFrame(nil)` — a hide — when it answers `(0, 0)`, which is every frame the
|
||||
window is not on the player's own zone.
|
||||
|
||||
On a stitched map that placement is wrong in both arms: the fraction is measured against
|
||||
Blizzard's own continent art, which is a different projection from OpenMap's grid, and the
|
||||
`(0, 0)` arm fires constantly because the map underneath is usually some other zone. The port's
|
||||
`Map/StockArrow.lua` already placed the arrow properly — on a `0.05 s` throttle. At 60 fps that
|
||||
does not *replace* 1.12's placement, it **alternates** with it. Counted over 199 frames of a
|
||||
capture, sampling the arrow model every frame:
|
||||
|
||||
```
|
||||
before shown 66, hidden 133 run: h h S h h S h h S h h S h h (stock hid it on 199 frames)
|
||||
after shown 199, hidden 0 run: S S S S S S S S S S S S S S
|
||||
```
|
||||
|
||||
That is the flashing, measured. It only started when `C_Map.GetPlayerMapPosition` began answering
|
||||
at all — before the unit feed stopped publishing Bevy's frame as world yards, `WorldMap.lua`'s
|
||||
`S.SetPlayer` path drew OpenMap's own arrow and the stock one stayed hidden, so there was nothing
|
||||
to fight.
|
||||
|
||||
The fix is not to write after 1.12 but to stop it writing. `StockArrow.lua` wraps
|
||||
`WorldMapButton_OnUpdate`'s global — the XML calls it by name, so replacing the global is enough —
|
||||
computes the placement first, and for the frames where it has an answer makes
|
||||
`PositionWorldMapArrowFrame` and `ShowWorldMapArrowFrame` no-ops for the length of the stock
|
||||
call. Re-asserted on the map's `OnShow`, because three addons in this realm's own folder
|
||||
(`FlightMap`, `LevelRange-Turtle`) replace that global outright rather than hooking it.
|
||||
|
||||
### Correcting *after* 1.12 would have cost a full UI pass every frame
|
||||
|
||||
The first version of this did run after the stock handler and re-show the arrow. It looked right
|
||||
and was expensive, which is worth writing down because the same shape will come up again: a frame
|
||||
going hidden → shown takes a **new link-stamp** (`WidgetArena::resequence_to_tail`, bits 21..38
|
||||
of the packed `ZKey`), the stamp is in the extracted entry, and the extract gate compares
|
||||
extracted entries. So an arrow hidden and shown once a frame moved one entry's sort key once a
|
||||
frame, and `ui_script`'s gate — which otherwise skips the whole conversion when nothing changed —
|
||||
missed on every single frame.
|
||||
|
||||
```
|
||||
WOW_UI_GATE=1, stitched map, settled
|
||||
correcting after [ui-gate] miss: entry 1292/1372 … ModelPane … z 6922020567064772608 → …066869760 every frame
|
||||
suppressing [ui-gate] miss: capture mode (the gate never skips under a capture) i.e. nothing differed
|
||||
```
|
||||
|
||||
With his own saved variables and the map open that conversion is **1.19–1.54 ms a frame** on top
|
||||
of a 0.88 ms extract (`WOW_UI_PHASES`, 5,148 quads at 1100x800). The map's whole UI pass is
|
||||
2.1–3.0 ms/f against 0.6 ms with the map closed, and the frame itself 8.67 ms open against 3.73
|
||||
closed — so the stitched map is the most expensive window in the interface and a gate miss on it
|
||||
is not a rounding error.
|
||||
|
||||
The *point* is still written every frame, and the distinction is worth keeping straight: a
|
||||
`SetPoint` is read by the extract once, after every handler has run, so the last writer wins and
|
||||
the rect it sees is constant. A `Show` is not — it moves the frame in the draw order and the new
|
||||
stamp is what the gate reads. Memoizing the point to skip the write looked like the same saving
|
||||
and was a bug: 1.12 re-anchors `WorldMapPlayer` from its own handler and only the arrow frames'
|
||||
two verbs are suppressed, so the frames where the value had not changed kept 1.12's answer on the
|
||||
button that carries the arrow's tooltip.
|
||||
|
||||
## The arrow's heading froze 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()` — and that field is `GetPlayerMapPosition` on the
|
||||
**displayed** map, which is a different question. 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 whatever 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 `player.facing()` unconditionally. Two
|
||||
tests hold both halves: the heading follows off the displayed map, and before any feed (login and
|
||||
loading screens) the arrow is left alone.
|
||||
|
||||
## pfQuest's pins were behind the map, and only ever for one zone
|
||||
|
||||
> "pfui pins are still messed up, can't see them at most zooms and the ones I can see them at are
|
||||
> pretty useless" — grom
|
||||
|
||||
Two separate defects with one symptom, and the zoom-announcement fix of the previous round was
|
||||
neither of them. With four nodes seeded into Dun Morogh and the rest from pfQuest's own database:
|
||||
|
||||
```
|
||||
zoom 4 (host = continent) pfQuest pins built 131, shown 0 ← GetCurrentMapZone() is 0
|
||||
zoom 8 / 16 / 30 (zone) built 131, shown 131, 13.6 / 15.2 / 17.9 screen px
|
||||
```
|
||||
|
||||
The sizes were right all along. What was wrong was the **set** and the **level**.
|
||||
|
||||
*The set.* `pfMap:UpdateNodes` opens `local map = pfMap:GetMapID(GetCurrentMapContinent(),
|
||||
GetCurrentMapZone())` and returns at once when that is nil. On Blizzard's map that is the whole
|
||||
story, because the window only ever shows one map. On a stitched map the view is usually looking
|
||||
at several zones and only the map underneath has any pins at all — and zoomed out far enough that
|
||||
OpenMap switches the map underneath to a continent, `GetCurrentMapZone()` is 0 and **every pin
|
||||
disappears**.
|
||||
|
||||
*The level.* pfQuest fixes no frame level: every pin is a child of `WorldMapButton` and takes its
|
||||
level, 3. `S.Build` lays 80 levels of stitched art over the canvas, `artTop` 188. So at zoom 8,
|
||||
131 pins answered `IsShown()` and **not one pixel of any of them was on the screenshot**. Questie's
|
||||
and Leatrix's are lifted for exactly this reason (`Map/Extras.lua`); pfQuest reached nobody's list
|
||||
because it does not place its pins through HereBeDragons.
|
||||
|
||||
`Map/PfQuest.lua` (new, port-authored) does both: it lifts pfQuest's own pins above `S.artTop`,
|
||||
and for every zone in view that is *not* the map underneath it places the **quest log's** nodes
|
||||
(`pfMap.nodes["PFQUEST"]`, what `quest.lua` writes) through pfQuest's own `BuildNode` /
|
||||
`UpdateNode` / `ResizeNode`, so they are its pins in every way a player can tell — its textures,
|
||||
its `WorldMapTooltip` tooltips, its zoom sizing. Quest-log nodes only, not the database search's:
|
||||
a search can put thousands of spawns on one zone and twenty zones of that is a different
|
||||
complaint. Bounded at 300 pins, refreshed on `OpenMap_ViewChanged` (throttled to a few a second)
|
||||
and on pfQuest's own `UpdateNodes`. A switch in the map's own checklist, shown only when pfQuest
|
||||
is loaded, as Questie's row is.
|
||||
|
||||
Measured after, same seeds: continent zoom 12 bridge pins shown where 0 pins showed before; zone
|
||||
zoom 131 of pfQuest's own now visible plus 5 neighbours'.
|
||||
|
||||
## Zone sheets butted against each other, so the continent looked like a grid of boxes
|
||||
|
||||
> "it looks super boxy and weird, edges of maps are the same, is there a way that we can blend
|
||||
> together maps that overlap?" — grom
|
||||
|
||||
`Media/Textures/MapZoneDilate/<uiMapID>.tga` is one 256x256 alpha mask per Classic outdoor zone,
|
||||
Voronoi-dilated so the forty of them tile each continent with no gap. They are **binary**:
|
||||
measured over four of them, 19k–48k pixels at alpha 0, 12k–51k at 255, and under 250 pixels
|
||||
anywhere in between. Every sheet, and every shade over an unexplored one, therefore ends on a
|
||||
crisp line against a neighbour painted at a different brightness, and the two sheets are different
|
||||
paintings of the same ground. Nothing blends because nothing was asked to.
|
||||
|
||||
`scripts/soften-zone-masks.py` writes `MapZoneSoft` beside it: the same coverage dilated 2 px,
|
||||
blurred 3 px, and multiplied by a rect ramp that reaches zero 5% in from the sheet's own border.
|
||||
The inset is the half that is not obvious — a 1.12 zone sheet paints its own torn parchment frame
|
||||
and its own sea right to the edge of its rect, and the shipped masks are cut so that none of that
|
||||
shows. Softening with no inset pulls it back in: measured, a hard straight line of Wetlands'
|
||||
parchment border across a view where the binary mask had a soft coast. With the inset, the land
|
||||
the mask gives up is covered by the neighbour's own dilated mask, which is what the dilation was
|
||||
for.
|
||||
|
||||
Captured at the same view before and after, the vertical and horizontal seams across Khaz Modan
|
||||
and the Wetlands are gone and the continent reads as one painting; at zone zoom (22x over
|
||||
Ironforge) the hard diagonal where the next sheet cut in is gone too. `MapZoneDilate` stays beside
|
||||
it as the script's source, so the transform is re-runnable and the two can be compared.
|
||||
|
||||
### Where "darken unexplored areas" did not reach
|
||||
|
||||
The shade over a zone the player has not found takes `ZoneMask(...)`, which answers only for the
|
||||
forty zones in `S.ZoneMaskIDs`; the zone's **art** falls back to the plain edge feather when there
|
||||
is no mask (`PlaceBase`). So a zone with no generated mask was drawn and never shaded — the art
|
||||
had a fallback and the shade did not. Both shade paths now take the same fallback the art takes.
|
||||
On this realm that reaches Hyjal (2482) and anything added later; the realm's other new land
|
||||
(Alah'Thalas, the Grim Reaches, Dun Kithas, Ambershire) is painted *inside* the vanilla sheets
|
||||
rather than given maps of its own — measured off the client, 47 zone maps in all, 1411–1458 plus
|
||||
2482, and no others.
|
||||
|
||||
## Cities: a half-faded plan over the zone's own painting, and the minimap you cannot have
|
||||
|
||||
> "on certain cities I can't get it to go into the minimap view, or it does but there is still an
|
||||
> overlap of the map art above it" — grom
|
||||
|
||||
Two different things, one of them not a defect.
|
||||
|
||||
*The overlap.* A city's own sheet crossfades in over the zone's painting of the same ground across
|
||||
`CITY_FADE`, a share of the view. Halfway through that band the city is a half-opaque sheet over
|
||||
the zone's art, and a 1.12 sheet is parchment with no transparent surround, so it reads as a dark
|
||||
rectangle sitting on the map. Measured over Ironforge: a grey box at zoom 30, the finished plan at
|
||||
70. The band is now `{ 0.26, 0.36 }` rather than `{ 0.2, 0.4 }` and the three cities with no
|
||||
traced outline (Stormwind, Ironforge, Darnassus) are drawn through `FEATHER_WIDE` rather than
|
||||
`FEATHER` — 16% of the sheet rather than 9%. Neither removes the blend; they shorten the zoom
|
||||
range that shows it and soften the box, which is the half a player can see.
|
||||
|
||||
*The minimap.* Over a surface city `Close.lua` capped the minimap at `CITY_MINI` (0.5, and 0.15
|
||||
over Orgrimmar and Thunder Bluff), on the reasoning that a city's own plan should lead. That is a
|
||||
taste, so it is now a setting — "Minimap detail in cities", default 100%, dialling down to
|
||||
upstream's own caps.
|
||||
|
||||
Ironforge and Undercity are a different case and the answer there is no. `Close.lua` keeps the
|
||||
minimap off them because the minimap at those coordinates is the *surface* — the mountain, the
|
||||
ruins — not the city. That is true of this install and not only of retail: `textures\Minimap` on
|
||||
the player's own chain holds `Azeroth` and `Kalimdor` and a folder per dungeon, and no folder for
|
||||
any city (measured across 112,411 files). There is no interior minimap to show, so the two
|
||||
underground cities keep their plan whatever the new setting says.
|
||||
|
||||
Reference in New Issue
Block a user