Instances built as one WMO take their zone from Map.dbc (#1670)

In the 17 instances built as one WMO (Wailing Caverns, Blackrock Depths, Molten Core, Dire Maul, the Deeprun Tram and others) benilla had no area: entering by portal kept the outside zone, no `ZONE_CHANGED_NEW_AREA` fired, and logging in inside left the zone texts empty.

On a map whose WDT carries MPHD bit 0 (`0x694904`), 1.12.1's area query `0x6703a0` answers the map's `Map.dbc` field 19 for every entity, ahead of any WMO or terrain read; a 0 there fails the query.

`MapCatalog` now reads field 19, the WDT exposes the bit, and `update_current_area` takes that leg first on such a map. A worldport also drops the old map's held area, which otherwise survived the transfer. Tests cover the leg, the flag's map, a zero field and the worldport; a live portal into Wailing Caverns fired `ZONE_CHANGED_NEW_AREA` with "Wailing Caverns".

Fixes #1636

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-17 00:14:47 +07:00
committed by GitHub
parent 87f3b24f68
commit cda3a91f7b
9 changed files with 303 additions and 13 deletions
+4
View File
@@ -5,6 +5,10 @@
//! - A zone id change fires `ZONE_CHANGED_NEW_AREA` alone; else a text change fires
//! `ZONE_CHANGED_INDOORS` or `ZONE_CHANGED` by the indoor bit. The first world-enter is a zone
//! id change from the zeroed cache.
//! - The leaf is the area query's answer (`0x6703a0`): on a map whose WDT holds one global WMO,
//! `Map.dbc` field 19 for every entity; elsewhere the down-ray's WMO row, else the terrain. The
//! indoor naming below still applies on such a map, as `0x67e510` takes the indoor arm by the
//! indoor bit alone.
//! - The zone is the leaf's single-hop parent (AreaTable field 2, itself when 0).
//! - The indoor bit is the faces-only down-ray (`0x6a87f0`): the surface below is a WMO face whose
//! group lacks MOGP `0x8` EXTERIOR.
+5
View File
@@ -20,6 +20,11 @@ impl WdtIndex {
.is_some_and(|t| t.has_adt)
}
/// `MPHD` bit 0, the client's map-is-one-object flag (`[0xc9e384]`).
pub fn is_one_object(&self) -> bool {
self.0.is_one_object()
}
/// The single global building of a map with no terrain (`MPHD` bit 0); `None` on an ADT map.
pub fn global_wmo(&self) -> Option<&GlobalWmo> {
self.0.global_wmo()
+60
View File
@@ -28,6 +28,9 @@ pub struct MapCatalog {
instance_types: HashMap<u32, u32>,
/// The battleground columns of every row, since the client resolves map 0's too.
battleground: HashMap<u32, MapBattlegroundColumns>,
/// Field 19 (`+0x4c`), the `AreaTable` id of a map built as one WMO, of every row; 0 on the
/// ordinary maps (`0x6703d1`).
area_table_ids: HashMap<u32, u32>,
}
/// The `Map.dbc` columns the battleground verbs read by row offset (`GetBattlefieldInfo`
@@ -71,6 +74,23 @@ impl MapBattlegroundColumns {
}
impl MapCatalog {
/// A catalog of `(id, directory, AreaTableID)` rows, for tests that run without an install.
pub fn from_rows<'a>(rows: impl IntoIterator<Item = (u32, &'a str, u32)>) -> Self {
let mut c = Self {
dirs: HashMap::new(),
names: HashMap::new(),
loading_screens: HashMap::new(),
instance_types: HashMap::new(),
battleground: HashMap::new(),
area_table_ids: HashMap::new(),
};
for (id, dir, area) in rows {
c.dirs.insert(id, dir.to_string());
c.area_table_ids.insert(id, area);
}
c
}
/// The MPQ directory name (`Directory`) for `map_id`, as `MapTiles::load(chain, dir)` takes it.
pub fn directory(&self, map_id: u32) -> Option<&str> {
self.dirs.get(&map_id).map(String::as_str)
@@ -102,6 +122,13 @@ impl MapCatalog {
self.battleground.get(&map_id)
}
/// Field 19 (`+0x4c`) for `map_id`: the area the whole map answers when its WDT holds one
/// global WMO (`0x6703a0` reads it at `0x6703d1`, and a 0 fails the query). `None` only for an
/// id with no row; the ordinary maps carry 0.
pub fn area_table_id(&self, map_id: u32) -> Option<u32> {
self.area_table_ids.get(&map_id).copied()
}
pub fn len(&self) -> usize {
self.dirs.len()
}
@@ -121,6 +148,7 @@ const MAX_PLAYERS_FIELD: usize = 15;
const FIELD_16: usize = 16;
const FIELD_17: usize = 17;
const FIELD_18: usize = 18;
const AREA_TABLE_FIELD: usize = 19;
const DESCRIPTION_0_FIELD: usize = 20;
const DESCRIPTION_1_FIELD: usize = 29;
const BRACKET_SPAN_FIELD: usize = 39;
@@ -148,6 +176,7 @@ fn map_schema() -> Schema {
LOADING_SCREEN_FIELD => {
s.add_field(SchemaField::new("LoadingScreenID", FieldType::UInt32))
}
AREA_TABLE_FIELD => s.add_field(SchemaField::new("AreaTableID", FieldType::UInt32)),
_ => s.add_field(SchemaField::new(format!("_pad{i}"), FieldType::UInt32)),
}
}
@@ -166,6 +195,7 @@ pub fn load_map_catalog(chain: &mut Chain) -> Result<MapCatalog> {
let mut loading_screens = HashMap::new();
let mut instance_types = HashMap::with_capacity(rs.records().len());
let mut battleground = HashMap::with_capacity(rs.records().len());
let mut area_table_ids = HashMap::with_capacity(rs.records().len());
for r in rs.records() {
let Some(id) = u32_at(r, 0) else { continue };
battleground.insert(
@@ -186,6 +216,7 @@ pub fn load_map_catalog(chain: &mut Chain) -> Result<MapCatalog> {
minimap_icon_scale: f32_at(r, MINIMAP_ICON_SCALE_FIELD).unwrap_or(1.0),
},
);
area_table_ids.insert(id, u32_at(r, AREA_TABLE_FIELD).unwrap_or(0));
if let Some(dir) = str_at(&rs, r, 1) {
dirs.insert(id, dir);
}
@@ -206,6 +237,7 @@ pub fn load_map_catalog(chain: &mut Chain) -> Result<MapCatalog> {
loading_screens,
instance_types,
battleground,
area_table_ids,
})
}
@@ -255,4 +287,32 @@ mod tests {
"every row carries the columns — the client resolves map 0's when nothing was listed"
);
}
/// Field 19 (`+0x4c`) is a one-object map's whole area (`0x6703d1`); a continent carries 0.
/// The query reads it only where the WDT sets `MPHD` bit 0, and Zul'Gurub and Blackwing Lair
/// carry the field without the flag.
#[test]
fn field_19_is_the_area_of_a_map_built_as_one_object() {
let data = crate::wow_data_or_skip!();
let mut chain = crate::open_chain(&data).expect("open chain");
let catalog = load_map_catalog(&mut chain).expect("Map.dbc");
assert_eq!(catalog.area_table_id(43), Some(718), "Wailing Caverns");
assert_eq!(catalog.area_table_id(230), Some(1584), "Blackrock Depths");
assert_eq!(catalog.area_table_id(0), Some(0), "Eastern Kingdoms");
assert_eq!(catalog.area_table_id(1), Some(0), "Kalimdor");
assert_eq!(catalog.area_table_id(9999), None);
let mut flag = |map: u32| {
let dir = catalog.directory(map).expect("row").to_string();
let bytes = chain
.read_file(&format!("World\\Maps\\{dir}\\{dir}.wdt"))
.expect("wdt");
crate::WdtReader::new(std::io::Cursor::new(bytes), crate::WowVersion::Classic)
.read()
.expect("parses")
.is_one_object()
};
assert!(flag(43), "Wailing Caverns");
assert!(!flag(0) && !flag(1));
assert!(!flag(309), "Zul'Gurub has a field 19 and terrain");
}
}
+17 -2
View File
@@ -25,8 +25,9 @@ pub struct WmoArea {
pub zone_music: u32,
/// `ZoneIntroMusicTable.dbc` id, the entry fanfare.
pub intro_sound: u32,
/// The `AreaTable.dbc` area this group counts as (0 = none), read by the area resolver
/// `0x670250` when the down-ray keeps the WMO over the terrain: Ironforge reports its own area.
/// The `AreaTable.dbc` area this group counts as (0 = none), read by the area query
/// `0x6703a0` (`0x670437`) when the down-ray keeps the WMO over the terrain: Ironforge reports
/// its own area.
pub area_table_id: u32,
/// The interior's display name in the locale's column, empty for an unnamed row.
pub name: String,
@@ -41,6 +42,20 @@ pub struct WmoAreaCatalog {
}
impl WmoAreaCatalog {
/// A catalog of `(WMOID, NameSetID, WMOGroupID, row)` rows, `u32::MAX` the whole-WMO row, for
/// tests that run without an install.
pub fn from_rows(rows: impl IntoIterator<Item = (u32, u32, u32, WmoArea)>) -> Self {
let (mut groups, mut defaults) = (HashMap::new(), HashMap::new());
for (wmo_id, name_set, group, area) in rows {
if group == u32::MAX {
defaults.insert((wmo_id, name_set), area);
} else {
groups.insert((wmo_id, name_set, group), area);
}
}
Self { groups, defaults }
}
/// The hit group's own row by exact key, the zone-text chain's second query (`0x69d8f0`, one
/// bsearch at `0x7ccd30` with no name-set retry, default fallback or overlay): its name, if
/// any, refills the subzone slot.
+14
View File
@@ -88,6 +88,9 @@ pub struct WdtFile {
/// `has_adt` per tile, indexed `y * 64 + x` (the on-disk MAIN order).
has_adt: Vec<bool>,
global_wmo: Option<GlobalWmo>,
/// `MPHD` dword 0 bit 0 alone: the client's map-is-one-object flag (`[0xc9e384]`, `0x694810`
/// to `0x694904`), set with or without a usable placement.
one_object: bool,
}
impl WdtFile {
@@ -102,6 +105,12 @@ impl WdtFile {
})
}
/// Whether `MPHD` bit 0 is set: the flag the client's area query tests (`0x6703a4`), which
/// makes the map's `Map.dbc` area the answer for every entity.
pub fn is_one_object(&self) -> bool {
self.one_object
}
/// The map's one building, on a WMO-only map (`MPHD` bit 0).
pub fn global_wmo(&self) -> Option<&GlobalWmo> {
self.global_wmo.as_ref()
@@ -199,6 +208,7 @@ impl<R: Read + Seek> WdtReader<R> {
std::io::Error::new(std::io::ErrorKind::InvalidData, "WDT missing MAIN chunk")
})?;
// The flag and a placement, or no global WMO: a stray header bit must not hide the grid.
let one_object = wmo_only;
let global_wmo = match (wmo_only, wmo_path, modf) {
(true, Some(model), Some(e)) => Some(GlobalWmo {
model,
@@ -214,6 +224,7 @@ impl<R: Read + Seek> WdtReader<R> {
Ok(WdtFile {
has_adt,
global_wmo,
one_object,
})
}
}
@@ -342,9 +353,12 @@ mod tests {
// Nor a file with the bit but no placement,
let half = parse(synth_wmo_only_wdt(true, false)).expect("parses");
assert!(half.global_wmo().is_none());
assert!(half.is_one_object(), "the client's flag is the bit alone");
// nor one with the placement but no bit.
let unflagged = parse(synth_wmo_only_wdt(false, true)).expect("parses");
assert!(unflagged.global_wmo().is_none());
assert!(!unflagged.is_one_object());
assert!(!wdt.is_one_object());
}
#[test]
@@ -75,6 +75,8 @@ pub struct TerrainStreamer {
wdt_ungated: bool,
/// This WMO-only map's one building is registered as a placement.
global_wmo: bool,
/// The loaded WDT sets `MPHD` bit 0, the client's map-is-one-object flag (`[0xc9e384]`).
one_object: bool,
/// [`ViewFocus::paced`] last frame: the residency latch re-arms on the edge into a load, since
/// a viewer with no avatar is unpaced forever and the level would re-arm it every frame.
was_paced: bool,
@@ -90,6 +92,12 @@ pub struct TerrainStreamer {
const GLOBAL_WMO_UID: u32 = u32::MAX;
impl TerrainStreamer {
/// Whether the loaded map, whose `Map.dbc` directory is `dir`, has its WDT's one-object flag
/// set; false until that WDT has loaded.
pub(crate) fn is_one_object_map(&self, dir: &str) -> bool {
self.one_object && self.map_dir.as_deref() == Some(dir)
}
/// `(spawned, requested)` tiles for the debug panel: furnished, and all in the window.
pub fn residency(&self) -> (usize, usize) {
let spawned = self.tiles.values().filter(|t| t.furnished).count();
@@ -381,6 +389,7 @@ impl Plugin for TerrainPlugin {
.init_resource::<StaticMerge>()
.init_resource::<crate::model_forms::ModelForms>()
.init_resource::<CurrentArea>()
.init_resource::<queries::AreaMap>()
// In `WorldStage::Stream`, between the teleport snap (Input) and the loading cover
// (Present), so a swap never renders uncovered. `finish_colliders` heads the chain so
// the collider queue read downstream is this frame's.
@@ -515,6 +524,10 @@ fn stream_terrain(
}
// A failed WDT falls back to ungated probing, warned once per map, never to no world.
let wdt_index = state.wdt.as_ref().and_then(|h| wdts.get(h));
let one_object = wdt_index.is_some_and(|w| w.is_one_object());
if state.one_object != one_object {
state.one_object = one_object;
}
if wdt_index.is_none() && !state.wdt_ungated {
if let Some(h) = &state.wdt {
if matches!(
@@ -980,6 +993,7 @@ fn drop_streamed_world(
if std::mem::take(&mut state.global_wmo) {
release_placement(commands, placements, GLOBAL_WMO_UID, activity);
}
state.one_object = false;
placements.materials.clear();
// Cleared here, not by the flush's dead-key discard: tile keys repeat across maps, and the
// new map's tiles are inserted this same frame. The same holds for the merge accumulators,
@@ -14,6 +14,10 @@ use super::TerrainStreamer;
#[derive(Resource, Default, PartialEq, Eq)]
pub struct CurrentArea(pub Option<u32>);
/// The map [`CurrentArea`] was last answered on, so a worldport drops the old map's zone.
#[derive(Resource, Default)]
pub(super) struct AreaMap(Option<u32>);
/// The set of [`update_current_area`]. Every consumer that acts on the area orders after it, so
/// leaf, indoor bit and names come from one frame (the reference resolves them in one pass).
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -46,8 +50,9 @@ pub fn doodad_ground_shade(
}
}
/// The MCNK `areaId` under a Bevy-space position: the outdoor leg of the reference's `GetAreaID`
/// (`0x670250`), for any unit, with no interior claim. An `areaId` of 0 (unassigned) is a miss.
/// The MCNK `areaId` under a Bevy-space position: the outdoor leg of the reference's area query
/// (`0x6703a0` → `0x69b120`), for any unit, with no interior claim. An `areaId` of 0 (unassigned)
/// is a miss.
pub fn area_id_under(
streamer: &TerrainStreamer,
adt_tiles: &Assets<AdtTile>,
@@ -74,7 +79,7 @@ pub fn ground_effect_under(
}
/// The terrain height (WoW `z`) under a Bevy-space position: the terrain leg of the reference's
/// down-ray arbitration (`0x6821f0`'s `0x69c320` probe, also in `GetAreaID` `0x670250`), which
/// down-ray arbitration (`0x6821f0`'s `0x69c320` probe, also in the unreached point query `0x670250`), which
/// wins the column when the ground is strictly nearer than the WMO. `None` is no terrain in the
/// column (off the ring, decoding, or an MCNK hole over a cave mouth), never ground at 0.
pub fn terrain_height_under(
@@ -107,10 +112,14 @@ pub fn terrain_height_under_cached<'a>(
benilla_formats::terrain_height_at(&adt.chunks, wow)
}
/// Tracks the `AreaTable` id under the player's feet as the reference's `GetAreaID` (`0x670250`)
/// does: a WMO interior wins when the faces-only down-ray (`0x6a8a20`) finds it nearer, by its
/// `WMOAreaTable.AreaTableID`; outdoors it is the chunk's MCNK `areaId`. Holds the last value
/// while a tile decodes.
/// Tracks the `AreaTable` id under the player's feet as the reference's area query (`0x6703a0`)
/// does. On a map whose WDT sets the one-object flag (`MPHD` bit 0, `[0xc9e384]`) it is that map's
/// `Map.dbc` field 19 for any entity, read ahead of every other leg (`0x6703a4` to `0x6703d1`),
/// and a 0 there fails the query rather than falling through. Elsewhere a WMO interior wins when
/// the faces-only down-ray (`0x6a8a20`) finds it nearer, by its `WMOAreaTable.AreaTableID`;
/// outdoors it is the chunk's MCNK `areaId`. Within a map it holds the last value while a tile
/// decodes, and on a failed query (the zone caches keep their state in the reference, `0x67e54d`);
/// a worldport drops it, since it is the old map's.
pub(super) fn update_current_area(
mut area: ResMut<CurrentArea>,
focus: Res<crate::terrain_stream::ViewFocus>,
@@ -118,6 +127,9 @@ pub(super) fn update_current_area(
adt_tiles: Res<Assets<AdtTile>>,
interior: Res<crate::wmo_portal::CurrentAreaInterior>,
wmo_areas: Option<Res<crate::wmo_portal::WmoAreas>>,
mut area_map: ResMut<AreaMap>,
current_map: Option<Res<crate::world_map::CurrentMap>>,
maps: Option<Res<benilla_assets::MapCatalogRes>>,
) {
let Some(wow) = focus.body_pos() else {
// No avatar: the area dies with the character session, or the next login reads this
@@ -128,11 +140,30 @@ pub(super) fn update_current_area(
}
return;
};
// The held answer is the previous map's after a worldport: the reference resolves each entity
// on its own map (`0x6703a0`), so no zone of the old map is ever read on the new one.
let map_id = focus.map(current_map.as_ref().map(|m| m.0));
if area_map.0.replace(map_id).is_some_and(|m| m != map_id) && area.0.is_some() {
*area = CurrentArea(None);
}
if !focus.body_settled() {
// A body whose world is still arriving holds the last answer: the reference is behind a
// loading screen for this window.
return;
}
// A one-object map answers its own `Map.dbc` area, with no WMO row and no terrain read.
let one_object_area = maps.as_ref().and_then(|maps| {
let dir = maps.0.directory(map_id)?;
streamer
.is_one_object_map(dir)
.then(|| maps.0.area_table_id(map_id).unwrap_or(0))
});
if let Some(id) = one_object_area {
if id != 0 && *area != CurrentArea(Some(id)) {
*area = CurrentArea(Some(id));
}
return;
}
// WMO interior first: the player's down-ray group resolved to its WMOAreaTable world area.
let interior_area = interior.0.zip(wmo_areas.as_ref()).and_then(|(k, cat)| {
cat.0
@@ -164,6 +195,7 @@ mod tests {
fn bare_world() -> World {
let mut w = World::new();
w.init_resource::<CurrentArea>();
w.init_resource::<AreaMap>();
w.init_resource::<crate::terrain_stream::ViewFocus>();
w.init_resource::<TerrainStreamer>();
w.init_resource::<Assets<AdtTile>>();
@@ -220,4 +252,149 @@ mod tests {
w.run_system_once(update_current_area).unwrap();
assert_eq!(w.resource::<CurrentArea>().0, Some(12));
}
const WAILING_CAVERNS: u32 = 43;
const WAILING_CAVERNS_AREA: u32 = 718;
/// A world on `map` with the catalog rows `rows`, a settled body in the open, and a streamer
/// that has loaded `dir`'s WDT with the one-object flag as `one_object` says.
fn map_world(map: u32, rows: &[(u32, &str, u32)], dir: &str, one_object: bool) -> World {
let mut w = bare_world();
w.insert_resource(crate::world_map::CurrentMap(map));
w.insert_resource(benilla_assets::MapCatalogRes(
benilla_formats::MapCatalog::from_rows(rows.iter().copied()),
));
w.insert_resource(crate::terrain_stream::ViewFocus::body(
[-10.0, 20.0, 30.0],
true,
));
let mut streamer = w.resource_mut::<TerrainStreamer>();
streamer.map_dir = Some(dir.to_string());
streamer.one_object = one_object;
w
}
fn wc_rows() -> [(u32, &'static str, u32); 2] {
[
(WAILING_CAVERNS, "WailingCaverns", WAILING_CAVERNS_AREA),
(1, "Kalimdor", 0),
]
}
/// `0x6703a0` answers the map record's field 19 for a one-object map (`0x6703a4`, `0x6703d1`),
/// with no WMO row and no terrain read: here the down-ray claims a group whose row names
/// another area, and no tile is resident.
#[test]
fn a_one_object_map_answers_its_map_record_area() {
let mut w = map_world(WAILING_CAVERNS, &wc_rows(), "WailingCaverns", true);
let keys = crate::wmo_portal::WmoInteriorKeys {
wmo_id: 7,
name_set: 0,
group_area_id: 9,
};
w.insert_resource(crate::wmo_portal::CurrentAreaInterior(Some(keys)));
w.insert_resource(crate::wmo_portal::WmoAreas(
benilla_formats::WmoAreaCatalog::from_rows([(
7,
0,
9,
benilla_formats::WmoArea {
id: 1,
sound_provider: [0; 2],
ambience: 0,
zone_music: 0,
intro_sound: 0,
area_table_id: 1519,
name: String::new(),
},
)]),
));
// Login inside: no earlier area.
w.run_system_once(update_current_area).unwrap();
assert_eq!(
w.resource::<CurrentArea>().0,
Some(WAILING_CAVERNS_AREA),
"field 19, not the group row's 1519"
);
// Entry by portal: the outside zone is replaced on the first settled frame.
w.insert_resource(CurrentArea(Some(40))); // Westfall
w.run_system_once(update_current_area).unwrap();
assert_eq!(w.resource::<CurrentArea>().0, Some(WAILING_CAVERNS_AREA));
// A later login on the same map: the area dies with the avatar and comes back.
w.insert_resource(crate::terrain_stream::ViewFocus::camera());
w.run_system_once(update_current_area).unwrap();
assert_eq!(w.resource::<CurrentArea>().0, None);
w.insert_resource(crate::terrain_stream::ViewFocus::body([0.0; 3], true));
w.run_system_once(update_current_area).unwrap();
assert_eq!(w.resource::<CurrentArea>().0, Some(WAILING_CAVERNS_AREA));
}
/// The flag, not the field, gates the leg: Zul'Gurub-like maps carry a field 19 and are read
/// off their terrain (`0x6703a4` falls through to the entity's node).
#[test]
fn a_map_without_the_flag_ignores_its_record_area() {
let rows = [(309, "Zul'gurub", 1977)];
let mut w = map_world(309, &rows, "Zul'gurub", false);
w.run_system_once(update_current_area).unwrap();
assert_eq!(w.resource::<CurrentArea>().0, None, "no tile, no area");
}
/// The streamer's flag is for the map it loaded: the previous map's WDT never answers for the
/// new map's record in the frames after a worldport.
#[test]
fn the_flag_belongs_to_the_loaded_map() {
let mut w = map_world(WAILING_CAVERNS, &wc_rows(), "Kalimdor", true);
w.run_system_once(update_current_area).unwrap();
assert_eq!(w.resource::<CurrentArea>().0, None);
}
/// A failed query (field 19 of 0, the dev maps) leaves the state alone, and never falls
/// through to the WMO and terrain legs (`0x6703d4`).
#[test]
fn a_one_object_map_with_no_record_area_fails_the_query() {
let rows = [(13, "test", 0)];
let mut w = map_world(13, &rows, "test", true);
w.insert_resource(CurrentArea(Some(40)));
w.run_system_once(update_current_area).unwrap();
assert_eq!(w.resource::<CurrentArea>().0, Some(40));
}
/// The settled gate stands ahead of the leg: a body whose world is still arriving holds.
#[test]
fn a_one_object_map_waits_for_the_settled_world() {
let mut w = map_world(WAILING_CAVERNS, &wc_rows(), "WailingCaverns", true);
w.insert_resource(crate::terrain_stream::ViewFocus::body([0.0; 3], false));
w.run_system_once(update_current_area).unwrap();
assert_eq!(w.resource::<CurrentArea>().0, None);
}
/// The held area is the old map's after a worldport. Kept, it fed the zone text a stale leaf
/// with the new map's building claim, and the indoor dedup then skipped the real zone.
#[test]
fn a_worldport_drops_the_old_maps_area() {
let rows = [(0, "Azeroth", 0), (WAILING_CAVERNS, "WailingCaverns", 718)];
let mut w = map_world(0, &rows, "Azeroth", false);
w.insert_resource(CurrentArea(Some(1519)));
w.run_system_once(update_current_area).unwrap();
assert_eq!(
w.resource::<CurrentArea>().0,
Some(1519),
"within the map the held value stands while the tile decodes"
);
w.insert_resource(crate::world_map::CurrentMap(WAILING_CAVERNS));
w.insert_resource(crate::terrain_stream::ViewFocus::body([0.0; 3], false));
w.run_system_once(update_current_area).unwrap();
assert_eq!(w.resource::<CurrentArea>().0, None);
// The WDT arrives and the world settles: the new map's own answer.
let mut streamer = w.resource_mut::<TerrainStreamer>();
streamer.map_dir = Some("WailingCaverns".to_string());
streamer.one_object = true;
w.insert_resource(crate::terrain_stream::ViewFocus::body([0.0; 3], true));
w.run_system_once(update_current_area).unwrap();
assert_eq!(w.resource::<CurrentArea>().0, Some(WAILING_CAVERNS_AREA));
}
}
+2 -1
View File
@@ -10,7 +10,8 @@
//! counts only within the 0.1 yd snap of its plane (`0x7c22b0`), and a crossing within about
//! 1e-4 of the nearest face hit still wins (`0x80c4f4`).
//! - The terrain race: the WMO hit is dropped only when terrain on the same segment is strictly
//! nearer (`0x6822a2`; `GetAreaID` `0x670250` arbitrates alike at `0x670345`). Terrain above the
//! nearer (`0x6822a2`; the unreached point query `0x670250` arbitrates alike at `0x670345`, and
//! the live entity query `0x6703a0` attaches by the same ray, `0x6a8a20`). Terrain above the
//! eye is off the segment, so a tunnel under a hill stays inside.
//!
//! The verdict is the in-group plus, for a portal win, the across-group: the containing-group set
+3 -3
View File
@@ -43,7 +43,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
#### area
- The indoor dedup keys `(WMOID, MOGP uniqueID)`, not the client's raw per-WMO group index, because the raw index can alias across adjacent buildings. ([`src/area.rs:26`](../crates/benilla-app/src/area.rs#L26))
- The indoor dedup keys `(WMOID, MOGP uniqueID)`, not the client's raw per-WMO group index, because the raw index can alias across adjacent buildings. ([`src/area.rs:30`](../crates/benilla-app/src/area.rs#L30))
#### assets/ui
@@ -798,7 +798,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
#### terrain_stream
- A tile is released only one chunk past the outer window (`StreamWindow::keeps`), where the reference releases by window membership (`0x6984f0`), because our reload is a re-decode and re-spawn, not a cheap async read. ([`src/terrain_stream.rs:617`](../crates/benilla-world/src/terrain_stream.rs#L617))
- A tile is released only one chunk past the outer window (`StreamWindow::keeps`), where the reference releases by window membership (`0x6984f0`), because our reload is a re-decode and re-spawn, not a cheap async read. ([`src/terrain_stream.rs:630`](../crates/benilla-world/src/terrain_stream.rs#L630))
- Tagged per submesh, where the reference tests the whole object, because a placement has no root entity to carry the bound. ([`src/terrain_stream/spawn/mod.rs:229`](../crates/benilla-world/src/terrain_stream/spawn/mod.rs#L229))
- The box, where the reference tests its circumsphere (`0x683700` → `0x682ef0` → `0x686b80` on the `[rec+0x5c]` centre and `[rec+0x68]` radius × scale), because Bevy culls by box and the box is tighter. ([`src/terrain_stream/spawn/mod.rs:896`](../crates/benilla-world/src/terrain_stream/spawn/mod.rs#L896))
- Whole tiles, because benilla loads, spawns and releases ADT tiles, not chunks: a tile is wanted when any of its chunks lies in the outer window, so the resident edge is a tile line where the reference's is a chunk line. ([`src/terrain_stream/window.rs:7`](../crates/benilla-world/src/terrain_stream/window.rs#L7))
@@ -827,7 +827,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
#### wmo_portal
- When legs A and B both miss and no terrain lies at or below the eye, Leg C runs the same race over the camera-only faces (DETAIL without NOCAMCOLLIDE), because the 1.12 client blanks the whole building from a pocket floored only with DETAIL faces, as at the Deadmines entrance. ([`src/wmo_portal/seed.rs:20`](../crates/benilla-world/src/wmo_portal/seed.rs#L20))
- When legs A and B both miss and no terrain lies at or below the eye, Leg C runs the same race over the camera-only faces (DETAIL without NOCAMCOLLIDE), because the 1.12 client blanks the whole building from a pocket floored only with DETAIL faces, as at the Deadmines entrance. ([`src/wmo_portal/seed.rs:21`](../crates/benilla-world/src/wmo_portal/seed.rs#L21))
#### zfill