Files
benilla-octo/crates/benilla-formats/src/world_map_overlay.rs
T
SamandClaude Fable 5 2ad0043523 benilla: initial public release
A from-scratch World of Warcraft 1.12.1 client in Rust + Bevy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:27:01 +07:00

172 lines
6.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `WorldMapOverlay.dbc` — the per-area zoomed-in overlay art: for a `WorldMapArea` row, up to four
//! `AreaTable` sub-regions plus the texture (and its placement/hit-rect) that draws over the base
//! map when a sub-region is discovered or its highlight is hovered (decision 0203 phase 3's
//! discovery overlays + phase 2's `map_proj` overlay hit-test).
//!
//! Layout — VERIFIED against build 5875 (header + full 526-row decode, cross-checked against
//! vmangos's *commented-out* `WorldMapOverlayEntry` (`src/game/Database/DBCStructure.h:752` — the
//! struct only keeps `ID`/`areatableID[4]`, the rest is server-unused and left as index comments)
//! and independently against a community reference struct for the same build, 2026-07-07: **526 ×
//! 17 × 68 B**: `ID(0), WorldMapAreaID(1), AreaID[4](2-5), MapPointX(6), MapPointY(7),
//! TextureName(8, string), TextureWidth(9), TextureHeight(10), OffsetX(11), OffsetY(12),
//! HitRectTop(13), HitRectLeft(14), HitRectBottom(15), HitRectRight(16)`. Sanity-checked over all
//! 526 rows: `HitRectTop < HitRectBottom` holds for 525/526, `HitRectLeft < HitRectRight` holds for
//! all 526, and `TextureWidth`/`TextureHeight` track the hit-rect span's rough magnitude — all
//! consistent with the field identities above. `MapPointX`/`MapPointY` are `0` on **every** row in
//! this data (matches vmangos's own `// to implement [?]` note on the struct: authored but unused
//! in 1.12).
use std::collections::HashMap;
use anyhow::{Context, Result};
use benilla_dbc::{FieldType, Schema, SchemaField};
use crate::dbc::{parse, str_at, u32_at};
use crate::Chain;
const WORLD_MAP_OVERLAY: &str = "DBFilesClient\\WorldMapOverlay.dbc";
/// One `WorldMapOverlay.dbc` row: a sub-region's overlay texture + placement.
#[derive(Clone, Debug)]
pub struct WorldMapOverlay {
/// `WorldMapArea.dbc` id this overlay draws over.
pub world_map_area_id: u32,
/// Up to 4 `AreaTable.dbc` ids this overlay's highlight covers (`0` = unused slot).
pub area_id: [u32; 4],
/// Always `0` in 5875 — an authored-but-unused map-point (see the module doc).
pub map_point_x: u32,
pub map_point_y: u32,
/// The overlay texture's base name (no path/extension) under
/// `Interface\WorldMap\<WorldMapArea name>\`.
pub texture_name: String,
pub texture_width: u32,
pub texture_height: u32,
pub offset_x: u32,
pub offset_y: u32,
pub hit_rect_top: u32,
pub hit_rect_left: u32,
pub hit_rect_bottom: u32,
pub hit_rect_right: u32,
}
/// `WorldMapOverlay.dbc` rows grouped by `WorldMapAreaID` — the join key against
/// [`crate::WorldMapAreaCatalog`].
pub struct WorldMapOverlayCatalog {
by_area: HashMap<u32, Vec<WorldMapOverlay>>,
}
impl WorldMapOverlayCatalog {
/// The overlays for `world_map_area_id`, or an empty slice if none.
pub fn for_area(&self, world_map_area_id: u32) -> &[WorldMapOverlay] {
self.by_area
.get(&world_map_area_id)
.map(Vec::as_slice)
.unwrap_or(&[])
}
pub fn len(&self) -> usize {
self.by_area.values().map(Vec::len).sum()
}
pub fn is_empty(&self) -> bool {
self.by_area.is_empty()
}
}
/// 17 fields per the module doc.
fn schema() -> Schema {
let mut s = Schema::new("WorldMapOverlay");
for name in ["ID", "WorldMapAreaID"] {
s.add_field(SchemaField::new(name, FieldType::UInt32));
}
s.add_field(SchemaField::new_array("AreaID", FieldType::UInt32, 4));
for name in ["MapPointX", "MapPointY"] {
s.add_field(SchemaField::new(name, FieldType::UInt32));
}
s.add_field(SchemaField::new("TextureName", FieldType::String));
for name in [
"TextureWidth",
"TextureHeight",
"OffsetX",
"OffsetY",
"HitRectTop",
"HitRectLeft",
"HitRectBottom",
"HitRectRight",
] {
s.add_field(SchemaField::new(name, FieldType::UInt32));
}
s
}
/// Read `WorldMapOverlay.dbc` off the patch chain into a [`WorldMapOverlayCatalog`].
pub fn load_world_map_overlay_catalog(chain: &mut Chain) -> Result<WorldMapOverlayCatalog> {
let bytes = chain
.read_file(WORLD_MAP_OVERLAY)
.context("reading WorldMapOverlay.dbc")?;
let rs = parse(&bytes, schema(), "WorldMapOverlay")?;
let mut by_area: HashMap<u32, Vec<WorldMapOverlay>> = HashMap::new();
for r in rs.records() {
let Some(world_map_area_id) = u32_at(r, 1) else {
continue;
};
let area_id = [
u32_at(r, 2).unwrap_or(0),
u32_at(r, 3).unwrap_or(0),
u32_at(r, 4).unwrap_or(0),
u32_at(r, 5).unwrap_or(0),
];
let overlay = WorldMapOverlay {
world_map_area_id,
area_id,
map_point_x: u32_at(r, 6).unwrap_or(0),
map_point_y: u32_at(r, 7).unwrap_or(0),
texture_name: str_at(&rs, r, 8).unwrap_or_default(),
texture_width: u32_at(r, 9).unwrap_or(0),
texture_height: u32_at(r, 10).unwrap_or(0),
offset_x: u32_at(r, 11).unwrap_or(0),
offset_y: u32_at(r, 12).unwrap_or(0),
hit_rect_top: u32_at(r, 13).unwrap_or(0),
hit_rect_left: u32_at(r, 14).unwrap_or(0),
hit_rect_bottom: u32_at(r, 15).unwrap_or(0),
hit_rect_right: u32_at(r, 16).unwrap_or(0),
};
by_area.entry(world_map_area_id).or_default().push(overlay);
}
Ok(WorldMapOverlayCatalog { by_area })
}
#[cfg(test)]
mod tests {
use super::*;
/// The real 5875 table: Teldrassil's `WorldMapArea` id (41) carries overlays with non-empty
/// texture names (e.g. the "Bane Thillhollow"/"Darnassus" sub-zone highlights). Skips without
/// client data.
#[test]
fn real_world_map_overlay_has_nonempty_textures_for_teldrassil() {
let data = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../WoW/Data");
if !data.is_dir() {
eprintln!("skipping: vanilla client not present at {}", data.display());
return;
}
let mut chain = crate::open_chain(&data).expect("open chain");
let cat = load_world_map_overlay_catalog(&mut chain).expect("load WorldMapOverlay");
assert_eq!(cat.len(), 526, "all 526 rows load");
let teldrassil = cat.for_area(41);
assert!(
!teldrassil.is_empty(),
"Teldrassil (WorldMapArea 41) has overlay rows"
);
assert!(
teldrassil.iter().all(|o| !o.texture_name.is_empty()),
"every overlay row names a texture"
);
assert!(
teldrassil.iter().any(|o| o.texture_name == "DARNASSUS"),
"the Darnassus sub-zone highlight is present"
);
}
}