world map: the AreaPOI walk extracted, with the Alterac Valley objectives gating by world state
`build_landmarks` is the `0x4a6819` walk of `[0xc0e054]` lifted out of `feed_world_map`, so the landmark list can be built against a world state without a live `WorldMapUiData` feed. The guard-directions marker stays with the caller, appended after it, as `0x4a69c7` has it. The new test drives the whole path — resolve the instance map, project through its rect, gate by flag, exploration and world state — against the real catalogues. Alterac Valley's towers and graveyards are `AreaPOI` rows on map 30, each objective owned by a separate `WorldStateID` row at one position, so the row whose state the server set is the one that lands: before any state arrives only the six ungated rows show, and afterwards the held objective shows once with its own POIIcon cell, not every owner variant.
This commit is contained in:
@@ -536,6 +536,41 @@ struct FeedMemos {
|
||||
map_synced: bool,
|
||||
}
|
||||
|
||||
/// The `AreaPOI.dbc` half of the landmark list (`0x4a6819`'s walk), in file order: every row the
|
||||
/// gates admit, projected onto the displayed map, the degenerate projection dropped. The
|
||||
/// guard-directions marker is appended by the caller, after this.
|
||||
fn build_landmarks(
|
||||
pois: Option<&benilla_formats::AreaPoiCatalog>,
|
||||
level: MapLevel,
|
||||
areas: &benilla_formats::AreaTableCatalog,
|
||||
explored: &[u32],
|
||||
world_states: &crate::world_state::WorldStates,
|
||||
project: impl Fn(u32, f32, f32) -> Option<(f32, f32)>,
|
||||
) -> Vec<WorldMapLandmarkView> {
|
||||
let Some(pois) = pois else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut landmarks = Vec::new();
|
||||
for (_, poi) in pois.rows() {
|
||||
if !landmark_gates_pass(poi, level, areas, explored, world_states) {
|
||||
continue;
|
||||
}
|
||||
let Some(uv) = project(poi.continent_id, poi.pos[0], poi.pos[1]) else {
|
||||
continue;
|
||||
};
|
||||
if is_degenerate(uv) {
|
||||
continue;
|
||||
}
|
||||
landmarks.push(WorldMapLandmarkView {
|
||||
name: poi.name.clone(),
|
||||
description: poi.description.clone(),
|
||||
texture_index: landmark_texture_index(poi, level),
|
||||
uv,
|
||||
});
|
||||
}
|
||||
landmarks
|
||||
}
|
||||
|
||||
fn feed_world_map(
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
data: Option<Res<WorldMapUiData>>,
|
||||
@@ -618,28 +653,16 @@ fn feed_world_map(
|
||||
.is_some_and(|k| k.matches(selection, map.0, states_gen, &explored, marker));
|
||||
if !unchanged {
|
||||
let level = MapLevel::of(selection);
|
||||
let mut landmarks = Vec::new();
|
||||
// The DBC rows first, in file order, the Lua landmark index (`0x4a6819`'s walk of
|
||||
// `[0xc0e054]`).
|
||||
if let Some(pois) = pois.as_ref() {
|
||||
for (_, poi) in pois.0.rows() {
|
||||
if !landmark_gates_pass(poi, level, &areas.0, &explored, &world_states) {
|
||||
continue;
|
||||
}
|
||||
let Some(uv) = project(poi.continent_id, poi.pos[0], poi.pos[1]) else {
|
||||
continue;
|
||||
};
|
||||
if is_degenerate(uv) {
|
||||
continue;
|
||||
}
|
||||
landmarks.push(WorldMapLandmarkView {
|
||||
name: poi.name.clone(),
|
||||
description: poi.description.clone(),
|
||||
texture_index: landmark_texture_index(poi, level),
|
||||
uv,
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut landmarks = build_landmarks(
|
||||
pois.as_deref().map(|p| &p.0),
|
||||
level,
|
||||
&areas.0,
|
||||
&explored,
|
||||
&world_states,
|
||||
project,
|
||||
);
|
||||
// Then the guard-directions marker, last (`0x4a69c7`): no gate and no icon substitution,
|
||||
// only the degenerate-projection test.
|
||||
if let Some(poi) = poi_marker.on_map(map.0) {
|
||||
@@ -1385,4 +1408,109 @@ mod direct_area_tests {
|
||||
"selecting a continent clears the direct cell"
|
||||
);
|
||||
}
|
||||
|
||||
/// Alterac Valley's towers and graveyards are `AreaPOI` rows on map 30, each owner a separate
|
||||
/// `WorldStateID` row at one position: the live one is the row whose state the server set.
|
||||
/// The whole data path — resolve the instance map, project through its rect, gate by flag,
|
||||
/// exploration and world state — must yield exactly the owned variant.
|
||||
#[test]
|
||||
fn the_alterac_valley_objectives_land_on_its_map_by_world_state() {
|
||||
use crate::world_state::WorldStates;
|
||||
|
||||
let Some((_views, data)) = real_catalog() else {
|
||||
return;
|
||||
};
|
||||
let chain_data = benilla_formats::wow_data_or_skip!();
|
||||
let mut chain = benilla_formats::open_chain(&chain_data).expect("chain");
|
||||
let pois = benilla_formats::load_area_poi_catalog(&mut chain).expect("AreaPOI");
|
||||
let areas = benilla_formats::load_area_table_catalog(&mut chain).expect("AreaTable");
|
||||
|
||||
// The engine's selection for a body on Alterac Valley (map 30, its WMA row 401).
|
||||
let player = resolve_player_selection(&data, 30, Some(2597));
|
||||
assert_eq!(
|
||||
player,
|
||||
PlayerSelection {
|
||||
zone: None,
|
||||
direct: Some(401),
|
||||
}
|
||||
);
|
||||
let selection = (0, 0, player.direct);
|
||||
let project = |pos_map: u32, px: f32, py: f32| {
|
||||
project_on_displayed(&data, selection, pos_map, px, py)
|
||||
};
|
||||
|
||||
// Before any world state arrives: only the six ungated rows (the tunnel mouths and the
|
||||
// three keep/outpost names) show; every tower and graveyard is held back.
|
||||
let none = WorldStates::default();
|
||||
let gated = build_landmarks(
|
||||
Some(&pois),
|
||||
MapLevel::Zone,
|
||||
&areas,
|
||||
&[u32::MAX; 64],
|
||||
&none,
|
||||
project,
|
||||
);
|
||||
assert!(
|
||||
gated.iter().all(|l| !l.name.contains("Graveyard")
|
||||
&& !l.name.contains("Tower")
|
||||
&& !l.name.contains("Bunker")
|
||||
&& !l.name.contains("Aid Station")),
|
||||
"no objective without its state: {:?}",
|
||||
gated.iter().map(|l| l.name.as_str()).collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
// The server's opening states: the four graveyards near a base are Alliance- or
|
||||
// Horde-held, one row each; the midpoint (Snowfall) starts uncontrolled.
|
||||
let mut states = WorldStates::default();
|
||||
states.init_scope(30, 0);
|
||||
states.write(&[
|
||||
(1333, 1), // Stormpike Graveyard, "Alliance Controlled"
|
||||
(1340, 1), // Frostwolf Graveyard, "In Conflict"
|
||||
(1302, 1), // Stonehearth Graveyard, "Alliance Controlled"
|
||||
(1966, 1), // Snowfall Graveyard, "Uncontrolled"
|
||||
(1364, 1), // Stonehearth Bunker, "Alliance Controlled"
|
||||
]);
|
||||
let live = build_landmarks(
|
||||
Some(&pois),
|
||||
MapLevel::Zone,
|
||||
&areas,
|
||||
&[u32::MAX; 64],
|
||||
&states,
|
||||
project,
|
||||
);
|
||||
|
||||
let names: Vec<&str> = live.iter().map(|l| l.name.as_str()).collect();
|
||||
assert!(
|
||||
names.contains(&"Stormpike Graveyard"),
|
||||
"the Alliance-held graveyard shows: {names:?}"
|
||||
);
|
||||
assert!(
|
||||
names.contains(&"Snowfall Graveyard"),
|
||||
"the uncontrolled one"
|
||||
);
|
||||
assert!(names.contains(&"Stonehearth Bunker"), "an owned tower");
|
||||
// One icon per objective: the sibling owner rows stay hidden.
|
||||
assert_eq!(
|
||||
names
|
||||
.iter()
|
||||
.filter(|n| **n == "Stormpike Graveyard")
|
||||
.count(),
|
||||
1,
|
||||
"one row per objective, not every owner variant: {names:?}"
|
||||
);
|
||||
// Every landmark landed inside the Alterac Valley rect, not off-map at (0, 0).
|
||||
assert!(
|
||||
live.iter().all(|l| l.uv != (0.0, 0.0)),
|
||||
"all project through the instance rect"
|
||||
);
|
||||
// And the rows carry the AV POIIcons cells, not the zone substitute.
|
||||
let stormpike = live
|
||||
.iter()
|
||||
.find(|l| l.name == "Stormpike Graveyard")
|
||||
.expect("Stormpike Graveyard");
|
||||
assert_eq!(
|
||||
stormpike.texture_index, 14,
|
||||
"the Alliance-controlled GY icon"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user