From 41512a04ef842d3f5628d5f19401c146096a1d1a Mon Sep 17 00:00:00 2001 From: grom Date: Tue, 3 Nov 2026 13:03:17 -0700 Subject: [PATCH] map: 1002x668 is a contract, and the canvas counted the windowed scale twice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-app/assets/ui/MapCanvas.lua | 124 +++++++++++++----- crates/benilla-app/src/local_state.rs | 8 +- .../src/ui_script/map_canvas_tests.rs | 54 ++++++++ docs/ADDON_COMPAT.md | 90 +++++++++++++ 4 files changed, 243 insertions(+), 33 deletions(-) diff --git a/crates/benilla-app/assets/ui/MapCanvas.lua b/crates/benilla-app/assets/ui/MapCanvas.lua index 75566eca..daf50772 100644 --- a/crates/benilla-app/assets/ui/MapCanvas.lua +++ b/crates/benilla-app/assets/ui/MapCanvas.lua @@ -707,28 +707,47 @@ function MapCanvasScrollControllerMixin:ResetZoom() end -------------------------------------------------------------------------------- --- The stock art, at a canvas size that is not 1.12's +-- 1002x668 is a contract, not a size -- --- 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 (1024x768 of --- art, the overhang clipped), each exploration overlay at the offset and size --- `WorldMapOverlay.dbc` gives it, and the hover highlight likewise. The canvas breaks that --- assumption the moment it sizes the frame to anything else — and it does, every time --- `OnCanvasSizeChanged` has no art layer to read and falls back to the container's own rect. +-- 1.12's `WorldMapDetailFrame` is 1002x668 and every single thing drawn into it is placed in +-- absolute pixels against that number: the twelve 256x256 tiles, each exploration overlay at the +-- offset `WorldMapOverlay.dbc` gives it, the hover highlight -- and, far more importantly, +-- **every addon's map art**, because that frame is where a 1.12 map addon puts its own layers. +-- pfUI's reveal is the one that reached grom (`pfUI/modules/mapreveal.lua:157`): one +-- `SetPoint("TOPLEFT", "WorldMapDetailFrame", "TOPLEFT", tile.offsetX, -tile.offsetY)` per tile, +-- raw, like everyone else's. -- --- Measured in grom's 2026-11-02 run, through `WOW_MAP_TRACE=1`: +-- So the frame's **own size must stay 1002x668**. Any rescaling this file does to compensate for +-- a different size can only ever reach the children it knows the names of, and the children it +-- does not know the names of are the whole point of the exercise. -- --- detail 1431x954 @scale 0.70 | 1 256x256 @(10,1122) | 2 256x256 @(266,1122) | ... +-- It did not stay 1002x668, and the reason is a second layout authority. 1.12's windowed map +-- shrinks its art by putting `WORLDMAP_WINDOWED_SCALE` (0.7) on `WorldMapDetailFrame` *and* +-- `WorldMapButton` (`WorldMapFrame.lua:205-208`), because there the window is a fixed sheet and +-- scaling the two frames is the only way to make the art smaller inside it. Under the canvas the +-- container already carries the window's size, so that 0.7 is counted twice -- and since the +-- frame is `SetAllPoints` to the canvas, the half that is not wanted comes back as **unit size**: +-- 1002 / 0.7 = 1431. Measured in grom's own session log, 1431x954 at scale 0.70 inside a 1002x668 +-- canvas at 0.70, the same screen rect described in two different numbers of units. -- --- A 1024x768 sheet of art pinned to the top-left of a 1431x954 frame: it covered 72% of the width --- and 80% of the height and the rest of the map drew **nothing**, which is the grey he reported, --- with the art magnified relative to what was left. +-- Everything absolute inside it then lands at 0.7 of where it belongs, measured off his +-- screenshot before any code was read: pfUI draws "Coldridge Valley" at (360,525) while the base +-- art draws it at (460,700), and both axes solve to the same origin (127,117) -- the frame's own +-- top-left -- at k = 1002/1431 = 0.700. -- --- So the art is rescaled to whatever size the frame has, by the same factors on each axis. The --- tiles need only their size set, because 1.12's XML chains them TOPRIGHT-to-TOPLEFT and --- BOTTOMLEFT-to-TOPLEFT — resize one and the grid follows. The overlays and the highlight carry --- absolute offsets, so those are scaled from the values the stock repaint last wrote, kept per --- texture so a second pass cannot compound. +-- `WorldMapButton` is the same bug wearing a different coat. It keeps its XML 1002x668 and is +-- anchored to the frame's TOPLEFT, so with 0.7 on it and 1431 units of art beside it, **the map's +-- mouse surface covered 70% of the map**: click-to-zoom, the hover highlight and every projection +-- through `WorldMapButton_OnClick`'s own width were all reading a rect the art had outgrown. +-- +-- `SeatOnCanvas` is the fix and it is one idea: the canvas is the only layout authority here, so +-- both frames go to scale 1, the frame fills the canvas and the button covers the frame. The +-- screen rect does not move (1431 x 0.49 and 1002 x 0.70 are the same pixels), so nothing about +-- the picture changes except that the units inside it are 1.12's again. +-- +-- `ScaleDetailArt` stays for the one case that cannot be fixed this way: a map with no art layer, +-- where the canvas falls back to the container's rect and the frame genuinely is not 1002x668. It +-- is a no-op whenever the contract holds, which is every map that has art. -------------------------------------------------------------------------------- local DETAIL_BASE_W, DETAIL_BASE_H = 1002, 668 @@ -744,16 +763,10 @@ local function ScaleDetailArt(w, h) if not d then return end - -- The detail frame's OWN size is the authority, because the tiles are its children and it is - -- not always the canvas's shape: 1.12's windowed mode re-anchors AND rescales it out from - -- under the canvas (`WorldMapFrame_Minimize` sets `WORLDMAP_WINDOWED_SCALE` on it and insets - -- it to TOPLEFT 15,-33), so the canvas's own size stops describing the frame the art lives - -- in. Measured in grom's session: `detail 1431x954 scale 0.56` inside `WorldMapCanvas - -- 1002x668 scale 0.80` — the same screen area, a different number of units. - -- - -- Scaled by the canvas's number instead, every tile came out 1.43x too small: the art - -- covered 1024x768 of a 1431x954 frame and the rest was bare parchment. His own art trace - -- caught both states 1.4 s apart, tiles 366 then 256, with the detail frame unchanged. + -- The detail frame's OWN size is the authority, because the tiles are its children. With + -- `SeatOnCanvas` holding the contract that size reads 1002x668 and the whole function is a + -- no-op; it earns its keep only on a map with no art layer, where the canvas takes the + -- container's rect and the frame really is some other size. -- -- `GetLeft` first: the size is derived, and without forcing the layout to settle a read taken -- straight after a resize answers the size from before it. The caller's `w`,`h` remain the @@ -801,6 +814,46 @@ local function CaptureOverlays() ScaleDetailArt() end +-- The two frames the canvas took over, re-seated on it: both at scale 1, the art filling the +-- canvas and the mouse surface covering the art. This is what keeps `WorldMapDetailFrame` at +-- 1002x668 — see "1002x668 is a contract, not a size". +-- +-- Called again after `WorldMapFrame_Minimize`/`WorldMapFrame_Maximize`, the only two writers of +-- that scale and the only two that re-anchor the frame (`SetPoint("TOPLEFT", 15, -33)` and the +-- positioning guide). Both are 1.12 telling a fixed-size window how small to draw its art; here +-- the container already answers that, so what they write is re-stated rather than argued with. +-- `w`,`h` are the canvas size a caller has just set and the layout cannot answer yet, as in +-- `ScaleDetailArt`. +local function SeatOnCanvas(w, h) + local canvas = WorldMapFrame and WorldMapFrame.Canvas + local detail, button = WorldMapDetailFrame, WorldMapButton + if not canvas or not detail then + return + end + detail:SetScale(1) + detail:ClearAllPoints() + detail:SetAllPoints(canvas) + if button then + -- 1.12's own anchor, with the art's size written on it. The button is the map's mouse + -- surface and `WorldMapButton_OnClick` divides the cursor's offset by the button's own + -- width, so it has to be the art's rect exactly -- but an explicit size rather than + -- `SetAllPoints`, because `WorldMapButton:GetSize()` is **read by addons**: pfUI sizes + -- the whole map window from it (`pfUI/modules/map.lua:111`, `wmWidth + 15`), at + -- `PLAYER_ENTERING_WORLD`, and a derived size answers 0 until the layout settles. + detail:GetLeft() + local bw, bh = detail:GetWidth(), detail:GetHeight() + if not bw or bw <= 0 or not bh or bh <= 0 then + bw, bh = w or DETAIL_BASE_W, h or DETAIL_BASE_H + end + button:SetScale(1) + button:ClearAllPoints() + button:SetPoint("TOPLEFT", detail, "TOPLEFT", 0, 0) + button:SetWidth(bw) + button:SetHeight(bh) + end + ScaleDetailArt(w, h) +end + -- Called when the container or the canvas changes size, and on the first show: the canvas is -- sized from the map's own art layer, the zoom ladder rebuilt and the view reset. function MapCanvasScrollControllerMixin:OnCanvasSizeChanged() @@ -824,10 +877,10 @@ function MapCanvasScrollControllerMixin:OnCanvasSizeChanged() end child:SetWidth(w) child:SetHeight(h) - -- The stock art is placed in 1002x668 pixels; this is where the frame holding it stops being - -- that size (see "The stock art, at a canvas size that is not 1.12's"). The size just set is + -- The art, its mouse surface and the canvas have to agree on one rect, and this is the only + -- place the rect changes (see "1002x668 is a contract, not a size"). The size just set is -- passed in, because the detail frame will not answer it until the layout pass. - ScaleDetailArt(w, h) + SeatOnCanvas(w, h) self:CreateZoomLevels() self:CalculateScrollExtents() self.currentScrollX, self.currentScrollY = self:ClampScrollToExtents( @@ -1665,9 +1718,16 @@ local function BuildCanvas() -- whole reason the canvas is built around them instead of beside them: a 1.12 quest icon stays -- on its quest while a canvas addon moves the view. detail:SetParent(canvas) - detail:ClearAllPoints() - detail:SetAllPoints(canvas) button:SetParent(canvas) + -- ...and the canvas becomes their only layout authority, which is a change of owner and not + -- just of parent: see "1002x668 is a contract, not a size". + SeatOnCanvas() + if WorldMapFrame_Minimize then + hooksecurefunc("WorldMapFrame_Minimize", SeatOnCanvas) + end + if WorldMapFrame_Maximize then + hooksecurefunc("WorldMapFrame_Maximize", SeatOnCanvas) + end -- The stock repaint places the exploration overlays in 1002x668 pixels and is the only moment -- those base offsets are readable; after it, they are scaled to whatever size the canvas has diff --git a/crates/benilla-app/src/local_state.rs b/crates/benilla-app/src/local_state.rs index e7cd6d70..84385a90 100644 --- a/crates/benilla-app/src/local_state.rs +++ b/crates/benilla-app/src/local_state.rs @@ -107,8 +107,14 @@ pub(crate) fn bindings_character_path(realm: &str, character: &str) -> Option Option { - home().map(|h| h.join("saved-variables.lua")) + saved_home().map(|h| h.join("saved-variables.lua")) } /// `benilla-config/addons/-.txt`: the AddOn enable state, per character in the diff --git a/crates/benilla-app/src/ui_script/map_canvas_tests.rs b/crates/benilla-app/src/ui_script/map_canvas_tests.rs index 220f898b..42545291 100644 --- a/crates/benilla-app/src/ui_script/map_canvas_tests.rs +++ b/crates/benilla-app/src/ui_script/map_canvas_tests.rs @@ -379,3 +379,57 @@ fn a_line_with_one_end_or_no_length_draws_nothing() { perpendicular is undefined" ); } + +/// 1.12's `WorldMapDetailFrame` is 1002x668 and **everything** drawn into it — the stock tiles, +/// the exploration overlays, the hover highlight, and every 1.12 map addon's own layers — is +/// placed in absolute pixels against that number. So the frame's size is a contract, and the +/// canvas has to express zoom as scale rather than by resizing it. +/// +/// What broke it was a second layout authority: `WorldMapFrame_Minimize` puts +/// `WORLDMAP_WINDOWED_SCALE` (0.7) on the frame and on `WorldMapButton`, and with the frame +/// `SetAllPoints` to the canvas the half nobody wanted came back as unit size — 1002 / 0.7 = +/// 1431. Everything absolute inside it then landed at 0.7 of where it belonged, which is what +/// grom's screenshot showed as the same zone name drawn twice, 0.700 apart about the frame's own +/// top-left. The mouse surface went with it: `WorldMapButton` kept its XML 1002x668 beside 1431 +/// units of art, so clicks were read against 70% of the map. +#[test] +fn the_windowed_scale_does_not_reach_the_detail_frames_own_units() { + let Some(mut s) = harness() else { return }; + s.run("WorldMapFrame_Minimize()").expect("windowed mode"); + s.resolve(); + + for frame in ["WorldMapDetailFrame", "WorldMapButton"] { + let (w, h, scale): (f64, f64, f64) = s + .eval(&format!( + "{frame}:GetLeft() return {frame}:GetWidth(), {frame}:GetHeight(), {frame}:GetScale()" + )) + .unwrap(); + assert!( + (scale - 1.0).abs() < 1e-6, + "{frame} must carry no scale of its own under the canvas, got {scale}" + ); + assert!( + (w - 1002.0).abs() < 1.0 && (h - 668.0).abs() < 1.0, + "{frame} must stay 1002x668, got {w}x{h}" + ); + } + + // The contract as a child sees it: a region pinned at a 1002x668-space offset has to land + // where that space says, which is the whole reason the size may not drift. + let (got, want): (f64, f64) = s + .eval( + r#" + local d = WorldMapDetailFrame + local probe = d:CreateTexture(nil, "OVERLAY") + probe:SetWidth(1) probe:SetHeight(1) + probe:SetPoint("TOPLEFT", d, "TOPLEFT", 500, -300) + d:GetLeft() + return probe:GetLeft(), d:GetLeft() + 500 * (d:GetWidth() / 1002) + "#, + ) + .unwrap(); + assert!( + (got - want).abs() < 0.5, + "a child pinned at 1.12's own (500, -300) landed at {got}, not {want}" + ); +} diff --git a/docs/ADDON_COMPAT.md b/docs/ADDON_COMPAT.md index b94f41d3..7e363da3 100644 --- a/docs/ADDON_COMPAT.md +++ b/docs/ADDON_COMPAT.md @@ -2538,3 +2538,93 @@ measured size is right. reproduces the addon set the player actually has. The report that needed it was "I turned the map addon off to test and it is still wrong" — without it a capture loads every addon in the folder, which cannot answer that at all. + +## 1002x668 is a contract, and the canvas was counting the windowed scale twice + +The previous section fixed the twelve detail tiles and was the wrong shape of fix. grom's next +report — "the map still paints the revealed stuff in the wrong spot" — came with a screenshot that +names the real fault without any code being read: **Coldridge Valley is drawn twice**, once by the +base art where it belongs and once, smaller and up-and-left, by the exploration overlay, with +pfQuest's pins riding the wrong copy. Both axes solve to the same origin, the detail frame's own +top-left, at k = 0.700. + +0.700 is `1002/1431`, and 1431 is what `WorldMapDetailFrame` measured at in his session log. + +### Why the frame was 1431 units wide + +1.12's windowed map shrinks its art by putting `WORLDMAP_WINDOWED_SCALE` (0.7) on +`WorldMapDetailFrame` **and** `WorldMapButton` (`WorldMapFrame.lua:205-208`). There the window is a +fixed sheet and scaling those two frames is the only way to make the art smaller inside it. Under +the canvas the container already carries the window's size, so that 0.7 is a second layout +authority on the same rect — and because the canvas `SetAllPoints`es the frame, the half nobody +wanted comes back as **unit size**: 1002 / 0.7 = 1431.4. Same screen rect, described in a different +number of units. + +Everything placed in absolute pixels inside that frame then lands at 0.7 of where it belongs. The +stock exploration overlays and the hover highlight are placed that way; so is **every 1.12 map +addon's art**, because `WorldMapDetailFrame` is where a 1.12 map addon puts its layers. pfUI's +reveal is one `SetPoint("TOPLEFT", "WorldMapDetailFrame", "TOPLEFT", tile.offsetX, -tile.offsetY)` +per tile (`pfUI/modules/mapreveal.lua:157`), raw, like everyone else's. + +`ScaleDetailArt` could never have fixed this, and that is the lesson worth keeping: **a +compensation can only reach the children it knows the names of, and the children it does not know +the names of are the whole point.** The frame's size is a contract, not a size. + +`WorldMapButton` was the same bug wearing a different coat. It keeps its XML 1002x668 and anchors +to the frame's TOPLEFT, so with 0.7 on it and 1431 units of art beside it **the map's mouse surface +covered 70% of the map** — click-to-zoom, the hover highlight and every projection through +`WorldMapButton_OnClick`'s own width were reading a rect the art had outgrown. That is the "map +scroll is being really weird" in the same report. + +### The fix + +`SeatOnCanvas` in `MapCanvas.lua`: the canvas is the only layout authority, so both frames go to +scale 1, the frame fills the canvas and the button is given the art's rect. Re-run after +`WorldMapFrame_Minimize`/`Maximize`, the only two writers of that scale, and from +`OnCanvasSizeChanged`, the only place the rect changes. + +The button gets an **explicit size** rather than `SetAllPoints`, and that is not a style choice: +`WorldMapButton:GetSize()` is read by addons — pfUI sizes the whole map window from it +(`pfUI/modules/map.lua:111`, `wmWidth + 15`) at `PLAYER_ENTERING_WORLD` — and a derived size +answers 0 until the layout settles. + +Measured through `addon_harness --probe-all --saved`, windowed, with a probe texture pinned at +1.12's own (500, -300): + +``` +control: detail 1416x921 scale 0.700 | button 1002x668 scale 0.700 | tile 362x353 + pin lands at (1181.3, 705.6); 1002x668-space says (1282.6, 649.8) +fixed: detail 1002x668 scale 1.000 | button 1002x668 scale 1.000 | tile 256x256 + pin lands at (1278.9, 658.8); 1002x668-space says (1278.9, 658.7) +``` + +and the same through the renderer, two captures of his own configuration differing only by this +change: **the base art is pixel-identical and every pin moved.** 1431 x 0.49 and 1002 x 0.70 are +the same pixels, so nothing about the picture changes except that the units inside it are 1.12's +again. + +### Why neither harness had seen it + +`WORLDMAP_WINDOWED` lives in `benilla-config/saved-variables.lua`, which a capture did not load, so +every capture opened the **maximized** map — the one mode in which the frame's scale is already 1 +and the bug cannot appear. `saved_variables_path` now goes through `saved_home`, like the addon +files and the enable state, and `WOW_CAPTURE_SAVED=-` reproduces +`detail 1431x954 scale 0.70` exactly as his log recorded it. + +A hazard that comes with it: a capture run with `$WOW_CAPTURE_SAVED` **writes saved variables back +at exit**, over the player's own files. Copy `benilla-config/saved` and `saved-variables.lua` aside +before a run and diff them after; one FishingBuddy counter moved on each of the runs above. + +### Still open, measured rather than guessed + +The art is a few pixels outside the window's left edge and about 13 past its bottom. The canvas +container keeps 1.12's **maximized** chrome offsets (`WorldMapPositioningGuide TOP -502,-69`), +which are computed against a fixed 1024x768 guide; windowed, the guide is stretched to the window +and `-502` stops meaning "centred". For his 1017x723 window that is 6.5 px of left margin instead +of 10 and a container bottom 13 px past the frame's. + +Not fixed here, and the reason is that the faithful answer is worse than the bug: 1.12's windowed +anchor is `WorldMapFrame TOPLEFT 15,-33` at scale 0.7, which would fill 70% of his window, because +pfUI sizes that window from the **unscaled** `WorldMapButton` — it is written for the maximized +map and he is running the windowed one. Flipping the map to maximized is the one-click test of +that reading.