Merge pull request #901 from luigi052005/better-traffic-lights

Better traffic lights
This commit is contained in:
Louis Erbkamm
2026-04-09 17:13:41 +02:00
committed by GitHub
5 changed files with 319 additions and 8 deletions

View File

@@ -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<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.)

View File

@@ -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]

View File

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

View File

@@ -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)))?;

View File

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