diff --git a/src/element_processing/barriers.rs b/src/element_processing/barriers.rs index 7a3ffd292..afaf04678 100644 --- a/src/element_processing/barriers.rs +++ b/src/element_processing/barriers.rs @@ -3,140 +3,270 @@ use crate::bresenham::bresenham_line; use crate::element_processing::bridges::BridgeSurfaceMap; use crate::osm_parser::{ProcessedElement, ProcessedNode}; use crate::world_editor::WorldEditor; +use fastnbt::Value; +use std::collections::HashMap; const BRIDGE_BARRIER_NEARBY_RADIUS: i32 = 2; -pub fn generate_barriers( - editor: &mut WorldEditor, - element: &ProcessedElement, - bridge_surface: &BridgeSurfaceMap, -) { - // Default values - let mut barrier_material: Block = COBBLESTONE_WALL; - let mut barrier_height: i32 = 2; - - match element.tags().get("barrier").map(|s| s.as_str()) { - Some("bollard") => { - barrier_material = COBBLESTONE_WALL; - barrier_height = 1; +// Should probably be moved to a different file so it can be shared and reused. +#[derive(Clone, Copy)] +enum Axis { + X, + Z, +} + +enum BarrierMaterial { + Simple(BlockWithProperties), + Axial { + x: BlockWithProperties, + z: BlockWithProperties, + }, +} + +impl BarrierMaterial { + fn simple(block: Block) -> Self { + Self::Simple(BlockWithProperties::simple(block)) + } + + fn simple_with_properties(block: BlockWithProperties) -> Self { + Self::Simple(block) + } + + fn axial(x: Block, z: Block) -> Self { + Self::Axial { + x: BlockWithProperties::new(x, None), + z: BlockWithProperties::new(z, None), } - Some("kerb") => { - // Ignore kerbs - return; + } + + fn axial_with_properties(x: BlockWithProperties, z: BlockWithProperties) -> Self { + Self::Axial { x, z } + } + + fn get(&self, axis: Axis) -> &BlockWithProperties { + match self { + BarrierMaterial::Simple(block) => block, + BarrierMaterial::Axial { x, z } => match axis { + Axis::X => x, + Axis::Z => z, + }, } - Some("hedge") => { - barrier_material = OAK_LEAVES; - barrier_height = 2; + } +} + +struct BarrierSetting { + style: BarrierStyle, + height: i32, +} + +impl BarrierSetting { + fn solid(block: Block, height: i32) -> Self { + Self { + style: BarrierStyle::Solid { + material: BarrierMaterial::simple(block), + top_material: Some(BarrierMaterial::simple(STONE_BRICK_SLAB)), + }, + height, } + } +} + +enum BarrierStyle { + Solid { + material: BarrierMaterial, + top_material: Option, + }, + Alternating { + post_material: BarrierMaterial, + link_material: BarrierMaterial, + top_material: Option, + spacing: usize, + use_post_on_edge: bool, + }, +} + +fn fence_axis_properties(axis: Axis) -> Value { + let mut props = HashMap::new(); + match axis { + Axis::X => { + props.insert("east".to_string(), Value::String("true".to_string())); + props.insert("west".to_string(), Value::String("true".to_string())); + } + Axis::Z => { + props.insert("north".to_string(), Value::String("true".to_string())); + props.insert("south".to_string(), Value::String("true".to_string())); + } + } + Value::Compound(props) +} + +fn chain_y_properties() -> Value { + let mut props = HashMap::new(); + props.insert("axis".to_string(), Value::String("y".to_string())); + Value::Compound(props) +} + +fn get_setting_for_barrier(element: &ProcessedElement) -> Option { + let iron_bars_x: BlockWithProperties = + BlockWithProperties::new(IRON_BARS, Some(fence_axis_properties(Axis::X))); + let iron_bars_z: BlockWithProperties = + BlockWithProperties::new(IRON_BARS, Some(fence_axis_properties(Axis::Z))); + let chain_y: BlockWithProperties = + BlockWithProperties::new(CHAIN_Z, Some(chain_y_properties())); + + let oak_fence_x: BlockWithProperties = + BlockWithProperties::new(OAK_FENCE, Some(fence_axis_properties(Axis::X))); + let oak_fence_z: BlockWithProperties = + BlockWithProperties::new(OAK_FENCE, Some(fence_axis_properties(Axis::Z))); + + // Determine the base settinguration from tags. + // https://wiki.openstreetmap.org/wiki/Key:barrier + let setting = match element.tags().get("barrier").map(|s| s.as_str()) { + Some("bollard") => Some(BarrierSetting::solid(COBBLESTONE_WALL, 1)), + Some("kerb") => None, // Ignore kerbs. + Some("hedge") => Some(BarrierSetting::solid(OAK_LEAVES, 2)), + Some("wall") => Some(BarrierSetting::solid(STONE_BRICK_WALL, 3)), Some("fence") => { // Handle fence sub-types + // https://wiki.openstreetmap.org/wiki/Key:fence%20type match element.tags().get("fence_type").map(|s| s.as_str()) { - Some("railing" | "bars" | "krest") => { - barrier_material = STONE_BRICK_WALL; - barrier_height = 1; + Some("corrugated_metal") => Some(BarrierSetting::solid(STONE_BRICK_WALL, 2)), // TODO: pale_oak_shelf could work well. + Some("knee_rail") => Some(BarrierSetting::solid(OAK_FENCE, 1)), // TODO: alterning between fence, slab and air. + Some("net") => Some(BarrierSetting::solid(COBWEB, 2)), + Some("concrete") => Some(BarrierSetting::solid(ANDESITE_WALL, 2)), + Some("stone") => Some(BarrierSetting::solid(STONE_BRICK_WALL, 2)), + Some("glass") => Some(BarrierSetting::solid(GLASS, 1)), + Some("panel" | "slatted") => Some(BarrierSetting { + // TODO: spruce trapdoors or shelves. + style: BarrierStyle::Solid { + material: BarrierMaterial::axial_with_properties( + oak_fence_x.clone(), + oak_fence_z.clone(), + ), + top_material: None, + }, + height: 2, + }), + Some("paling" | "pole" | "post_and_rail" | "roundpole" | "split_rail" | "wood") => { + Some(BarrierSetting { + style: BarrierStyle::Solid { + material: BarrierMaterial::axial_with_properties( + oak_fence_x.clone(), + oak_fence_z.clone(), + ), + top_material: None, + }, + height: 1, + }) } Some( - "chain_link" | "metal" | "wire" | "barbed_wire" | "corrugated_metal" - | "electric" | "metal_bars", - ) => { - barrier_material = STONE_BRICK_WALL; // IRON_BARS - barrier_height = 2; - } - Some("slatted" | "paling") => { - barrier_material = OAK_FENCE; - barrier_height = 1; - } - Some("wood" | "split_rail" | "panel" | "pole") => { - barrier_material = OAK_FENCE; - barrier_height = 2; - } - Some("concrete" | "stone") => { - barrier_material = STONE_BRICK_WALL; - barrier_height = 2; - } - Some("glass") => { - barrier_material = GLASS; - barrier_height = 1; - } - _ => {} + "bars" + | "railing" + | "krest" + | "metal" + | "chain_link" + | "metal_bars" + | "temporary" + | "welded_diamond_mesh" + | "wire", + ) => Some(BarrierSetting { + style: BarrierStyle::Alternating { + post_material: BarrierMaterial::simple_with_properties(chain_y.clone()), + link_material: BarrierMaterial::axial_with_properties( + iron_bars_x.clone(), + iron_bars_z.clone(), + ), + top_material: None, + spacing: 2, + use_post_on_edge: true, + }, + height: 2, + }), + Some("barbed_wire") => Some(BarrierSetting { + style: BarrierStyle::Alternating { + post_material: BarrierMaterial::simple(ANDESITE_WALL), + link_material: BarrierMaterial::axial_with_properties( + iron_bars_x.clone(), + iron_bars_z.clone(), + ), + top_material: Some(BarrierMaterial::simple(COBWEB)), + spacing: 3, + use_post_on_edge: true, + }, + height: 2, + }), + Some("electric") => Some(BarrierSetting { + style: BarrierStyle::Alternating { + post_material: BarrierMaterial::simple(OAK_FENCE), + link_material: BarrierMaterial::axial(CHAIN_X, CHAIN_Z), + top_material: None, + spacing: 2, + use_post_on_edge: true, + }, + height: 1, + }), + _ => None, } } - Some("wall") => { - barrier_material = STONE_BRICK_WALL; - barrier_height = 3; - } - _ => {} - } - // Tagged material takes priority over inferred + _ => None, + }; + + let Some(mut setting) = setting else { + return None; // Skip processing if no valid barrier type is found. + }; + + // Apply tag overrides for the material. if let Some(barrier_mat) = element.tags().get("material") { - if barrier_mat == "brick" { - barrier_material = BRICK; - } - if barrier_mat == "concrete" { - barrier_material = LIGHT_GRAY_CONCRETE; + if let BarrierStyle::Solid { + ref mut material, .. + } = setting.style + { + match barrier_mat.as_str() { + "brick" => *material = BarrierMaterial::simple(BRICK), + "concrete" => *material = BarrierMaterial::simple(LIGHT_GRAY_CONCRETE), + "metal" => *material = BarrierMaterial::simple(STONE_BRICK_WALL), + _ => {} + } } - if barrier_mat == "metal" { - barrier_material = STONE_BRICK_WALL; + } + + // Apply tag overrides for the height. + if let Some(h_str) = element.tags().get("height") { + if let Ok(h_f32) = h_str.parse::() { + // Apply custom height (with a floor of 1 if user manually overrode it). + setting.height = (h_f32.round() as i32).max(1); } } + Some(setting) +} + +pub fn generate_barriers( + editor: &mut WorldEditor, + element: &ProcessedElement, + bridge_surface: &BridgeSurfaceMap, +) { + let Some(setting) = get_setting_for_barrier(element) else { + return; // Skip processing if no valid barrier type is found. + }; + if let ProcessedElement::Way(way) = element { - // Determine wall height - let wall_height: i32 = element - .tags() - .get("height") - .and_then(|height: &String| height.parse::().ok()) - .map(|height: f32| height.round() as i32) - .unwrap_or(barrier_height) - .max(2); // Minimum height of 2 - - // Process nodes to create the barrier wall for i in 1..way.nodes.len() { - let prev: &crate::osm_parser::ProcessedNode = &way.nodes[i - 1]; - let x1: i32 = prev.x; - let z1: i32 = prev.z; - - let cur: &crate::osm_parser::ProcessedNode = &way.nodes[i]; - let x2: i32 = cur.x; - let z2: i32 = cur.z; - - // Generate the line of coordinates between the two nodes - let bresenham_points: Vec<(i32, i32, i32)> = bresenham_line(x1, 0, z1, x2, 0, z2); - - for (bx, _, bz) in bresenham_points { - let deck = bridge_surface.nearby_deck_y(bx, bz, BRIDGE_BARRIER_NEARBY_RADIUS); - match deck { - Some(deck_y) => { - for y in 1..=wall_height { - editor.set_block_absolute( - barrier_material, - bx, - deck_y + y, - bz, - None, - None, - ); - } - if wall_height > 1 { - editor.set_block_absolute( - STONE_BRICK_SLAB, - bx, - deck_y + wall_height + 1, - bz, - None, - None, - ); - } - } - None => { - for y in 1..=wall_height { - editor.set_block(barrier_material, bx, y, bz, None, None); - } - if wall_height > 1 { - editor.set_block(STONE_BRICK_SLAB, bx, wall_height + 1, bz, None, None); - } - } - } + let prev = &way.nodes[i - 1]; + let cur = &way.nodes[i]; + + let bresenham_points = bresenham_line(prev.x, 0, prev.z, cur.x, 0, cur.z); + + let axis = if prev.x.abs_diff(cur.x) > prev.z.abs_diff(cur.z) { + Axis::X + } else { + Axis::Z + }; + + for (counter, (bx, _, bz)) in bresenham_points.into_iter().enumerate() { + let edge = (bx, bz) == (prev.x, prev.z) || (bx, bz) == (cur.x, cur.z); + let deck_y = bridge_surface.nearby_deck_y(bx, bz, BRIDGE_BARRIER_NEARBY_RADIUS); + place_barrier(&setting, editor, bx, bz, deck_y, counter, axis, edge); } } } @@ -148,14 +278,13 @@ pub fn generate_barrier_nodes( bridge_surface: &BridgeSurfaceMap, ) { let deck = bridge_surface.nearby_deck_y(node.x, node.z, BRIDGE_BARRIER_NEARBY_RADIUS); - let place = |editor: &mut WorldEditor<'_>, block: Block, dy: i32| match deck { + let place_block = |editor: &mut WorldEditor<'_>, block: Block, dy: i32| match deck { Some(deck_y) => editor.set_block_absolute(block, node.x, deck_y + dy, node.z, None, None), None => editor.set_block(block, node.x, dy, node.z, None, None), }; + match node.tags.get("barrier").map(|s| s.as_str()) { - Some("bollard") => { - place(editor, COBBLESTONE_WALL, 1); - } + Some("bollard") => place_block(editor, COBBLESTONE_WALL, 1), Some("stile" | "gate" | "swing_gate" | "lift_gate") => { /*editor.set_block( OAK_TRAPDOOR, @@ -200,13 +329,63 @@ pub fn generate_barrier_nodes( None, );*/ } - Some("block") => { - place(editor, STONE, 1); + Some("block") => place_block(editor, STONE, 1), + Some("entrance") => place_block(editor, AIR, 1), + _ => {} + } +} + +#[allow(clippy::too_many_arguments)] +fn place_barrier( + setting: &BarrierSetting, + editor: &mut WorldEditor, + bx: i32, + bz: i32, + deck_y: Option, + counter: usize, + axis: Axis, + edge: bool, +) { + let place_block = |editor: &mut WorldEditor, block: BlockWithProperties, dy: i32| match deck_y { + Some(y) => editor.set_block_with_properties_absolute(block, bx, y + dy, bz, None, None), + None => editor.set_block_with_properties(block, bx, dy, bz, None, None), + }; + + match &setting.style { + BarrierStyle::Solid { + material, + top_material, + } => { + for y in 1..=setting.height { + place_block(editor, material.get(axis).clone(), y); + } + if setting.height > 1 { + if let Some(top_material) = top_material { + place_block(editor, top_material.get(axis).clone(), setting.height + 1); + } + } } - Some("entrance") => { - place(editor, AIR, 1); + BarrierStyle::Alternating { + post_material, + link_material, + top_material, + spacing, + use_post_on_edge, + } => { + let is_post = (edge && *use_post_on_edge) || counter.is_multiple_of(spacing + 1); + let material = if is_post { + post_material.get(axis).clone() + } else { + link_material.get(axis).clone() + }; + + for y in 1..=setting.height { + place_block(editor, material.clone(), y); + } + + if let Some(top_material) = top_material { + place_block(editor, top_material.get(axis).clone(), setting.height + 1); + } } - None => {} - _ => {} } } diff --git a/src/world_editor/mod.rs b/src/world_editor/mod.rs index 8eb9ded7a..59a850880 100644 --- a/src/world_editor/mod.rs +++ b/src/world_editor/mod.rs @@ -839,6 +839,37 @@ impl<'a> WorldEditor<'a> { ); } + /// Sets a block with properties of the specified type at the given coordinates. + /// + /// Y value is interpreted as an offset from ground level. + #[inline] + pub fn set_block_with_properties( + &mut self, + block_with_props: BlockWithProperties, + x: i32, + y: i32, + z: i32, + override_whitelist: Option<&[Block]>, + override_blacklist: Option<&[Block]>, + ) { + // Short-circuit for out-of-bbox writes before we pay for a + // ground-level lookup (bilinear interpolation of the elevation + // grid). The downstream `set_block_with_properties_absolute` + // does the same check, but only *after* we would have done the + // elevation work. + if !self.xzbbox.contains(&XZPoint::new(x, z)) { + return; + } + self.set_block_with_properties_absolute( + block_with_props, + x, + self.get_absolute_y(x, y, z), + z, + override_whitelist, + override_blacklist, + ); + } + /// Sets a block of the specified type at the given coordinates with absolute Y value. #[inline] pub fn set_block_absolute(