diff --git a/src/bedrock_block_map.rs b/src/bedrock_block_map.rs index 7527c7b..a704c47 100644 --- a/src/bedrock_block_map.rs +++ b/src/bedrock_block_map.rs @@ -491,12 +491,23 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock { ), // Plain terracotta "terracotta" => BedrockBlock::simple("hardened_clay"), - + // Wall banner — Bedrock uses "minecraft:wall_banner" with a + // "facing_direction" int state: 2=north, 3=south, 4=west, 5=east. + // The color is stored in the block entity (Base field), not the block state. + // The facing string→int mapping is handled by to_bedrock_block_with_properties. + "light_gray_wall_banner" => BedrockBlock::with_states( + "wall_banner", + vec![("facing_direction", BedrockBlockStateValue::Int(2))], // default north + ), // Wool colors "white_wool" => BedrockBlock::with_states( "wool", vec![("color", BedrockBlockStateValue::String("white".to_string()))], ), + "black_wool" => BedrockBlock::with_states( + "wool", + vec![("color", BedrockBlockStateValue::String("black".to_string()))], + ), "red_wool" => BedrockBlock::with_states( "wool", vec![("color", BedrockBlockStateValue::String("red".to_string()))], @@ -821,6 +832,11 @@ pub fn to_bedrock_block_with_properties( return convert_rail(props_map); } + // Handle wall banners with facing property + if java_name == "light_gray_wall_banner" { + return convert_wall_banner(props_map); + } + // Fall back to basic conversion without properties to_bedrock_block(block) } @@ -1247,6 +1263,40 @@ fn convert_bed( } } +/// Convert Java wall banner to Bedrock format. +/// +/// Java stores facing as a string ("north"/"south"/"east"/"west") on the block state. +/// Bedrock uses `facing_direction` as an integer on `minecraft:wall_banner`: +/// 2 = north, 3 = south, 4 = west, 5 = east +/// +/// The banner color (light_gray = 7) and patterns live in the block entity, not here. +fn convert_wall_banner( + props: Option<&std::collections::HashMap>, +) -> BedrockBlock { + let facing_direction = props + .and_then(|p| p.get("facing")) + .and_then(|v| match v { + fastnbt::Value::String(s) => Some(s.as_str()), + _ => None, + }) + .map(|f| match f { + "north" => 2, + "south" => 3, + "west" => 4, + "east" => 5, + _ => 2, // default north + }) + .unwrap_or(2); + + BedrockBlock::with_states( + "wall_banner", + vec![( + "facing_direction", + BedrockBlockStateValue::Int(facing_direction), + )], + ) +} + /// Convert Java rail to Bedrock format with rail_direction from shape property. /// /// Java uses `shape` strings ("north_south", "east_west", "ascending_east", etc.) diff --git a/src/block_definitions.rs b/src/block_definitions.rs index 9979963..3662765 100644 --- a/src/block_definitions.rs +++ b/src/block_definitions.rs @@ -332,6 +332,8 @@ impl Block { 251 => "cactus", 252 => "gray_concrete_powder", 253 => "cyan_terracotta", + 254 => "black_wool", + 255 => "light_gray_wall_banner", _ => panic!("Invalid id"), } } @@ -980,6 +982,8 @@ pub const RED_SANDSTONE: Block = Block::new(250); pub const CACTUS: Block = Block::new(251); pub const GRAY_CONCRETE_POWDER: Block = Block::new(252); pub const CYAN_TERRACOTTA: Block = Block::new(253); +pub const BLACK_WOOL: Block = Block::new(254); +pub const LIGHT_GRAY_WALL_BANNER: Block = Block::new(255); /// Maps a block to its corresponding stair variant #[inline] diff --git a/src/element_processing/highways.rs b/src/element_processing/highways.rs index 3be269b..f34e883 100644 --- a/src/element_processing/highways.rs +++ b/src/element_processing/highways.rs @@ -114,13 +114,43 @@ fn generate_highways_internal( let x: i32 = node.x; let z: i32 = node.z; - for dy in 1..=3 { - editor.set_block(COBBLESTONE_WALL, x, dy, z, None, None); - } + // Traffic light blocks + editor.set_block(COBBLESTONE_WALL, x, 1, z, None, None); + editor.set_block(IRON_BARS, x, 2, z, None, None); + editor.set_block(IRON_BARS, x, 3, z, None, None); + editor.set_block(BLACK_WOOL, x, 4, z, None, None); + editor.set_block(BLACK_WOOL, x, 5, z, None, None); - editor.set_block(GREEN_WOOL, x, 4, z, None, None); - editor.set_block(YELLOW_WOOL, x, 5, z, None, None); - editor.set_block(RED_WOOL, x, 6, z, None, None); + // Banner placement logic + let abs_y = editor.get_absolute_y(x, 5, z); + let banner_offsets: [(i32, i32, &str); 4] = [ + (0, -1, "north"), + (0, 1, "south"), + (-1, 0, "west"), + (1, 0, "east"), + ]; + + // patterns expressed as (java_color, java_pattern_id) pairs + // so both Java and Bedrock writers can use them. + const BANNER_PATTERNS: &[(&str, &str)] = &[ + ("red", "minecraft:triangle_top"), + ("lime", "minecraft:triangle_bottom"), + ("yellow", "minecraft:circle"), + ("black", "minecraft:curly_border"), + ("black", "minecraft:border"), + ]; + + for (dx, dz, facing) in &banner_offsets { + editor.place_wall_banner( + LIGHT_GRAY_WALL_BANNER, + x + dx, + abs_y, + z + dz, + facing, + "light_gray", + BANNER_PATTERNS, + ); + } } } } diff --git a/src/world_editor/bedrock.rs b/src/world_editor/bedrock.rs index 299c113..199bc8d 100644 --- a/src/world_editor/bedrock.rs +++ b/src/world_editor/bedrock.rs @@ -526,7 +526,17 @@ impl BedrockWriter { return Ok(()); } - let data = nbtx::to_le_bytes(&deduped).map_err(|e| BedrockSaveError::Nbt(e.to_string()))?; + // Bedrock block entities and entities are stored as CONCATENATED individual + // NBT compounds — NOT as a single NBT list. Each compound is serialised + // back-to-back with no wrapper. nbtx::to_le_bytes() on a Vec would produce + // a TAG_List header, which Bedrock cannot parse. + let mut data: Vec = Vec::new(); + for compound in &deduped { + let bytes = + nbtx::to_le_bytes(compound).map_err(|e| BedrockSaveError::Nbt(e.to_string()))?; + data.extend_from_slice(&bytes); + } + let key = build_chunk_key_bytes(chunk_pos, Dimension::Overworld, key_type, None); db.put(&key, &data) .map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?; diff --git a/src/world_editor/mod.rs b/src/world_editor/mod.rs index c00eed0..68c4c03 100644 --- a/src/world_editor/mod.rs +++ b/src/world_editor/mod.rs @@ -236,6 +236,223 @@ impl<'a> WorldEditor<'a> { let absolute_y = self.get_absolute_y(x, y, z); self.world.get_block(x, absolute_y, z).is_some() } + #[allow(clippy::too_many_arguments)] + pub fn place_wall_banner( + &mut self, + block: Block, + x: i32, + y: i32, + z: i32, + facing: &str, // "north" / "south" / "east" / "west" + base_color: &str, // "light_gray" etc. + patterns: &[(&str, &str)], // [("red", "minecraft:triangle_top"), ...] + ) { + // Apply Block rotation + self.set_block_with_properties_absolute( + crate::block_definitions::BlockWithProperties::new( + block, + Some(fastnbt::nbt!({ "facing": facing })), + ), + x, + y, + z, + None, + None, + ); + match self.format() { + crate::world_editor::WorldFormat::JavaAnvil => { + self.set_banner_block_entity_absolute(x, y, z, patterns); + } + crate::world_editor::WorldFormat::BedrockMcWorld => { + self.set_bedrock_banner_block_entity_absolute(x, y, z, base_color, patterns); + } + } + } + + fn insert_block_entity(&mut self, x: i32, z: i32, be: HashMap) { + if !self.xzbbox.contains(&XZPoint::new(x, z)) { + return; + } + let chunk_x = x >> 4; + let chunk_z = z >> 4; + let region_x = chunk_x >> 5; + let region_z = chunk_z >> 5; + + let region = self.world.get_or_create_region(region_x, region_z); + let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31); + + const BLOCK_ENTITIES_KEY: &str = "block_entities"; + + match chunk.other.entry(BLOCK_ENTITIES_KEY.to_string()) { + Entry::Occupied(mut entry) => { + if let Value::List(list) = entry.get_mut() { + list.push(Value::Compound(be)); + } + } + Entry::Vacant(entry) => { + entry.insert(Value::List(vec![Value::Compound(be)])); + } + } + } + + /// Places a banner block entity at the given coordinates (absolute Y). + /// This writes the pattern data into the chunk's block_entities list, + /// which is required for the banner patterns to appear in-game. + fn set_banner_block_entity_absolute( + &mut self, + x: i32, + absolute_y: i32, + z: i32, + patterns_list: &[(&str, &str)], + ) { + let mut be = HashMap::new(); + be.insert( + "id".to_string(), + Value::String("minecraft:banner".to_string()), + ); + be.insert("x".to_string(), Value::Int(x)); + be.insert("y".to_string(), Value::Int(absolute_y)); + be.insert("z".to_string(), Value::Int(z)); + be.insert("keepPacked".to_string(), Value::Byte(0)); + let patterns: Vec = patterns_list + .iter() + .map(|(color, pattern)| { + let mut entry = HashMap::new(); + + entry.insert("color".to_string(), Value::String(color.to_string())); + entry.insert("pattern".to_string(), Value::String(pattern.to_string())); + Value::Compound(entry) + }) + .collect(); + be.insert("patterns".to_string(), Value::List(patterns)); + be.insert("components".to_string(), Value::Compound(HashMap::new())); + self.insert_block_entity(x, z, be); + } + + /// Places a Bedrock-format banner block entity at the given coordinates (absolute Y). + /// + /// Bedrock banners use a completely different block entity schema from Java: + /// - `Base`: Int — base color index (0=black … 15=white; light_gray=7) + /// - `Patterns`: List — each entry has `Color` (Int) and `Pattern` (String, short code) + /// - `Type`: Int — 0 = normal banner + /// + /// Java color names and pattern resource-path IDs are converted here to their + /// Bedrock integer color indices and short pattern codes. + fn set_bedrock_banner_block_entity_absolute( + &mut self, + x: i32, + absolute_y: i32, + z: i32, + base_color: &str, + patterns: &[(&str, &str)], // &[(java_color_name, java_pattern_id)] + ) { + /// Maps a Java color name to the Bedrock integer color index used in banner + /// block entities. The ordering is the standard Minecraft dye index. + fn java_color_to_bedrock_int(color: &str) -> i32 { + match color { + "black" => 0, + "red" => 1, + "green" => 2, + "brown" => 3, + "blue" => 4, + "purple" => 5, + "cyan" => 6, + "light_gray" => 7, + "gray" => 8, + "pink" => 9, + "lime" => 10, + "yellow" => 11, + "light_blue" => 12, + "magenta" => 13, + "orange" => 14, + "white" => 15, + _ => 0, + } + } + + /// Maps a Java banner pattern resource-path ID (e.g. "minecraft:triangle_top") + /// to the Bedrock short pattern code (e.g. "tts"). + fn java_pattern_to_bedrock_code(pattern: &str) -> &'static str { + // Strip the optional "minecraft:" namespace prefix + let key = pattern.strip_prefix("minecraft:").unwrap_or(pattern); + match key { + "base" => "b", + "square_bottom_left" => "bl", + "square_bottom_right" => "br", + "square_top_left" => "tl", + "square_top_right" => "tr", + "stripe_bottom" => "bs", + "stripe_top" => "ts", + "stripe_left" => "ls", + "stripe_right" => "rs", + "stripe_center" => "cs", + "stripe_middle" => "ms", + "stripe_downright" => "drs", + "stripe_downleft" => "dls", + "stripe_small" => "ss", + "cross" => "cr", + "straight_cross" => "sc", + "triangle_bottom" => "bt", + "triangle_top" => "tt", + "triangles_bottom" => "bts", + "triangles_top" => "tts", + "diagonal_left" => "ld", + "diagonal_right" => "rd", + "diagonal_up_left" => "lud", + "diagonal_up_right" => "rud", + "circle" => "mc", + "rhombus" => "mr", + "half_vertical" => "vh", + "half_vertical_right" => "vhr", + "half_horizontal" => "hh", + "half_horizontal_bottom" => "hhb", + "border" => "bo", + "curly_border" => "cbo", + "gradient" => "gra", + "gradient_up" => "gru", + "bricks" => "bri", + "globe" => "glb", + "creeper" => "cre", + "skull" => "sku", + "flower" => "flo", + "mojang" => "moj", + "piglin" => "pig", + "flow" => "flw", + "guster" => "gus", + _ => "b", // fallback: solid base + } + } + + let bedrock_patterns: Vec = patterns + .iter() + .map(|(color, pattern)| { + let mut entry = HashMap::new(); + entry.insert( + "Color".to_string(), + Value::Int(java_color_to_bedrock_int(color)), + ); + entry.insert( + "Pattern".to_string(), + Value::String(java_pattern_to_bedrock_code(pattern).to_string()), + ); + Value::Compound(entry) + }) + .collect(); + + let mut be = HashMap::new(); + be.insert("id".to_string(), Value::String("Banner".to_string())); + be.insert("x".to_string(), Value::Int(x)); + be.insert("y".to_string(), Value::Int(absolute_y)); + be.insert("z".to_string(), Value::Int(z)); + be.insert( + "Base".to_string(), + Value::Int(java_color_to_bedrock_int(base_color)), + ); + be.insert("Patterns".to_string(), Value::List(bedrock_patterns)); + be.insert("Type".to_string(), Value::Int(0)); + + self.insert_block_entity(x, z, be); + } /// Sets a sign at the given coordinates #[allow(clippy::too_many_arguments, dead_code)]