the cooldown goes stock, early-Z comes back, and lists reach the end

- a size-less `<Model>` pane's rect is its file's box in layout units: the reference's geometry overrides (`0x76d080`/`0x76d0d0`) answer the bbox extent whenever no size is authored, `bboxExtent · 768·√(a²+1) · layoutScale` FrameXML units (`1280 · extent` at 4:3), written into the pane's layout input when the file's facts land, re-derived on an aspect change, and ended for good by any authored size; `SetPosition` is the same unit, which is how `0x4a7b20` centres the arrow at `½·GetWidth, ½·GetHeight` through the override — so the map arrow's sprite arm and its hand-sized 33.6 px footprint go (its rect is `0.0262 × 0.0263` layout units, 33.5 × 33.7 FrameXML units at 4:3, 41.0 × 41.2 at 16:9), and the stock `ItemAnim` pane plays the item push unadapted (`ReplaceIconTexture` on the type-14 batch, the 1000 ms clamp, the stock `OnAnimFinished`), `ContainerFrameAdapters.xml` losing 0887's Lua card and its hand-measured 42.41 × 124.72 rect — the implicit one at 16:9 — and keeping the keyring fork and the three bag verbs (2015); and the cooldown is the reference's own model: the texture-transform law `uv' = R_q((uv + t − p) ⊙ s) + p`, `p = (½, ½)`, the raw quaternion component-lerped and never normalised, is one function (`uv_transform`) with the shader's UV fold as its twin — rotation and scale tracks bake per file sequence slot beside the translation, the mat-anim table gains an affine row `[cos − 1, sin, sx − 1, sy − 1]` on `anim_slots.z` whose identity is the zero row so every static batch reads exactly what it did, and a UI tile clones the twin material per animated batch and writes both rows every frame off the pane's play head, never the world clock; the stock `Cooldown.xml` runs off the chain on 2007's two handlers, the file's four quadrant quads turned by their own rotation tracks (`0 → −90°` over each quadrant's 250 ms window, clockwise from 12 o'clock) and its alpha tracks revealing the star for the flash; retired: the native `Cooldown` widget and `SetCooldown` (an Era API 1.12 never had — `CreateFrame("Cooldown")` is the reference's unknown-type refusal now), the loader's `Model → Cooldown` substitution, the app's hand-built pie and star, and our `Cooldown.xml`; `assets/ui` is eight files (2019)
- `UpdateMapHighlight` gets its zone-level exits (B360): at zone zoom ours returned the nil tail for every cursor position, so hovering Deadwind Pass from Duskwood showed no label where 1.12 shows "Deadwind Pass"; the reference's `0x4a7fa0` runs two lookups at zone level and answers a NAME only — first the current zone's exploration-gated overlay list (the builder at `0x4a6ad9` gating on the explored byte), each row's HitRect scaled by 1/1002 and 1/668 against the normalized cursor, edges inclusive, the first hit naming its Area1, then the continent grid re-windowed through the displayed zone's own rect, naming the neighbouring zone or city and nothing for the displayed zone itself — and only the name-only tail is reachable from there, fileName nil and the six coords zero, so the stock frame hides the highlight quad, as the director's own 1.12 shot shows; and `SetVerticalScroll` stores the offset as given (B370, B363): the reference's `0x786db0` compares the new offset against the old and writes it to `[+0x328]` verbatim, `0x787100` re-anchors the scroll child off it, and the range at `[+0x320]` is never read on that path — every clamp the reference exhibits is FrameXML's, through the scroll bar's `[min, max]`; ours clamped into `[0, GetVerticalScrollRange()]`, the child's overflow past the FRAME, which is 40 px short of where `FauxScrollFrame_Update` sends the bar on the 220-tall skills list (three rows lost) and 15 px short on the 287-tall who list (one); the setter fires `OnVerticalScroll` only on a change, as the bytes do, `BenillaScroll_Step` bounds its wheel in Lua where the reference's handler does, and the ScrollFrame kind's prose stops calling its offset unpinned (2017)
- overdraw's cheap half, the sky first: since 0588 every sky element wrote `frag_depth = 0.0`, which costs a pipeline its early-Z, so on an outdoor frame every full-screen dome fragment under the ground and the buildings was shaded in full (the gradient's `asin`, `atan2` and eight table lookups at dawn and dusk) and thrown away; one shared `sky_vertex.wgsl` — bevy's mesh vertex verbatim plus `out.position.z = 0.0` — is now the vertex stage of the gradient dome, the cloud dome, the star patches and the celestial discs and glares, and the model lane's `WOW_SKY_DEPTH` branch moves from its fragment to its vertex, so no sky fragment declares `frag_depth` and the rasterizer rejects the covered screen before the shader runs; every sky `specialize` zeroes the rasterizer bias constant the `sky_order` rung had been leaking into `DepthBiasState::constant`, harmless only while the fragment overwrote its depth, and the prose beside the pin follows it; then the retained `static_gx` pass draws BEFORE bevy's opaque pass (`StartMainPass → static_gx → MainOpaquePass`) — buildings and doodads are the frame's best occluders and a terrain fragment its dearest, six samples a pixel — so walls reject the terrain behind them instead of the reverse, the pass taking the first-use clear with it and the dev depth probe ordered after it; measured on 16 capture scenarios: 14 bit-identical, `northshire-dusk` 1 px and `water-night` 11 px, attributed to exact depth ties on static/terrain silhouettes and last-bit interpolation noise; a depth prepass and near-to-far bins are declined until a journal says where the time goes (2016)
- the grass cut goes per pixel because of a quarter-mip: `0x6813d0` sets `D3DSAMP_MIPMAPLODBIAS = +0.25` on stage 0 (`0x6813f4`) once per frame before the detail-doodad pass — capture-confirmed on all 29 doodad batches, and outside the `[0x6b1000, 0x6b3200)` scan 2004 had called complete; it decides the whole look because the `clutter_cut` probe finds 6 of 31 shipped detail atlases carry a BINARY alpha pyramid (mip 0 has DXT3's 16 levels, every level below exactly two, both starting zones among them), so `a × ramp` degenerates to a global on/off and every texel of a leaf crosses at the same depth — Elwynn's survivor coverage is flat at 47.6 % across the whole 52.5–61.1 yd band, then 0.0 %, while the probe's box-filtered control erodes 47.7 → 20.9 over the same band, so the art has the gradient the shipped pyramid threw away, and `clutter_size` puts the median tuft at 0.92 yd tall, 1.03 yd deep, a full yard for the boundary plane to sweep; the escape is bilinear: full alpha needs a 2×2 all-opaque texel neighbourhood, and those run out between mip 2 and 3 (179/56/10/0/0/0 on the dominant Elwynn cell), so the reference's λ ≈ 2.9–3.3 sits past the cliff and the tuft erodes continuously — 2.2 % of a tuft's pixels leave in the last half-yard with the bias, 11.6 % without, 24.2 % one mip sharper; wgpu has no sampler LOD bias, so the clutter lane re-samples base colour at `view.mip_bias + 0.25`, keeping the sampler every other model batch shares at the reference's 0; ruled out on the way: our BLP decoder (all nine uploaded levels byte-identical against the archive's 52 793 BLP2s), mip regeneration, the `has_mipmaps` byte's value 2 (a plain boolean, an authoring marker), anisotropy and geometry; what it does not fix is resolution-dependent, and so is the reference's own look — λ with the bias is 3.22 at the capture's 648p but 2.48 at 1080p and 1.48 at 2160p, so at a modern resolution the reference itself pops leaf by leaf, and scaling the bias with the viewport is one reversible line left for a look call (2021)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-06 19:35:31 +07:00
co-authored by Claude Fable 5.1
parent c92ca1b8ec
commit c3555b4eea
93 changed files with 2423 additions and 1643 deletions
+35 -57
View File
@@ -123,8 +123,8 @@ pub enum FrameKind {
ColorSelect,
SimpleHtml,
MovieFrame,
/// The `GameTooltip` widget family (decision 0274). Like [`FrameKind::Minimap`]/
/// [`FrameKind::Cooldown`] a *game-layer* factory over `CSimpleFrame`; its modeled behavior —
/// The `GameTooltip` widget family (decision 0274). Like [`FrameKind::Minimap`], a
/// *game-layer* factory over `CSimpleFrame`; its modeled behavior —
/// the line stack, owner/anchor law, auto-size, fade — lives in [`KindState::Tooltip`] and
/// `script::tooltip`. The real class's Lua surface is the 38-binding family wow-re pinned
/// (`ui/scratch/bindings.md` 0x530c40–0x5364a0); the line/color primitives are byte-diffed
@@ -137,12 +137,6 @@ pub enum FrameKind {
/// the app renderer's job (decision 0203); the engine core carries only the rect and the zoom
/// state ([`KindState::Minimap`]).
Minimap,
/// The cooldown sweep widget (decision 0137 phase 4). The 1.12 reference builds it as a
/// `Model` playing `UI-Cooldown-Indicator.mdx` (`CooldownFrameTemplate` + `Cooldown.lua`'s
/// scrub/flash/hide machine); benilla models the *mechanism* as a first-class widget — the
/// Era API's own `Cooldown` frame type — whose state machine lives engine-side
/// ([`KindState::Cooldown`]) and whose pie-wipe/flash pixels are the app renderer's job.
Cooldown,
}
/// Whether a [`Region`] leaf is a texture or a text string. These are the client's two non-frame
@@ -190,9 +184,10 @@ pub enum KindState {
Message(MessageFrameState),
/// `CSimpleScrollFrame` (decision 0112 — the ScrollFrame mechanism, the engine's last structural
/// gap: the quest log's detail pane, chat history, and every long-content window need it): the
/// scroll child + the vertical scroll offset. The mechanism is spec-faithful (the documented
/// `SetScrollChild`/`SetVerticalScroll` contract, same posture as StatusBar's fill), not
/// byte-pinned. Horizontal scroll is out of scope (no 1.12 template drives it).
/// scroll child + the vertical scroll offset. The offset setter and the range are byte-pinned
/// (2017: `0x786db0` stores the offset as given, no clamp; 1338: `0x786e30` measures the
/// child's subtree); the rest is spec-faithful to the documented contract, same posture as
/// StatusBar's fill. Horizontal scroll is out of scope (no 1.12 template drives it).
Scroll(ScrollFrameState),
/// `CSimpleSlider` (factory `0x6eee40`; LoadXML table `0x789580`, RF-28): a value in `[min, max]`
/// with a step and orientation, positioning a thumb texture along the track. The mechanism is
@@ -207,16 +202,13 @@ pub enum KindState {
/// widget's whole modeled behavior here; see [`crate::script::colorselect`].
ColorSelect(ColorSelectState),
/// The `Model` widget's scene state — the 3D pane an addon or a FrameXML frame parks a model
/// in. **The same split as [`KindState::Minimap`] and [`KindState::Cooldown`]**: the engine
/// core carries exactly what the Lua API reads and writes, and the pixels are the app
/// renderer's job. See [`ModelState`].
/// in. **The same split as [`KindState::Minimap`]**: the engine core carries exactly what
/// the Lua API reads and writes, and the pixels are the app renderer's job. See
/// [`ModelState`].
Model(ModelState),
/// The `<Minimap>` widget's zoom state (decision 0203). The engine core carries only what the
/// Lua API reads/writes (`GetZoom`/`SetZoom`/`GetZoomLevels`); the tile/blip render is app-side.
Minimap(MinimapState),
/// The cooldown widget's timer ([`CooldownState`]) — the reference `Cooldown.lua` machine's
/// inputs; the sweep/flash phases derive from them at extract time.
Cooldown(CooldownState),
/// The GameTooltip widget's line stack + owner/fade state ([`TooltipState`], decision 0274).
Tooltip(TooltipState),
}
@@ -345,38 +337,6 @@ pub const TOOLTIP_FADE_SECS: f64 = 0.5;
/// description/trigger-line wrap by eye.
pub const TOOLTIP_WRAP_WIDTH: f32 = 260.0;
/// The cooldown widget's timer, in the engine's `GetTime` clock (seconds): the reference
/// `CooldownFrame_SetTimer(start, duration, enable)` stores exactly this pair (its `enable == 0`
/// / non-positive gate hides instead of storing — kept in the Lua helper, ref-verbatim). The
/// three phases the reference machine derives (`Cooldown.lua`, byte-authored):
/// - `t < start + duration` — the sweep: sequence 0 scrubbed to `(now-start)/duration`.
/// - the next [`COOLDOWN_FLASH_SECS`] — the finish flash: sequence 1 played realtime.
/// - after that — hidden (`OnAnimFinished` → `Hide`), done engine-side in `tick`.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct CooldownState {
/// `GetTime`-clock start seconds (0 = no timer set).
pub start: f64,
/// Duration seconds (0 = no timer set).
pub duration: f64,
}
/// The finish flash's length: the model's sequence 1 is authored at exactly 1.000 s
/// (`UI-Cooldown-Indicator.m2` m2seq; played realtime by `CooldownFrame_OnUpdateModel`'s
/// `AdvanceTime`).
pub const COOLDOWN_FLASH_SECS: f64 = 1.0;
impl CooldownState {
/// The end of the sweep (= the flash's start).
pub fn sweep_end(&self) -> f64 {
self.start + self.duration
}
/// The moment the whole display is over (flash finished → hide).
pub fn finished_at(&self) -> f64 {
self.sweep_end() + COOLDOWN_FLASH_SECS
}
}
/// The model-pane scene state — **shared by [`FrameKind::Model`] and [`FrameKind::PlayerModel`]**,
/// because the client's `CGCharacterModelBase` (`0x505680`) *extends* `CSimpleModel` (`0x76c8e0`)
/// rather than replacing it: every field below is a `CSimpleModel` member both classes carry.
@@ -385,8 +345,8 @@ impl CooldownState {
/// little scene of its own — the character pane, the tabard designer, the minimap ping, the pet
/// bar's autocast shine, and every addon that wants a 3D thing in a frame. The engine core holds
/// exactly the scene an addon can read back or write; **the render is the app's**, the same
/// contract [`MinimapState`] and [`CooldownState`] already run under, and the reason both of those
/// exist as state-only kinds here.
/// contract [`MinimapState`] already runs under, and the reason both exist as state-only kinds
/// here.
///
/// **Every field is a 1.12 binding's storage, and which binding is read off the registrar, not off
/// a string scan.** wow-re enumerated the whole family at the pair bytes on 2026-08-30
@@ -442,6 +402,13 @@ pub struct ModelState {
/// `0x710ec0`), which lives on the model instance and dies with it: `SetModel` and
/// `ClearModel` clear it. `None` = the file's own textures.
pub icon: Option<String>,
/// The frame's size is the **implicit rect** — the file's bounding-box extent in layout units
/// (`bboxExtent · 768·√(a²+1)` FrameXML units; render law §3, `implicit-size-law.md` §1),
/// written by the engine because the pane authored no size (decision 2015). The geometry
/// getters `0x76d080`/`0x76d0d0` answer it whenever no size is authored; here it is written
/// into the layout input when the file's facts land and re-derived when the screen's aspect
/// moves, and an authored `SetWidth`/`SetHeight`/`SetSize` clears it for good.
pub implicit_size: bool,
/// The pane's yaw in radians — `CSimpleModel+0x39c`. **One slot, written by two verbs on two
/// different classes**: `Model:SetFacing` (`0x76dce0`) and `PlayerModel:SetRotation`
/// (`0x505f00` → `0x505bb0`, whose last act is `0x505c44 mov [esi+0x39c], eax` — literally the
@@ -489,6 +456,7 @@ impl Default for ModelState {
armed: None,
pending_seed: false,
icon: None,
implicit_size: false,
facing: 0.0,
scale: 1.0,
camera: 0,
@@ -575,6 +543,14 @@ impl ModelFileFacts {
self.sequences.iter().find(|s| s.anim_id == anim_id)
}
/// The header bounding box's `(x, y)` extent in model units — the implicit rect of a
/// size-less pane (render law §3) and the arrow's re-centring (`0x4a7b20`: `½·GetWidth`,
/// `½·GetHeight`, both the geometry override's bbox extent).
pub fn extent(&self) -> (f32, f32) {
let (min, max) = self.bbox;
((max[0] - min[0]).max(0.0), (max[1] - min[1]).max(0.0))
}
/// The loader's idle seed (`0x70ebd0`'s tail, `0x710153`–`0x71019b`): **id 0 (`Stand`) if
/// the file owns it, else `animations[0]`'s own id**; `None` only for a file with no
/// sequences at all.
@@ -842,17 +818,19 @@ pub const MINIMAP_DEFAULT_PLAYER_MODEL: &str = "Interface\\Minimap\\MinimapArrow
/// A `CSimpleScrollFrame`'s runtime state: the frame whose anchors are overridden to track the
/// scroll offset ([`crate::script::UiScript::resolve`]'s scroll-child override), and the current
/// vertical scroll position. `SetVerticalScroll` clamps into `[0, GetVerticalScrollRange()]`, where
/// the range is always computed live from the resolved rects (never cached here) — so this struct
/// carries only the two members the client's `SetScrollChild`/`SetVerticalScroll` actually set.
/// vertical scroll position. `SetVerticalScroll` stores the offset VERBATIM — the reference's
/// `0x786db0` never reads the range (decision 2017) — and the range is always computed live from
/// the resolved rects (never cached here), so this struct carries only the two members the
/// client's `SetScrollChild`/`SetVerticalScroll` actually set (`[+0x318]`, `[+0x328]`).
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ScrollFrameState {
/// The scroll child (`SetScrollChild`) — the one frame whose content pans within this frame's
/// rect. `None` = no child (nothing to clip or offset).
pub child: Option<FrameHandle>,
/// The vertical scroll offset in px (`SetVerticalScroll`), always in `[0, range]`. XML
/// y-positive-up: a positive offset lifts the child (`child.top = scrollframe.top + vertical`),
/// bringing content below the fold into view.
/// The vertical scroll offset in px (`SetVerticalScroll`), unclamped — the reference's
/// `[+0x328]`. XML y-positive-up: a positive offset lifts the child
/// (`child.top = scrollframe.top + vertical`), bringing content below the fold into view. The
/// scroll bar's `[min, max]` is what keeps it inside the range — in FrameXML, never here.
pub vertical: f32,
}
+7 -9
View File
@@ -174,13 +174,13 @@ impl<T> Arena<T> {
mod kinds;
pub use kinds::{
model_key, slider_fraction, slider_grab, ArmedSequence, ButtonFont, ButtonState,
ButtonVisualState, ColorSelectState, CooldownState, EditAction, EditBoxState, EditOutcome,
EditUnit, FrameKind, InsertMode, KindState, MessageFrameState, MessageLine, MinimapState,
ModelFileFacts, ModelPlayHead, ModelState, RegionKind, ScrollFrameState, ScrollingMessageState,
SequenceFacts, SliderState, StatusBarState, TooltipState, COOLDOWN_FLASH_SECS,
MINIMAP_DEFAULT_ARROW_MODEL, MINIMAP_DEFAULT_MASK, MINIMAP_DEFAULT_PLAYER_MODEL,
MINIMAP_DEFAULT_ZOOM, MINIMAP_ENGINE_CHILDREN, MINIMAP_ZOOM_LEVELS, TOOLTIP_DOUBLE_GAP,
TOOLTIP_FADE_SECS, TOOLTIP_LINE_GAP, TOOLTIP_PAD, TOOLTIP_WRAP_WIDTH,
ButtonVisualState, ColorSelectState, EditAction, EditBoxState, EditOutcome, EditUnit,
FrameKind, InsertMode, KindState, MessageFrameState, MessageLine, MinimapState, ModelFileFacts,
ModelPlayHead, ModelState, RegionKind, ScrollFrameState, ScrollingMessageState, SequenceFacts,
SliderState, StatusBarState, TooltipState, MINIMAP_DEFAULT_ARROW_MODEL, MINIMAP_DEFAULT_MASK,
MINIMAP_DEFAULT_PLAYER_MODEL, MINIMAP_DEFAULT_ZOOM, MINIMAP_ENGINE_CHILDREN,
MINIMAP_ZOOM_LEVELS, TOOLTIP_DOUBLE_GAP, TOOLTIP_FADE_SECS, TOOLTIP_LINE_GAP, TOOLTIP_PAD,
TOOLTIP_WRAP_WIDTH,
};
// ─────────────────────────────────────────────────────────────────────────────────────────────
@@ -723,7 +723,6 @@ impl WidgetArena {
| FrameKind::DressUpModel
| FrameKind::TabardModel => KindState::Model(kinds::ModelState::default()),
FrameKind::Minimap => KindState::Minimap(kinds::MinimapState::default()),
FrameKind::Cooldown => KindState::Cooldown(kinds::CooldownState::default()),
FrameKind::GameTooltip => KindState::Tooltip(kinds::TooltipState::default()),
_ => KindState::None,
},
@@ -739,7 +738,6 @@ impl WidgetArena {
kind,
FrameKind::ScrollingMessageFrame
| FrameKind::MessageFrame
| FrameKind::Cooldown
| FrameKind::Model
| FrameKind::PlayerModel
| FrameKind::DressUpModel