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:
@@ -27,12 +27,18 @@ use super::Model;
|
||||
/// The arrow's model — `0x8453c0`, and NOT `Rotating-MinimapArrow`.
|
||||
pub const ARROW_MODEL: &str = "Interface\\Minimap\\MinimapArrow.mdx";
|
||||
|
||||
/// The arrow's footprint at the reference basis: `MinimapArrow.mdx`'s single quad at 1280 px per
|
||||
/// model unit — 33.6 px on the 1024×768 sheet, the same number the minimap's player arrow is
|
||||
/// drawn at (`minimap::blips::PLAYER_ARROW_QUAD_PX`). The world-map arrow's model scale is
|
||||
/// `G48 · 5/3` (`= 1.0` at 4:3, `G48 = 1/√(aspect² + 1)`) and the mini's `G48 · 10/9`, so both
|
||||
/// hold a constant apparent size as the window's shape changes.
|
||||
pub const ARROW_FOOTPRINT_PX: f32 = 33.6;
|
||||
/// The arrow's rect is the file's own bounding box — the implicit rect of a size-less pane
|
||||
/// (decision 2015; `MinimapArrow.mdx`'s `0.0262 × 0.0263` model units read as layout units,
|
||||
/// `1280·extent = 33.5 × 33.7` FrameXML units at 4:3). The world-map arrow's model scale is
|
||||
/// `G48 · 5/3` (`= 1.0` at 4:3, `G48 = 1/√(aspect² + 1)`) and the mini's `G48 · 10/9`, so the
|
||||
/// quad holds a constant apparent size as the window's shape changes while its rect grows with
|
||||
/// `√(a²+1)` (render law §2, the worked arrow).
|
||||
fn centre_of(facts: Option<&crate::widget::ModelFileFacts>) -> (f32, f32, f32) {
|
||||
facts.map_or((0.0, 0.0, 0.0), |f| {
|
||||
let (x, y) = f.extent();
|
||||
(x * 0.5, y * 0.5, 0.0)
|
||||
})
|
||||
}
|
||||
|
||||
/// Which of the two singletons a binding addresses.
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -144,18 +150,17 @@ fn create(lua: &Lua, which: Arrow, parent: Value) -> mlua::Result<()> {
|
||||
("Model".to_string(), None, Some(Value::Table(t)), None),
|
||||
)?;
|
||||
let id = decode_id(&wrapper)?;
|
||||
let (scale, side) = {
|
||||
let scale = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let scale = arrow_scale(&model, which);
|
||||
(scale, ARROW_FOOTPRINT_PX * scale)
|
||||
arrow_scale(&model, which)
|
||||
};
|
||||
// The widget's rect is the resident model's bounding box (§2.2); the footprint at this scale.
|
||||
wrapper.call_method::<()>("SetWidth", side)?;
|
||||
wrapper.call_method::<()>("SetHeight", side)?;
|
||||
// `SetModel("Interface\Minimap\MinimapArrow.mdx")` in C++ (`0x4a7a80` → `0x76c8e0`): the
|
||||
// same file set every pane takes, seeded with the file's facts when the host has them
|
||||
// (decision 2007 — the arrow's Stand loops its 3.333 s with no bone keyed, so nothing
|
||||
// moves; the arm is the reference's, not a look).
|
||||
// moves; the arm is the reference's, not a look). No authored size: the widget's rect is
|
||||
// the file's bounding box (§2.2, decision 2015), and `0x4a7b20`'s `SetPosition(½·GetWidth,
|
||||
// ½·GetHeight, 0)` — the geometry override's bbox extent, in layout units — centres the
|
||||
// model on it.
|
||||
let facts = lua
|
||||
.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
@@ -163,9 +168,11 @@ fn create(lua: &Lua, which: Arrow, parent: Value) -> mlua::Result<()> {
|
||||
super::modelframe::with_model(lua, &wrapper, |m| {
|
||||
m.set_file(ARROW_MODEL.to_string(), facts.as_deref());
|
||||
m.scale = scale;
|
||||
m.position = (side * 0.5, side * 0.5, 0.0);
|
||||
m.position = centre_of(facts.as_deref());
|
||||
})?;
|
||||
let h = frame_handle_of(lua, &wrapper)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.apply_implicit_rect(h);
|
||||
which.set_slot(&mut model, id);
|
||||
Ok(())
|
||||
}
|
||||
@@ -187,17 +194,16 @@ fn update(lua: &Lua) -> mlua::Result<()> {
|
||||
let Some(wrapper) = arrow_wrapper(lua, which)? else {
|
||||
continue;
|
||||
};
|
||||
let (facing, tracked) = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let (facing, tracked, facts) = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
(
|
||||
model.worldmap.player_facing,
|
||||
model.worldmap.player_uv.is_some(),
|
||||
model.model_facts_for(ARROW_MODEL),
|
||||
)
|
||||
};
|
||||
let w: f32 = wrapper.call_method("GetWidth", ())?;
|
||||
let h: f32 = wrapper.call_method("GetHeight", ())?;
|
||||
super::modelframe::with_model(lua, &wrapper, |m| {
|
||||
m.position = (w * 0.5, h * 0.5, 0.0);
|
||||
m.position = centre_of(facts.as_deref());
|
||||
if tracked {
|
||||
m.facing = facing;
|
||||
}
|
||||
@@ -334,6 +340,22 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::script::UiScript;
|
||||
|
||||
/// The arrow file's facts, as the host hands them over (2007/2015): the header box the
|
||||
/// implicit rect and the re-centring read.
|
||||
fn arrow_facts(s: &mut UiScript) {
|
||||
s.set_model_facts(
|
||||
ARROW_MODEL,
|
||||
crate::widget::ModelFileFacts {
|
||||
sequences: vec![crate::widget::SequenceFacts {
|
||||
anim_id: 0,
|
||||
duration_ms: 3333,
|
||||
looping: true,
|
||||
}],
|
||||
bbox: ([-0.0127, -0.0118, 0.0], [0.0135, 0.0145, 0.0]),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn vm() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
@@ -350,7 +372,7 @@ mod tests {
|
||||
/// loaded with the arrow, born shown, sized to its footprint; a second create a no-op.
|
||||
#[test]
|
||||
fn create_validates_its_parent_and_makes_one_arrow_per_session() {
|
||||
let s = vm();
|
||||
let mut s = vm();
|
||||
for (call, want) in [
|
||||
(
|
||||
"CreateWorldMapArrowFrame(5)",
|
||||
@@ -388,9 +410,28 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(kind, "Model");
|
||||
assert!(shown, "born shown");
|
||||
assert_eq!(w, 0.0, "no authored size, no facts yet: no rect");
|
||||
// The file lands: the rect is its box in layout units — 0.0262 × 1280 at 4:3 — and the
|
||||
// model is centred on it (`0x4a7b20`, half the box, in layout units).
|
||||
arrow_facts(&mut s);
|
||||
// The C++ re-centres on create and on every `UpdateWorldMapArrowFrames` (`0x4a7b20`);
|
||||
// a file that lands after the create is picked up by the next update.
|
||||
s.run("UpdateWorldMapArrowFrames()").unwrap();
|
||||
s.resolve();
|
||||
let w: f32 = s.eval("return ({WM:GetChildren()})[2]:GetWidth()").unwrap();
|
||||
assert!((w - 0.0262 * 1280.0).abs() < 0.05, "the box at 4:3: {w}");
|
||||
let pos = {
|
||||
let model = s.lua().app_data_ref::<Model>().expect("model");
|
||||
let id = model.worldmap.arrow_world.expect("the singleton");
|
||||
let fh = model.id_to_frame.get(&id).copied().expect("live");
|
||||
match &model.arena.frame(fh).expect("frame").kind_state {
|
||||
crate::widget::KindState::Model(m) => m.position,
|
||||
_ => panic!("not a Model"),
|
||||
}
|
||||
};
|
||||
assert!(
|
||||
(w - ARROW_FOOTPRINT_PX).abs() < 1e-3,
|
||||
"the footprint at 4:3: {w}"
|
||||
(pos.0 - 0.0131).abs() < 1e-5 && (pos.1 - 0.01315).abs() < 1e-5 && pos.2 == 0.0,
|
||||
"centred on the box: {pos:?}"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return select('#', CreateWorldMapArrowFrame(WM)) == 0")
|
||||
@@ -408,6 +449,8 @@ mod tests {
|
||||
s.run("PositionWorldMapArrowFrame(1, 2) ShowWorldMapArrowFrame('x') UpdateWorldMapArrowFrames()")
|
||||
.unwrap();
|
||||
s.run("CreateWorldMapArrowFrame(WM)").unwrap();
|
||||
// The file's facts: a size-less pane has no rect to anchor until its box is known.
|
||||
arrow_facts(&mut s);
|
||||
s.run("arrow = ({WM:GetChildren()})[2]").unwrap();
|
||||
for (call, want) in [
|
||||
(
|
||||
|
||||
Reference in New Issue
Block a user