diff --git a/crates/benilla-app/src/ui_world_map.rs b/crates/benilla-app/src/ui_world_map.rs index c700069d..f69d5a20 100644 --- a/crates/benilla-app/src/ui_world_map.rs +++ b/crates/benilla-app/src/ui_world_map.rs @@ -69,6 +69,16 @@ struct DirectAreaEntry { /// through the host's size probe, which is why a scratch region measures the file rather than the /// tile (the tile declares 256x256 in `WorldMapFrame.xml` and would answer that whatever the file /// holds). +/// What `UiScript::world_map_selection` answers: `(continent, zone, direct map id)`. Named so the +/// memo fields below can carry it without clippy counting the parentheses. +type MapSelection = (u32, u32, Option); + +/// Frames the art line waits after the selection changes before reading the tiles. The repaint +/// that fills them is Lua's, off a `WORLD_MAP_UPDATE` that is queued rather than called, so the +/// tiles are a frame or more behind the selection and a line printed on the change itself names +/// the map that just left. +const ART_TRACE_DELAY: u8 = 3; + const MAP_ART_TRACE: &str = r#" local d = WorldMapDetailFrame if not d then return "no WorldMapDetailFrame" end @@ -698,10 +708,16 @@ struct FeedMemos { zone_logged: Option<(u32, u32, u32, u32, u32, u32, u32)>, /// The map selection the art line last reported, so it prints once per map /// rather than once per frame. - art_logged: Option<(u32, u32, Option)>, + art_logged: Option, + /// A selection whose art line is waiting out [`ART_TRACE_DELAY`] frames, and how many are + /// left: the repaint that puts the new map's tiles up is Lua's own, off a queued + /// `WORLD_MAP_UPDATE`, so a line printed in the frame the selection changed reads the + /// *previous* map's tiles. That is how a reading of "the map shows Dun Morogh and the tiles + /// are Azeroth's" got into a log with nothing wrong behind it. + art_pending: Option<(MapSelection, u8)>, /// The resolved pair last seen, so the watchdog in [`feed_world_map`] can tell the player /// moving to a new zone from the map being browsed away from the player's. - last_resolved: Option<(u32, u32, Option)>, + last_resolved: Option, /// An episode of the selection not following the player, and when it started. `None` whenever /// the map is on the player's zone, and whenever it is merely being browsed off it. selection_lag: Option, @@ -1042,10 +1058,28 @@ fn feed_world_map( // pitch, or whose file is not the size the layout assumes, names the fault outright. It was // behind `WOW_MAP_TRACE` and that cost a round trip: a play report about the map arrives // without it, and the next one cannot be asked for until the player plays again. + // + // (The claim above that "the capture harness seats no UI" is no longer true of either half: + // `WOW_CAPTURE=ui-worldmap` seats the map, and `WOW_CAPTURE_ADDONS=1` beside it puts the + // player's own map addon in front of the real renderer — see `capture::fixtures`. This line + // still earns its place, because neither harness is what a player is running.) { let key = script.world_map_selection(); let memo = memo.get(&script); - if memo.art_logged != Some(key) { + if memo.art_logged != Some(key) && memo.art_pending.is_none_or(|(k, _)| k != key) { + memo.art_pending = Some((key, ART_TRACE_DELAY)); + } + let due = match memo.art_pending { + Some((k, 0)) if k == key => true, + Some((k, n)) if k == key => { + memo.art_pending = Some((k, n - 1)); + false + } + // The selection moved again while the wait ran: start it over for the new one. + _ => false, + }; + if due { + memo.art_pending = None; memo.art_logged = Some(key); match script.eval::(MAP_ART_TRACE) { Ok(line) => info!("world map art: {line}"), diff --git a/docs/ADDON_COMPAT.md b/docs/ADDON_COMPAT.md index 9806c7ba..bf8672fb 100644 --- a/docs/ADDON_COMPAT.md +++ b/docs/ADDON_COMPAT.md @@ -2060,3 +2060,162 @@ after any port: each one is something a player is not seeing.** The grips are drawn now the way 1.12 windows draw a corner grip — two nested squares of `Interface\Tooltips\UI-Tooltip-Border` turned 45 degrees with the eight-argument `SetTexCoord`, AceGUI's own sizer art, brightened on hover. + +## `GetCursorPosition()` answered `0, 0` for every session there has ever been + +Last round's grips were invisible but alive. Painting them did not make the map resizable, and +grom said so again. The grip's code is the universal idiom: + +```lua +g:SetScript("OnMouseDown", function() + local x, y = GetCursorPosition() + start = { x = x, y = y, w = map:GetWidth(), h = map:GetHeight(), ... } +end) +g:SetScript("OnUpdate", function() + if not start then return end + if not IsMouseButtonDown("LeftButton") then Finish() return end + local x, y = GetCursorPosition() + local s = map:GetEffectiveScale() + local w = math.max(MIN_W, start.w + (left and -(x - start.x) or (x - start.x)) / s) + ... +end) +``` + +Stamp the cursor on the press, subtract it each frame, size by the difference. `Model::cursor_pos` +is read by `GetCursorPosition` in two places and **was written by nothing at all** — declared, +initialised to `(0.0, 0.0)`, and never assigned anywhere in the workspace. So every call of every +session answered the origin, every delta came out zero, and the window was re-anchored to its own +current size sixty times a second while the player dragged. + +Nothing raises. Nothing logs. `Finish` sees a width that moved by less than half a pixel, decides +the drag was a click, and declines to save — which is exactly right for a click and indis­tinguish­able +from a drag that measured nothing. + +This is not one addon's bug. The same four lines move a frame, drive a custom slider, draw a +selection box, place a world marker and resize a chat window; `IsMouseButtonDown` was already fed +from the host's own device state (`ui_script/input.rs`) and the cursor beside it was not. It is fed +now, in the same place and for the same stated reason — the pointer feed below it is skipped for a +cursor off the window and for a UI hidden by TOGGLEUI, and the reference answers through both. + +**Units.** UI units, y-up from the bottom-left, undivided: `UIParent` is scale 1 in this client, so +the universal `x / frame:GetEffectiveScale()` lands in the frame's own units exactly as it does on +the reference. `script::tests::input::get_cursor_position_drives_the_drag_to_size_idiom` pins the +whole idiom end to end rather than the getter alone, because the getter answering *something* was +never the question. + +## The later clients' per-kind nameplate CVars: a nil answer greys a whole row + +> "I can't edit anything on the friendly npc nameplates in there amongst others" + +Cinematic's Nameplates page asks before it offers: + +```lua +function ns.HasPlateSettings(kind) + for _, cvar in ipairs(PLATE_KIND_CVARS[UNIT_ROW_KIND[kind] or kind]) do + if GetCVar(cvar) ~= nil then return true end + end + return false +end +``` + +`nil` is not "off" to an addon written for a later client — it is "this client has no such +setting", and the row goes grey. The session log named the whole set, one line each: + +``` +ui_script: unknown CVar 'nameplateShowFriendlyNPCs' (not host-registered) — ignored +ui_script: unknown CVar 'nameplateShowFriendlyPlayers' … +ui_script: unknown CVar 'nameplateShowEnemyPlayers' … (17 nameplate rows in all) +``` + +The two rows that *did* work were the two whose `PLATE_KIND_CVARS` entry happens to name +`nameplateShowEnemies` or `nameplateShowFriends` — the only two nameplate CVars this client had, +because 1.12 has no nameplate CVar at all and those two names were borrowed from later clients to +persist its `[0xc4da34]` bitmask. + +Seventeen rows are registered now. Sixteen of them are live: friendly players, friendly NPCs, +hostile players, and the pets/guardians/totems/minions of each side — 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 because an addon that means "show them" writes the +whole set in one go (`Effects.lua`'s `PLATE_KIND_CVARS.FriendlyMinions`), so last-writer-wins is +what the caller meant. `nameplateMaxDistance` joins `NameplateRange` and `NP_NameplateDistance` as +a third name for the one reach. + +Every one defaults `"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. They are paired against `VPlateMode::default()` in `cvars.rs`'s own +defaults test, the same pairing the derived-`Default` trap forced on `occlusion`. + +**Five are registered and inert, and that is written on each row.** `nameplateShowAll` is `"1"` +because 1.12 plates whatever the masters allow whether or not you are fighting — "always" is +already what happens and there is no combat gate to switch off. `nameplateShowSelf` is `"0"` +because nothing here draws a plate over the player. `nameplateShowOnlyNameForFriendlyPlayerUnits` +and `nameplateForceShowUnitName` name a plate mode this client does not have. `nameplateShowEnemyMinus` +needs a creature's `Creature.dbc` rank, which the plate path never looks up — it reads reaction and +owner off the object update and nothing else. + +Still nil, and so still greying their own rows elsewhere in that window: the sixteen later-client +`UnitName*` names (`UnitNameFriendlyPlayerName` and its family — this client has the four 1.12 +ones), `raidFramesDisplayBuffs`/`Debuffs`, the six `floatingCombatText*` names, +`worldMapShowPlayerCoords`/`worldMapShowCursorCoords` and `cooldownViewerEnabled`. + +## Putting an addon's own drawing in front of the real renderer + +The map reports could not be answered from either harness, and the reason is worth writing down: +`addon_harness` runs the VM with no renderer, so it can say what a region *is* and never what it +looks like; 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. + +So a stitched-map addon drawing its own art was unseeable without a server, and the only evidence +was a player's screen recording with no knobs on it. `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. `capture::fixtures` will also *ask* for a view through the addon's own +published API, retrying until the map is stitched: + +``` +WOW_CAPTURE=ui-worldmap WOW_CAPTURE_ADDONS=1 \ +WOW_CAPTURE_MAP_ZOOM=8 WOW_CAPTURE_MAP_AT=0.7655,0.4942 \ +WOW_CAPTURE_AGE=700 WOW_CAPTURE_OUT=/tmp/omc.png cargo run -q --release -p benilla +``` + +`scripts/visual.sh` never sets it, so no baseline sees an addon. The first shot taken this way +answered the open question outright: with OpenMapClassic loaded on its shipped defaults, this +client draws the stitched Dun Morogh map correctly — zone art, neighbours, labels, lore panel and +player dot — so what grom is looking at is not the art pipeline coming apart. + +**Two limits, measured rather than assumed.** The zoom ask does not stick: shots at 8, 20, 24 and +48 came out byte-identical, because OpenMapClassic re-seats its own remembered view +(`reopenView`) after the window opens and `Glide` is travelling toward it. And the player's *saved* +settings — the window size and max zoom that his map is actually stuck at — cannot be reproduced, +because loading saved variables would also mean writing them back at exit, over a real player's +file. Both are why this instrument narrows the question rather than closing it. + +## Open: the map's Continent and Zone pickers are blank on the first open — with the addons loaded + +Found in grom's recording rather than reported: over Dun Morogh both dropdowns carry no text and +Zoom Out is dead; later in the same clip, over Elwynn, both read correctly. Reproduced offline: + +``` +addon_harness … --probe OpenMapClassic --eval 'ToggleWorldMap()' --tick … + after open1: [nil] + after open2: [Eastern Kingdoms] zone=[Alah'Thalas] +``` + +Blank on the first open, correct from the second, and a `WORLD_MAP_UPDATE` fired by hand after the +first open fills both — so the mechanism is clear: the realm's `WorldMapFrame.xml` fills them from +exactly one place, the Zone dropdown's `` on `WORLD_MAP_UPDATE` (:321), and that dropdown +registers the event in its own `` (:318 — the realm commented the `OnLoad` out and moved it +there) while `WorldMapFrame`'s `` fires the event through `SetMapToCurrentZone()` (:711). A +parent's OnShow runs before its children's, so the event arrives one step early. + +**What is not established is that this is ours.** The same open on the whole shipped interface with +*no* addons fills both pickers on the first try — measured, as a test that passed with and without +a candidate fix, which is what sent that fix back. So something in OpenMapClassic or one of its +dependencies changes the order, and the engine-side change that looked obvious (queueing a second +`WORLD_MAP_UPDATE` behind the inline one) buys nothing measurable and costs a second +`WorldMapFrame_Update` per call — the cost `ui_script/lifecycle.rs:107` already warns about. It is +not in the tree. + +**Next step, written down so it is not re-derived:** bisect the probe — the stock interface alone +is clean, `--probe OpenMapClassic` is not, and that probe loads only that addon and its +dependencies. Whatever re-shows or re-parents those dropdowns is in that short list.