5e2f1f3ac8db4af5202292e141e82759d0c873fa
150
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
ee9cfdcacf |
Two settings for things arriving: a foliage distance row, and a ramp for lights
**Foliage Distance** (`foliageDistance`, Advanced Graphics, 70..300 yd). The reference draws ground clutter to a hardcoded 70 yd (`[0x867958]`) and we inherited it, so grass, reeds and small bushes arrive at a fixed ring around the player — which is exactly where the eye is drawn to them appearing. The row moves the ring; it changes nothing about what grass looks like. Measured on an Elwynn fence line (`fence-shadowline-day`, clutter density 2), p50 frame time / main thread: 70 yd **3.25/3.26 ms**, 140 **3.32/3.24**, 200 **3.48/3.38**, 300 **3.69/3.42**, with the GPU flat throughout. Doubling it is free; past 140 it costs main thread, which is the budget this client is short of. The registered default IS the reference's own 70 and the row is in no preset ladder, so nothing moves until a player asks it to. It commits on Apply: `fade_far` is baked into the clutter material key, so a change drops every built patch to be re-cut (338 meshes on one measured write) and a drag would do that thirty times a second. `clutter::remesh_on_cutout_change` is now `remesh_on_material_change` and watches both the alpha ref and the fade for that reason. What it buys in a STILL is small — 70 -> 300 moves 0.6% of a frame, the far tufts being sub-pixel — and that is the wrong instrument: the complaint is grass ARRIVING, which no static capture can show. **Light arrival fade** (`lighting::global_light`). No setting: a correction, not a taste. A fixture enters the packed light table for several unrelated reasons — the portal walk reaching its room, its WMO group streaming in, the camera crossing `INTERIOR_NEAR_ADMIT` or `POINT_PACK_RADIUS`, the 255-entry capacity cut re-sorting — and every one of them was a hard edge that switched a light on at full brightness between one frame and the next. Measured in Ironforge by diffing the admitted set between static vantages: walking **3 yd exchanged 2 of 172 admitted lights, and 10 yd exchanged 8** — about one arriving per yard, which at a walk is several a second. The table ran 110-196 across five vantages and never reached the cap, so this is admission churn and NOT capacity; the cap does bind transiently during a world load (305 candidates seen once), which is why the boundary fade below is kept. Arrivals now ramp over 0.35 s, whichever rule admitted them. The scale lands on the light's colour before the pack, so the GPU table, the CPU mirror, the fog lamps and the froxel volume all follow from one write. Deliberately NOT symmetric: a departing fixture still goes at once, because keeping it alive to dim means keeping its room claim alive with it, and a stale claim gates a live light with a dead building's identity. **Spell lights are exempt**, and that is the exemption that matters: a fireball's light lives about as long as the ramp does, so ramping it would spend the whole effect at half strength. Beside it the capacity cut itself is softened — the last 24 admitted ranks scale toward zero across the distance band they span — and nothing fades while the table fits, which is what makes the control possible: with the ramp on against off, the Goldshire inn and the Great Forge both settle BYTE-IDENTICAL. Also recorded in docs/GRAPHICS.md: captures force `WOW_CLUTTER_DENSITY=0` for byte-stable baselines, so a run that sets only `WOW_CLUTTER_FADE` measures a scene with no grass in it and reports every horizon as free. Three rounds of this measurement were worthless before the starved control caught it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
33fc0c2023 |
Froxel volumetrics: the haze stops painting shadowed air the colour of sunlit air
`volumetricFog` gains tier 3, "Volume", with a "Shadowed Air" dial beside it; the Ultra rung of Lighting Quality takes it. A view-space 160x90x64 froxel grid — a fixed BUDGET, so every resolution costs the same — marched one compute thread per screen COLUMN, front to back, with a reprojected eight-frame history. Inject and integrate in one pass, because a froxel past the last shadow cascade has no evidence either way and the honest answer is the mean of what the rest of its column could see; a thread that owns the column can carry that and a froxel-per-invocation dispatch cannot. It publishes two things: - **the lit share** — how much of the air in front of each pixel the sun actually reaches, weighted by each slice's own contribution to what the eye sees through it. `volumetric_fog`'s haze multiplies its colour by it. The haze below this rung is the reference's: a zone colour mixed in by distance, the SAME colour whether the air it tints stands in open sun or a mountain's shadow, and no screen pass can tell it otherwise because a shaft march only ever ADDS light. On an Elwynn overlook at noon, 74% of the frame is air the sun does not fully reach. - **the lamps' in-scatter**, free once a volume exists: the phase lobe evaluated at every point of the air rather than once per ray at its closest approach, attenuated by the medium it crosses and strengthened when the medium is. `lampFog`'s analytic halos retire into this solve at tier 3, and that row still chooses which lamps and how many. The gain is derived, `0.9 / density(clear night)`, so the two lanes match on a clear night. At the Goldshire inn, isolating each by rendering lampFog 0 against 2 on both (the bases are byte-identical, which is what makes it a comparison): the volume's are 1.30x and wider. The sun's SHAFTS deliberately stay on `volumetric_light`'s per-pixel march, and that is a measurement. A volume bands in hard horizontal steps at 160x90, 320x180 and 534x300 alike, because one froxel column hands the same value to about a hundred pixels and a canopy's shadow is finer than any grid worth paying for. Spreading each column's samples across its own screen footprint (R2 down the column) turns the steps into a gradient and is worth knowing for any volume, but the march still wins on detail. docs/GRAPHICS.md has the sweep. Safety is one number: fully lit air multiplies by EXACTLY 1, so at Shadowed Air 0 the lane renders byte-identical to High over all 1,440,000 pixels with every pass still running. Getting that exact needed `1 - (1-floor)*(1-lit)` and not the `mix(floor, 1, lit)` it equals — `mix` is a fused multiply-add and lands a last ulp below 1.0, which moved five of nine tier 0-2 control pairs with the rung switched OFF. The shipped floor, 0.85, is deliberately half the depth of the first one tried, and the dial reaches twice it. Control: nine tier 0-2 pairs against the previous build, five byte-identical and four differing by one or two PIXELS out of 1,440,000 at 1-2 of 255 — every tier-0 pair identical, and tier 0 is the tier that switches the fog pass off, so the remainder is the fog shader's instruction schedule changing because the shader did. Cost is under the noise of both instruments: the frame probe puts Volume inside High's own run-to-run spread, and the GPU pass timer puts `gpu_other` at 0.75 -> 0.76 ms. Also recorded: `water-lake` leaves the deterministic row once shadows are forced on — it differs from a build of ITSELF by up to 88/255 on 0.03% of pixels. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
848c840ecf |
SSILVB: the AO lane stops only darkening and starts bouncing light
`ambientOcclusion` 3 (Indirect Light), the Ultra rung. Tier 3 of the AO row rather than a row of its own, because it is one trace: the bitmask that measures occlusion properly is what produces the bounce, so a player choosing between them would be choosing between two halves of the same thing. Classic SSAO sums a cosine term per tap, so two occluders covering the same direction count twice and a cluttered corner over-darkens. A bitmask counts DIRECTIONS: each slice owns a 32-bit word of angular sectors, a tap sets the run it spans, overlap is a no-op. And the bits a tap sets that were NOT already set are exactly the share of the hemisphere it is the nearest thing in — i.e. the fraction of the receiver's light it alone is responsible for. Its own pixel colour, weighted by that, IS the one-bounce indirect term. No second trace, no probes, no extra pass. ## The lane was already shaped for it - `STAGE_AO` already bound the resolved scene colour, for the bright-pixel protection mask. That buffer is exactly what a screen-space gather reads. - The apply needed no new pass and no dual-source blending: visibility in `src.a`, bounce in `src.rgb`, blend `One, SrcAlpha` = `bounce + dst*vis`. - Only the half-res stages grew, by one attachment each, and the uniform's two spare floats carried the strength and thickness, so its layout never moved. ## The one part that is ours and not the paper's **The bounce is scaled BY THE OCCLUSION.** A physically-measured bounce is wrong here: 1.12's lighting already carries an ambient term standing in for indirect light, so adding a measured one double-counts it. Unscaled it lifted a daylight exterior's median from 85.7 to 101.3 and washed the frame flat. Scaled by `occ`, the lane puts back only the share of the ambient the occlusion just took away, in the occluder's colour. An open field gets nothing, because nothing was removed there; a corner gets the colour of the wall shading it. Redridge overlook noon median 85.7 -> 86.7 Goldshire inn noon median 26.7 -> 29.0 Neutral in daylight, +8.6% in the shaded room — the same shape as the auto-exposure lane's floor of 1.0, for the same reason. The gather runs in LINEAR (`c*c` in, `sqrt` out): the scene is stored in gamma, and 0.5 gamma is 0.22 of the light, not half of it. ## Cost Frame time is unchanged (inn 2.22 -> 2.21 ms over 600 frames; overlook 2.25 -> 2.11). That is the frame being CPU-bound, not the lane being free. The GPU meter is the honest instrument: whole-frame GPU 1.68 -> 1.81 ms, and the bucket these passes land in 0.40 -> 0.54. So **+0.14 ms of GPU over High and nothing on the main thread** — which is why the Options text says "a fraction of a millisecond" rather than what I first wrote before reading the meter. ## Two bugs, both found in the intermediate rather than the frame - **Screen +y is view -y.** `view_at` flips clip y, so a slice direction built as `vec3(omega, 0.0)` measures its taps against a mirrored normal. One slice of two was right and the other inverted, averaging to A FLAT FLOOR READING 50% OCCLUDED. `WOW_AO_DEBUG=4` showed it in one run; the composited frame never would have. - **`a / b` and `a * (1.0 / b)` are not the same number.** Factoring the blur's divisor into one reciprocal — a tidy-up, not a change — moved `water-lake` at tier **1** by up to 53/255 on 0.021% of its pixels, because the AO is multiplied into the colour the water lane's depth-march then refines by bisection, and one ulp picks a different step. Restored to two divides. ## Control Tiers 0-2 byte-identical to the build before this, on three deterministic scenarios at three tiers each, nine of nine. `fence-shadowline-night` had to be dropped mid-control, and it sharpens the determinism table added last commit: it screens as deterministic at the DEFAULT tier and is not at tier 1. Screen a scenario at the setting you are about to measure it at, not at its default. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
4c3c128594 |
Screen the capture scenarios for determinism, and correct what rested on one
Routine work on the sharpening lane needed a byte-identity control, so I checked the baseline against itself first. Three of seven scenarios do not reproduce run to run with ONE binary: overlook-noon, water-lake, water-ocean, fence-shadowline-night identical stormwind-canal-near bimodal: 7 of 15 pairs identical, rest 0.01% / max 96 inn-noon, volfog-inn usually the bottom bit; one pair 37.8% / max 199 `stormwind-canal-near` is the one that matters, because the water-mirror write-up used it as a byte-identity control and it cannot be one. The identity measured there was a coin flip that landed well. The finding it supported still stands, but on its EXTENT and not on the amplitude it was also quoted at. "0.75% of pixels by up to 96/255" reads as one observation; it is two, and only the first is evidence. 96 is what the canal's own flicker reaches with nothing changed at all — the amplitude says nothing. 0.75% against the scene's own 0.01% is 75x, which is the part that was real, and the run condition is still the right fix. docs/GRAPHICS.md's harness section now carries the table, so the next person reaching for a control picks from the first row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
11dda4f9cf |
Two cheap lanes: RCAS sharpening, and a field of view a player can move
Both were named in docs/GRAPHICS.md as nearly free and neither had been built. They are unrelated to each other; they land together because each is small. ## Sharpening (`sharpening`, 0 Off / 1 Low / 2 Medium / 3 High) RCAS, FSR 1.0's sharpening half taken on its own. It does not resample, so it needs none of the upscaler's motion vectors, jitter or history — which is the whole reason it can land while §7 cannot. One 5-tap cross; the lobe is not a constant but the widest one that cannot push the result outside the ring's own range, solved per channel against a peak of 1.0 and taken as the most conservative of the three. Flat regions get nothing and nothing rings. The seat is most of the design. `Core3d`, after `FfxGlowLabel` and before `Node3d::Bloom`: - After the glow, not before. `FfxGlowNode` builds the blur chain the glow and the underwater haze read, and a sharpened bloom source is how an unsharp mask turns a lit window into a ringed halo. - The world view only. The world's last reader is the FFXGlow combine drawn inside the UI camera's main pass, which binds `main_texture_view()` at node-run time per frame, precisely so a `post_process_write` upstream lands. The interface is drawn afterwards into a different target — and sharpening text is the one thing this must never do. - Every tap saturates, as the combine's own scene read does one pass later: the limiter is solved against a peak of 1.0, so a super-white tap would invert the bright side's sign. Nothing is lost, the blur chain already took its copy. - No denoise term. The deband dither it would exist to suppress is added downstream, in the combine's exit. Measured, 600 frames: Redridge overlook 2.95 -> 3.02 (Medium) -> 3.01 (High); Goldshire inn 3.46 -> 3.51 -> 3.29. Inside the noise, the inn reading *faster* at High being how you know. CPU flat. What it buys: 3.8% / 19.9% / 48.6% of the frame moved by more than 2/255. **The first run hit the trap this repo already has a note about.** `max(4.0 * mx, 1e-5)` where `mx` is a `vec3` is `error: inconsistent type passed as argument #2 to max`. naga refused the shader, the pass drew nothing, and the capture came out looking perfectly normal. One `grep -ciE 'error:'` on the capture log answered it before a single image was diffed. ## Field of view (`fieldOfView`, vertical degrees, 40..70, default 45) Field of View on the Graphics page under Render Scale, live as you drag. It is the BASE lens, not a fourth factor. `player::scoped_view` already multiplied two things in and neither is a field of view: SuperWoW's `FoV` is the reference's `[camera+0x40]` as a ratio, the spyglass is a zoom off normal. Both answer "how much wider than normal"; nothing a player could move answered what normal IS, and the answer was a `const` in `benilla-world`. So the term order is `base × FoV × zoom`, and an addon writing `FoV` still means the same fraction of the lens the player chose. Vertical degrees, not horizontal, which keeps the reference's Hor+ framing: a fixed vertical lens widens horizontally as the window does, so an ultrawide player gains picture at the sides instead of losing it off the top. The default is `CAM_FOVY` to the bit — `45f32.to_radians()` and `FRAC_PI_4` are both `0x3f490fdb` — which the defaults-mirror test now asserts on `to_bits()` rather than against a tolerance. An ulp of projection is a pixel in a capture diff, and a row that reframes the world for a player who never opened Options is not a row, it is a regression. ## Both lanes off: byte-identical Against a `git stash`ed build of the same tree: `overlook-noon` and `water-lake` identical, and `fieldOfView=45` written explicitly identical to the row never touched (40 and 65 move 97.6% and 99.3% of the frame). `inn-noon` is excluded WITH PROOF rather than quietly: two runs of the *same* binary differ on 37.8% of their pixels by up to 199, because that scene's flame emitters do not reproduce. Its own before/after delta is 0.00% of pixels at a maximum of 1. A scenario that cannot reproduce itself cannot be a control, and the number to check first is always the baseline against itself. ## Seats Six Options edits each, per the recipe, including the silent one (`OPTIONS_PAGE_ROWS`). Sharpening is a dropdown on Advanced Graphics with the other screen effects and reuses the existing `qOff/qLow/qMedium/qHigh` labels; Field of View is a `raw`-format slider on Graphics, whose label is literal because that page is not localised. Preset ladder: `sharpening` takes `ambientOcclusion`'s shape (`0 0 1 2 2`) because a look change starts at Medium in this file's convention; `fieldOfView` is on no rung at all, because it changes how the game feels to play and that is the player's call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
280c2e886f |
Planar water: the mirror works, and ladders in at waterQuality 3 (Ultra)
The starburst was three defects, and none of them was the oblique projection I blamed it on in the last commit. What found them was looking at the mirror's own render target instead of reasoning about the composited frame — the water reads it through a fresnel weight, a rippled UV, a plane-match band and an SSR march that paints over it, so from the outside a broken mirror and a broken water shader look identical. WOW_MIRROR_DUMP=1 now writes that target beside the capture. (Bevy's own save_to_disk cannot: try_into_dynamic knows four 8-bit formats and answers UnsupportedFormat for Rgba16Float, logging and writing nothing.) - The oblique row's scale. Replacing the near plane with the water plane costs the FAR plane: `z >= 0` stops meaning "nearer than CAM_FAR" and becomes `depth >= a*d`, a plane through the eye tilted by a. Lengyel's own scale puts that plane exactly through the frustum's far corner, and the sky dome is pinned just inside the far plane, so it straddled the boundary and was sliced along its own tessellation. Scale by the supremum of d/depth over the frustum instead, with 2% of margin. - The water surface lay ON its own clip plane. This was the starburst and it was fatal rather than untidy: depth now means "distance above the plane", so a fragment at d=0 reads z/w=1, the nearest value reverse-Z has, and beats every other surface in the frame. The mesh is wave-displaced, so its triangles straddled the plane instead of lying in it, and each was clipped to a sliver that kept the winning depth. SURFACE_CLEARANCE lifts the plane a yard: the swell sums to 0.34 and the probed height answers for the surface under the eye, not this fragment. - The mirror was stored upside down. A true reflection is what a camera on the far side sees with its up vector reflected too — an improper basis no Transform can hold. The honest right-handed camera sees the same surfaces through the same frustum and stores the image flipped, so the shader samples 1 - v; that costs no winding flip, so backface culling stays correct. It read as a lake reflecting the ridge the RIGHT way up. Measured: Lake Everstill 2.87 -> 3.36 ms mean over 600 frames (+17%), the Stormwind canal inside the noise, CPU +0.2 to +1.1 ms. It is the first lane in docs/GRAPHICS.md that spends main thread, so it is a rung of its own above High and the default stays 2. Seated as a setting, per the standing rule that no graphics lane hides behind an env var: waterQuality gains tier 3, the Ultra preset takes it, and the Advanced Graphics dropdown gains a fourth entry in both locales. With the lane off, both scenarios are byte-identical to the build before it — which took two findings of its own: - The system could not live in Update at all. `&mut Transform` on a camera is shared with everything that moves anything; declaring it put 59 undeclared orders into Update and tripped the schedule ratchet, which is what kept this behind an env var. It belongs in PostUpdate with the skybox and the billboards, after propagation and before visibility, where it also reads the eye's settled pose instead of a frame-old one. (The comment blaming ResMut<Assets<Image>> for those 59 was wrong — dropping the asset handle changed the count by zero.) - A system that runs and returns on its first line is still not free. Holding &mut Camera and &mut GlobalTransform for a frame is enough to change which undeclared orders the executor picks: the canal moved 0.75% of its pixels by up to 96/255 with the lane switched off. So the whole system sits behind a run condition rather than an early return — and the same effect killed a .before(UpdateDirectionalLightCascades) order that was correct in itself. PostUpdate's ambiguity ceiling goes 466 -> 495 with the reason (the pairs are structurally impossible: nothing else in the schedule names the mirror camera); Update's ratchets down 4973 -> 4952. Also restores scripts/gates.sh's `where` example, which had gone missing. Its absence was silent — cargo's error went to /dev/null and `|| true` made it read as "no install" — so the memo key and the player-tests gate saw none even when WOW_DATA was set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
355bac7b0e |
Planar water: the mirror pass, built and inert behind a dev lever
A second camera on the far side of the water plane, rendering into a half-viewport target that `enhanced_water.wgsl` reads BENEATH the screen-space march — so SSR still wins wherever it finds something, and the mirror only fills the hole SSR cannot reach: the ray leaving the screen, and the top-edge fade, which is exactly where a shallow-angle lake wants its far bank. **It does not work yet, and it is not offered to anyone.** The mirrored eye sits as far below the surface as the real eye sits above it — 26 yd down on Lake Everstill — which is inside the lake bed, so the pass must clip at the water plane or it renders sand. The clip is implemented (Lengyel, adapted to this client's reverse-Z, around a finite far plane because the oblique row makes depth affine and an infinite far collapses every distant fragment onto one value) and it is WRONG: the mirror comes back a radial smear. So this is `WOW_WATER_MIRROR=1`, a dev lever — no CVar, no row in Options. It graduates to both, like every other lane, once a capture shows a reflection instead of a starburst. What IS established, and is why this lands rather than waiting: - **With the lane off the water is byte-identical** to before the pass existed, verified against a `git stash`ed build of the same tree. - With it on it reaches the water and moves 63% of a lake frame — the plumbing, the binding, the uniform and the composite all carry. - Off, the plugin **registers nothing at all**, the posture `static_gx::enabled` takes. That is not tidiness: `drive_mirror` declares `ResMut<Assets<Image>>`, and merely registering it dormant added 59 undeclared orders to `Update` and tripped the schedule ratchet. Three things this cost, all of them now written into `docs/GRAPHICS.md` and all of them my own: 1. **The pipeline failed to compile for most of a session while every A/B read a clean zero.** The field is `clip_position`; `WaterFragment` has no `position`. Naga refused the shader, the water fell back to another path, and I measured the fallback over and over — including twice mistaking the lake bed seen through clear water for a working mirror. The client logs pipeline errors rather than panicking, by design, so the answer was in the log the whole time. **Grep the capture log for `error:` before believing any shader A/B.** 2. **A disabled lane is not automatically free.** Starting SSR's march from the fogged `reflection` instead of `sky` looked equivalent and moved 10.1% of a lake frame with the mirror switched off. The control that catches that is diffing lane-off against a stashed build — not against an older capture, which is what I reached for first. 3. **A flag colour has to be one the scene cannot produce.** My first three "every gate passes" measurements were counting the orange lake bed as my red flag, which sent the hunt in the wrong direction for an hour. The pass's own cost is deliberately unmeasured: it draws the scene a second time, so it is the first item on the programme that spends main thread, and pricing a broken render against item 1's view-distance table would mean nothing. That happens when the projection does. Verified: ALL GATES GREEN (591 s); `benilla-app --lib` against the install at the exact 269-failure baseline, set-identical by `comm`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
77fce9210e |
The water already reflected the scenery; it was switched off
`docs/GRAPHICS.md` said we had refraction and foam but no mirror. That was
wrong. `waterQuality` 2 has carried a depth-marched screen-space
reflection of the scenery all along (`enhanced_water.wgsl`, "HIGH"), with
bisection refinement against the real scene depth rather than the fixed
probe its WarcraftXL source took. What was missing was the switch: the
registered default was Enhanced (sky only), and of the five lighting rungs
only Ultra reached High.
The line holding it back said exactly what it was waiting for —
"Mirror reflections stay opt-in until their cost is measured." So I
measured it, `WOW_FPS_PROBE=300`:
Classic Enhanced High
Lake Everstill 1.91 ms 1.84 ms 1.90 ms
open ocean 1.89 ms 1.88 ms 1.89 ms
Every tier is inside the run-to-run noise — Enhanced lands FASTER than
Classic on the lake, which is how you know you are reading noise rather
than a cost. What it buys is not noise: the far bank and the mountains
appear in the water, 18.5% of the lake frame and 35.1% of the ocean frame.
So High becomes the default and the High rung. That is a weld across four
places the tests hold together, and each one bit in turn and was fixed as
it asked: `REGISTERED`, `VideoConfig`'s literal, `WaterQuality::default`,
and the rung itself — the High rung IS the registered defaults, which
`the_high_preset_is_the_registered_defaults` states and enforces.
Low and Medium keep Enhanced deliberately, and that needed saying in the
one test that assumed every rung between Off and Ultra sits AT its
default: free on the machine it was measured on is an argument for the
default, not for the rung whose whole job is hardware that cannot afford
what this one can. `water_and_lava_observers_clamp_and_water_high_is_opt_in`
asserted the old decision in its name and its body, so it is renamed and
now pins the pairing the other way: High reflects, Medium does not.
The honest remaining gap is written into the doc rather than claimed as
done: SSR gives up when the ray leaves the screen and fades toward the top
edge, which is where a shallow-angle lake wants its far bank. A planar
pass would fill that — and it is the first item on the list that draws the
scene twice, so it costs main thread and gets priced against item 1's
view-distance table before anyone builds it.
Verified: ALL GATES GREEN (492 s); `benilla-app --lib` against the install
at the exact 269-failure baseline, set-identical by `comm`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
88b6998453 |
Auto exposure: dark rooms read at their own level
The programme's item 1 was terrain horizon occlusion, ranked first on the reasoning that it would make the frame cheaper on the thread that is actually full. **I measured it before building it, and the premise does not hold for this renderer**, so this is item 2 instead. The numbers are in `docs/GRAPHICS.md`: cutting the view distance from 777 yd to 177 — removing four fifths of the visible world — saves 0.44 ms of a 3.6 ms frame, p95 is flat at 6.4 ms at every distance, no main-world system in a steady vista exceeds 0.12 ms/run, and the model cull already takes 6,016 submeshes to 29 drawn. Horizon occlusion can only remove the fraction of that which sits behind a hill. The reference needed it on a 2004 CPU with no GPU-side culling; we do not. What the same harness said about item 2 was the opposite. Before anything was written: the Elwynn vista renders at a gamma median of 87.6/255 with 1.4% of its pixels under 32, the Goldshire inn at noon at 31.9 with 50.1% under 32, and the inn's kitchen at **12.8 with 94.5% under 32**. That is not a dim room, it is a black screen — and the reference has the same problem, because it has no metering either. A histogram-and-reduce over the HDR view target, smoothed in the log domain, applied ahead of the shafts, bloom and glow chain so FFXGlow's blur — which is built from the scene — sees the same exposed image everything else does. Two departures from the textbook version, both forced by what this renderer's pixels mean (the world is drawn in the reference's gamma convention, which the combine clamps to 1.0): - The gain has a **floor of 1.0**: it only ever brightens. Aiming at the exterior's own measured level is what lands it at 1.0 outdoors with no special case — the vista's median moves 87.6 -> 87.5, and 0.12% of its pixels with it. Daylight is the one look this client is measured against and nothing here was worth moving it. - It has a **ceiling**, which is the tier. The kitchen meters to roughly 24x; uncapped it comes out as daylight, which erases the room instead of making it readable. Subtle and Full are 2x and 4x: "a dark room you can see in" and "one you can read in", neither "a lit room". Measured after: inn noon goes 50.1% -> 4.6% of pixels under 32, the kitchen 94.5% -> 40.3%, and the vista does not move. **+0.08 ms of GPU**, nothing on the main thread. The calibration is the one thing to get right if this is ever re-derived, and it is written down because I got it wrong first: the aim level is the level of the scene THIS PASS SEES, not of the screenshot. The glow adds `w·blur²` downstream, so the finished vista log-averages to 0.0931 linear and the pass's own input to 0.060. Calibrating against the former aimed the lane 1.5x high and brightened the daylight exterior it exists to leave alone — which the vista capture caught immediately. Two rows on Advanced Graphics, both locales, the blend greyed while there is no gain to blend; on the Graphics Preset ladder from Medium, in the shape the other post lanes take. Classic keeps it off, as the reference has no metering at all. Verified: ALL GATES GREEN (550 s); `benilla-app --lib` against the install at the exact 269-failure baseline, set-identical by `comm`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
c315fb4f35 |
Anisotropic filtering and trilinear apply in the session
The Options row shipped last commit said the setting needed a relaunch.
It does not have to. The reference's "enabled upon restart" is about its
TEXTURES, not its CVars — both register with `flags = 1`, live — because
its sampler state was baked inside each `TextureCreate` (`0x449ae0`) and
it kept no list of live textures. Ours can be re-stamped in place.
Two things make it possible, and neither is new work:
- A governed sampler now wears `POLICY_SAMPLER_LABEL`, written by the one
constructor `TexFilterSetting::descriptor`. That is the `applyGlobal`
split the reference draws, made legible at runtime: terrain, WMO/M2 and
the liquid sheets take the policy; weather sprites, the minimap, glue,
Lua widgets, the sky and decals keep their own mode. All four governed
lanes went through that constructor, so a lane cannot forget the label
and silently opt itself out of every later change.
- Extracting a `RENDER_WORLD`-only `Image` takes its *pixels* and leaves
the asset behind (`take_gpu_data` is an `Option::take` on `image.data`).
So every resident texture's own descriptor is still readable in the main
world, for nothing — there is no ledger to keep and none to go stale.
The policy itself stops being a `OnceLock` and becomes one atomic word
plus a revision, so it can be published again on every write and both
halves of the refresh can key off the same number in the same frame:
`refresh_policy_samplers` (render world, `ExtractSchedule`, before every
Prepare) swaps `GpuImage::sampler`, and `invalidate_materials::<M>` raises
`AssetEvent::Modified` so bind groups that captured the OLD sampler are
built again. `static_gx` carries two samplers of its own and rebuilds its
cached cell bind groups in place rather than dropping them — clearing the
cache would be one line, but a region with no GPU state does not draw, so
every building in view would blink out and trickle back under the bake
budget.
Measured, not asserted. `WOW_CVAR_LATE=<name>=<value>` is new here: it
writes CVars *after* the scene has settled, then re-settles the image
around them. A latched lever and a live one give the same boot picture, so
no boot-time A/B can tell them apart. On the Elwynn vista:
anisotropic 16 at boot vs 16 written late:
0 of 1,440,000 pixels differ
and the control that proves the harness is not doing the work (boot 16,
written 16 again late, so no refresh runs but the re-settle is still paid)
is byte-identical to a plain boot. The cost of the change is 873 samplers,
75 material bind groups and 264 static_gx cell bind groups, once; nothing
is re-uploaded, since the pixels never move.
The first run of that A/B was NOT identical — 6.7% of the frame, a dapple
of stale tree shadow on a road that was otherwise correct. That is
`CutoutShadowCasterMaterial`, which deliberately reuses the forward pass's
own leaf handle and so holds a governed sampler, and which lives in
`benilla-app` rather than the engine. Hence the shape the invalidation
took: a public `invalidate_materials::<M>` each crate registers beside its
own `MaterialPlugin`, published through the world-API wall with the record
that says why (the engine naming every material type in the program is the
inversion that wall exists to stop). Forgetting one is silent in exactly
that way — a bind group built after the change is right and one already
cached is wrong — so the capture A/B is written into `docs/GRAPHICS.md` as
the check for this feature, not a unit test pretending to be one.
The Options row keeps its deferral to Apply, now for the real reason: the
commit is cheap once and silly sixteen times, which is what a drag across
the groove would ask for.
Verified: ALL GATES GREEN (520 s); `benilla-app --lib` against the install
at the exact 269-failure baseline, set-identical by `comm`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
b6f3bb001c |
Anisotropic filtering joins the Graphics Preset ladder from High
The reference ships `anisotropic` at 1, which is *off*, so ground texture at a
grazing angle smears to mush a few yards out — which is most of the screen in
an outdoor scene. The row was registered and wired (`benilla_assets::
TexFilterSetting`, the reference's own `[1, 16]` clamp at `0x689110`) but sat
outside the preset ladder, so no rung ever spent it and even Ultra left it off.
Measured on an Elwynn vista (`WOW_CAPTURE=vista`, the offline harness
`docs/GRAPHICS.md` now documents), one knob changed, 1600x900:
* **34.1% of the frame moves** (490,600 of 1,440,000 pixels by more than 2),
mean |d| 4.21, max 212. By band it peaks at **66.5%** across y 420-479 and
runs 48-66% from y 300 to y 780 — exactly where the ground recedes to the
horizon, which is what anisotropy is for. For scale, the interior
prop-probe table's whole stake is 7.6% of pixels by at most 14.
* **+1.0 ms of GPU and nothing on the main thread** (gpu 0.8 -> 1.8 ms, main
unchanged at 4.2-8.4 ms). The filter policy confirms it lands: "mode 4
(trilinear, aniso off)" -> "mode 5 (trilinear + aniso) anisotropic=16".
That ms is one this client is not spending. It is main-thread bound with the
GPU about two thirds idle — flat ~3 ms in play at every framerate, 0.8 ms in
this capture — so a sampler state is the cheapest picture on the table.
Ladder: Classic and Low keep the reference's 1, Medium takes 4, High and Ultra
16. Classic is the reference client, so it is judged against the registered
default the way `farclip` already is, not against zero — `anisotropic` is not a
0/1 lane, its "off" *is* 1. The High column differs from the reference, so it
is declared in `SEEDED_DEVIATIONS` with its reason, and
`graphics_rows_pass_their_observers_unclamped` now drives all five rungs
through the real setter and reads the level back off the sampler policy.
The sampler is baked into each texture at load, so a rung change lands on the
next launch, as the reference's own options window says ("enabled upon
restart"). A player already on a Custom preset is not seeded and keeps their
saved value; `/console anisotropic 16` and a restart is the whole fix there.
`docs/GRAPHICS.md` is new: where the frame's budget actually is, the offline
capture-and-diff harness every claim here uses, what is already shipped and
unspent, and the ordered list of what the picture is missing — costed on this
engine, with upscaling placed where the measurement puts it rather than where
it is usually asked for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
ba909276a0 |
docs: record the bourbon fork, the engine wall, and what each tree holds
`docs/BOURBON_SYNC.md`, beside the existing `UPSTREAM_SYNC.md`: what
tripleslash/benilla-bourbon is, measured rather than read off its README, and
the three things that decide what can be taken from it.
* It is published as **one squash commit** — one branch, no tags, 1,662 files
and 1,139,563 insertions in `b91c12e5` on samwhosung's `359eff416`. There is
no per-change history, so a pick is the whole fork or nothing.
* Its renderer is a **Bevy 0.18 -> 0.19 engine move**, not a file copy. Bevy
0.19 made the render graph systems: bourbon has 0 files implementing a
`Node` and this tree has 14, ten of them our own graphics. samwhosung is on
0.18.1/wgpu 27 at both that base and his head, so the move is bourbon's own
and lives inside the squash.
* Its Turtle WoW 1.18.1 support is **web-only** in its own tree, and this
chain is Turtle-derived rather than Turtle: no `patch-T`, instead
`patch-1`..`patch-5` and `patch-A`..`patch-Q`. The content and protocol
fixes inside that programme are the portable part; the layer switch cannot
fire here.
Also the file-set split (339 files only ours, 1,374 only theirs), what each side
holds that the other has no answer for — bourbon has no SuperWoW, nampower,
ClassicAPI, UnitXP_SP3, VanillaUtils or VanillaHelpers surface at all — the four
fixes this sync landed with the numbers measured over this install's chain, the
thirteen things refused with a reason each, and the three shapes a renderer take
could have, with their costs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
44c0b1269c |
docs: record the upstream sync, the divergence, and what was refused
A record so this is not re-derived. The measurement (349 upstream commits since the 2026-10-12 base, 589 files a full merge conflicts in), why most of it cannot be cherry-picked (upstream replaced mlua with its own Lua 5.0 VM, and ~200 commits are the deferred-to-inline conversion of the app/VM contract), the two resolution traps this sync hit, and all 27 refusals with the reason each. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
694ef5832d |
A boot with no readable WoW install is fatal, with an error dialog (#1630)
With no WoW install found, or a `Data` folder whose archives will not open, benilla booted into a window of textless buttons, since fonts, art and the stock interface all come from the install. It now ends before any window exists. A native error dialog (`rfd`: AppKit, Win32, `zenity` on Linux) names every folder searched, or the folder and the archive error, and says where to put the install; then it exits 1. An unattended run prints the same text to stderr and exits without a dialog. 1.12.1 ends at a fatal dialog for a present but corrupt base archive (`0x403b6f`) and only logs a missing one (`0x403b00`); ending at no install at all is recorded as a `Deviation:`. Tests cover both messages and the failing exit; both cases were run live unattended. Built on macOS, Linux and Windows. Fixes #836 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 6f6531078333fcd0c0c1bcbbd8e61ce531cdf607) # Conflicts: # docs/DEVIATIONS.md # docs/MAP.md Absorbed into the octo fork. Generated docs reconciled: MAP.md stays ours, DEVIATIONS.md stays absent. 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 |
||
|
|
3b6e91be35 |
superwow: RAW_COMBATLOG and CombatLogAdd, the only two names his addons call
Of SuperWoW's whole surface these were the two names grom's corpus calls
that this client lacked (`scripts/dll-surface.py --wanted`), and the first
is load-bearing out of proportion to its size: `SuperCleveRoidMacros`
funnels FIVE subsystems through one `RAW_COMBATLOG` frame — immunity and
resist learning, hidden-CC detection, debuff-duration learning, and the
reactive-proc window that lights Overpower and Revenge. Its own comment
says why the last of those can come from nowhere else: "SPELL_GO only
provides hit/miss COUNTS, not miss TYPES".
`route` is the fire site: the one funnel every combat line already passes
on its way to a window and to WoWCombatLog.txt. `arg1` is the printed
sentence; `arg2` is the same sentence with each unit name spelled as its
GUID, composed a second time through the same `compose_line` so the two
can only differ in the name slots. Fired before the `CHAT_MSG_*`, which is
what "raw" means upstream. Both the fire and the `arg2` composition are
gated on `has_listener` — a raid's combat log is tens of lines a second.
`CombatLogAdd` is also BigWigs' SuperWoW detection (`if CombatLogAdd
then`), so defining it turns that module's path on.
Three corrections came out of reading the DLL's own usage strings, which
it publishes beside the names — the arity and argument meaning of every
remaining SuperWoW function is now measured rather than inferred, and
`docs/ADDON_COMPAT.md` carries the manifest:
- `CombatLogAdd("text" [, addToRawLog])` has a SECOND argument. Nothing
in the corpus passes it; it would have been missed.
- `SUPERWOW_COMBAT_LOG_EVENT` does not exist. The DLL registers exactly
three events, and that name is in no part of its string pool.
- `IsIndoors` is capital-I beside `IsSwimming`/`IsMounted`, not the
lowercase `isIndoors` two files guessed at — all three are already live
off one feed, so that clause of the "blocked" row was already answered.
- The detection pair is `SUPERWOW_STRING="SuperWoW 2.2 by Balake"` and
`SUPERWOW_VERSION="2.2"`, both quoted. Ours had the string without its
author clause and the version as a number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
5a824f1625 |
app: the press queue was switched off in play — a derived Default is the live config
`cvars.rs` announces a `CvarChanged` only when a value *moves*, and `config.toml`
holds only the rows that left their defaults ("CVar values that moved off their
defaults" — its own header). So a row the player never touched never reaches an
`on_cvar` observer, and the resource's own `Default` is what the client runs with
for the whole session.
`PressRules` and `SpellQueue` both derived `Default`. Every toggle was false, every
window 0 and every buffer 0, so the press-interception queue held nothing, the mount
refusal never fired, and `SpellQueue`'s zero window expired each explicit entry on
the frame it was queued — the day after all of it was committed. Fourteen of the
sixteen rows have a non-zero registered default.
grom's own `WoWCombatLog.txt` is what named it, with no live run to ask for: 147
`Sunder Armor: Not yet recovered` against 163 landed Sunders, in bursts 0.17-0.23 s
apart — every press refused, none held. Sunder Armor rank 1 on this realm is
`rec=0 cat=0 catrec=0 gcdcat=133 gcd=1500`, read off its own `Spell.dbc`: a global
cooldown and nothing else, which is precisely the case the queue must hold.
Both now hand-write `Default` with the registered values, which is the pattern
`cvars.rs::registered_defaults_mirror_the_code_truths` already welds for the engine
knobs; `np_press.rs::the_default_is_the_registry` welds these sixteen the same way.
The test-only `defaults_for_tests()` constructors are gone — they spelled out the
registered numbers for tests to read while the client could not reach them, which is
the whole shape of the bug. A sweep of the other nine CVar-cached resources (list
every `fn on_cvar`, take its `ResMut` types, check for a hand-written `Default`, look
up each CVar it names in `REGISTERED`) found no further victims.
Two things so the next session can speak for itself:
- `a_press_made_during_the_global_cooldown_is_held_and_sent_when_it_ends` — the
end-to-end cycle with the GCD as the only guard, which is what a class with no cast
bar presses into all day. The existing end-to-end test arms `PendingCast` by hand
and so never starts a GCD at all; this one presses through the real ladder, so the
commit arms it the way a cast does.
- one `info!` a minute from the drain, and only when something moved: held, sent,
dropped, retried, fell-through, channels cut — plus the live windows and buffers.
Every other line this half writes is `debug!` and the default filter is INFO, so a
67-minute session could not say one word about the queue. A line of zeroed knobs
now diagnoses this class of fault by itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|