diff --git a/crates/benilla-app/src/ui_world_map.rs b/crates/benilla-app/src/ui_world_map.rs index 5271b324..3cf4a1b2 100644 --- a/crates/benilla-app/src/ui_world_map.rs +++ b/crates/benilla-app/src/ui_world_map.rs @@ -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 { + 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>, data: Option>, @@ -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::>() + ); + + // 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" + ); + } }