Files
benilla-octo/crates/benilla-formats/src/world_map_continent.rs
T
SamandClaude Fable 5 bfb3e3addc the player build: one binary that carries its content
- a player build ships beside the dev one — dev chords and probe surfaces go dark, and a gate enforces what a player can still reach, by medium not by module
- the binary carries its own shaders and its shipped UI, finds the WoW folder by itself, and keeps its local state in one benilla-config folder
- the glue screens get a capture shutter, and the engine stops depending on the protocol crate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:18:02 +07:00

191 lines
6.8 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.
//! `WorldMapContinent.dbc` — one row per continent (Azeroth, Kalimdor) framing the continent-level
//! map art: which ADT tile-grid span it covers, its placement offset/scale in the shared world-map
//! coordinate space, and the taxi-map's own bounding rect (decision 0203 phase 2's continent view).
//!
//! **Client-only: vmangos carries no struct for this table** (never loaded server-side). Layout
//! **VERIFIED against the 5875 binary** (wow-re Q2 verdict, 2026-07-07, recorded in
//! `system/ui/scratch/geometry.md` "Worldmap data model"): **2 × 13 × 52 B** —
//! `ID(0), MapID(1)`; fields 2-5 = ADT tile bounds (ints; the world-map builder `0x4a5d00`
//! converts them to each continent's normalized-UV sheet rect — the world-level click AABB;
//! `map_proj::continent_sheet_rect` transcribes the kernel); fields 6/7 = the per-axis
//! world-sheet offsets and field 8 = the scale, exactly the values `0x4a72b0`/`0x4a7360`
//! world-mode read at record `+0x18/+0x1c/+0x20` (5875: EK `{14.5, −7.0, 0.75}`, Kalimdor
//! `{−19.0, −0.32249799, 0.75}`); fields 9-12 = the taxi-map world rect, **unused** by all four
//! projection functions.
use std::collections::HashMap;
use anyhow::{Context, Result};
use benilla_dbc::{FieldType, Schema, SchemaField};
use crate::dbc::{f32_at, parse, u32_at};
use crate::Chain;
const WORLD_MAP_CONTINENT: &str = "DBFilesClient\\WorldMapContinent.dbc";
/// One `WorldMapContinent.dbc` row (all fields decomp-verified — see the module doc).
#[derive(Clone, Debug)]
pub struct WorldMapContinent {
/// `Map.dbc` id (`0` = Azeroth, `1` = Kalimdor in 5875).
pub map_id: u32,
/// ADT tile-grid column bounds — the `0x4a5d00` sheet-rect kernel's inputs (see module doc).
pub left_boundary: u32,
pub right_boundary: u32,
/// ADT tile-grid row bounds.
pub top_boundary: u32,
pub bottom_boundary: u32,
/// The continent's world-sheet offsets — the `f` terms of the world-mode projections
/// (`0x4a7360`: `u = offset_x/62.625 + 0.5 − wy·k·scale`), x → the u axis, y → v.
pub offset_x: f32,
pub offset_y: f32,
/// The world-sheet scale the projections multiply by (`0.75` on both 5875 rows — there is
/// no separate world-level zoom variable).
pub scale: f32,
/// The taxi-map's own bounding rect in world units — carried for the future taxi layer;
/// unused by the map projections.
pub taxi_min: (f32, f32),
pub taxi_max: (f32, f32),
}
/// `WorldMapContinent.dbc` rows keyed by `MapID`.
pub struct WorldMapContinentCatalog {
by_map_id: HashMap<u32, WorldMapContinent>,
}
impl WorldMapContinentCatalog {
pub fn get(&self, map_id: u32) -> Option<&WorldMapContinent> {
self.by_map_id.get(&map_id)
}
pub fn len(&self) -> usize {
self.by_map_id.len()
}
pub fn is_empty(&self) -> bool {
self.by_map_id.is_empty()
}
}
/// 13 fields: `ID, MapID, LeftBoundary, RightBoundary, TopBoundary, BottomBoundary, OffsetX,
/// OffsetY, Scale, TaxiMinX, TaxiMinY, TaxiMaxX, TaxiMaxY` (see the module doc for confidence).
fn schema() -> Schema {
let mut s = Schema::new("WorldMapContinent");
for name in [
"ID",
"MapID",
"LeftBoundary",
"RightBoundary",
"TopBoundary",
"BottomBoundary",
] {
s.add_field(SchemaField::new(name, FieldType::UInt32));
}
for name in [
"OffsetX", "OffsetY", "Scale", "TaxiMinX", "TaxiMinY", "TaxiMaxX", "TaxiMaxY",
] {
s.add_field(SchemaField::new(name, FieldType::Float32));
}
s
}
/// Read `WorldMapContinent.dbc` off the patch chain into a [`WorldMapContinentCatalog`].
pub fn load_world_map_continent_catalog(chain: &mut Chain) -> Result<WorldMapContinentCatalog> {
let bytes = chain
.read_file(WORLD_MAP_CONTINENT)
.context("reading WorldMapContinent.dbc")?;
let rs = parse(&bytes, schema(), "WorldMapContinent")?;
let mut by_map_id = HashMap::with_capacity(rs.records().len());
for r in rs.records() {
let Some(map_id) = u32_at(r, 1) else { continue };
let (
Some(left_boundary),
Some(right_boundary),
Some(top_boundary),
Some(bottom_boundary),
Some(offset_x),
Some(offset_y),
Some(scale),
Some(taxi_min_x),
Some(taxi_min_y),
Some(taxi_max_x),
Some(taxi_max_y),
) = (
u32_at(r, 2),
u32_at(r, 3),
u32_at(r, 4),
u32_at(r, 5),
f32_at(r, 6),
f32_at(r, 7),
f32_at(r, 8),
f32_at(r, 9),
f32_at(r, 10),
f32_at(r, 11),
f32_at(r, 12),
)
else {
continue;
};
by_map_id.insert(
map_id,
WorldMapContinent {
map_id,
left_boundary,
right_boundary,
top_boundary,
bottom_boundary,
offset_x,
offset_y,
scale,
taxi_min: (taxi_min_x, taxi_min_y),
taxi_max: (taxi_max_x, taxi_max_y),
},
);
}
Ok(WorldMapContinentCatalog { by_map_id })
}
#[cfg(test)]
mod tests {
use super::*;
/// The real 5875 table: exactly the two verified rows (Azeroth mapId 0, Kalimdor mapId 1),
/// byte-exact on every field. Skips without client data.
#[test]
fn real_world_map_continent_has_azeroth_and_kalimdor() {
let data = crate::wow_data_or_skip!();
let mut chain = crate::open_chain(&data).expect("open chain");
let cat = load_world_map_continent_catalog(&mut chain).expect("load WorldMapContinent");
assert_eq!(cat.len(), 2);
let azeroth = cat.get(0).expect("Azeroth (mapId 0)");
assert_eq!(
(
azeroth.left_boundary,
azeroth.right_boundary,
azeroth.top_boundary,
azeroth.bottom_boundary
),
(23, 47, 15, 61)
);
assert!((azeroth.offset_x - 14.5).abs() < 0.01);
assert!((azeroth.offset_y - (-7.0)).abs() < 0.01);
assert!((azeroth.scale - 0.75).abs() < 0.001);
assert!((azeroth.taxi_min.0 - (-15980.0)).abs() < 0.1);
assert!((azeroth.taxi_min.1 - (-11880.0)).abs() < 0.1);
assert!((azeroth.taxi_max.0 - 5817.0).abs() < 0.1);
assert!((azeroth.taxi_max.1 - 9917.0).abs() < 0.1);
let kalimdor = cat.get(1).expect("Kalimdor (mapId 1)");
assert_eq!(
(
kalimdor.left_boundary,
kalimdor.right_boundary,
kalimdor.top_boundary,
kalimdor.bottom_boundary
),
(23, 48, 9, 52)
);
assert!((kalimdor.scale - 0.75).abs() < 0.001);
}
}