Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion src/bedrock_block_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))],
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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<String, fastnbt::Value>>,
) -> 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.)
Expand Down
4 changes: 4 additions & 0 deletions src/block_definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
}
}
Expand Down Expand Up @@ -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]
Expand Down
42 changes: 36 additions & 6 deletions src/element_processing/highways.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

// 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,
);
}

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);
}
}
}
Expand Down
12 changes: 11 additions & 1 deletion src/world_editor/bedrock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8> = 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)))?;
Expand Down
219 changes: 219 additions & 0 deletions src/world_editor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,225 @@ 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"), ...]
) {
Comment thread
louis-e marked this conversation as resolved.
// 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<String, Value>) {
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<Value> = 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,
Comment thread
louis-e marked this conversation as resolved.
}
}

/// 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
Comment thread
louis-e marked this conversation as resolved.
}
}

let bedrock_patterns: Vec<Value> = 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)]
Expand Down
Loading