- 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>
452 lines
19 KiB
Rust
452 lines
19 KiB
Rust
//! The world-map bindings (decision 0203 phase 2): catalog/feed pushes, the engine-owned
|
||
//! selection, and the deferred WORLD_MAP_UPDATE queue.
|
||
|
||
use super::common::script;
|
||
use crate::script::*;
|
||
|
||
/// A two-continent catalog shaped like the real 5875 one (Kalimdor first — the app's push order
|
||
/// defines every Lua-visible index; the sheet rects are the real disjoint kernel outputs). The
|
||
/// EK zone grid is sparse: exactly the real Goldshire cell (12735, verified against the shipped
|
||
/// Azeroth.zmp) marked as zone 1.
|
||
fn push_catalog(s: &mut UiScript) {
|
||
let mut ek_grid = vec![0u16; 128 * 128];
|
||
ek_grid[12735] = 1;
|
||
// Cell 8801 = the child (0.5, 0.5) resolves to through Elwynn's OWN loc rect — a "Stormwind
|
||
// City" peer child. Clicking it from the Elwynn zone map drills into the city (the §3 path).
|
||
ek_grid[8801] = 2;
|
||
// Cell 9060 = the child (0.82, 0.82) resolves to through Elwynn's own rect — Elwynn itself,
|
||
// the case the zone-level hover must stay silent on (the displayed zone never names itself).
|
||
ek_grid[9060] = 1;
|
||
s.set_world_map_catalog(vec![
|
||
WorldMapContinentView {
|
||
name: "Kalimdor".into(),
|
||
map_file: "Kalimdor".into(),
|
||
world_rect: (0.08882, 0.07910, 0.40020, 0.86952),
|
||
loc_rect: (17066.6, -19733.2, 12799.9, -11733.3),
|
||
zone_grid: Vec::new(),
|
||
zones: vec![
|
||
WorldMapZoneView {
|
||
name: "Durotar".into(),
|
||
area_id: 14,
|
||
map_file: "Durotar".into(),
|
||
loc_rect: (-1800.0, -6800.0, -3800.0, -8500.0),
|
||
overlays: Vec::new(),
|
||
},
|
||
WorldMapZoneView {
|
||
name: "The Barrens".into(),
|
||
area_id: 17,
|
||
map_file: "Barrens".into(),
|
||
loc_rect: (-1000.0, -11000.0, -5500.0, -17000.0),
|
||
overlays: Vec::new(),
|
||
},
|
||
],
|
||
},
|
||
WorldMapContinentView {
|
||
name: "Eastern Kingdoms".into(),
|
||
map_file: "Azeroth".into(),
|
||
world_rect: (0.62375, 0.02695, 0.92315, 0.87126),
|
||
loc_rect: (16000.0, -19199.9, 7466.6, -16000.0),
|
||
zone_grid: ek_grid,
|
||
zones: vec![
|
||
WorldMapZoneView {
|
||
name: "Elwynn Forest".into(),
|
||
area_id: 12,
|
||
map_file: "Elwynn".into(),
|
||
// A clean synthetic loc rect inside the EK continent rect: w=2000, h=1500 → the
|
||
// continent-highlight formula lands on round-ish expected coords (asserted below).
|
||
loc_rect: (-8000.0, -10000.0, -400.0, -1900.0),
|
||
// Three overlays. Two are shaped like real Elwynn rows: Northshire reveals
|
||
// on explore bit 125, Goldshire on bit 124 (the real AreaTable exploreFlags);
|
||
// their hit rects (top, left, bottom, right; px of 1002×668) sit apart so a
|
||
// point is in at most one. The first is a row whose first area slot resolves
|
||
// to nothing (bit 126; its rect overlaps Northshire's) — the zone-level hover
|
||
// must walk past it, not stop at it.
|
||
overlays: vec![
|
||
WorldMapOverlayView {
|
||
texture: "Interface\\WorldMap\\Elwynn\\STONECAIRNLAKE".into(),
|
||
width: 128,
|
||
height: 128,
|
||
offset_x: 450,
|
||
offset_y: 300,
|
||
explore_bits: vec![126],
|
||
hit_rect: (300, 450, 400, 560),
|
||
area_name: None,
|
||
},
|
||
WorldMapOverlayView {
|
||
texture: "Interface\\WorldMap\\Elwynn\\NORTHSHIREVALLEY".into(),
|
||
width: 200,
|
||
height: 176,
|
||
offset_x: 490,
|
||
offset_y: 200,
|
||
explore_bits: vec![125],
|
||
hit_rect: (200, 490, 376, 690),
|
||
area_name: Some("Northshire Valley".into()),
|
||
},
|
||
WorldMapOverlayView {
|
||
texture: "Interface\\WorldMap\\Elwynn\\GOLDSHIRE".into(),
|
||
width: 172,
|
||
height: 128,
|
||
offset_x: 380,
|
||
offset_y: 320,
|
||
explore_bits: vec![124],
|
||
hit_rect: (400, 300, 520, 460),
|
||
area_name: Some("Goldshire".into()),
|
||
},
|
||
],
|
||
},
|
||
// A city peer of Elwynn — a WorldMapArea child of the continent with its own art
|
||
// folder, indistinguishable from a zone in code (wow-re 15b2a8ea Part 2a).
|
||
WorldMapZoneView {
|
||
name: "Stormwind City".into(),
|
||
area_id: 1519,
|
||
map_file: "StormwindCity".into(),
|
||
loc_rect: (-8200.0, -8900.0, -400.0, -1100.0),
|
||
overlays: Vec::new(),
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
}
|
||
|
||
/// The navigation surface: lists come from the catalog, SetMapZoom's selection reads back
|
||
/// synchronously in the same Lua breath (the reference's dropdown-click contract), zone/continent
|
||
/// arguments clamp, and GetMapInfo names the displayed art folder (nil at the world level — the
|
||
/// reference Lua's own "World" fallback).
|
||
#[test]
|
||
fn worldmap_navigation_and_map_info() {
|
||
let mut s = script();
|
||
push_catalog(&mut s);
|
||
|
||
s.run(
|
||
r#"
|
||
conts = { GetMapContinents() }
|
||
kalimdor_zones = { GetMapZones(1) }
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
assert_eq!(
|
||
s.eval::<(String, String)>("return conts[1], conts[2]")
|
||
.unwrap(),
|
||
("Kalimdor".into(), "Eastern Kingdoms".into()),
|
||
"display names are the Map.dbc localized ones, not the art folders"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<(String, String)>("return kalimdor_zones[1], kalimdor_zones[2]")
|
||
.unwrap(),
|
||
("Durotar".into(), "The Barrens".into())
|
||
);
|
||
|
||
// World level by default: continent 0, nil map info.
|
||
assert_eq!(
|
||
s.eval::<(i64, i64)>("return GetCurrentMapContinent(), GetCurrentMapZone()")
|
||
.unwrap(),
|
||
(0, 0)
|
||
);
|
||
assert!(s.eval::<bool>("return GetMapInfo() == nil").unwrap());
|
||
|
||
// The synchronous read-back the reference relies on, and the art-folder name per level.
|
||
s.run("SetMapZoom(2)").unwrap();
|
||
assert_eq!(
|
||
s.eval::<(i64, i64, String)>(
|
||
"return GetCurrentMapContinent(), GetCurrentMapZone(), GetMapInfo()"
|
||
)
|
||
.unwrap(),
|
||
(2, 0, "Azeroth".into())
|
||
);
|
||
s.run("SetMapZoom(1, 2)").unwrap();
|
||
assert_eq!(
|
||
s.eval::<String>("return GetMapInfo()").unwrap(),
|
||
"Barrens",
|
||
"a zone selection names the ZONE's art folder"
|
||
);
|
||
|
||
// Out-of-range arguments clamp (never a Lua error — addons pass garbage).
|
||
s.run("SetMapZoom(9, 9)").unwrap();
|
||
assert_eq!(
|
||
s.eval::<(i64, i64)>("return GetCurrentMapContinent(), GetCurrentMapZone()")
|
||
.unwrap(),
|
||
(2, 2),
|
||
"continent clamps to the catalog, zone to that continent's list (EK has 2 zones)"
|
||
);
|
||
}
|
||
|
||
/// SetMapToCurrentZone lands on the app-fed player zone; the feed's projection + facing surface
|
||
/// through GetPlayerMapPosition/GetPlayerFacing — for `"player"` and, since report B320, for the
|
||
/// `party1..4` slots too. A `raid` token still answers the off-map sentinel.
|
||
#[test]
|
||
fn worldmap_current_zone_and_player_feed() {
|
||
let mut s = script();
|
||
push_catalog(&mut s);
|
||
|
||
s.set_world_map_feed(
|
||
Some((1, 2)),
|
||
Some((0.25, 0.75)),
|
||
1.5,
|
||
None,
|
||
vec![Some((0.1, 0.2)), None],
|
||
Vec::new(),
|
||
);
|
||
s.run("SetMapToCurrentZone()").unwrap();
|
||
assert_eq!(
|
||
s.eval::<(i64, i64)>("return GetCurrentMapContinent(), GetCurrentMapZone()")
|
||
.unwrap(),
|
||
(1, 2)
|
||
);
|
||
let (x, y) = s
|
||
.eval::<(f64, f64)>(r#"return GetPlayerMapPosition("player")"#)
|
||
.unwrap();
|
||
assert!((x - 0.25).abs() < 1e-6 && (y - 0.75).abs() < 1e-6);
|
||
assert!((s.eval::<f64>("return GetPlayerFacing()").unwrap() - 1.5).abs() < 1e-6);
|
||
let (px, py) = s
|
||
.eval::<(f64, f64)>(r#"return GetPlayerMapPosition("party1")"#)
|
||
.unwrap();
|
||
assert!(
|
||
(px - 0.1).abs() < 1e-6 && (py - 0.2).abs() < 1e-6,
|
||
"a party slot reads its own projection (B320)"
|
||
);
|
||
for token in ["party2", "party5", "party0", "raid1", "nonsense"] {
|
||
assert_eq!(
|
||
s.eval::<(f64, f64)>(&format!(r#"return GetPlayerMapPosition("{token}")"#))
|
||
.unwrap(),
|
||
(0.0, 0.0),
|
||
"{token} answers the off-map sentinel"
|
||
);
|
||
}
|
||
|
||
// No feed → the world sheet (never an error).
|
||
s.set_world_map_feed(None, None, 0.0, None, Vec::new(), Vec::new());
|
||
s.run("SetMapToCurrentZone()").unwrap();
|
||
assert_eq!(s.eval::<i64>("return GetCurrentMapContinent()").unwrap(), 0);
|
||
}
|
||
|
||
/// World-level ProcessMapClick picks the continent whose sheet-rect contains the click (the
|
||
/// 0x4a7100 AABB walk — the real kernel rects are disjoint); continent-level clicks resolve
|
||
/// through the 0x4a6ec0 zone grid; hover names ride the same cell law.
|
||
#[test]
|
||
fn worldmap_click_containment_and_zone_grid() {
|
||
let mut s = script();
|
||
push_catalog(&mut s);
|
||
|
||
// Mid-Kalimdor on the world sheet.
|
||
s.run("ProcessMapClick(0.25, 0.45)").unwrap();
|
||
assert_eq!(s.eval::<i64>("return GetCurrentMapContinent()").unwrap(), 1);
|
||
|
||
// Kalimdor ships no grid in this fixture: continent-level clicks are inert.
|
||
s.run("ProcessMapClick(0.5, 0.5)").unwrap();
|
||
assert_eq!(
|
||
s.eval::<(i64, i64)>("return GetCurrentMapContinent(), GetCurrentMapZone()")
|
||
.unwrap(),
|
||
(1, 0)
|
||
);
|
||
|
||
// Back at the world level, EK land picks continent 2.
|
||
s.run("SetMapZoom(0)").unwrap();
|
||
s.run("ProcessMapClick(0.72, 0.63)").unwrap();
|
||
assert_eq!(s.eval::<i64>("return GetCurrentMapContinent()").unwrap(), 2);
|
||
|
||
// Continent level: Goldshire's UV inside the EK rect lands on grid cell 12735 (the real
|
||
// Azeroth.zmp index for that world position) → zone 1. The zone lights up: name + fileName +
|
||
// the six geometry values (wow-re 15b2a8ea continent branch), all against the zone's loc rect
|
||
// within the continent. cont_w=35199.9, cont_h=23466.6; w=2000, h=1500.
|
||
let (name, file, tpx, tpy, tx, ty, sx, sy) = s
|
||
.eval::<(String, String, f64, f64, f64, f64, f64, f64)>(
|
||
"return UpdateMapHighlight(0.452843, 0.720880)",
|
||
)
|
||
.unwrap();
|
||
assert_eq!(name, "Elwynn Forest");
|
||
assert_eq!(
|
||
file, "Elwynn",
|
||
"fileName = the zone art folder → <file>Highlight"
|
||
);
|
||
assert_eq!(
|
||
tpx, 1.0,
|
||
"continent-branch texPercentageX is the constant 1.0"
|
||
);
|
||
assert!(
|
||
(tpy - 0.75).abs() < 1e-6,
|
||
"texPercentageY = dim/potdim = 96/128 = 0.75, got {tpy}"
|
||
);
|
||
assert!(
|
||
(tx - 2000.0 / 35199.9).abs() < 1e-4,
|
||
"textureX = w/contW, got {tx}"
|
||
);
|
||
assert!(
|
||
(ty - 1500.0 / 23466.6).abs() < 1e-4,
|
||
"textureY = h/contH, got {ty}"
|
||
);
|
||
assert!(
|
||
(sx - 24000.0 / 35199.9).abs() < 1e-4,
|
||
"scrollChildX = (contX0-zoneX0)/contW, got {sx}"
|
||
);
|
||
assert!(
|
||
(sy - 7866.6 / 23466.6).abs() < 1e-4,
|
||
"scrollChildY = (contY0-zoneY0)/contH, got {sy}"
|
||
);
|
||
// Ocean at continent level: off the grid → the nil/zero tail (frame hides the quad).
|
||
assert!(s
|
||
.eval::<Option<String>>("return UpdateMapHighlight(0.99, 0.01)")
|
||
.unwrap()
|
||
.is_none());
|
||
s.run("ProcessMapClick(0.452843, 0.720880)").unwrap();
|
||
assert_eq!(
|
||
s.eval::<(i64, i64)>("return GetCurrentMapContinent(), GetCurrentMapZone()")
|
||
.unwrap(),
|
||
(2, 1),
|
||
"the grid click drills into the zone"
|
||
);
|
||
|
||
// Now at the ZONE level (Elwynn): a click re-expressed through ELWYNN's own rect lands on
|
||
// cell 8801 = the Stormwind City peer child → drills into the city map. This is the path that
|
||
// was a no-op before (wow-re 15b2a8ea Part 2: continent- and zone-level clicks run identical
|
||
// code, only the windowing rect differs).
|
||
s.run("ProcessMapClick(0.5, 0.5)").unwrap();
|
||
assert_eq!(
|
||
s.eval::<(i64, i64)>("return GetCurrentMapContinent(), GetCurrentMapZone()")
|
||
.unwrap(),
|
||
(2, 2),
|
||
"a zone-level click on a city footprint drills into the city"
|
||
);
|
||
|
||
// Open ocean at the world level: outside both rects, no selection change.
|
||
s.run("SetMapZoom(0)").unwrap();
|
||
s.run("ProcessMapClick(0.01, 0.99)").unwrap();
|
||
assert_eq!(s.eval::<i64>("return GetCurrentMapContinent()").unwrap(), 0);
|
||
}
|
||
|
||
/// At ZONE level `UpdateMapHighlight` answers a NAME only (report B360; wow-re 15b2a8ea §1a/1b/1d):
|
||
/// a revealed overlay's sub-area when the cursor is inside its hit rect, else the neighbouring
|
||
/// zone or city whose grid cell the cursor is in through the DISPLAYED zone's rect window — never
|
||
/// the displayed zone itself — and always a nil fileName + six zeros, so the stock frame hides the
|
||
/// highlight quad. Fogged sub-areas have no name; an overlay with no resolvable first area is
|
||
/// walked past. The reference's own Duskwood shot: "Deadwind Pass" in the label, no highlight.
|
||
#[test]
|
||
fn worldmap_zone_level_hover_names_without_highlight() {
|
||
let mut s = script();
|
||
push_catalog(&mut s);
|
||
s.run("SetMapZoom(2, 1)").unwrap(); // the Elwynn zone map
|
||
|
||
type Answer = (Option<String>, Option<String>, f64, f64, f64, f64, f64, f64);
|
||
let hover = |s: &mut UiScript, x: f32, y: f32| {
|
||
s.eval::<Answer>(&format!("return UpdateMapHighlight({x}, {y})"))
|
||
.unwrap()
|
||
};
|
||
let name_only =
|
||
|name: &str| -> Answer { (Some(name.into()), None, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) };
|
||
let miss: Answer = (None, None, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
|
||
|
||
// Nothing explored. (0.5, 0.5) through Elwynn's rect is cell 8801 = the Stormwind City peer:
|
||
// the neighbouring child is NAMED, and nothing else — the B360 answer, Deadwind Pass from
|
||
// Duskwood in the report.
|
||
assert_eq!(hover(&mut s, 0.5, 0.5), name_only("Stormwind City"));
|
||
// Cell 9060 = Elwynn itself: the displayed zone never names itself.
|
||
assert_eq!(hover(&mut s, 0.82, 0.82), miss);
|
||
// An empty cell, no overlay: the tail.
|
||
assert_eq!(hover(&mut s, 0.2, 0.2), miss);
|
||
// Inside Goldshire's hit rect but fogged (bit 124 clear): no name — the pre-search walks the
|
||
// REVEALED list, and cell 8929 under it is empty.
|
||
assert_eq!(hover(&mut s, 0.42, 0.7), miss);
|
||
|
||
// Reveal Northshire (bit 125 = word 3, bit 29). Its rect contains (0.5, 0.5): the overlay
|
||
// pre-search runs BEFORE the grid, so the sub-area wins over the city under the same point.
|
||
let mut explored = vec![0u32; 64];
|
||
explored[3] = 1 << 29;
|
||
s.set_world_map_explored(explored.clone());
|
||
assert_eq!(hover(&mut s, 0.5, 0.5), name_only("Northshire Valley"));
|
||
assert_eq!(hover(&mut s, 0.42, 0.7), miss, "Goldshire is still fogged");
|
||
// Reveal Goldshire too (bit 124).
|
||
explored[3] |= 1 << 28;
|
||
s.set_world_map_explored(explored.clone());
|
||
assert_eq!(hover(&mut s, 0.42, 0.7), name_only("Goldshire"));
|
||
|
||
// Only the nameless overlay revealed (bit 126 = word 3, bit 30): its rect contains (0.5, 0.5)
|
||
// but its first area resolves to nothing, so the walk passes it and the grid answers.
|
||
let mut explored = vec![0u32; 64];
|
||
explored[3] = 1 << 30;
|
||
s.set_world_map_explored(explored.clone());
|
||
assert_eq!(hover(&mut s, 0.5, 0.5), name_only("Stormwind City"));
|
||
// Nameless first, Northshire second, both revealed: the walk continues to Northshire.
|
||
explored[3] |= 1 << 29;
|
||
s.set_world_map_explored(explored);
|
||
assert_eq!(hover(&mut s, 0.5, 0.5), name_only("Northshire Valley"));
|
||
|
||
// Control: back at the continent level the hovered zone still lights up — fileName is the art
|
||
// folder and the quad geometry is non-zero.
|
||
s.run("SetMapZoom(2)").unwrap();
|
||
let (name, file, tpx, _, tx, ty, _, _) = hover(&mut s, 0.452843, 0.720880);
|
||
assert_eq!(
|
||
(name.as_deref(), file.as_deref(), tpx),
|
||
(Some("Elwynn Forest"), Some("Elwynn"), 1.0)
|
||
);
|
||
assert!(tx > 0.0 && ty > 0.0);
|
||
}
|
||
|
||
/// SetMapZoom queues WORLD_MAP_UPDATE through the pending-event queue: nothing fires inside the
|
||
/// call (a binding can't re-enter dispatch), the registered frame hears it on the next tick.
|
||
#[test]
|
||
fn worldmap_update_event_fires_on_next_tick() {
|
||
let mut s = script();
|
||
push_catalog(&mut s);
|
||
// Drain the catalog push's own queued event first.
|
||
s.tick(0.01);
|
||
s.run(
|
||
r#"
|
||
heard = 0
|
||
f = CreateFrame("Frame", "MapListener")
|
||
f:RegisterEvent("WORLD_MAP_UPDATE")
|
||
f:SetScript("OnEvent", function() heard = heard + 1 end)
|
||
SetMapZoom(1)
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
assert_eq!(
|
||
s.eval::<i64>("return heard").unwrap(),
|
||
0,
|
||
"nothing fires inside the SetMapZoom call itself"
|
||
);
|
||
s.tick(0.01);
|
||
assert_eq!(s.eval::<i64>("return heard").unwrap(), 1);
|
||
s.tick(0.01);
|
||
assert_eq!(
|
||
s.eval::<i64>("return heard").unwrap(),
|
||
1,
|
||
"the queue drains — no re-fire"
|
||
);
|
||
}
|
||
|
||
/// The exploration fog: overlays reveal per the pushed bitset — none before any push, the
|
||
/// matching subset after (bit 125 = Northshire in the fixture), the full info tuple comes back,
|
||
/// and re-pushing the same bitset queues no extra repaint.
|
||
#[test]
|
||
fn worldmap_overlays_reveal_by_explored_bits() {
|
||
let mut s = script();
|
||
push_catalog(&mut s);
|
||
s.run("SetMapZoom(2, 1)").unwrap(); // the Elwynn zone map (EK's only fixture zone)
|
||
|
||
// Nothing explored: the zone shows no overlays (all parchment).
|
||
assert_eq!(s.eval::<i64>("return GetNumMapOverlays()").unwrap(), 0);
|
||
|
||
// Bit 125 set (word 3, bit 29): Northshire reveals, Goldshire stays fogged.
|
||
let mut explored = vec![0u32; 64];
|
||
explored[3] = 1 << 29;
|
||
s.set_world_map_explored(explored);
|
||
assert_eq!(s.eval::<i64>("return GetNumMapOverlays()").unwrap(), 1);
|
||
let (tex, w, h, ox, oy) = s
|
||
.eval::<(String, i64, i64, i64, i64)>("return GetMapOverlayInfo(1)")
|
||
.unwrap();
|
||
assert_eq!(tex, "Interface\\WorldMap\\Elwynn\\NORTHSHIREVALLEY");
|
||
assert_eq!((w, h, ox, oy), (200, 176, 490, 200));
|
||
|
||
// Both bits: both overlays, catalog order; out-of-range asks read nil.
|
||
let mut explored = vec![0u32; 64];
|
||
explored[3] = (1 << 29) | (1 << 28);
|
||
s.set_world_map_explored(explored);
|
||
assert_eq!(s.eval::<i64>("return GetNumMapOverlays()").unwrap(), 2);
|
||
assert!(s
|
||
.eval::<bool>("return GetMapOverlayInfo(3) == nil")
|
||
.unwrap());
|
||
|
||
// At the continent level the overlay family reads empty (fog is a zone-map thing).
|
||
s.run("SetMapZoom(2)").unwrap();
|
||
assert_eq!(s.eval::<i64>("return GetNumMapOverlays()").unwrap(), 0);
|
||
}
|