fff48cd75caba0c40ef78225a55bd013bd7e61ba
441
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f4960efc53 |
The realm update lands clean, and three of the "269 known red" were real
His 2026-10-28 20:18 update rewrote `Data/patch-4.mpq` (1.37 GB) and
`patch-5.mpq`
(230 MB). Verified rather than assumed, and nothing of ours broke: the realm's
`FrameXML.toc` went 146 → 151 rows and patch-5 holds the richest copy, so
`chain::demote_stale_snapshots` fires and it wins the manifest; the five new
rows
are realm modules (`OctoChallengesSession`, `OctoHardcoreBGMenu`, `OctoReplay`,
`OctoReplayBar`, `OctoReport`) that `unnamed_chain_rows` picks up by design and
that load with **0 load errors and 0 session errors**, every global they name
resolving; DBC field counts are unchanged everywhere, so no parser is at risk,
while record counts moved (Spell 27992 → 28026, CreatureDisplayInfo 13423 →
13451), which is the proof the new data is live; a `WOW_CAPTURE=vista` at
Ironforge prints **0 ERROR lines**; and both golden baselines held exactly, 269
with an identical failing name set and 80, `goldshire_cell` still at 1874.
Two examples answer "did it land", because "the file is in the archive" never
does. The reference mounts `patch-?.MPQ` by case-folded name, so every
`patch-A..Z` outranks `patch-4`/`patch-5`, and the toc-subset demotion cannot
see
an archive that carries no toc at all. `chain_report` prints the `FrameXML.toc`
copies in priority order and, with `--toc`, the winner of every row of the
realm's manifest with a tally — 68 to patch-4, 43 to patch-5, 32 to `patch.MPQ`,
7 to `interface.MPQ`, 1 to patch-3, and **none to a stale letter patch**.
`chain_shadow` answers from the archive's side: patch-4 carries 20326 paths of
which 10220 are shadowed, patch-5's 1494 are not shadowed at all.
Then, grouping the red bucket by panic *site* rather than trusting its label:
252 of the 269 are shipped-UI tests and **136 share one site**. Three of the
rest
were real, and are fixed here.
Six `OptionsFrame_*` enable/disable helpers were missing outright. `benilla.toc`
is the only thing that loads two of the reference's settings windows — the
realm's own toc lists neither `SoundOptionsFrame.xml` nor `UIOptionsFrame.xml` —
stock 1.12 keeps those six beside them in its own `OptionsFrame.lua`, and the
realm replaced that file with Turtle's rewrite, which drops them. Checked
against
all 151 interface rows, Lua and XML: nothing defines one. So those windows held
controls that could not grey out, with the first raise waiting on the click that
reached one. `assets/ui/OptionsFrameAdapters.lua` carries the reference's own
bodies, read out of `patch.MPQ` rather than from memory.
`CHARACTERFRAME_SUBFRAMES` was asserted to hold exactly five. The realm's
`CharacterFrame.lua` carries a sixth, `ArenaFrame` — which this client already
stubs (`CharacterFrameAdapters.xml`), so the count was the only thing wrong. It
now checks the reference's five by membership and order, which is what the test
is for; a realm may append.
And the kit's stand-ins were anonymous. `CreateFrame("Frame")` with no name
answers `GetName()` nil and `GetID()` 0, the publish is `setglobal` either way,
so naming them costs nothing and the absence cost two of the 136's three errors:
`FCF_SetWindowName` concatenating a nil name and `SetChatWindowDocked` refusing
window 0. `ChatFrame1`/`ChatFrame2` are no longer seated at all — every row of
`UIPARENT_MANAGED_FRAME_POSITIONS` is read through `frame = _G[index]; if
( frame )`, so a missing one is skipped, and seating them only shadowed the real
windows for good. The pass runs at load, from `ShapeshiftBar_OnLoad`, twelve toc
rows before the chat files, which is why the stand-ins were always first.
That leaves 136 on one cause, and the two tests added to `loader/tests.rs` are
what ruled out the first hypothesis: a template's `parent="UIParent"` does reach
the frame that inherits it, one level and two, which is how `ChatFrame1` is
built. The next suspects are written down in the gates memory.
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
50fd1f158e |
The interior lighting rig gets its ten rows, and the audit closes
The last ten CVars `video::on_cvar` answers that a player could not reach. With these the sweep is done: every graphics and video row the observer takes now has a seat, bar the three that are not settings — `frillDensity` (World Detail already drives it, and two dials on one knob is worse than none), `nearclip` (the projection near plane: changing it is a bug, not a preference) and `interiorDebug` (a visualiser). Seven shape the room — the light budget, the ambient floor under every indoor surface, the indoor daylight floor, the bounce gain, the reach past each fixture's authored window, the share of painted-in lighting kept where nothing reaches, and the room gate — and three are the indoor shadow budgets. Named for what they do to a room, not for the quantity they scale, because the quantity is the renderer's word and the room is the player's: "Unlit Room Floor", not "interior bake floor"; "Lights Stay in Their Room", not "room gate". Each description says what the setting is FOR rather than what it multiplies, and three of them carry the thing a player would otherwise misread: - `interiorAttenScale` at 0 is not darkness, it is the authored window switched OFF, so every fixture reaches the whole room. A left end that looks like "no light" and means "all light" needs saying. - `interiorRoomGate` is the one setting whose job is diagnosis: a room the gate leaves on ambient alone looks exactly like a bug, and one click tells the two apart. That belongs in the options window and not only in `/console`. - `interiorShadowEntityRate` is the only row in the indoor shadow block that actually costs frame time — neither count dial above it moved it, which is why it exists as its own row — so its description says to lower it first. Two grooves deliberately offer less than their CVar's clamp, as `RowRenderScale` already does (50-200% inside a 0.25-4 clamp): ambient and indoor daylight run 0..0.1 of a 0..1 clamp, because the shipped value is 0.015 and past about 0.1 a room is not lit, it is fogged. `/console` still reaches the rest. One offers less than its *registration* says: `interiorShadowDynamic`'s help line reads 0..16, but the observer raises 0 to 1 and clamps to `MAX_TORCH_DYNAMIC` (8), so a groove to 16 would be seven stops that all mean 8 and a zero that means 1. The gating is two-level: every row needs the live-fixture lane, and the three shadow budgets need the shadows on top of it. A budget row left live while its shadows are off is a dial that does nothing, which is how settings earn the reputation these ones had. Russian for all ten rows and their descriptions is written to match the register of the column already there; a native reader would still improve it. Verified: ALL GATES GREEN (346 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 |
||
|
|
2d1dbd5639 |
Seven more graphics settings get a seat: the filter pair, MSAA, the shadow rig
The sweep behind this: every CVar `video::on_cvar` answers, against every CVar the Options window names. Twenty of fifty-five had no seat at all, so the only way to them was `/console` — which is not a setting. Seven of them here; the ten interior-lighting rows follow. **Graphics page.** Trilinear Filtering under Anisotropic, because they are one policy: aniso at 2 or more writes filter mode 5 and trilinear writes mode 4 only while that bit is clear (`0x688d0d`), so the row is GREYED while anisotropy is on — not hidden, and not rewritten. It keeps the value a player set, and turning anisotropy off hands it straight back. The gating reads the STAGED anisotropy where there is one, since both rows defer: otherwise a player who moves one and has not pressed Apply sees the row it is about to gate stay live for the rest of the visit. Multisampling beside Vertical Sync and Resolution, the latched `gx*` rows it commits with. 1.12 had this row and we did not. Backed by the index verbs its own dropdown used (`0x48c580` / `0x48c640`) rather than by `gxMultisample`, because picking a format writes the colour and depth rows with it as one record — and it rebuilds its list on open, like Resolution, since at OnLoad the engine has published no formats yet. Only the sample count is named: the colour and depth halves of each triple describe our swapchain and steer nothing, so offering them would offer a choice that does not exist. Its description says plainly that multisampling does less here than in a modern game, because it misses the alpha-tested cutouts most of this game's foliage and fencing is made of. **Advanced Graphics.** The shadow rig's four unreachable rows — Soft Shadow Edges (the 9-tap gaussian against the card's own 2x2; Bevy's third mode is deliberately not offered, since it only resolves under TAA this camera does not run), Shadow Caster Range, and the two update-rate caps — plus Torch Shadow Darkness beside the softness dial it shares a shadow with. The rate caps needed their own readout. `0` there is **every frame**, the opposite of what it looks like, so a bare "0 per second" would read as the shadows being switched off — which is what the two toggles above them are for. They now say "Every frame", in both locales. Gating is per-lane where the lane is what matters: each rate cap is dead only while its own lane is, while the filter and the reach ride the cascade and need just one sun lane left. Russian for all five rows and their descriptions is written to match the register of the column already there; it would still benefit from a native reader. One thing worth recording for the next row: `OPTIONS_PAGE_ROWS` has positional contracts in its tests, and the list's own comments say "appended so the other rows keep their indices". It drives refresh, search and Restore Defaults — never layout, which is the frames' own anchors — so appending costs nothing and seats each row where it belongs. Verified: ALL GATES GREEN (322 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
|
||
|
|
1c786b7ecb |
Anisotropic Filtering takes its seat on the Graphics page
The CVar has been registered and read since the filter policy landed, and last commit put it on the Graphics Preset ladder — but a player had no way to reach it. The reference's own `OptionsFrameSliders` declares the row at index 7; ours left that index a hole, so the only route to anisotropy was `/console anisotropic 16`, and a player whose preset reads Custom (the seeder never overwrites one) never met the setting at all. Seated where the reference's own slider order puts it: after Brightness, before Weather Intensity. Three things it does differently, each stated beside it: - The groove runs 1 to 16, the CVar's own `ANISO_RANGE`, not the stock table's 1 to 4. That ceiling was the reference *slider's*, not the CVar's — its callback parses 1-16 (`0x689110`) — and both the High and the Ultra rung ask for 16. So the row carries literal bounds and stays out of `BENILLA_GRAPHICS_SLIDER_ROWS`, exactly as `RowRenderScale` does for a CVar the stock table never had, which leaves `OptionsFrameSliders` verbatim for any addon that reads it. - `1` reads out as "Off", not "1x": the reference writes filter mode 5 only at 2 and above, so 1 is the feature off and not its weakest stop. - The description is ours. 1.12's own OPTION_TOOLTIP_ANISOTROPIC describes a four-stop slider, and this one is sixteen. Deferred to Apply, as Vertical Sync and Render Scale are: a sampler is baked into each `Image` at load, so the write cannot reach a texture that is already resident. The tooltip says so in the row rather than leaving a player to wonder why the ground did not change — the same thing the reference's UI means by "enabled upon restart". Verified: `benilla-app --lib` at the exact 269-failure data-gated baseline, set-identical by `comm`, with the new row's own test and the two census counts (tooltip keys, raised plates) moved 116 -> 117 as the row asks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
89ae4699bb |
Re-seat two picks the #1730 revert and the realm's interface displaced
Two follow-ups to the series, both consequences of the same root cause. **#2150's OptionsFrame.xml hunk, re-ported.** It cherry-picked with no conflict only because the botched #1730 resolution had already replaced this fork's `OptionsFrame.xml` with upstream's, so it applied against upstream's file. The revert then restored ours and took it with it. Ported by hand here: our rows write `self.applyFunc` where upstream's write `this.applyFunc`, and inherit `OptionsCheckboxRowTemplate`/`OptionsDropdownRowTemplate` where upstream's inherit the `Benilla`-prefixed names. Both `TargetofTarget_Update` applyFuncs are gone and the reference note comes along. **#1972's blanket dialog test, scoped to our two dialogs.** The fix — this page's two confirms declaring `timeout`, `whileDead`, `showAlert` — is right and stands. Its test asserted a numeric `timeout` on *every* entry in `StaticPopupDialogs`, which holds for stock 1.12.1 FrameXML but not here: this fork loads the realm's own interface, whose `StaticPopup.lua` leaves `timeout` off 73 of its dialogs. Those are the realm's to declare, and failing on them says nothing about this fix. benilla-app --lib is back to the 269-failure baseline exactly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
7a8765aa52 |
Revert "The Benilla scroll bar template scrolls its parent itself (#1730)"
This reverts
|
||
|
|
73f4bf808d |
The target-of-target rows only write their globals, as 1.12's options do (#2150)
1.12's options declare Show Target of Target as a bare uvar (UIOptionsFrame.lua:65) and the mode is read on save (UIOptionsFrame.lua:365); neither calls TargetofTarget_Update. The frame follows from TargetFrame_OnUpdate and TargetofTarget_OnUpdate on the next frame (TargetFrame.lua:492-501). Our rows called it as their applyFunc, with the row as `this`; the function refreshes `this`'s debuffs (RefreshBuffs(this, 0, "targettarget"), TargetFrame.lua:538), so with a target's target up the switch raised "BuffFrame.lua:281: attempt to index local 'debuffBorder' (a nil value)" (seen on a Linux client). Verified: the_target_of_target_switch_leaves_the_frame_to_its_own_updates (with a target's target up: no error, the frame shows after one tick on and hides after one tick off) fails with the applyFunc put back, with exactly that error, and passes without it; the options and tot_frame tests pass on a deDE install; cargo test -p benilla-app --lib 5145, clippy -D warnings, fmt. Built on macOS only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 4d6f9ecc8a0d99e6d3ecf7fa4a8da229ebad92c2) |
||
|
|
8f658cb516 |
Key-binding confirm dialogs no longer raise a Lua error each frame (#1972)
Unticking Character Specific Key Bindings, or pressing Defaults, raised `StaticPopup.lua:1713: attempt to compare number with nil` every frame the confirm stayed up. 1.12.1: `StaticPopup_Show` copies an entry's `timeout` into `dialog.timeleft`, and `StaticPopup_OnUpdate` compares it with 0 each frame; every stock entry declares one, the stock key-binding confirm included (`Blizzard_BindingUI.lua:10-24`, `timeout = 0`, `whileDead = 1`, `showAlert = 1`). Our key-bindings page's two confirms declared none. Both now do, the character-set one with the stock entry's three fields. The checkbox test now runs a frame with the confirm up, and a new test fails on any dialog the loaded interface defines without a numeric timeout; both fail without the fix. Fixes #1971 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 881152bda6d48cc3fd1a47679a7465add212ae05) |
||
|
|
0c247396b7 |
The ESC menu's Support rung opens the help window (#1807)
The ESC menu's Support rung was greyed with nothing behind it. The 1.15 era menu that the layer's ladder follows opens the help window from Support. In 1.12 that window is the stock `HelpFrame`, opened by the "?" micro button's `ToggleHelpFrame()` (`MainMenuBarMicroButtons.xml:217`). Support is now live: it plays the menu click, closes the menu as the Macros rung does, and toggles the help window. Edit Mode stays greyed. Test: `support_closes_the_menu_and_toggles_the_help_window` clicks the rung and checks the toggle and the closed menu; the live-entries test now lists Support. Fixes #1800 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit bb434a181f6a3815ec6fe3e5ace89c864fa1808c) Absorbed into the octo fork. Ported by hand: upstream wires Support in GameMenuAdapters.xml, a Lua adapter layered over stock FrameXML that this fork does not carry — our menu declares its own rungs in GameMenuFrame.xml, where GameMenuButtonSupport was GameMenuButton_Pending (SetText + Disable). It now sets its text on load and, on click, plays igMainMenuOption, hides the menu as the Macros rung does and calls ToggleHelpFrame(). Our rungs carry no Benilla prefix, so upstream's test names are retargeted; our own narrowing_the_frame_keeps_the_button_column_centred test is kept. The 6 FloatingChatFrame failures in this module are pre-existing baseline (10 passed/6 failed before, 11/6 after). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
e0895013ed |
The Benilla scroll bar template scrolls its parent itself (#1730)
`BenillaScrollBarTemplate`'s own OnValueChanged was a faux-list handler that both instances replaced with the real scroll, so a new bar that forgot the override wrote `offset`/`updateFunc` on its parent instead of scrolling. The reference's `UIPanelScrollBarTemplate` carries `this:GetParent():SetVerticalScroll(arg1)` itself (`UIPanelTemplates.xml:156-158`). The template now calls `BenillaScrollBar_OnValueChanged(this, arg1)`; the two instance overrides, the dead faux handler and its header text are gone. Test: a bare bar from the template scrolls its ScrollFrame parent with no `offset` or `updateFunc` write; it fails with the old template. Fixes #359 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 142ed718d3a360a5992c78b53540a356cacf6d1e) # Conflicts: # crates/benilla-app/assets/ui/OptionsFrame.xml # crates/benilla-app/assets/ui/ScriptLogFrame.xml # crates/benilla-app/assets/ui/ScrollTemplates.xml # docs/DEVIATIONS.md Absorbed into the octo fork. Upstream's side taken on all three XML files, and it fixes a live fault in this fork as well: our BenillaScrollBarTemplate called BenillaFauxScrollBar_OnValueChanged, a global defined nowhere in the repo, so every bar built from the template raised on value change — survived only because OptionsFrame and ScriptLogFrame each overrode the handler per instance. The template now carries the real handler and the two overrides are gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
02e6fc4208 |
formats+app: a stale snapshot archive mounts below the realm's patches
grom is keeping `patch-A.mpq` — it is almost certainly textures — so the shadowing it causes is handled in the chain instead of on his disk. `mount_order` stays the reference's law, letter above digit and all, and `demote_stale_snapshots` runs after it: an archive whose `FrameXML.toc` is a **strict subset** of another's is an earlier edition of the realm's manifest, so it mounts just below the patch set — above `patch.MPQ` and the base archives, so anything only it carries still resolves. A toc that differs by *removal* is a different manifest rather than an older one and is left exactly where its name puts it. `WOW_CHAIN_DEMOTE=0` keeps the reference's order for comparison. On his install `patch-A.mpq` (Aug 7) now loses to `patch-5.mpq`, so the realm's current data is what loads: its `FrameXML.toc`, its `GlobalStrings.lua` and a `Spell.dbc` with **28019** rows against the 27992 his last session reported — 27 spells the update added that were invisible. Reading the realm's real files then broke ten tests, all of which had pinned August's behaviour, and each is now pinned the realm's way: - `AIOHandlers.lua` claimed the `OctoRadio` block as a no-op on the evidence that the radio file carried no AIO call. The update built the radio out into a file twice the size whose `RegisterNowPlayingFeed` registers that name at `VARIABLES_LOADED`, and `AIO.RegisterEvent` asserts on a duplicate, so our shim raised through seven tests. The row is gone, and the shim now claims at `PLAYER_LOGIN` through `pcall` so a realm that implements one of these takes the name and we step aside. `no_aio_block_we_shim_is_one_the_realm_now_handles` fails a row the realm has implemented — verified by putting `OctoRadio` back. - `BATTLEFIELDS_CLOSED` no longer hides the battlefield list, by the realm's decision: the server stamps `SMSG_BATTLEFIELD_LIST`'s battlemaster guid with the player's own, so the client anchors the window to the spot it opened and closes it when you step away, on foot or on a boat (bug tracker #330 and its follow-up). Pinned that way, with the close button proving the list still closes. - `MAX_QUESTLOG_QUESTS` is 25, not 1.12's 20. The test reads the realm's global now, so the next bump is data and not a failure. - The realm's `OctoQuestLogPoller` redraws the log every 0.2 s, which legitimately costs a layout gate walk. `POLLS_ON_A_TIMER` stands it down by name before the quiet-frame bench measures, rather than raising the threshold and blunting the one instrument that catches an every-frame writer. A timed realm handler that is not listed fails. 269 data-gated failures, identical to the baseline at HEAD name for name, and all nine gates green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
9ce0fe7b3f |
app: load the five FrameXML rows this client update added
benilla loads its own curated `benilla.toc`, not the chain's `FrameXML.toc`, so a row the realm adds is silently absent: no "not found", no warning, just a missing global. grom updated the client on 2026-10-23 and `/xpboost` did nothing. The chain itself was fine — every archive mounted, no DBC field-count mismatch, and the two sessions' warning sets differ by two lines *fewer* after the update. The command was simply undefined. Diffing the chain's toc against ours found five rows, all added here in the chain's own order: - `OctoXpBoost`: `/xpboost`, the bonus-experience opt-out. Slash commands never reach the server, so it sends the server's `.xpboost` as a SAY, which the server runs as a command and never broadcasts. This is the reported bug. - `OctoTurtleBridge`: Turtle_General's addon-message dispatcher, for the realm's inspect replies and `RESPONSE_PLAYER_CHALLENGES`. - `Turtle_GroupUI` + `OctoGroupUIFix`: the compact group/raid frames and three fixes to them. The fix stays immediately after, as the chain has it; the xml pulls its own .lua and templates. - `OctoTurtleStore`: transmog outfits and LFT presets kept on the realm over AIO, FrameXML having no SavedVariables. Verified with the real data: `b_opens_the_backpack_and_shift_b_opens_every_bag` (the only test that reads a new row's OnLoads) stays green, and the producer gate goes from red to green with it. Through the addon harness against grom's own corpus: `SLASH_OCTOXPBOOST1` is `/xpboost` with a function behind it, the bridge and all nine group frames exist, no session errors — and the group frames are hidden with no OnUpdate armed while solo, so they cost nothing until a group fills them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
d06bb9fae9 |
options: Interior Darkness says what it does not scale
grom turned this dial down, hit snow in Dun Morogh, and got rooms that read "really grey and flat" — then spent his own time hunting it before settling on 19% by trial. The dial was not lying, it was just silent about the half of the picture it does not own. A storm's fog reaches indoors at full strength, which is faithful: the MFOG selector bails on a one-record root (`0x69de4b`) so the room keeps the SCENE fog, and the crossfade staged at `0x6cef43` carries it in "storm veil included". Measured at his own spot, snow against clear: the fog start goes 0 -> -105 over a 350 end, which is 23.1% fog at ZERO distance — the wall in front of you. That predicts [36.0, 50.2, 57.8] against a measured [36.9, 50.3, 58.0], so it is the whole effect. Turn the room light down under that and the veil is most of what is left. So the tooltip now says it, in both locales, and points at the dial as the answer instead of leaving the next player to re-derive it. No behaviour changes — the fog is the reference's and stays. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
4764e4fb2d |
framexml: the twenty-one rows of the chain's own toc the manifest still lacked
benilla loads `assets/ui/benilla.toc`, not the player's `Interface\FrameXML\FrameXML.toc`, so a row the chain carries and the manifest omits is silently absent — no read attempt, no warning. Six went in with the arena templates; this is the rest of the diff but one, each with a comment on what it is for and in the chain's own order. The six baked Octo extensions are the player-visible half. `/pvp` now asks before a War Mode character spends its permanent, persisted opt-out. The exhaustion tooltip reads this realm's 175% instead of 1.12's baked 150%, and `/rested` answers. A player's hover shows the realm's own two challenges, Samurai and Together Forever: the server was already setting bits 10 and 11, and `TargetFrame_UpdateChallenges` walks a list that stopped at 10. The one-shot War Mode offer on arrival from the hardcore realm renders instead of being dropped. `/bug` and `/suggest` exist again — Turtle's FrameXML dropped the BugReport frame, and this rebuilds the dialog over chunked `.bug` SAY commands. And on C'Thun the Ghost debuff's tooltip carries the hardcore afterlife line, which is why the realm reverted that string in Spell.dbc. With them: `OctoToolReqTip`, the realm's own 40 KB recipe-to-tool table wrapping `GetTradeSkillTools`, so a recipe whose tool OctoWoW changed names its real one; six `OctoLocale` rows, each gated on `GetLocale()` and inert here; and the three Turtle windows the manifest said needed verbs built first — arena (4 rows), barbershop, guild bank (2). They need exactly one, `CameraZoomOut`, now built. All three publish their frames and open only on an exact gossip trigger string, so answering nil for `GetGossipText` cannot hide ordinary gossip. `StatsFrame.xml` is the row left out, and the manifest says so where it would sit: the debug-build overlay whose OnUpdate is the only reader of `GetDebugStats`, a binding we do not have, so the row would publish a `ToggleStats` that raises twice a second once pressed. `UNLEARNED_SPELL` joins `UNPRODUCED` rather than getting a producer. It is the only entry there that is not a 1.12 event at all: 5875's event-name pool carries `LEARNED_SPELL_IN_TAB` and `SPELLS_CHANGED` and no unlearn sibling, so the reference ignores that registration exactly as we do, and the War Mode re-scan rides PLAYER_LOGIN and LEARNED_SPELL_IN_TAB, which both fire. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
981e2ee498 |
framexml: six rows of the chain's own toc the manifest never had
benilla loads `assets/ui/benilla.toc`, not the player's `Interface\FrameXML\FrameXML.toc`, so a row the chain carries and the manifest omits is silently absent — no read, no warning, just a missing global or template surfacing as somebody else's error. Diffing the two found 29 such rows. Six of them land here. `Turtle_ArenaUI\Turtle_ArenaUITemplates.xml` is the one that was costing four errors per inspect: `Blizzard_InspectUI\InspectArenaFrame.xml` inherits `ArenaTeamTemplate` from it and then reads `$parentText`, `$parentDetails` and nine `$parentDetails*` children by name, so all three team-button OnLoads and `InspectArenaFrame_OnShow` raised on nil. `Globals.lua` publishes `print`, `TURTLE_WOW_VERSION`, `TW_CLASS_TOKEN`, `Turtle_AvailableChallenges`, `GAME_YELLOW` and the realm's three `ADDON_MSG_*_DELIMITER`s. `Overrides.lua` is Turtle's two-continent map. `Hooks.lua` is the realm's tooltip layer: the guild line, the shift-click hint and `FixSetInspect`. `ChronoboonTimers.lua` and `SpellChatLinks.lua` are self-contained. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
81d96af35d |
Everwood: the HD/OctoWoW line of benilla, imported onto upstream
This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (
|
||
|
|
706cd717a3 |
merge: absorb samwhosung/main on top of the upstream merge
Same fork policy: upstream structure wins, local-unique re-instated. Cargo.toml unions both measured dev-profile philosophies (codegen-units=1 + lto=off from samwhosung, debug=line-tables-only + split-debuginfo from the fork's disk measurements). glue_booth.rs takes samwhosung's typed GlueScene (token/of_race/from_model_path) and re-adds the fork's TurtleWoW races (Goblin=9, BloodElf=10) as variants; their glue fog stays on Human's row, exactly as the fork's scene_fog fallback did. ui script takes samwhosung's mouseover/hyperlink rework (world_mouseover, minimap_blip, hyperlink.rs) over the fork's inline hover_link/span_at/pointer path — the rework covers the same OnHyperlinkEnter/Leave behaviour. game_plugins.rs keeps the fork's re-measured ceilings (466 / 5,509); mail.rs keeps the explicit 13-tuple with a float daysLeft; MAP.md takes the superset. |
||
|
|
26d714fd1d |
merge: absorb upstream/main into the Turtle fork
Fork policy (docs/METHOD.md): upstream structure wins; local-unique work re-applied on top. This commit is the upstream half of the reconciliation. Upstream is the same program at a rewritten history ( |
||
|
|
781bf36ab3 |
Key input follows the client's walks; stock login takes typing (#1027)
Key input follows the client's walks: key-down (`0x765f10`), char (`0x765df0`), key-up to the recorded consumer (`0x765fd0`) and repeat (`0x766070`). - EditBoxes are in the walk from creation (`0x779ced`), so the stock login's boxes take typing; only a focused box consumes. A box declines PRINTSCREEN and unaltered arrows on an `ignoreArrows` box, so stack-split steps under a focused chat box. A binding's press records the world frame. - Repeats follow the pressed-key list, so a held key resumes after alt-tab. Chords reach a box only through the walk. Keys the client can't name walk nowhere. - The stock login reads the agreements before its first screen. Cinematics returns to login (no movie player; Deviation). The Realmlist box has its middle border. - Alt+F4 closes the window on Windows and Linux (Deviation: the reference relies on the OS). Fixes #179 Fixes #1003 Fixes #1004 Fixes #1005 Fixes #1019 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7158c58575 |
Hidden frames defer their layout; GetWidth answers the set size (#973)
A hidden frame resolved and fired `OnSizeChanged` at once, a getter's resize fired at the drain, and `GetWidth`/`GetHeight` answered the rect over scale. In 1.12.1 hiding sets the deferral flag `0x2` (`0x76ad50`); while set, an invalidate marks the frame pending and passes it to its dependents without a resolve (`0x768178`). Show resolves it first (`0x76ae58`), a getter resolves a pending one and fires `OnSizeChanged` inside the read (`0x7a1980`, `0x76b580`), and an anchor read pulls it for good (`0x7a2fc0`). `GetWidth` answers the set size when non-zero, else the rect span, unscaled (`0x7a1e93`, `0x7a1ee6`). The deferral is modelled for frames and regions, getters fire inside the read, layout ids mint in handle order (deterministic), and the options window measures its scroll by its edges. Tests bite on the parent; the verifier is green; stock quads match. Fixes #948 Fixes #971 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
45b7914c9e |
The stock login screen logs in; realmd 0x0E is a success (#970)
Under `WOW_GLUE_STOCK=1` the stock login screen now logs in. Separately, realmd's 0x0E answer was refused by the native login too. In 1.12.1 the glue manager runs the screen states (`0xb41da0`): `DefaultServerLogin` (`0x46afb0`) needs the agreements accepted and opens the status dialog; each tick re-texts it from the logon's state and result (`0x46b140`, tables `0x836b40`, `0x836b78`); the world connect re-texts it every frame (`0x46be8b`). The agreements seed from `readTOS`/`readEULA` (`0x46a400`). Realmd 0x0E is a success (`0x5b2cd4`); a proof's 0x0A changes nothing (`0x5bada2`). The glue core verbs, the glue manager over the native login, the agreement CVars, `SimpleHTML file=`, case-insensitive loose files and a benilla Realmlist layer file are built; the protocol accepts 0x0E. A recorded `Deviation:` keeps the interface signature checks off. Tests: fake-realmd refusals and 0x0E; stock-screen login reaching the native submit. Part of #856 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
55729baff9 |
Message fades and scroll ranges run in the walk; OnUpdateModel gates (#913)
Hidden chat frames kept fading their lines, a scroll frame recomputed its range on every read and fired `OnScrollRangeChanged` on every `UpdateScrollChildRect`, hidden or not, and `OnUpdateModel` fired for panes with no size or off screen. In the reference, each of these runs in the frame's own update slot, only while shown: message frames trim and fade there; a scroll frame recomputes a range its writers marked, stores it and notifies only on a real move. The model paint fires only for a resolved rect whose screen-clamped viewport has size. Message frames and scroll frames are now walk kinds, the range is stored and marked, and the paint applies the rect and viewport gates. benilla's options scroll measures its body's own overhang. Tests pin the hidden fades, the stored range and its notify, and the paint gates. Fixes #182 Fixes #175 Fixes #892 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f545081e9f |
MONKEY (planar water): planar replaces screen-space reflections - the Water Reflections option is gone
Owner: planar is cheap enough (~1 ms CPU, GPU about even) to replace SSR outright. High water always reflects with the planar mirror; the waterReflections cvar, its preset rows, the options row (EN/RU) and the debug knob are removed. The screen-space march stays only as the mirror's fallback for water at another height than the mirror plane; WOW_WATER_REFLECT=0 remains a dev A/B switch. |
||
|
|
52bc8e2f94 | Merge branch 'gfx/1009-water' into gfx/1009-upstream | ||
|
|
8279d3b92d |
MONKEY (planar water): SSR stretch fixed; planar water reflections (Water Reflections: Screen Space / Planar)
SSR: the march accepted rays up to 2 + 0.1*dist yd behind the scene depth and returned the bisected pixel unchecked, so rays passing BEHIND a thin dock deck converged on its front silhouette - a vertical smear. After bisection a hit may now sit at most 0.25 + 1.5%*dist + the last interval behind; deeper = passing behind an occluder, march on. Planar: liquid/mirror.rs (WaterMirrorPlugin) renders a half-res mirrored camera (oblique near plane at the water) before the world camera into binding 107; no water, no sun shadows, no post. The plane is voted by a 7x6 ray fan with hysteresis; water off that plane fades back to SSR. Cost ~+1 ms CPU, GPU about even (SSR march skipped) -> its own cvar waterReflections (0 Screen Space / 1 Planar, EN+RU option row), on for High/Ultra. The High preset (= registered defaults) now carries High water (waterQuality 2); Low and Medium keep Enhanced. Classic and Enhanced captures byte-identical. |
||
|
|
ee6c76b988 |
Merge upstream main (samwhosung/benilla c2fc3cf3) into the graphics line
Conflicts: capture fixtures/scenarios (both sides appended: kept both), options_tests row counts (upstream 80->81 rows: ours 100->101 and 99->100), docs/MAP.md regenerated with scripts/genmap.sh. |
||
|
|
c2fc3cf304 |
OnShow and OnHide fire children first, and after OnLoad, as in 1.12.1 (#842)
Show and hide propagated a whole subtree first, then fired from a snapshot, parent first: a parent's `OnShow` ran before its children's, and a child hidden by an earlier handler still got its `OnShow`. A frame visible at the end of its load got no `OnShow` at all. In 1.12.1 show `0x76ae10` and hide `0x76ad50` mark the frame, walk the live child list re-checking each child's gate, and fire the frame's own handler last (`0x76aef5`, `0x76adfd`). The post-load hook fires `OnShow` after `OnLoad` for a visible frame (`0x76a3b3`), from XML and `CreateFrame` alike. Now handlers run inside a live post-order walk, and the post-load `OnShow` fires. Tests cover the order, a sibling hidden mid-walk, and one or two `OnShow`s after a load; they fail without the fix. Four existing tests' expected orders flip to the reference's. Fixes #350 Fixes #59 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dc63a06178 |
The UI scale is UIParent's own scale over a 768-unit root (#841)
The UI scale was folded into the pixel seam, with the root and `UIParent` at scale 1. Frames outside `UIParent` (dropdown menus, need/greed rolls, the world map) shrank with it, `UIParent:GetScale()` answered 1, and `GetCursorPosition()` was off by the scale. In 1.12.1 the scale is `UIParent`'s own frame scale (`0x494550` → `0x76ac10`, floored at 0.64) over a 768-unit root. `GetScreenWidth`/`GetScreenHeight` divide by it (`0x494590`), and `GetCursorPosition` (`0x48b820`) does not. Now the seam is `height/768`, and the scale is applied to `UIParent` at the load's end and on a resize. The nameplates' compensation is gone. Backdrops, hit insets and minimap pings now follow a frame's own scale (`0x76b580`, `0x77e8d0`); tooltips at the cursor and `OnCursorChanged` divide by it (`0x530b20`, `0x77db23`). Tests cover each, and a stock-UI comparison shows nothing inside `UIParent` moves. Fixes #172 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d729fc1145 | ui: define the missing ArenaTeamTemplate so Turtle's inspect arena buttons load | ||
|
|
198b6cd894 |
fix: overhead names carry the player's PvP title, as 1.12.1 does (#792)
- capture: a name-rank scenario shoots a ranked player's overhead name line - Overhead names carry the player's PvP title under UnitNamePlayerPVPTitle, as 1.12.1 does (#550) - review: bundle the rank-title inputs; doc the snapshot and rank-19 limits - nameplates: read rank strings per VM; test the composed name line - pvp: correct the rank-title docs; convert the credit team without a cast - Fix the titles row comment; name-rank reuses WOLF_POS, cites :900 - rank strings: the honor feed reads them off the VM; nameplates only reads the value Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-Authored-By: Sam <samuv.eth@gmail.com> |
||
|
|
36c9dc2aaa | ui: load TurtleWoW's Custom_Merchant frame, so the armoury's PvP gear opens instead of the trigger text | ||
|
|
a0d9952d90 | ui: load Turtle's Transmog/Shop Templates xmls, so the windows don't raise on nil | ||
|
|
622e1c3e7a |
fix: overhead names carry the player's PvP title, as 1.12.1 does (#792)
- capture: a name-rank scenario shoots a ranked player's overhead name line - Overhead names carry the player's PvP title under UnitNamePlayerPVPTitle, as 1.12.1 does (#550) - review: bundle the rank-title inputs; doc the snapshot and rank-19 limits - nameplates: read rank strings per VM; test the composed name line - pvp: correct the rank-title docs; convert the credit team without a cast - Fix the titles row comment; name-rank reuses WOLF_POS, cites :900 - rank strings: the honor feed reads them off the VM; nameplates only reads the value Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-Authored-By: Sam <samuv.eth@gmail.com> |
||
|
|
445624217a |
ui: load TurtleWoW's Transmog and Shop windows
Both are the client half of Turtle's custom systems, off the chain's own `FrameXML.toc`: `Turtle_TransmogUI` hooks `GOSSIP_SHOW`, hides the stock gossip frame for its NPCs and drives the transmog window over `TW_TRANSMOG`; `Turtle_ShopUI` (prefix `TW_SHOP`) is the custom merchant/shop. Without these rows the server's trigger gossip text is what the stock gossip window shows and neither window exists. Inspect-Talents and Guild-Bank stay out until `ToggleInspect`/`SetTalentButtonLocation` and the GuildBank globals are built. |
||
|
|
bd0e24e9a9 |
ui: load TurtleWoW's LFT group finder, so its minimap eye and /lft exist
benilla.toc sources the stock FrameXML off the player's chain, and it omitted the LFT (Looking-For-Turtle) files, so the group finder never loaded. Add LFT_Data.lua, LFT.lua, LFTTemplates.xml and LFT.xml in the reference toc's order, with `json.lua` (the codec its group payloads decode through) and OctoWoW's blink-pause overlay last. `CharacterFrameTemplates.xml` comes in ahead of its users: LFT's Browse/Queue tabs, and CharacterFrame.xml's own tabs, inherit `CharacterFrameTabButtonTemplate`, which only that file defines and the reference toc lists apart. |
||
|
|
494d232c11 |
Merge upstream benilla main (b396bbf6) into everwood-graphics
105 upstream commits (v0.2.0 |
||
|
|
0b71937624 |
Options: the era AddOns page, one checkbox per installed addon, then Load out of date AddOns
The era's tenth category was a button in the tree that went nowhere. It now opens onto the era's own per-addon enable list, rebuilt on show from the AddOn API: one checkbox per installed addon carrying its `GetAddOnInfo` enabled state, then the Load out of date AddOns row under it (`checkAddonVersion`, inverted, so checked means "0"). A toggle calls `EnableAddOn`/`DisableAddOn`, which the host persists to `AddOns.txt` at logout — the reference's own contract for this screen, where the change lands on the next world entry. `the_defaults_button_is_armed_by_rows_not_by_a_category` is the invariant that every category leads somewhere and that Defaults is live on every page with a CVar behind it, and AddOns is the second category to run its own page after Keybindings. It is exempted from the `OPTIONS_PAGE_ROWS` half for that reason, and from the Defaults half because an addon's enabled state is not a CVar: there is nothing there for Defaults to write back. |
||
|
|
2ce6a3e881 |
feat: click to move walks to an out-of-reach target, then interacts (#752)
- Click to Move: with AutoInteract on, a right-click on an NPC, a body to loot or skin, or a GameObject out of reach walks there and runs the verb on arrival - Click to Move: the arrival scales by the mover's current speed, and a Use still out of range walks again - Click to Move: a Loot walk stops at the square root of the melee reach - Put the meeting-stone test's doc comment back on its test - A GameObject click out of range raises ERR_USE_TOO_FAR unless the walk starts - Wrap the finish's doc lines at the column limit Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-Authored-By: Sam <samuv.eth@gmail.com> |
||
|
|
f573d1ad67 | TurtleWoW compat: restore OptionsFrameSliders, resolve races 9/10, append .blp to bare M2 textures, nil-safe wipe, explode + addon delimiters, CONTINENTS_LENGTH | ||
|
|
031acdd8bb | Fix TurtleWoW baked NPC atlases (append .blp) plus in-progress GFX work: moonlight, torch depth, water/scene-depth, skybox | ||
|
|
7e117809b2 |
/errors: the selected error is copyable, in a read-only EditBox (#730)
`/errors` showed the selected error in a FontString, so a player could not select or copy the message and traceback they came to report. The detail pane now hosts a multi-line EditBox the way stock `MailFrame.xml` hosts the mail body: - `autoFocus="false"`, so the keyboard stays with the game until a click; - Escape clears focus; - a mouse-enabled scroll child that focuses the box on a click below its text; - `ScrollingEdit_OnTextChanged` for the range, since the box is as tall as its text. The box is read-only: an edit is undone on `OnTextChanged`. Focus selects all, so Ctrl+C copies the whole error, and a new error arriving keeps the selection. The text keeps its font, its 6/6 inset and its 270 wrap. Tests cover the whole text in the box, the undone edits, select-all and copy, a click below the text, Escape, and the range over a long traceback. Fixes #703 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
cd34ab77d2 |
/errors: put Clear and Close in the panel art's sockets (#720)
The `/errors` window wears the ClassTrainer panel art, but its Clear and Close buttons sat at y −455, below the art, and its detail pane ran to −442, into the button sockets. On that art the class trainer lays out (`Blizzard_TrainerUI.xml`, `ClassTrainer_SetToClassTrainer`): - a 296×184 list at (−67, −96) from the top right, with 11 rows; - a 119-tall detail pane 8 below the list, ending at −407; - Train and Exit, 80×22, centred at (224, −420) and (305, −420). The log now uses those frames: Clear and Close sit in the sockets, the detail ends above them, and the list shows 11 rows, not 13. The new test lays out the stock trainer and the log in one UI and checks that each log frame equals the trainer's, with the buttons inside the window. It fails on the old layout. Fixes #698 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
34d35881c2 |
GFX: volumetric light (sun/moon shafts) and moonlight
Volumetric light: the shadow rig's directional light (sun by day, moon at night) is ray-marched through its cascades at half resolution (16 Medium / 32 High jittered samples from 0.35 yd, height-thinned air, HG g 0.6), then a depth-aware blur and upsample composite it over the fog pass. Density and colour follow the zone fog; no shafts in WMO interiors. The volumetric fog pass retires its own midpoint shafts while this is on. Cvars volumetricLight 0/1/2 and volumetricLightStrength, env WOW_VOLLIGHT; Graphics Preset Medium 1, High/Ultra 2. Moonlight: a cool, dim directional term from the visible moon added to the stock night on terrain, models, WMO exteriors and doodads, shadowed by the moon's map (shadow_hook::realtime_shadow_moonlit .z), ramping with the moon's elevation and handing back to the sun at dawn. MonkeyFrame grows one row (per-frame blob 8800 B). Cvar moonLight 0..2 (0 = the reference night). Advanced Graphics rows (EN + RU), capture scenarios vol-*, docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |