d5beabab423345ba70aea4e19938703acae70261
88
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d5beabab42 |
The map's mouse surface shrank with the zoom, and four things fell through the hole
The canvas gave the map no mouse surface of its own, on the reasoning that 1.12's
`WorldMapButton` covers the same rect and sits above it. The button is pinned INSIDE the canvas
child, so it scales with it: measured at the world view with OpenMapClassic driving, at grom's
own 2560x1440, a 23x16 px button inside a 701x468 px window. Every press on the other 99%
reached the next frame that takes the mouse — `WorldMapFrame`, which pfUI makes a
`RegisterForDrag("LeftButton")` handle for the whole window. So dragging the map moved the
window, and a click never reached `ProcessCanvasClickHandlers`, which is the only way a map pin
is ever dropped.
The container takes the mouse and the wheel now and drives the same controller. The frame level
mattered as much: the hit sweep probes one `(strata, level)` plane in link order and a parent is
linked before its children (`0x764aa0`), so a container tied with `WorldMapFrame` still lost
every press to its own parent. It is re-seated to `map:GetFrameLevel() + 1` on every show,
because the window is level 0 while the canvas builds and level 1 once `ShowUIPanel` has raised
it.
With the click arriving, a pin still stored nothing: `Map/Pins.lua:57` trims the note with
`strtrim`, which was deliberately absent. `script/stdlib.rs` still refuses it on the 1.12
surface, for the reason written there; this build claims ClassicAPI, whose client does have it,
so it lands in `compat/classic_api/string_trim.rs` with both spellings. Eight bare call sites in
the corpus, one of them in pfUI's search box on every keystroke.
`SCALE_RATE` 10 -> 24. A 0.7 s half-life was tolerable only while a wheel notch could not
accumulate; now that they add up the glide is what you feel ("scrolls past where I stop
scrolling").
`BenillaMapTextures(uiMapID)` is `GetExploredMapTextures` without the explored filter. Reveal
mode draws unexplored overlays out of a retail FileDataID table this client resolves none of —
captured with the fog default and with reveal, the same view came back pixel-identical, 0 by
`compare -metric AE`. The overlays were never missing, only their names: `Map/RevealFromClient.lua`
in `Addon_Ports/openmap_port` rebuilds the table from this, with paths.
Also in the port, not here: the arrow is placed through OpenMap's own coordinates, because
1.12 measures `GetPlayerMapPosition` against Blizzard's world and continent art and the stitched
map is a different projection.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
c92f078c39 |
A wheel notch stepped from the easing scale, so notches did not accumulate
`ZoomIn`/`ZoomOut` walked the zoom ladder from `GetCanvasScale()` — the live value the smooth zoom is still easing — so the notch after the first re-read a scale that had barely moved, found the same rung still under it, and re-set the target it already had. The ease closes about 63% of the gap a second, which is slow enough that a player never gets a second rung. Driven at grom's own 2560x1440 with his addons loaded, over OpenMapClassic's 27-step ladder (0.0330 .. 2.3734): four wheel-down notches at the map's centre all set target 0.9030 — one rung — and two more at the far right set nothing. That is his "it will only scroll very little out … and will only very little scroll in period". `GetZoomStepScale()` steps from `targetScale` instead, so notches accumulate and either end of a long ladder is reachable. The same measurement rules out the position dependence he saw: the wheel's target over the centre and 19 px from the right edge is `WorldMapButton` both times, because `wheel_target` walks past frames that take no wheel the way `0x7664f0` does. `docs/ADDON_COMPAT.md` also records the two addon-side causes of "I can't see any buttons but I can use them", both fixed in `Addon_Ports/` rather than here: OpenMapClassic's rail tabs are never dressed because `mod.FaceSideTab` ships in a file the release omits, and the OpenMap tab's own icon is absent from this patch chain. And pfQuest's minimap icons now fade with the minimap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
a32963b3f8 |
Find the frame that was eating every map click, and the realm LibStub behind it
A real pointer at the middle of the world map reached nothing: a hook on
`WorldMapButton`'s `OnMouseDown` counted 0 hits from a real click although the
button was shown, mouse-enabled and held the point. A Lua walk of every visible
mouse-enabled frame containing that point answered with exactly one frame, and
it was an unnamed 700x500 `FULLSCREEN_DIALOG` child of `UIParent` — AceGUI's
default container, carrying the title "SoftSpoken Stories" and no content. The
blank login popup grom has reported twice was parked over the centre of the
screen swallowing the map's clicks. One frame, both defects.
`hit_test_name` had been printing `<nothing>` there all along: it answers `None`
for an unnamed frame. Ask what holds the point, not what it is called.
The window is left shown because `AceGUI:Create` shows a container on acquire
and `self.frame:Hide()` is the last line of a `pcall`ed build step, so anything
raising in between leaves it up. The recorded reason was
`AceGUI-3.0.lua:282: attempt to perform arithmetic on local 'argc'`, and the
cause is the realm's own
`Interface\FrameXML\AIO\Dep_LibWindow-1.1\LibStub.lua`, loaded by its
`FrameXML.toc:162` before any addon, which hardcodes `minor = 2` where upstream
parses it — 1.12's Lua 5.0 has no `strmatch`. Every library therefore registers
as minor 2 and the first copy loaded keeps it, while `RegisterWidgetType` still
upgrades individual widgets by their real version: an Ace3v core
(`Fire(self,name,argc,…)`) hosting modern widgets (`Fire(self,name,…)`), whose
first payload value lands in `argc`.
Client side, one real divergence came out of it: `string.split` answered a table,
because BigWigs' `Plugins/SpellRequests.lua` defines `function string:split` and
never calls it, while Ace3's `AceConfigDialog-3.0` and `TreeGroup` both spell
`("\001"):split(uniquevalue)`. `string.split` is now `strsplit` — same function,
same argument order — in the beyond-1.12 string family, with a test pinning the
Ace3 call shape and the arity.
Also: `Step::Drag` moved the cursor eight times without advancing a frame, and a
canvas pans from its `OnUpdate`. It now ticks per move, and the same drag walks
0.5000,0.5000 -> 0.7582,0.3073 -> 0.5080,0.5161 — exactly what driving
`OnMouseDown` by hand produced. The frame between the moves is the gesture.
Four worth recording as dead ends, each of which looked like a client bug: a
verbatim replica of the LibStub file registers correctly; `strfind(34,"(%d+)")`
coerces the number and captures `34` as the reference does; the live function's
`getfenv` is `_G` with every global correct; and a known-good `NewLibrary`
installed on the live table stores the right value. `debug` is absent here, so
the way to ask a Lua function where it came from is to make it raise.
Addon-side edits (gitignored live copies, mirrored to Addon_Ports): the `Fire`
arity shim in both Ace3v AceGUI copies, `issecretvalue` guarded in
OpenMapClassic's vendored Heading widget the way its own Core.lua guards it, and
BigWigs' unused `string:split` removed.
Verified: SoftSpoken `initializationErrors` 0 and `shown=0` at login, nothing
over the map, `--drag` pans it, all three registered options tables open clean
(MasterTradeSkills 4 labels, OpenMapClassic 83, SpokenQuests 6), tabs switch
pages with no raise, session errors 0. fmt and clippy clean; benilla-ui 2119
pass; benilla-app 12 failures, the recorded pre-existing data-golden baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
b7a8e1c406 |
ui: a Slider answers SetEnabled, because OpenMap's options tab called it unguarded
Rendering a panel is not the same as it working. OpenMap's options menu opens 500x500 with 104 frames and 49 placed labels, and its Look tab died on click: Map/Panel.lua:1084, attempt to call method SetEnabled (a nil value) -- so a third of its options was unreachable. SetEnabled is already composed here for Button, CheckButton, EditBox and LootButton, and was deliberately withheld from a Slider, because addons duck-type on the enabled family to tell a control from a bar. What they duck-type on is the TRIO (if widget.IsEnabled then), that is what tests/slider.rs pins, and the trio is still absent; SetEnabled is beyond 1.12 by construction, so a Slider answering it invents no 1.12 behaviour. OpenMapClassic calls it unguarded on every options row with a condition, which is what settles it. On a Slider it writes the mouse -- input refused, art not greyed, which 1.12 has no slot for; the caller dims it itself on the next line. The argument goes through GetBoolOrDefault, not Lua truthiness, because DoiteAuras writes SetEnabled(ok and 1 or 0) and 0 is truthy in Lua. Also records a count that was wrong until checked: grep says 39 calls across five addons, but AdvancedTradeSkillWindow2 and SoftSpoken define their own SetEnabled and SoftSpoken's slider row is fully guarded. The real unguarded widget calls are OpenMap's three and DoiteAuras' two -- and SoftSpoken's blank settings page is NOT this, and is still open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
2281179874 |
capture: a setup slot, for the moment before the scene ages
$WOW_CAPTURE_SETUP runs a chunk at the first aging frame -- early enough to change a setting or create a frame and still have the scene settle around it. $WOW_CAPTURE_EVAL keeps the shutter, where a read and the pixels it explains come from the same frame; anything created there is a frame too late to be in the image. The deep-zoom map shot needed it. OpenMapClassic resolves minimap tiles by FileDataID by default and this client has none, so its own status reports "loaded YES=0 NO=8"; it already carries a path mode for exactly that, asking for textures\Minimap\<folder>\map<col>_<row>, of which this install ships 10,884. Switching to it raised, because S.ResetMinimapTiles reads lodPieces/lodPool sixteen lines before they are declared and so bound them to nil globals -- an upstream bug, and the only two callers are the two switches that reach the mode. Port edits mirrored to Addon_Ports/openmap_port/: the declaration moved above its use, and the tile mode defaulted to the one that can work here. Captured at zoom 40: the minimap's terrain draws through the map. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
62c019e744 |
blp: a short colour map is a sliver tile, not a corrupt file
Seven of the client's own Interface\WorldMap textures never decoded. They are the last column or row of an exploration overlay -- a few pixels wide, a handful of colours -- so they store a SHORT BGRA palette and end: RuinsOfThaurissan4.blp is 404 bytes in all, LakeEverstill6.blp is 276. The parser sliced the nominal 256 entries and refused anything shorter. A refused decode is a quad that draws nothing, and until this week a texture that drew nothing also said nothing, so the symptom was a hole in a zone's revealed art -- indistinguishable from not having explored it. It surfaced only once the stitched world's geometry was right and the render reached those tiles. The palette is now as much of it as the file has, and an index past the end is transparent black, which is what an unused index means. every_world_map_texture_in_the_chain_decodes walks the folder: 3553 textures, 7 failures before, 0 after, and it fails on the old decoder. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
6c01be926d |
map: a map's rect on another map describes art, not a click box
C_Map.GetMapRectOnMap answered, for a continent on the world sheet, the world-level CLICK box -- continent_sheet_rect over the WorldMapContinent tile bounds, which trace the landmass. That is the reference's own 0x4a5d00 and right for what 1.12 uses it for; the two boxes are disjoint so a world-level click can tell the continents apart. It is the wrong answer for art. A continent's map is a 1002x668 sheet with the land inside it and ocean around, so its art rect is 792x528 px on the world where its click box is 300x564. Every 1.12 WorldMapArea rect is 3:2, because every sheet is, and the click box is 0.355 of that. A zone's rect on its continent was already a yard un-lerp and correct, so the error entered once and multiplied through every zone: Dun Morogh landed 42x79 where its art is 1002x668. A stitched world built on that draws each zone a third as wide as it should be, which is why the canvas was grey between the zoom steps. map_proj::continent_art_rect carries the continent's own WorldMapArea rect through world_uv, the projection a position on it already takes. The view keeps both rects; the Era map graph publishes the art one. Measured after: EK, Kalimdor, Dun Morogh and Elwynn all 1.500. Rendered: the zone view stitches its neighbours in, zoom 3 is the continent with its zones, zoom 1 is the world sheet. The stock 1.12 map is unchanged. Three instruments came with it and are kept: $WOW_CAPTURE_EVAL, a chunk run in a capture's VM at the shutter; $WOW_CAPTURE_EXPLORED, so a stitched map's fog can be ruled out in one run; and a named warning for a UI texture that is not in the chain, which is what refuted the other hypothesis -- a grep said the addon's art was missing, a real render asked for zero missing files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
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 |
||
|
|
3e8c995e27 |
map: clamp the canvas onto whatever zoom ladder is in force
CreateZoomLevels is the extension point a canvas addon replaces wholesale, and OnLoad seeds a scale (1) before any addon has run. OpenMapClassic's ladder is 27 steps from 0.0125 to 0.899 -- its whole range below this file's own 1, because its stitched world is reached by zooming out past a single zone -- so the canvas sat above its own ceiling, and ZoomIn, which walks the ladder by value, found no step at all above it. ClampZoomToLevels is called from GetCanvasScale rather than from CreateZoomLevels, because CreateZoomLevels is exactly the function the addon will not have. Also records where OpenMapClassic stands, measured: 808 textures in the canvas subtree, 166 placed, spanning ~4600x3270 units of a 1002x668 canvas, with a ladder floor implying a world ~80000 units wide. The Era map tree under it answers correctly; GetMapArtLayers answering one constant for every map is the thread to pull next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
07dd5ba7e1 |
capture: a shot taken of a player's config must not write it back
`$WOW_CAPTURE_SAVED` points the saved-variable READS at a real player's configuration so a capture reproduces their interface. The writes went to the same place, so taking the shot overwrote the configuration it was taken to study -- with a fixture character's state, since a capture logs in as nobody. Three of grom's files came back changed from one run, his pfUI chat scrollback among them. The shutdown tail returns before all four writes under $WOW_CAPTURE. Verified by md5 across a run: zero files touched, zero "wrote" lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
abb4dc1dd3 |
ui: a Slider's thumb had no rect, so no scrollbar could be skinned or seen
"Half of the info on the right is out of the frame" was a scroll frame with 699 of content in a 451 window, a scroll range of 228, and a scrollbar with nothing on it. The thumb's rect was only ever computed inside the draw pass and never published: the region sweep skips anchor-less regions and a thumb is anchor-less by construction, because its slider places it. That is invisible until something anchors to the thumb, and pfUI does -- SkinScrollbar clears the stock knob and hangs its own off the thumb's two corners, so every pfUI-skinned scrollbar in the interface was a bare track, which is a scroll frame that silently cannot be scrolled. The thumb is on the layout roster now, survives the unanchored prune, and carries its slider's (vertical, fraction) in its node hash -- without the last one a named touch moved no hash and seeded nothing, which WOW_LAYOUT_VERIFY caught as an incremental pass disagreeing with a full derivation. The draw pass keeps computing its own rect: extract takes &self and cannot resolve, so a SetValue with no resolve behind it still has to draw where the value now is. map: the window has two layouts and the canvas only knew the maximized one Pressing maximize left the art a 1002x668 patch in a full-screen window and minimize could not put it back, because the container was anchored once at build to WorldMapPositioningGuide TOP -502,-69 -- 1.12's maximized anchor against a fixed 1024x768 guide. Read as insets instead (15/4/33/20 windowed, 10/12/69/31 maximized) the same numbers reproduce 1.12 at 1.12's sizes and fit any other window, which is also the left-edge bleed: -502 against a guide stretched to pfUI's 1017-wide window starts the art 6.5 px in, not 10. The zoom ladder's floor follows: it was the constant 1, true only while the container was the art's own size, and is the measured fit now. A view outside a rebuilt ladder is pulled onto its nearest end, since ZoomIn and ZoomOut walk it by value -- OpenMapClassic's whole ladder sits below ours. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
41512a04ef |
map: 1002x668 is a contract, and the canvas counted the windowed scale twice
Every 1.12 map addon places its art in absolute pixels against WorldMapDetailFrame's 1002x668 — pfUI's reveal does it one SetPoint per tile — so that size is a contract, not a size. 1.12's windowed mode puts WORLDMAP_WINDOWED_SCALE (0.7) on the frame and on WorldMapButton; under the canvas the container already carries the window, so that scale was a second authority on the same rect and, with the frame SetAllPoints to the canvas, came back as unit size: 1002 / 0.7 = 1431. Everything absolute inside it landed at 0.700 of where it belonged, which is grom's screenshot drawing Coldridge Valley twice, and the mouse surface kept its XML 1002x668 beside 1431 units of art, so clicks read 70% of the map. SeatOnCanvas makes the canvas the only authority: both frames to scale 1, the frame filling the canvas, the button given the art's rect — explicitly sized, because pfUI reads WorldMapButton:GetSize() to size the whole map window. Re-run after Minimize/Maximize and on every canvas resize. Rescaling the children this file knows the names of could never have fixed it; the children it does not know the names of are the point. saved-variables.lua now goes through saved_home, so a capture with $WOW_CAPTURE_SAVED opens the player's windowed map — the only mode the bug exists in, and the reason neither harness had ever seen it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
d11b9ac3ae |
map: the art was scaled by the canvas while living in the detail frame
Grom's report was "textures are not where they should be", and still wrong with OpenMapClassic switched off — which is what made it ours rather than the addon's. His own session log had both states 1.4 s apart, same map, same frame: detail 1431x954 with tiles 366x366 (correct, 1431/1002 = 1.428), then the same frame with tiles back at the XML's raw 256x256. `ScaleDetailArt` was being handed the CANVAS's size by `OnCanvasSizeChanged`, on the assumption that the detail frame is `SetAllPoints` to the canvas. 1.12's own windowed mode breaks that: `WorldMapFrame_Minimize` puts `WORLDMAP_WINDOWED_SCALE` (0.7) on `WorldMapDetailFrame` and `WorldMapButton` and insets the frame to TOPLEFT 15,-33, so the same screen area is a different number of units in each — his trace reads `detail 1431x954 scale 0.56` inside `WorldMapCanvas 1002x668 scale 0.80`. Scaled by the canvas's number every tile came out 1.43x too small: the art covered 1024x768 of a 1431x954 frame, the rest bare parchment, and the exploration overlays scaled to match landed nowhere near their terrain. The detail frame's own size is the authority, since the tiles are its children. `GetLeft()` first to settle the layout, because that size is derived and an unforced read answers the size from before the last change; the caller's w,h stay as the fallback for what cannot be measured. Reproduced and controlled in the harness with the windowed scale applied as the stock code applies it: tile 362 where 362 is wanted, against the control's 256 — the number his log recorded. Two earlier attempts are written up in docs/ADDON_COMPAT.md as dead ends, because both looked right: caching the canvas size prefers the wrong quantity, and converting it by the effective-scale ratio gives 366 against a true 362, since Minimize also insets the frame. `$WOW_CAPTURE_SAVED` now reaches the enable-state file too, so a capture reproduces which addons the player has turned off — without which "I turned it off and it is still wrong" cannot be answered offline at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
5e2f1f3ac8 |
ui: SetClipsChildren, the verb an addon asked for by name and was refused in silence
Era's map canvas is built on one verb this client did not have. Its `ScrollContainer` is a plain Frame with `SetClipsChildren(true)`, and everything below it is cropped to that rect. `assets/ui/MapCanvas.lua` substituted a real ScrollFrame, because this client crops a ScrollFrame's scroll child and every descendant of it — which covers the canvas and nothing parented to the container beside it. On Era that distinction does not exist, so an addon has no reason to respect it. OpenMapClassic parents its pin layer exactly there and asks for the verb by name, behind an `if` (`Map/Stitch.lua:1070`), so the absence was not a raise. Measured on grom's own config with the map open: the container sits at L=1151 R=2153 and its seventeen direct children span L=-765 R=2153 — about 1900 units off the left edge, cropped by nothing. Implemented as a frame flag with `SetClipsChildren`/`DoesClipChildren` and the `clipChildren` XML attribute; `effective_clip` now intersects the rect of every ancestor that sets it alongside the ScrollFrame rule, so the two nest and compose. An ancestor rather than the frame itself: the verb clips a frame's children, and stock art overhangs its own frame on purpose. `MapCanvas.lua` sets it on the container too, so a layer parked there is cropped whether or not its addon thought to ask. Not claimed as the whole of grom's map report: the screenshot behind it has not been reproduced offline, because a capture's view is not the view his session was in. This is the part that was provably drawing uncropped. Also here, both from the same play-test: - `C_Spell.CancelSpellByID` answered with `SpellStopCasting` only. pfUI's buff bar cancels with it on a right click where 1.12's calls `CancelPlayerBuff`, so with nothing in flight the click did nothing and said nothing — grom had never been able to right-click a buff off on this client. It now finds the id among the player's own auras and queues it on the same CMSG_CANCEL_AURA path, behind the same `cancel_authorized` gate, and falls through to the cast only when there is no such aura. - `$WOW_CAPTURE_SAVED=<Realm>-<Character>` points a capture at a player's own saved variables. A capture is hermetic by design, but with addons and no saved variables it renders an interface nobody is configured to be looking at — which is why the world map came out correct in a capture while being wrong on his screen. It doubles as the identity because a capture never logs in and `pfUI_config` is SavedVariablesPerCharacter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
79e2494fc6 |
compat: math.huge and SetColorTexture, the two load blockers of the ported pair
Measured through `addon_harness` before anything was written, which is the only reason this
is two small changes rather than a guess at a list. Both `Addon_Ports` addons failed to load
at all; these are their first two stops.
**`math.huge` was nil, deliberately.** `stdlib_backport.rs` restores the 5.1 stdlib members
ClassicAPI backports, scoped to what the corpus calls, and `math.huge` was on the stated
"nothing calls them" list. `Cinematic/Core.lua:1103` is `local lastMovingOff = -math.huge`
at **file scope**, so the nil stopped the addon's main file dead ("attempt to perform
arithmetic on field `?`") and nothing after it ran — one site, but a load-time one, which is
the bar that table sets. The real `ClassicAPI.dll` carries `huge` in its own stdlib name
table beside `fmod`, `modf`, `gmatch`, `match`, `select`, `maxn`, `cosh` and `reverse`, so
this is what the client we already claim to be actually has.
Two tests pinned it absent and are updated rather than deleted: `lua50.rs`'s keeps asserting
this *layer* removes it (the registry stash is the proof), and `stdlib_backport.rs`'s keeps
the narrowness assertion for the four that stay gone. The new assertion is `-math.huge`,
because file-scope arithmetic is the shape that failed.
**`Texture:SetColorTexture`** is the modern spelling of `SetTexture`'s colour arm, which
1.12 already has (`0x770360`'s 8×8 solid surface, and `region/paint.rs` defaults the alpha
to 1 as it does). Retail split the names when `SetTexture` stopped taking a colour, so every
Era-era addon writes the new one. `Cinematic/Options.lua:701` paints its list border with it
and died at session start; OpenMapClassic its map chrome. Four lines of the same composition
chunk `SetShown` and the geometry pair use, and an `ALLOWED_BEYOND` row with both call sites
— `docs/ADDON_COMPAT.md` had it on the same "plausible and untraced" line as
`RegisterUnitEvent`, and this is that line's follow-through.
Movement, same harness both ways:
- Cinematic load: `Core.lua:1103` → `Frames.lua:2611` (~10 files further; wants
`RegisterUnitEvent` next). Session: `Options.lua:701` → `Options.lua:4130` (3400 lines on,
wants `SetWordWrap`).
- OpenMapClassic: unchanged at `DialogBorderOpaqueTemplate`, which neither fix touches.
benilla-ui 2076 passed, 0 failed. Full chain green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
e646b5a678 |
compat: the usable pair tells the truth — 1/0 from nampower's verb, the walk behind C_Spell's
Two defects in one pair of verbs, both found by reading what grom's addons do with what we answer rather than from a report. A DLL whose source is not to hand still has its contract written down: in the addons written against it. **`IsSpellUsable(id|name)` answers two numbers.** Ours gave 1/nil for the first — transcribed from `IsUsableAction`'s own convention — while already giving 0/1 for the second. Four readings in the corpus say otherwise, and `AlternativeClassicActionBars/Button.lua:1907` is the clearest: `elseif usable and usable ~= 0 then` is not a line anyone writes against a function that answers 1/nil. `SuperCleveRoidMacros/Core.lua:1035` labels its own branch "IsSpellUsable returned 0 = wrong stance/form, not enough power type", reading the value straight out of `pcall(IsSpellUsable, …)`. And the one with teeth: DoiteAuras' `_SafeSpellUsable` keeps a verdict only `if ok and u ~= nil` (`DoiteConditions.lua:970`), so a nil sent it down a fallback chain ending at `:1022`'s "treat as usable so icon doesn't die forever" — **every** Usable-mode aura lit on cooldown alone, stance, rage, range and reagents ignored. Answering the wrong shape and not answering at all land in the same place. 0 is truthy in Lua, so that was checked before changing it: no caller tests the value for truth unguarded, and the rest compare `== 1`. The DLL's string pool publishes the signature and the name-lookup error but not the return shape, and the five mirrors of the fork's `SCRIPTS.md` I could reach are each behind a Gitea login or 404 — the corpus was the better source. A name it cannot resolve raises there, which is why every caller wraps the call in `pcall`; ours answers `nil, nil`, and all four callers treat those identically. **`C_Spell.IsSpellUsable` was computing the wrong thing and raising on its caller's argument.** It was approximated as "known and not on cooldown" — the one reason upstream does *not* count (its own list is "hasn't learned it, lacks mana or reagents, reactive conditions haven't been met") and none of the ones it does. It now reads the same per-spell map the global does, the reference's usable walk (`0x6e3d60`), which agrees about cooldowns: "a running cooldown never greys here" (`spell/usable.rs` leg 11), leaving `GetSpellCooldown` the question about cooldowns. That makes it the honest answer for a reactive proc, which is exactly what its only caller asks: Overpower greys on the caster's combo-point byte (leg 5, `PLAYER_FIELD_BYTES` byte 1, the byte the server sets when the proc banks), so the walk answers it inside the proc window and nowhere else, while "off cooldown" showed it nearly always. pfUI's reactive indicator (`modules/nampower.lua:130`) passes a **name** against a `u32` signature, which mlua refuses for a non-numeric string — a raise once per frame while the indicator was switched on. `spell_ident` is shared now, so a name resolves through the book as the global's does. Both returns there stay booleans, unlike the global's: the caller hands the first straight to `SetShown`, where a numeric 0 would be truthy and show every icon forever. Two conventions, each its own caller's, written down where they live. Tests: the global's pair is asserted as `1|0` and `0|1`, with the callers' own accept test (`pcall` + `u ~= nil`) quoted as an assertion; `C_Spell`'s is asserted over Overpower outside and inside the proc window with nothing about cooldowns changing, by id and by name, plus the not-in-book case answering false rather than raising. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
9df4b6abd8 |
Read the client's key enum out of WoW.exe — the fact KEY_DOWN was blocked on
`KEY_DOWN`/`KEY_UP` were the last two nampower events left unfired, and the blocker was never effort: it was the client's numeric `KEY_*` enum. The DLL's own strings publish four values (`KEY_NONE=-1`, `KEY_SHIFT=0`, `KEY_A=65`, `KEY_ESCAPE=512`, `KEY_F1=768`) — enough to see three banks and not enough to name a key past the first in any of them, and a guessed code is worse than no event, since an addon switching on `arg1` would silently never match. It is in the binary, in the name parser `SetBinding` resolves a key string through (`0x4886f0`), and the arithmetic is in the instructions rather than inferred: - **ASCII, bank 0** — a printable key is its own byte (`movsbl (%esi)`), which is why `KEY_A` is 65. - **Numpad, `0x100`** — the digit character plus `0xd0` (`add $0xd0,%edi` at `0x48875b`), so `NUMPAD0 = 0x100`..`NUMPAD9 = 0x109`, then `+ - * / .` at `0x10a`..`0x10e`, each reached by its own character compare. - **Specials, `0x200`** — contiguous from `ESCAPE = 0x200` through `PAGEDOWN = 0x20d`, then a hole, `NUMLOCK = 0x20f`, two more holes, `PRINTSCREEN = 0x212`. Every one is a `mov $imm,%edi` in the chain at `0x4888a0`..`0x488a20`; the holes are codes this parser assigns to no name. - **Function keys, `0x300`** — `0x2ff + n` (`add $0x2ff,%edi`), so `F1 = 768` as published, and `NUMPADEQUALS` sits past them at `0x30c`, from the parser's own `=` arm. Transcribed into `bindings::chord::key_code`, which composes with the existing `key_token`, with a test per bank and the three published values as its anchors. `CAPSLOCK`, `SPACE` and the mouse-wheel names are in the client's name table at `0x846c20` but take no code from this parser, so they answer `None` rather than a number nobody can check — and a second test walks every token `key_token` can produce and fails on any that has neither a code nor a written-down reason. No caller yet, and that is the honest state: what is left for the events is a **producer** on the keyboard path, which is nampower's hook and the same hook the press-interception queue uses, so it belongs with that decision rather than ahead of it. `docs/ADDON_COMPAT.md` now says so instead of naming the enum as the blocker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
f8e93c4484 |
MasterSoundEffects 0→1 is how a 1.12 addon stops a sound — make it stop sounds
grom: SoftSpoken "played a voice line over and over", and its **Silence NPC Voices**
option did nothing although it was switched on. Both are one deviation, and it is ours.
There is no Dialog channel to mute on 1.12, and SoftSpoken's own shim says so:
function SoundUtils:MuteChannel(channel, muted, fadeOut)
-- Intentionally unsupported on this client.
end
function SoundUtils:StopSound(clip, fadeMs)
SetCVar("MasterSoundEffects", 0)
SetCVar("MasterSoundEffects", 1)
end
That toggle is its whole repertoire, and it is not a hack the addon invented — the
reference makes it work on the **enabling** edge:
0x457500 the CVar callback: cl = !!value, tail-calls…
0x7a6570 …the flag store, edge-triggered (7a657f: je on an unchanged flag)
0x7a67a0 walks the live instance list; per node:
flags & 0x2 → skipped (the cinematic bypass)
flags & 0x4000008 → muted, not stopped (0x7a65a0)
otherwise, and ONLY when enabling (7a67eb: test cl,cl / je):
flags & 0x4 → 0x7a5950 stop
else → 0x7a4200 stop, then unlink (0x7604f0)
`0x7a5950` is unmistakable: it takes the node's stream handle (+0x34) and channel id
(+0x30) and calls the FMOD stop thunks. The *disabling* edge stops nothing at all —
sounds run on, gated silent at the pump, which is exactly what `category_amp` already
did. So we had half the CVar, and it was the half that does not matter. The comment on
`SoundConfig::enabled` claimed the two "sound the same"; they do for a player moving a
slider, and do not for an addon.
Both symptoms follow. `MuteGameDialogue` is gated on `AutoToggleDialog`, whose default is
`(Version.IsLegacyVanilla or …)` — true here, which is why his saved profile has no
`Audio` block and the option was on anyway — and on this client its body is nothing but
the toggle. The gate passed, the toggle ran, the NPC greeting played straight over the
line being read. The repeat is the same toggle spent on SoftSpoken's own clip:
`PlaySound` cuts *before* it plays ("On Vanilla, interrupt NPC audio BEFORE playback"),
and `StopKeeping` is documented as "silence the head but keep its place: it replays from
the start when Advance next reaches it, since the client has no seek". With the cut inert
the head never stopped, so the replay layered a second copy on a first still going.
`kit::cut_effects_on_reenable` now stops every `SoundCategory::Sfx` channel when the edge
is spent. Music and ambience are spared (the reference's mute-not-stop class) and
cinematic audio never enters `SoundOutput::channels` at all (the `flags & 0x2` bypass).
Two details are load-bearing, both pinned:
- **The edge is armed by the write, never sampled from the value.** The addon writes 0
and 1 inside one Lua call, so a system reading `SoundConfig::enabled` once a frame sees
`true` on both sides and no edge at all. `EffectCutArmed` is set in the CVar observer,
the one place that sees every write. It is its own resource rather than a `SoundConfig`
field because spending it needs `ResMut`, which as a field measured **43** new
ambiguous pairs against every `Res<SoundConfig>` reader in the frame.
- **The cut is spent between the Lua pass and the sound drain.** The reference stops
synchronously inside `SetCVar`, which is before the `PlaySoundFile` the addon calls
next. Applied after the drain it would stop the new clip instead of the old one — the
symptom inverted, not fixed.
The ceiling rises by 1, for the pair against the collider finisher: the same exclusive
`&mut World` shape `log_new_corpses` and `textinput::open_early` already record.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
08d574b09c |
GetCastTime applies spell-mod op 0xa itself — so a talented Slam is 2.0 s, not 2.5
grom's probe closed the slam hunt in one line:
mx=2.7 st=1.0255 u=2.7 c=2.5 w=7.4074
Both of my remaining candidates were wrong. The swing speed arrived correctly
(2.7, not the 2000 ms stand-in the last commit instrumented) and the swing timer
was running, so neither the feed nor the combat events were at fault. That leaves
one term: `c`, the cast time, and `w = (2.70 - 2.50) / 2.70` falls straight out
of it.
`SuperCleveRoidMacros` does not ask a verb for that number. `GetSlamCastTime()`
walks the spellbook for Slam, builds a nil-parent `GameTooltipTemplate` scanning
tooltip, calls `SetSpell`, and reads the `"%d+%.?%d* sec cast"` line — so the
whole conditional rests on one line of our spellbook tooltip.
`Spell_C::GetCastTime` (`0x6e3340`) applies `SPELLMOD_CASTING_TIME` **inside
itself**, after the row's minimum clamp:
6e33b0: cmp %esi,%eax ; row minimum (2500 for Slam)
6e33b4: mov %esi,-0x4(%ebp) ; ...clamped to it
6e33b7: push $0xa ; op 10 = SPELLMOD_CASTING_TIME
6e33be: call 0x6e6af0 ; value = (value + flat) * pct / 100
and the tooltip's cast line is a call to it, not a DBC read (`52eb48`/`52eb4b`/
`52eb4d: call 0x6e3340`). We read `SpellCastTimes.dbc` directly, so a
talent-shortened cast showed its base length — a deviation `ui_action/mod.rs` and
`cast_times.rs` both *documented* without anyone pricing it.
The realm's own data prices it. Slam (1464/8820/11604/11605) is `CastingTimeIndex`
20, row `{base 2500, perLevel 0, minimum 2500}`. Improved Slam is two ranks —
12862 and 12330, aura 107 / misc 10 / `basePoints` -251 and -501 with `dieSides`
1, a flat -250 or -500 ms on op 10 against `SpellFamilyFlags` bit 21, family 4
(warrior, `ChrClasses.dbc` field 15; the five-rank version is `zzOLD`). So a 2/2
warrior's Slam is 2.0 s, and on his 2.70 s main hand:
2.5 s (ours) 0.20 s window 7.4% of the swing
2.0 s (his client) 0.70 s window 25.9% of the swing
It is a size, not a sign. 0.20 s is narrower than human reaction time, so a
player pressing by hand overshoots it every press; 0.70 s he lands in every
swing. "477 Heroic Strikes, zero Slams" and "works just fine on the normal
client" are the same observation.
Note the order: the minimum clamp is 2500 and it runs *before* the modifier.
Applying op 0xa first would pin Slam at 2.5 s for ever and look identical to the
bug it was meant to fix.
`Spells::cast_time_ms` is now `GetCastTime` — row resolve, minimum clamp, op 0xa
— with `cast_time_signed_ms` for the reference's `allowNegative` form the
tooltip's "Instant cast" ladder needs (`0x52ebce`), and `cast_time_row_ms` kept
for the one question that wants the row alone: whether a spell has a cast time
*authored* at all, which is `opens_cast_bar`'s gate. The tooltip's cast line and
the four live readers pass the modifier tables through — including both
`PendingCast` arms, so nampower's local lockout on a talented cast was 500 ms too
long as well. The tooltip feed re-pushes on a level change now that the cast line
is level-scaled.
Still deviating, written where it lives: the final `UNIT_MOD_CAST_SPEED`
multiplier (`6e342c`, taken only when `Attributes & 0x30` is clear) is not
applied, because no site streams unit field 139. It is 1.0 without a casting-speed
aura, so it moves nothing a physical ability does.
Tests: `a_talent_shortened_cast_line_shows_its_talented_length` grades Slam on the
real DBCs untalented, at 2/2, and against a foreign class family (the gate that
stops a mage's table shortening a warrior's Slam). The harness test now drives
grom's own addon chain through both widths — `WINDOW_PCT` 7.4074074074074 and
`WINDOW_PCT_TALENTED` 25.925925925926 — with the addon's own
`RefreshSlamCastTime` between them, because its scan is cached for 2 s and a test
that re-pushed without it would measure the stale value and pass.
Both earlier answers are written down in `WINDOW_PCT`'s doc and in
docs/ADDON_COMPAT.md: I called this "not ours" twice, from two green tests, and
each was green because it asserted a number I had supplied.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
89dc99f4d1 |
What the second player actually saw: four panel entries and no nameplates
A second player's four `/errors` entries, all from one session, none of them a
fault in the two addons he was reporting:
1. `FloatingChatFrame.xml: SetScript("OnInputLanguageChanged") unsupported`.
Ours. The slot is real 1.12 and the *realm's own* FrameXML sets it on
`ChatFrameEditBox`, so every player met a red error on every login for a
line none of them wrote. `SCRIPT_KINDS` now accepts it and never fires it,
which is also what the reference does — a client with no IME never fires it
either. The rule the old test stated is unchanged and restated: a 1.12 slot
benilla neither fires nor finds in a shipped file still raises.
2. `bindings.xml: root element is <Bindings>, not <Ui>; tolerated`. Every
`Bindings.xml` in the corpus has that root and `bindings_xml` reads it
correctly. No longer said.
3. `bindings.xml: Unknown frame type: Binding`, counted 69. Faithful — the
reference logs the same line (`0x871124`) for every row while `0x51f400`
registers them separately — and ours registers them too:
AlternativeClassicActionBars' 69 bindings all answer `GetBinding`. Now a
trace, so the terminal keeps it and the panel does not.
4. `SetOwner: unknown anchor 'BOTTOMRIGHT' on owner 'WorldFrame'`, counted 292.
The hunt for that caller ended a while ago: it is `Shop_ProcessEntries` in
the realm's own `Turtle_ShopUI`, off the player's patch chain, and the
reference accepts an unknown anchorType silently (mode 0, ANCHOR_LEFT), as
we do. The diagnostic has done its job; it is now host-log-once
(`Model::warn_host_only_once`) and not retained.
The panel is where someone looks to find what is broken in *their* interface.
A line that is faithful, or names a file they cannot reach, crowds out the ones
that are theirs.
----
A second player sent a video: Options → Nameplates with all six boxes ticked,
the V binding set, and no plates at all — while the overhead *names* drew fine.
`VPlateMode::default().range_sq` was `0.0`, and the gate is
`distance_squared > range_sq`. Zero is not "no cull"; it is "cull everything, at
any distance". `NameplateRange` is a registered CVar with default `"80"`, and a
registered default is not written to `config.toml`, so no `CvarChanged` ever
fired and the field never left its derived value.
This is the trap the paragraph three fields above already documents, and which
`occlusion` and `spread` were fixed by writing their registered defaults into
`Default`; `range_sq` was left behind. It stayed hidden because
`NameConfig::default()` — the overhead names — already carried `80.0 * 80.0`, so
names drew and only plates were missing, which reads as a nameplate-addon
problem rather than an engine one. It could not be reproduced here either:
grom's own `config.toml` carries `NP_NameplateDistance = "62"`, which seeds the
same field through the other observer arm.
`the_plate_range_default_agrees_with_its_registered_cvar` now pins the default
to `registered_pairs()`'s row and pins the plate and the name culls to each
other, since a disagreement between those two is what made this read as
somebody else's bug.
It also answers the other half of his report. The gate `continue`s before the
plate is seated at all, so with `range_sq` at 0 no plate frame ever exists —
and UnitPlates works by adopting the plate frames under WorldFrame. One bug,
both symptoms: "nameplates will not turn on" and "UnitPlates does nothing" are
the same line.
And a packaging answer that comes before both, for UnitPlates specifically:
GitHub's "Download ZIP" unpacks `Redbu11dev/UnitPlates` as `UnitPlates-main`
with `UnitPlates.toc` inside it. The client looks for `<Folder>/<Folder>.toc`,
so the addon never loads — here or on the real client. The harness says it in
as many words: "no manifest under other-addons/UnitPlates-main — is that an
addon folder?". It has to be renamed `UnitPlates`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
5751857890 |
The realm's client is not stock 1.12, and its Lua grammar is not 1.12's either
SoftSpoken (z1xel/SoftSpoken, a 1.12 narration port) failed to load: nine files across six addons would not compile, and the three that did then raised at runtime indexing namespaces the failed chunks never built. Every one of the nine is valid Lua 5.1 and invalid Lua 5.0 — `#`, `%`, `0x3040`, `local NAME = ...`. `third_party/lua-src` is a fork that deliberately 5.0-ifies the grammar: three arms deleted from `lparser.c` (`simpleexp`'s `TK_DOTS`, `getunopr`'s `'#'`, `getbinopr`'s `'%'`) and `read_numeral` replaced with 5.0's, which has no hex. Every deletion is byte-read out of `WoW.exe`'s own parser and every one is right about stock 1.12. No OctoWoW client runs stock 1.12. ClassicAPI.dll is in the realm's bundle and rewrites every chunk at the `luaL_loadbuffer` chokepoint — file scripts, `loadstring` and XML `OnLoad` alike — adding exactly five constructs: `#`, `%`, `...` as an expression, `0x` hex and `[=[ ]=]`. Confirmed three ways: the repo's own highlights line, PR #28, and the DLL's string pool, which twice says "Our 5.1 syntax backport makes the probe pass". `docs/ADDON_COMPAT.md`'s row for this said "free — benilla's VM is mlua/Lua 5.1 already". That was the error. So the target grammar is the union, and the fork now carries it: * the three parser arms restored, as stock 5.1 writes them; * `read_numeral` grown an explicit `0x` arm. It could not take 5.1's version back wholesale: 5.1 reached hex through a trailing `isalnum` slurp, and that slurp has to stay gone or `1then` breaks again (`AdvancedTradeSkillWindow2.lua:3878`, which the client compiles). The prefix arm gives both; * addon file chunks now called with `(addonName, addonPrivateTable)` (`UiScript::run_addon_chunk`, four call sites). Taking `...` without passing the arguments only moves the failure one line later, which is what ClassicAPI's own notes record; * `string.reverse` added to the stdlib backport — SoftSpoken calls it. 1.12's own dialect is untouched. `LUA_COMPAT_VARARG` withholds the `arg` table only from a function that *uses* `...` as a value, so the shipped FrameXML's `function f(...) … arg.n … end` still gets `arg`: the two dialects coexist one function at a time, which is what the client-plus-ClassicAPI does. The cost is the era probe. 170 corpus `loadstring` sites ask `loadstring("return function(...) return ... end")` and 92 library files in 24 folders branch on the answer; they now take the 5.1 branch, as they do on the real client. Two branch wrongly there and ClassicAPI ships a compensation for each — Compost-2.0 rev 17406+'s no-argument `Erase` stub (bundled by BigWigs and QuickHeal; it silently stops clearing in place) and Cartographer_Notes' `WorldMapDeathRelease`. Both are now carried in `lua51_probe_fallout.rs`, probed by behaviour rather than by revision and re-checked on every `ADDON_LOADED`, because AceLibrary can upgrade a library mid-session. Measured on grom's own AddOns folder: SoftSpoken, SoftSpoken_Stories, SoftSpoken_Places, SoftSpoken_Writings, SpokenZonesAudio, SpokenBooksAudio, SpokenQuestsAudio{Horde,Alliance,Gossip,Shared}, SSS_VOFallback and AI_VoiceOverData_Turtle all load with zero load errors and zero session errors, 98 `Spoken*` globals, `/softspoken` registered, and PLAYER_ENTERING_WORLD, QUEST_DETAIL, GOSSIP_SHOW and ITEM_TEXT_BEGIN reaching 108/108, 1/1, 6/6 and 2/2 handlers clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
f3fec804eb |
Two addons another player asked for, and the Linux-only gap that killed one
AlternativeClassicActionBars and UnitPlates, both now in `other-addons/`
(untracked — third-party source, not ours to vendor). Measured with the survey
and the read-back probe on a root holding only those two, so neither borrows the
other's globals. Both already loaded clean; three things were missing, and the
first was costing an entire addon's keybinding surface.
**A binding table spelled `bindings.xml`.** `ui_script::addons` asks for
`Bindings.xml` by the reference's own fixed name — `0x51f400` reads the name,
not a `.toc` entry — and the open was case-sensitive, so on Linux ACAB's 69
`<Binding>` rows never registered. Silently: an absent binding table is the
normal case, so nothing raised and nothing warned. Hover-bind, edit-mode escape
and every bar page key were dead. The texture and font side of the same file
namespace has always resolved case-insensitively (`loose_addon_file`, which
walks the path a component at a time and enumerates a directory when the spelling
does not match); the Lua and XML side now shares one resolver with it, and both
crates' private `read_under` copies delegate to it. Measured after: 69 bindings
listed by `GetBinding`, `SetBinding("CTRL-9", "ACABBIND1")` round-trips, and the
addon's drawn-frame count went 42 -> 175, because its login sequence had been
stopping short of its own bars.
**`PixelUtil`.** Called unguarded on ACAB's bar-geometry path at four sites,
three of them the same question — one screen pixel in UI units, which is what
every pixel-perfect border in that addon is drawn from. Not in ClassicAPI's
published list, but ACAB disables itself without ClassicAPI, so on this server's
kit it is present by implication. A Lua chunk beside `math_ext`, whose `Round` it
uses. One deviation, written down where it lives: upstream reads
`region:GetEffectiveScale()`, which a 1.12 texture does not answer, so the scale
read falls back to the parent and then `UIParent` — the same number in every case
that matters.
**`UnitXP("nop", "nop")` answering the boolean.** The presence probe.
SuperCleveRoidMacros keeps only `pcall`'s `ok`, but ACAB's `HasUnitXPSP3` wants
`ok and result == true`, and the fall-through handed it the stock XP getter's
`0` — truthy, so it passed the loose reading and failed the strict one. ACAB
reported UnitXP_SP3 absent on a client that answers the whole family.
Left alone with the reason in `docs/ADDON_COMPAT.md`: `IsStackableAction`
(post-1.12, guarded, and stock 1.12's own ActionButton.lua shows a count off
`IsConsumableAction` alone), `CursorHasMacro` (post-1.12 and redundant — ACAB
tests `GetCursorInfo() == "macro"` first), and five third-party tables that are
missing because those addons are not installed.
The harness grew a `--plates [n]` step, because a nameplate addon is the one kind
it could not see working: the subject is a `WorldFrame` child `vplates.rs` mints
per frame, so with none seated the addon's scan finds nothing, does nothing, and
reports a clean load over an empty screen. It seats n live plates and counts how
many an addon hung a frame on, which doubles as the contract check on the plate's
own shape — a plate that stopped being a `Button`, or stopped putting its border
texture first, would read as an addon adopting nothing. UnitPlates adopts every
plate and reads its name, health and level off ours.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
b5259bd608 |
The missing item values were eight absent id getters, not the tooltips
grom reported no item values on quest-reward tooltips. The value line is pfUI's, and `modules/sellvalue.lua` says plainly how it works: it hooks every `Set*Item` tooltip verb and takes the row's **id** — never its link — then asks `C_Item.GetItemSellPriceByID`. An absent getter answers nil and `AddVendorPrices` returns on its first line. So it was never about quests. This client answered exactly two members of that family, `C_Container.GetContainerItemID` and `GetInventoryItemID`, which is why his bag and equipped items had a price line and his quest rewards, loot rows, loot rolls, merchant rows, mail attachments and trade-skill rows did not. The other eight are here: `GetQuestItemID`, `GetQuestLogItemID`, `GetLootSlotItemID`, `GetLootRollItemID`, `GetMerchantItemID`, `GetInboxItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`. Each sits beside that row's own `*Link` getter, in that row's own module, reading state already held — no new feed, no new state. The id is also the **earlier** fact, which is the part worth keeping in mind. An item id rides the packet; a link is built only once the item's template has landed to give it a name and quality, and `ui_quest::resolve_item` says so in as many words. A quest reward is close to the worst case for that — it is usually an item the player has never seen, so its template is not in the 14,000-row cache and the first hover beats the answer home. These getters answer there, where the link beside them is still nil; the test asserts exactly that contrast. Also, from the same thread of play reports: the sell-junk sweep now says what it could not judge. `SellAllJunkItems` and `GetNumJunkItems` both gate on a *known* poor quality, and a carried slot's quality is `None` until its template arrives, so a grey sitting in such a slot is neither counted nor sold — which is what "not all grey items sell" looks like from outside. Whether that is in fact the cause is not something this end can tell, so the sweep records the slots it skipped for that reason, with their item ids, once per click. If the next log names some, the fix is to wait for those templates; if it names none, the cause is downstream in the drain or the server's refusal, and the line says that too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
c8c4e7f855 |
Four defects out of his log, and the 65 warnings that are not ours
His 19-minute session put 8 errors on screen from two causes, and 73 WARN lines of which 8 are ours. The whole triage is written down in the play-test memory, because most of the log is the addons' own and nothing should re-chase it: five files their `.toc`s name and do not ship, a QuickHeal template that is never defined, an ATSW global string that only exists in its deDE branch, inspect frames from a load-on-demand addon with no `## Dependencies` line, `<Binding>` elements the frame loader is *supposed* to log, and a world-map lag line that reported its own recovery 5.2 s later. Four of the eight are fixed here. `GameTooltip:SetSpellByID` was nil, and it is not a keypress: the traceback is pfUI's `ButtonEnter` into `SuperCleveRoidMacros/Core.lua:4217`, so it raised on every action-bar hover of a macro that resolves to a spell — 7 times, each leaving the button with no tooltip at all. `compat/classic_api/tooltip.rs` had declined the whole `Set*ByID` family pending "an argument-shape check against the stock verb each aliases". Done, and the check is why this is four lines: there is no aliasing to do, because `tooltip_spell::set_spell_by_id` is the body all four of 1.12's spell verbs already funnel through, and it takes a bare id — the book slot or list row is what *they* have to convert first. `show_rank` is the action's rather than the spellbook's, because the book draws "Rank N" itself as the button's `subSpellName` and an id-named caller has no such label. The caller's own comment says it reaches this only for a spell in neither spellbook, so the cold-view path is the common one here; that needs no special case, since the app's feed builds a view for any id in the catalog and an action button re-asks its tooltip every `TOOLTIP_UPDATE_TIME` while hovered. `resolve_spell_fx` despawned an entity the world no longer had. `FxInstance`'s root is a *stored handle*, and `attach_spell_fx` is the system that notices one lost with the unit's whole visual and re-arms or drops the instance — so between that loss and that pass, the same-slot replace, the spell-id reap and the self-termination clock all hold handles to entities that are gone. `try_despawn` at those three, as `lifecycle.rs` already does; `tend_world_plants` keeps its plain `despawn` because its entity comes from a query, not from a record. The first paste of every Wayland session could never have worked. `smithay-clipboard` sets `latest_seat` only on a `wl_keyboard` Key/Modifiers event or a pointer press (`state.rs:522`, `:386`) — `Enter` sets `has_focus` and nothing else — and refuses to load a selection without it. `HostClipboard` opened its backend on first *use*, so the backend that read the clipboard was created by the very Ctrl+V asking for it and its keyboard object was younger than that chord's own key events. It is now opened as soon as the window can give the `wl_display`. Costs +2 on the `Update` ambiguity ceiling, both pairs against the schedule's two exclusive `&mut World` systems, which pair with everything they have no declared order against. And the one `changed_windows` ERROR a login prints had no remedy behind it: no client can warp a free pointer on Wayland, so the loading screen's cursor hand-back now asks only where the answer can be yes — off Wayland, or with the pointer locked, which is winit's own rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
44aac335fd |
classicapi: the last two with data behind them — item sets and area triggers
Both over DBCs 1.12 reads C-side and publishes to Lua nowhere, so both
are facts no addon on this realm could reach.
`C_Item.GetItemSetInfo`: the tooltip prints a set block and `GetItemInfo`
reports no set at all, so nothing can tell which pieces complete one. The
table's shape is its caller's own documentation (`SuperCleveRoidMacros/
ClassicAPI.lua:171-174`) — including the bonus pair being
`{ spellID, threshold }`, which is the REVERSE of the DBC's column order,
so the swap happens once in the host and not in every reader.
`C_Map.GetAreaTriggerInfo`: pfQuest's own comment says it reads trigger
geometry from this "instead of a shipped coordinate table", and the three
fields it takes — areaID, mapX, mapY — go straight into `pfMap:AddNode`,
whose coordinates are 0-100. So the percentages are the consumer's
measured shape rather than a guess. The zone resolution is the host's,
through the same `zone_uv` law the map window's own hover uses
(`WorldMapUiData::zone_at`), so a trigger lands in the zone the map would
name for that point — quirk included: a point exactly on a rect's east or
south edge reads as outside, which is that law's own `(0, 0)`. The three
map fields are absent TOGETHER when the centre resolves to no zone (an
instance interior), which is exactly the case pfQuest guards for.
Both ride host probes rather than pushed tables: the resolution needs the
`WorldMapArea` rects, which do not cross the seam, and neither catalog is
something to copy per VM for a verb nothing calls at boot. Two catalogs
gained an `iter()` for it, each said to be for this.
That leaves four of the original 56. Two have no caller in the corpus;
`C_FriendList.SetFriendNotes` has no note field on the 1.12 wire at all,
so a note typed and lost at logout would be worse than a button that does
nothing; and `C_BattleNet.SendGameData` stays deliberately absent, because
every caller guards it and registering it would claim a BattleNet this
client does not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
546e60765d |
classicapi: fourteen members over four namespaces — the measured tail down to six
Of the 153 `C_<Namespace>.<Member>` names grom's addons reference, the fresh VM was missing 56; this takes it to 6. Fourteen members here, each over data or a stock verb that already existed: `C_Spell` gains the four melee-timing predicates and `IsSpellInRange`. 1.12 answers all of those only about an ACTION BAR SLOT — `IsAttackAction` and `IsAutoRepeatAction` — so a spell the player knows but has not placed on a bar cannot be asked at all, which is the gap a condition engine keeps running into. The three attribute answers ride named booleans on the spell lookup rather than a raw attribute word, so the bits and their reference citations stay in `benilla-formats` where they can be checked; `do_not_reset_combat_timers` is named there for the first time, beside the `ranged_speed_cooldown` that already tested the same bit. The melee auto-attack is an ID (6603) rather than an attribute, because no bit marks it — the ranged half has the auto-repeat flag and the melee half is that spell. `C_SpellBook.GetSkillLineName` is the most-read missing member of the whole family (nine call sites) and is one delegation to `GetSpellTabInfo`. `IsAutoAttackSpellBookItem` resolves by id against the same 6603, so the two cannot disagree about what auto-attack is. `C_Item.GetCurrentItemLevel` answers a number 1.12 prints in a tooltip and returns from nothing, so no addon can compare two pieces of gear. `RequestLoadItemDataByID` is the modern async load over the ask-once queue `ItemMixin:ContinueOnItemLoad` already drives. `GetWeaponEnchantInfo` is a pass-through. `C_QuestLog`'s abandon pair delegates to the stock two-step, so the mark, the still-in-the-log search and the send keep one owner — a namespaced abandon cannot drop a quest the stock path would have refused. The six left are each written down with what they need. Two are a host probe over a DBC `benilla-formats` already parses (`AreaTrigger` for pfQuest's trigger geometry, `ItemSet`), two have no caller in the corpus, `SetFriendNotes` has no note field on the 1.12 wire at all — a note typed and lost at logout is worse than a button that does nothing — and `C_BattleNet.SendGameData` stays deliberately absent, because every caller guards it and registering it would claim a BattleNet we do not have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
38a6b6f149 |
unitxp: inSight and behind — four macro conditionals that silently never worked
Not a gap: a defect. Defining `UnitXP` at all is what makes
SuperCleveRoidMacros believe the whole family is present —
`hasUnitXP = pcall(UnitXP, "nop", "nop")` (`Init.lua:274`), which
succeeds here because the dispatcher's fall-through does not raise — so
four macro conditionals have been running against commands this client
did not implement: `[insight]`, `[noinsight]`, `[behind]`, `[nobehind]`,
each with a count mode too.
The fall-through answers the stock `UnitXP(unit)` getter's 0, and every
one of those conditionals compares against `true` LITERALLY (`== true`,
`~= true`). So `[insight]` and `[behind]` could never fire while
`[noinsight]` and `[nobehind]` always did, with nothing to notice it by.
That is why both commands answer a boolean rather than 1.12's `1`/nil
flag shape, and why a pair they cannot measure answers nil rather than a
fabricated `false`: the two directions are read separately, so "cannot
say" has to be distinguishable from "no".
`inSight`'s blocker is gone, and it was gone before this: the row said
"needs a real raycast with no current binding-layer access", and the
nameplate-occlusion work built exactly that raycast. This is the same
body-layer ray from the player's chest instead of the camera's eye, fed
into the per-token `UnitReach` record `distanceBetween` already reads —
only the app can trace the world, which is the whole reason the command
sat blocked in the binding.
`behind` this file called *unconfirmed* ("treat as unconfirmed until seen
elsewhere"). It is now seen elsewhere: ten call sites, with the argument
order documented by its own readers, so `behind(A, B)` asks whether A
stands in B's rear arc. The arc is the rear HEMISPHERE, which is what
this realm's server means by behind — vmangos tests `!HasInArc(M_PI,
attacker)`, the same rule that accepts or refuses Backstab — taken flat,
so a ledge above a mob's back counts and directly overhead does not.
Two things my own tests caught, both worth keeping:
- A two-argument `inSight` answered `true`. These are recognised
commands, so they now match on the NAME alone and say nil when the
arguments make no pair; the arity fall-through is right for an
unrecognised command and wrong here, because neither name is a unit
token so the stock getter can only answer a meaningless 0 — which is
truthy in Lua, i.e. a confident "yes, in sight".
- A `#[cfg(test)]` module placed mid-file hid every `fire_event` after it
from `event_shape_gate`, which truncates each file at the first one:
six unit-field bridge events read as unproduced. The test moved to the
file's own tests module, where the one-test-module-per-file convention
that gate assumes puts it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
434d514879 |
superwow: the chat bubbles are widgets — CREATE_CHATBUBBLE and pfUI's skin
The last real SuperWoW feature, and one piece of work closed three things: `CREATE_CHATBUBBLE` (which could not honestly be fired, because its payload can only be the bubble), `C_ChatBubbles.GetAllChatBubbles` (which answered an empty list, truthfully, for the same reason) and pfUI's whole bubble skin, which walks that list. `benilla-ui`'s `script/chat_bubble.rs` is a pool of real frames — anonymous WorldFrame children with a backdrop (`0x4b0a35` IS `SetBackdrop`), a tail texture and a FontString, which is what the reference's own `CGChatBubbleFrame` is and how addons of the era found bubbles before SuperWoW published the event. The app keeps everything a bubble's existence depends on — the spawn gate, the lifetime, the fade, the range re-test, the stacking sort and the text measurement that sizes the frame — and hands it over as `BubbleState`: the same split `vplates.rs` already makes for the plates, so the engine decides what is on screen, the widget layer decides what Lua sees, and neither can disagree about a bubble because there is one feed. Three things improved by the move rather than being preserved by it: - The bubbles sit at frame levels 2, 3, 4… over the V-plates at level 1, which is the reference's own order (`0x4b12d5`-`0x4b1312`). The quad band had them UNDERNEATH — a deviation `overlay_z` carried in a comment. That band is gone and the lane is down to one tenant. - The art is loaded, tiled and bleed-inset by the UI pass like any other frame's, so this file drops its own texture resource and its own half-texel inset helper. - The system moved BEFORE the paint. It sat in `UiQuadAppend`, the window *after* the extract: right for an appender, a frame late for a feed. The test that matters transcribes pfUI's own `ScanBubbles` loop — it finds the FontString with the line and its chat-type colour, blanks the one Texture region and parents its own frame to the bubble — and then checks the EXTRACT for drawn quads: both backdrop halves, the tail, and the text at the em the app measured the box with. A test that only checked the widgets would have passed with nothing on screen. Two architectural gates caught this and both were answered rather than silenced. The one-shot gate: `drive_bubbles` now holds the VM and drains a queue, so it is EXEMPT with the argument — the queue's product is app-side state, the widget sync is idempotent and binds on the first frame there is a WorldFrame, and gating the system instead would pile lines up and spawn them all at once with fresh lifetimes. The VM wall: its ceiling rises by one with the reason, which is that this file is `vplates.rs`'s exact twin. `CREATE_CHATBUBBLE` fires per APPEARANCE, not per pool slot ever built — the opposite of `NAME_PLATE_CREATED`'s rule, because a recycled frame has been re-anchored, re-sized and re-texted since it was last seen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
bead2cb22b |
superwow: Clickthrough is live — the last function on the DLL's manifest
It had been stored and read by nothing, which is "a knob with two positions and one picture". Now the world pick looks through a dead unit that holds no loot for us, which is what SuperAPI's own description asks for: "click through corpses to loot corpses underneath them" — the pile after an AoE pull, where the one body you want is behind four empty ones. The test is `CanLootUnit`'s own, negated (`clickthrough_skips`), so "lootless" means one thing to that verb, to this pick and to the loot sparkle; and because `UNIT_DYNFLAG_LOOTABLE` is per-viewer on the server, a corpse somebody else may loot is lootless TO US, which is exactly the pile the feature is for. A living unit is never skipped whatever its flags — that would make mobs unclickable — and a unit whose descriptor has not streamed is not known to be empty. The flag is mirrored out of the VM once a frame rather than read in the pick, which is a `SystemParam` with no reach into the script; a one-frame lag on a preference set once is immaterial. The resource is `Option` in the pick, like `factions` beside it: a harness that drives this pick builds the world it needs and nothing more, and absent means off, which is also the default. The stale aggregate row this closes is rewritten rather than deleted, because what it got wrong is the lesson: it called six functions "blocked — each needs new app-side state", and three of those blockers were answered by data the tree already had. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
0e02401325 |
superwow: CastSpellByName at a unit, FoV, NameplateMotion, the raw log
The DLL publishes its own **usage strings**, in one block beside the names it registers, and reading them turned up four things this file had wrong — three of them bugs rather than gaps. `CastSpellByName(name [, "unit"])`. A unit ID, which is what every real call site passes and what `Model::unit` already resolves. It used to fall through to the ordinary queue, which casts at the player's OWN selection — a silent wrong target at six sites in grom's corpus: pfUI's /castfocus (a GUID) and /castmouseover (a token), Decursive's cure-at-guid, BigWigs' spell requests and two encounter scripts. Each asked for a unit and got whatever was targeted. The stock `onSelf` meaning keeps its behaviour: only a string that resolves to a live unit takes the new path. `CombatLogAdd`'s second argument names a FILE, not the event. There is a third log literal in the same pool — `Logs\WoWRawCombatLog.txt` — and it is the GUID-form log, the same lines RAW_COMBATLOG carries as arg2. `ui_chat/logging.rs` keeps it beside the ordinary one now, opened by the same `LoggingCombat`, which also makes the arg2 composition worth paying for when nothing is registered but the log is on (`wants_raw_form`). My first reading, "also fire the event", was an inference from the name alone, and the second literal settles it. `FoV`'s hold-back is resolved, and the answer was already in the tree. The row was held because 1.57 looked irreconcilable with CAM_FOVY's 0.785. It is the same number: 1.57 is π/2, the reference's own `[camera+0x40]` default of 90° in radians, and that field is not a vertical FOV at all — `scoped_view.rs` already divides by it as a DENOMINATOR, because what it expresses is a ratio. So upstream's own default is this client's existing lens by construction, which is what the test asserts; the DLL even registers the literal `1.57079`. Live, where upstream's panel says "Requires UI Reload". `NameplateMotion` asked for an investigation that was also already answered: benilla spreads plates with the reference's own SmartScreenRect solver. 0 (Overlap) bypasses it, 1 (Default spread) is it; 2 and 3 are the mod's own algorithms and fall back to 1, a documented silent-equal rather than a refused row. Also: `GetTradeSkillItemLink(index[, spellLink])`, the only way to link a recipe that makes no item — an enchant, whose product_item is 0 and which answers nothing at all on the stock surface. And four stored-only rows (SelectionCircleStyle, UncapSounds, HealingText, InterfaceVolume), each with its own reason, so SuperAPI's options panel round-trips. Three ghosts retired: outline/shadow text, force-show-auras and XP text are in no part of the DLL's string pool, nor is NameplateVerticalSpacing. They came from the early wiki reading, not from the binary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
323f3ae03c |
superwow: the map pair, GetQuestLinkForID, GetWeaponEnchantID
Four more off the DLL's manifest, and two measured corrections. GetMapPositionWorldLoc is the INVERSE of GetPlayerMapPosition, which is what the test asserts rather than three numbers: `map_proj::zone_world` is the reference's `0x4a7100`, whose own note is that used one axis per call it inverts the `zone_uv` that verb answers from. The law is transcribed into benilla-ui (which cannot depend on benilla-world) and the round trip is welded, which is the thing that actually keeps the two in step. GetMapBoundaries shares one new `worldmap::loc_rect_at` with the hover grid's own windowing, so no two readers of a (continent, zone) selection can disagree about which rect it means. GetWeaponEnchantID is the one thing GetWeaponEnchantInfo cannot say: the reference reads that very dword as its gate and then reports only that there WAS an enchant, so nothing on the stock surface can tell Windfury from Rockbiter. The id was already in hand and discarded as a `?` gate, so this was one field on the snapshot, plus a test that the two functions cannot disagree about which hands are enchanted. GetQuestLinkForID answers for a quest in the log and nil otherwise — which is also what upstream answers on the first call for one it fetches. The gap is one feed, not new protocol: the ask-once CMSG_QUEST_QUERY cache already exists app-side. That row also corrected the LINK FORMAT for both quest getters. The DLL's own formatter is `|cffffff00|Hquest:%d:%d|h[%s]|h|r`; ours was built without the colour wrapper on the reasoning that it matched the stock quest-log shift-click — but 1.12 has no quest links at all, so there was no stock shape to match and the measured string is the only authority. The four sound-driver getters are deliberately NOT built, with the reason recorded: every one of their return shapes is unknowable from here, none is called in the corpus, and this client has one backend and no enumeration to report — the same bargain KEY_DOWN/KEY_UP lost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
400457ff82 |
superwow: TrackUnit/UntrackUnit, and the four chat-bubble CVars
TrackUnit is the one with a live consumer and a real feature behind it: 1.12 draws party members on the minimap and raid members NOWHERE, and pfUI puts the whole raid on it with these two — for every party and raid token on PARTY_MEMBERS_CHANGED/RAID_ROSTER_UPDATE/PLAYER_ENTERING_WORLD, each inside a pcall, so on a client without the function the feature is simply absent, which is what it has been here. The set is guids, not tokens, so a tracked raid member keeps its dot while the roster shuffles its token out from under it. The dot itself is drawn by a fourth always-show clause beside the reference's own three (`shows_tracking_dot`) — `tracked_creature` stays exactly `0x5ed210`, so the predicate that file documents is still the client's, and a tracked unit gets the ordinary cell, hover name and distance cull with nothing new drawn. The set is read off the VM handle the blip system already holds rather than behind a resource of its own: `BlipInputs` is at the 16-param ceiling and its last element is already a nested pair for that very reason. The bubble four come straight off the DLL's string pool. Three are nampower's `NP_ChatBubbles*` under SuperWoW's unprefixed names, writing the same fields — two mods, one knob, last writer wins, as the nameplate reach already is — each keeping 0 because the reference bubbles none of those kinds. `ChatBubblesCreatures` is SuperWoW's alone and the only behaviour of the four: it NARROWS `ChatBubbles` rather than standing beside it, because the mod hooks the reference's own gate and a switch added at that site can only subtract; the other reading would bubble creatures with the master switch off. It ships 1, so an untouched client bubbles monster say and yell exactly as 1.12 does. All four are welded to their struct literals in the defaults-mirror test. CREATE_CHATBUBBLE is deliberately NOT fired, and the row says why: its payload can only be the bubble, and this client's bubbles are not Lua frames — the same wall `C_ChatBubbles.GetAllChatBubbles` already answers an empty list for. Argless would be worse than silence, since an addon written against upstream reaches for arg1's regions and raises once per bubble. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
ac857ada36 |
superwow: CanLootUnit, GetSpeed and UnitNameplate
Three of the DLL's own manifest, and two of them needed no new feed at all — which is the point worth recording, because the row that called this family "todo, blocked: each needs new app-side state" was wrong about two of the three. `CanLootUnit` is the loot sparkle's test read through the token map: `UnitState` already carries `dead` and raw `dynamic_flags`, so a corpse that sparkles answers 1 and there is one source of truth for "has loot for me". The bit is per-viewer on the server, so "can" means can. `UnitNameplate` goes through the SAME `plate_frame` wrapper as ClassicAPI's `C_NamePlate.GetNamePlateForUnit`, welded by a test that asserts both names hand back one object — an addon may feature-detect either, and two different frames for one plate is the kind of bug that only shows up as an anchor in the wrong place. `GetSpeed` is the one that needed a field: the posture snapshot gained `speed`, filled from `net::current_speed`, the reference's own `CMovement::GetCurrentSpeed 0x7c4c90`. What could not be settled is written down rather than guessed over — upstream publishes no prose and nothing in the corpus calls it, so "current" is a reading, taken because it is the more informative of the two and because it is the only function in the client named after speed that a hook would return. Standing still it is 0, as that getter is. `SelfMovement` loses its `Eq` (an f32 field); nothing compared two snapshots — the feed assigns a fresh one each tick. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |