diff --git a/Cargo.lock b/Cargo.lock index 72a48c5a3..d216fa9c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,6 +195,7 @@ version = "2.3.0" dependencies = [ "clap", "colored", + "dashmap", "dirs", "fastanvil", "fastnbt", @@ -1114,6 +1115,19 @@ dependencies = [ "syn 2.0.95", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "deranged" version = "0.3.11" diff --git a/Cargo.toml b/Cargo.toml index be8e06a81..42cdd041d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ dirs = {version = "6.0.0", optional = true } fastanvil = "0.32.0" fastnbt = "2.5.0" flate2 = "1.1" +dashmap = "5" fnv = "1.0.7" fs2 = "0.4" geo = "0.30.0" diff --git a/src/block_definitions.rs b/src/block_definitions.rs index 27ccf7b88..1cc7cf85a 100644 --- a/src/block_definitions.rs +++ b/src/block_definitions.rs @@ -1,9 +1,11 @@ #![allow(unused)] +use dashmap::DashMap; use fastnbt::Value; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::sync::Mutex; use crate::colors::RGBTuple; @@ -57,7 +59,7 @@ type ColorBlockMapping = (ColorTuple, BlockOptions); #[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Debug)] pub struct Block { - id: u8, + name: &'static str, } // Extended block with dynamic properties @@ -82,239 +84,53 @@ impl BlockWithProperties { impl Block { #[inline(always)] - const fn new(id: u8) -> Self { - Self { id } - } - - #[inline(always)] - pub fn id(&self) -> u8 { - self.id + const fn new(namespaced_name: &'static str) -> Self { + // Names are expected to include the namespace, e.g. "minecraft:oak_planks" + Self { + name: namespaced_name, + } } #[inline(always)] - pub fn namespace(&self) -> &str { - "minecraft" - } - pub fn name(&self) -> &str { - match self.id { - 0 => "acacia_planks", - 1 => "air", - 2 => "andesite", - 3 => "birch_leaves", - 4 => "birch_log", - 5 => "black_concrete", - 6 => "blackstone", - 7 => "blue_orchid", - 8 => "blue_terracotta", - 9 => "bricks", - 10 => "cauldron", - 11 => "chiseled_stone_bricks", - 12 => "cobblestone_wall", - 13 => "cobblestone", - 14 => "polished_blackstone_bricks", - 15 => "cracked_stone_bricks", - 16 => "crimson_planks", - 17 => "cut_sandstone", - 18 => "cyan_concrete", - 19 => "dark_oak_planks", - 20 => "deepslate_bricks", - 21 => "diorite", - 22 => "dirt", - 23 => "end_stone_bricks", - 24 => "farmland", - 25 => "glass", - 26 => "glowstone", - 27 => "granite", - 28 => "grass_block", - 29 => "short_grass", - 30 => "gravel", - 31 => "gray_concrete", - 32 => "gray_terracotta", - 33 => "green_terracotta", - 34 => "green_wool", - 35 => "hay_block", - 36 => "iron_bars", - 37 => "iron_block", - 38 => "jungle_planks", - 39 => "ladder", - 40 => "light_blue_concrete", - 41 => "light_blue_terracotta", - 42 => "light_gray_concrete", - 43 => "moss_block", - 44 => "mossy_cobblestone", - 45 => "mud_bricks", - 46 => "nether_bricks", - 47 => "netherite_block", - 48 => "oak_fence", - 49 => "oak_leaves", - 50 => "oak_log", - 51 => "oak_planks", - 52 => "oak_slab", - 53 => "orange_terracotta", - 54 => "podzol", - 55 => "polished_andesite", - 56 => "polished_basalt", - 57 => "quartz_block", - 58 => "polished_blackstone", - 59 => "polished_deepslate", - 60 => "polished_diorite", - 61 => "polished_granite", - 62 => "prismarine", - 63 => "purpur_block", - 64 => "purpur_pillar", - 65 => "quartz_bricks", - 66 => "rail", - 67 => "poppy", - 68 => "red_nether_bricks", - 69 => "red_terracotta", - 70 => "red_wool", - 71 => "sand", - 72 => "sandstone", - 73 => "scaffolding", - 74 => "smooth_quartz", - 75 => "smooth_red_sandstone", - 76 => "smooth_sandstone", - 77 => "smooth_stone", - 78 => "sponge", - 79 => "spruce_log", - 80 => "spruce_planks", - 81 => "stone_slab", - 82 => "stone_brick_slab", - 83 => "stone_bricks", - 84 => "stone", - 85 => "terracotta", - 86 => "warped_planks", - 87 => "water", - 88 => "white_concrete", - 89 => "azure_bluet", - 90 => "white_stained_glass", - 91 => "white_terracotta", - 92 => "white_wool", - 93 => "yellow_concrete", - 94 => "dandelion", - 95 => "yellow_wool", - 96 => "lime_concrete", - 97 => "cyan_wool", - 98 => "blue_concrete", - 99 => "purple_concrete", - 100 => "red_concrete", - 101 => "magenta_concrete", - 102 => "brown_wool", - 103 => "oxidized_copper", - 104 => "yellow_terracotta", - 105 => "carrots", - 106 => "dark_oak_door", - 107 => "dark_oak_door", - 108 => "potatoes", - 109 => "wheat", - 110 => "bedrock", - 111 => "snow_block", - 112 => "snow", - 113 => "oak_sign", - 114 => "andesite_wall", - 115 => "stone_brick_wall", - 116..=125 => "rail", - 126 => "coarse_dirt", - 127 => "iron_ore", - 128 => "coal_ore", - 129 => "gold_ore", - 130 => "copper_ore", - 131 => "clay", - 132 => "dirt_path", - 133 => "ice", - 134 => "packed_ice", - 135 => "mud", - 136 => "dead_bush", - 137..=138 => "tall_grass", - 139 => "crafting_table", - 140 => "furnace", - 141 => "white_carpet", - 142 => "bookshelf", - 143 => "oak_pressure_plate", - 144 => "oak_stairs", - 155 => "chest", - 156 => "red_carpet", - 157 => "anvil", - 158 => "note_block", - 159 => "oak_door", - 160 => "brewing_stand", - 161 => "red_bed", // North head - 162 => "red_bed", // North foot - 163 => "red_bed", // East head - 164 => "red_bed", // East foot - 165 => "red_bed", // South head - 166 => "red_bed", // South foot - 167 => "red_bed", // West head - 168 => "red_bed", // West foot - 169 => "gray_stained_glass", - 170 => "light_gray_stained_glass", - 171 => "brown_stained_glass", - 172 => "tinted_glass", - 173 => "oak_trapdoor", - 174 => "brown_concrete", - 175 => "black_terracotta", - 176 => "brown_terracotta", - 177 => "stone_brick_stairs", - 178 => "mud_brick_stairs", - 179 => "polished_blackstone_brick_stairs", - 180 => "brick_stairs", - 181 => "polished_granite_stairs", - 182 => "end_stone_brick_stairs", - 183 => "polished_diorite_stairs", - 184 => "smooth_sandstone_stairs", - 185 => "quartz_stairs", - 186 => "polished_andesite_stairs", - 187 => "nether_brick_stairs", - _ => panic!("Invalid id"), - } + self.name } + /// Returns the canonical default properties for this block, if any. + /// + /// These defaults represent the block's standard state in Minecraft and + /// are used throughout the codebase as the baseline configuration. Using + /// them ensures consistent behaviour, for example when placing signs whose + /// rotation and waterlogged state start from these defaults before + /// applying overrides. pub fn properties(&self) -> Option { - match self.id { - 3 => Some(Value::Compound({ + match self.name { + "minecraft:birch_leaves" => Some(Value::Compound({ let mut map: HashMap = HashMap::new(); map.insert("persistent".to_string(), Value::String("true".to_string())); map })), - - 49 => Some(Value::Compound({ + "minecraft:oak_leaves" => Some(Value::Compound({ let mut map: HashMap = HashMap::new(); map.insert("persistent".to_string(), Value::String("true".to_string())); map })), - - 105 => Some(Value::Compound({ + "minecraft:carrots" => Some(Value::Compound({ let mut map: HashMap = HashMap::new(); map.insert("age".to_string(), Value::String("7".to_string())); map })), - - 106 => Some(Value::Compound({ - let mut map: HashMap = HashMap::new(); - map.insert("half".to_string(), Value::String("lower".to_string())); - map - })), - - 107 => Some(Value::Compound({ - let mut map: HashMap = HashMap::new(); - map.insert("half".to_string(), Value::String("upper".to_string())); - map - })), - - 108 => Some(Value::Compound({ + "minecraft:potatoes" => Some(Value::Compound({ let mut map: HashMap = HashMap::new(); map.insert("age".to_string(), Value::String("7".to_string())); map })), - - 109 => Some(Value::Compound({ + "minecraft:wheat" => Some(Value::Compound({ let mut map: HashMap = HashMap::new(); map.insert("age".to_string(), Value::String("7".to_string())); map })), - - 113 => Some(Value::Compound({ + "minecraft:oak_sign" => Some(Value::Compound({ let mut map: HashMap = HashMap::new(); map.insert("rotation".to_string(), Value::String("6".to_string())); map.insert( @@ -323,142 +139,7 @@ impl Block { ); map })), - - 116 => Some(Value::Compound({ - let mut map = HashMap::new(); - map.insert( - "shape".to_string(), - Value::String("north_south".to_string()), - ); - map - })), - - 117 => Some(Value::Compound({ - let mut map = HashMap::new(); - map.insert("shape".to_string(), Value::String("east_west".to_string())); - map - })), - - 118 => Some(Value::Compound({ - let mut map = HashMap::new(); - map.insert( - "shape".to_string(), - Value::String("ascending_east".to_string()), - ); - map - })), - - 119 => Some(Value::Compound({ - let mut map = HashMap::new(); - map.insert( - "shape".to_string(), - Value::String("ascending_west".to_string()), - ); - map - })), - - 120 => Some(Value::Compound({ - let mut map = HashMap::new(); - map.insert( - "shape".to_string(), - Value::String("ascending_north".to_string()), - ); - map - })), - - 121 => Some(Value::Compound({ - let mut map = HashMap::new(); - map.insert( - "shape".to_string(), - Value::String("ascending_south".to_string()), - ); - map - })), - - 122 => Some(Value::Compound({ - let mut map = HashMap::new(); - map.insert("shape".to_string(), Value::String("north_east".to_string())); - map - })), - - 123 => Some(Value::Compound({ - let mut map = HashMap::new(); - map.insert("shape".to_string(), Value::String("north_west".to_string())); - map - })), - - 124 => Some(Value::Compound({ - let mut map = HashMap::new(); - map.insert("shape".to_string(), Value::String("south_east".to_string())); - map - })), - - 125 => Some(Value::Compound({ - let mut map = HashMap::new(); - map.insert("shape".to_string(), Value::String("south_west".to_string())); - map - })), - 137 => Some(Value::Compound({ - let mut map = HashMap::new(); - map.insert("half".to_string(), Value::String("lower".to_string())); - map - })), - 138 => Some(Value::Compound({ - let mut map = HashMap::new(); - map.insert("half".to_string(), Value::String("upper".to_string())); - map - })), - - // Red bed variations by direction and part - 161 => Some(Value::Compound({ - let mut map: HashMap = HashMap::new(); - map.insert("facing".to_string(), Value::String("north".to_string())); - map.insert("part".to_string(), Value::String("head".to_string())); - map - })), - 162 => Some(Value::Compound({ - let mut map: HashMap = HashMap::new(); - map.insert("facing".to_string(), Value::String("north".to_string())); - map.insert("part".to_string(), Value::String("foot".to_string())); - map - })), - 163 => Some(Value::Compound({ - let mut map: HashMap = HashMap::new(); - map.insert("facing".to_string(), Value::String("east".to_string())); - map.insert("part".to_string(), Value::String("head".to_string())); - map - })), - 164 => Some(Value::Compound({ - let mut map: HashMap = HashMap::new(); - map.insert("facing".to_string(), Value::String("east".to_string())); - map.insert("part".to_string(), Value::String("foot".to_string())); - map - })), - 165 => Some(Value::Compound({ - let mut map: HashMap = HashMap::new(); - map.insert("facing".to_string(), Value::String("south".to_string())); - map.insert("part".to_string(), Value::String("head".to_string())); - map - })), - 166 => Some(Value::Compound({ - let mut map: HashMap = HashMap::new(); - map.insert("facing".to_string(), Value::String("south".to_string())); - map.insert("part".to_string(), Value::String("foot".to_string())); - map - })), - 167 => Some(Value::Compound({ - let mut map: HashMap = HashMap::new(); - map.insert("facing".to_string(), Value::String("west".to_string())); - map.insert("part".to_string(), Value::String("head".to_string())); - map - })), - 168 => Some(Value::Compound({ - let mut map: HashMap = HashMap::new(); - map.insert("facing".to_string(), Value::String("west".to_string())); - map.insert("part".to_string(), Value::String("foot".to_string())); - map - })), - 173 => Some(Value::Compound({ + "minecraft:oak_trapdoor" => Some(Value::Compound({ let mut map = HashMap::new(); map.insert("half".to_string(), Value::String("top".to_string())); map @@ -468,12 +149,31 @@ impl Block { } } -// Cache for stair blocks with properties -use std::sync::Mutex; +// Cache for dynamically created blocks by string name +static BLOCK_NAME_CACHE: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); +impl Block { + /// Construct a `Block` from an arbitrary namespaced string. + /// + /// The string is stored in a global cache to obtain a `'static` lifetime. + pub fn from_str(name: &str) -> Block { + let mut cache = BLOCK_NAME_CACHE.lock().unwrap(); + if let Some(block) = cache.get(name) { + *block + } else { + let leaked: &'static str = Box::leak(name.to_string().into_boxed_str()); + let block = Block::new(leaked); + cache.insert(name.to_string(), block); + block + } + } +} + +// Cache for stair blocks with properties #[allow(clippy::type_complexity)] -static STAIR_CACHE: Lazy>> = - Lazy::new(|| Mutex::new(HashMap::new())); +static STAIR_CACHE: Lazy> = + Lazy::new(DashMap::new); // General function to create any stair block with facing and shape properties pub fn create_stair_with_properties( @@ -481,14 +181,11 @@ pub fn create_stair_with_properties( facing: StairFacing, shape: StairShape, ) -> BlockWithProperties { - let cache_key = (base_stair_block.id(), facing, shape); + let cache_key = (base_stair_block, facing, shape); // Check cache first - { - let cache = STAIR_CACHE.lock().unwrap(); - if let Some(cached_block) = cache.get(&cache_key) { - return cached_block.clone(); - } + if let Some(cached_block) = STAIR_CACHE.get(&cache_key) { + return cached_block.value().clone(); } // Create properties @@ -510,193 +207,191 @@ pub fn create_stair_with_properties( let block_with_props = BlockWithProperties::new(base_stair_block, Some(properties)); // Cache the result - { - let mut cache = STAIR_CACHE.lock().unwrap(); - cache.insert(cache_key, block_with_props.clone()); - } + STAIR_CACHE.insert(cache_key, block_with_props.clone()); block_with_props } // Lazy static blocks -pub const ACACIA_PLANKS: Block = Block::new(0); -pub const AIR: Block = Block::new(1); -pub const ANDESITE: Block = Block::new(2); -pub const BIRCH_LEAVES: Block = Block::new(3); -pub const BIRCH_LOG: Block = Block::new(4); -pub const BLACK_CONCRETE: Block = Block::new(5); -pub const BLACKSTONE: Block = Block::new(6); -pub const BLUE_FLOWER: Block = Block::new(7); -pub const BLUE_TERRACOTTA: Block = Block::new(8); -pub const BRICK: Block = Block::new(9); -pub const CAULDRON: Block = Block::new(10); -pub const CHISELED_STONE_BRICKS: Block = Block::new(11); -pub const COBBLESTONE_WALL: Block = Block::new(12); -pub const COBBLESTONE: Block = Block::new(13); -pub const POLISHED_BLACKSTONE_BRICKS: Block = Block::new(14); -pub const CRACKED_STONE_BRICKS: Block = Block::new(15); -pub const CRIMSON_PLANKS: Block = Block::new(16); -pub const CUT_SANDSTONE: Block = Block::new(17); -pub const CYAN_CONCRETE: Block = Block::new(18); -pub const DARK_OAK_PLANKS: Block = Block::new(19); -pub const DEEPSLATE_BRICKS: Block = Block::new(20); -pub const DIORITE: Block = Block::new(21); -pub const DIRT: Block = Block::new(22); -pub const END_STONE_BRICKS: Block = Block::new(23); -pub const FARMLAND: Block = Block::new(24); -pub const GLASS: Block = Block::new(25); -pub const GLOWSTONE: Block = Block::new(26); -pub const GRANITE: Block = Block::new(27); -pub const GRASS_BLOCK: Block = Block::new(28); -pub const GRASS: Block = Block::new(29); -pub const GRAVEL: Block = Block::new(30); -pub const GRAY_CONCRETE: Block = Block::new(31); -pub const GRAY_TERRACOTTA: Block = Block::new(32); -pub const GREEN_STAINED_HARDENED_CLAY: Block = Block::new(33); -pub const GREEN_WOOL: Block = Block::new(34); -pub const HAY_BALE: Block = Block::new(35); -pub const IRON_BARS: Block = Block::new(36); -pub const IRON_BLOCK: Block = Block::new(37); -pub const JUNGLE_PLANKS: Block = Block::new(38); -pub const LADDER: Block = Block::new(39); -pub const LIGHT_BLUE_CONCRETE: Block = Block::new(40); -pub const LIGHT_BLUE_TERRACOTTA: Block = Block::new(41); -pub const LIGHT_GRAY_CONCRETE: Block = Block::new(42); -pub const MOSS_BLOCK: Block = Block::new(43); -pub const MOSSY_COBBLESTONE: Block = Block::new(44); -pub const MUD_BRICKS: Block = Block::new(45); -pub const NETHER_BRICK: Block = Block::new(46); -pub const NETHERITE_BLOCK: Block = Block::new(47); -pub const OAK_FENCE: Block = Block::new(48); -pub const OAK_LEAVES: Block = Block::new(49); -pub const OAK_LOG: Block = Block::new(50); -pub const OAK_PLANKS: Block = Block::new(51); -pub const OAK_SLAB: Block = Block::new(52); -pub const ORANGE_TERRACOTTA: Block = Block::new(53); -pub const PODZOL: Block = Block::new(54); -pub const POLISHED_ANDESITE: Block = Block::new(55); -pub const POLISHED_BASALT: Block = Block::new(56); -pub const QUARTZ_BLOCK: Block = Block::new(57); -pub const POLISHED_BLACKSTONE: Block = Block::new(58); -pub const POLISHED_DEEPSLATE: Block = Block::new(59); -pub const POLISHED_DIORITE: Block = Block::new(60); -pub const POLISHED_GRANITE: Block = Block::new(61); -pub const PRISMARINE: Block = Block::new(62); -pub const PURPUR_BLOCK: Block = Block::new(63); -pub const PURPUR_PILLAR: Block = Block::new(64); -pub const QUARTZ_BRICKS: Block = Block::new(65); -pub const RAIL: Block = Block::new(66); -pub const RED_FLOWER: Block = Block::new(67); -pub const RED_NETHER_BRICK: Block = Block::new(68); -pub const RED_TERRACOTTA: Block = Block::new(69); -pub const RED_WOOL: Block = Block::new(70); -pub const SAND: Block = Block::new(71); -pub const SANDSTONE: Block = Block::new(72); -pub const SCAFFOLDING: Block = Block::new(73); -pub const SMOOTH_QUARTZ: Block = Block::new(74); -pub const SMOOTH_RED_SANDSTONE: Block = Block::new(75); -pub const SMOOTH_SANDSTONE: Block = Block::new(76); -pub const SMOOTH_STONE: Block = Block::new(77); -pub const SPONGE: Block = Block::new(78); -pub const SPRUCE_LOG: Block = Block::new(79); -pub const SPRUCE_PLANKS: Block = Block::new(80); -pub const STONE_BLOCK_SLAB: Block = Block::new(81); -pub const STONE_BRICK_SLAB: Block = Block::new(82); -pub const STONE_BRICKS: Block = Block::new(83); -pub const STONE: Block = Block::new(84); -pub const TERRACOTTA: Block = Block::new(85); -pub const WARPED_PLANKS: Block = Block::new(86); -pub const WATER: Block = Block::new(87); -pub const WHITE_CONCRETE: Block = Block::new(88); -pub const WHITE_FLOWER: Block = Block::new(89); -pub const WHITE_STAINED_GLASS: Block = Block::new(90); -pub const WHITE_TERRACOTTA: Block = Block::new(91); -pub const WHITE_WOOL: Block = Block::new(92); -pub const YELLOW_CONCRETE: Block = Block::new(93); -pub const YELLOW_FLOWER: Block = Block::new(94); -pub const YELLOW_WOOL: Block = Block::new(95); -pub const LIME_CONCRETE: Block = Block::new(96); -pub const CYAN_WOOL: Block = Block::new(97); -pub const BLUE_CONCRETE: Block = Block::new(98); -pub const PURPLE_CONCRETE: Block = Block::new(99); -pub const RED_CONCRETE: Block = Block::new(100); -pub const MAGENTA_CONCRETE: Block = Block::new(101); -pub const BROWN_WOOL: Block = Block::new(102); -pub const OXIDIZED_COPPER: Block = Block::new(103); -pub const YELLOW_TERRACOTTA: Block = Block::new(104); -pub const SNOW_BLOCK: Block = Block::new(111); -pub const SNOW_LAYER: Block = Block::new(112); -pub const SIGN: Block = Block::new(113); -pub const ANDESITE_WALL: Block = Block::new(114); -pub const STONE_BRICK_WALL: Block = Block::new(115); -pub const CARROTS: Block = Block::new(105); -pub const DARK_OAK_DOOR_LOWER: Block = Block::new(106); -pub const DARK_OAK_DOOR_UPPER: Block = Block::new(107); -pub const POTATOES: Block = Block::new(108); -pub const WHEAT: Block = Block::new(109); -pub const BEDROCK: Block = Block::new(110); -pub const RAIL_NORTH_SOUTH: Block = Block::new(116); -pub const RAIL_EAST_WEST: Block = Block::new(117); -pub const RAIL_ASCENDING_EAST: Block = Block::new(118); -pub const RAIL_ASCENDING_WEST: Block = Block::new(119); -pub const RAIL_ASCENDING_NORTH: Block = Block::new(120); -pub const RAIL_ASCENDING_SOUTH: Block = Block::new(121); -pub const RAIL_NORTH_EAST: Block = Block::new(122); -pub const RAIL_NORTH_WEST: Block = Block::new(123); -pub const RAIL_SOUTH_EAST: Block = Block::new(124); -pub const RAIL_SOUTH_WEST: Block = Block::new(125); -pub const COARSE_DIRT: Block = Block::new(126); -pub const IRON_ORE: Block = Block::new(127); -pub const COAL_ORE: Block = Block::new(128); -pub const GOLD_ORE: Block = Block::new(129); -pub const COPPER_ORE: Block = Block::new(130); -pub const CLAY: Block = Block::new(131); -pub const DIRT_PATH: Block = Block::new(132); -pub const ICE: Block = Block::new(133); -pub const PACKED_ICE: Block = Block::new(134); -pub const MUD: Block = Block::new(135); -pub const DEAD_BUSH: Block = Block::new(136); -pub const TALL_GRASS_BOTTOM: Block = Block::new(137); -pub const TALL_GRASS_TOP: Block = Block::new(138); -pub const CRAFTING_TABLE: Block = Block::new(139); -pub const FURNACE: Block = Block::new(140); -pub const WHITE_CARPET: Block = Block::new(141); -pub const BOOKSHELF: Block = Block::new(142); -pub const OAK_PRESSURE_PLATE: Block = Block::new(143); -pub const OAK_STAIRS: Block = Block::new(144); -pub const CHEST: Block = Block::new(155); -pub const RED_CARPET: Block = Block::new(156); -pub const ANVIL: Block = Block::new(157); -pub const NOTE_BLOCK: Block = Block::new(158); -pub const OAK_DOOR: Block = Block::new(159); -pub const BREWING_STAND: Block = Block::new(160); -pub const RED_BED_NORTH_HEAD: Block = Block::new(161); -pub const RED_BED_NORTH_FOOT: Block = Block::new(162); -pub const RED_BED_EAST_HEAD: Block = Block::new(163); -pub const RED_BED_EAST_FOOT: Block = Block::new(164); -pub const RED_BED_SOUTH_HEAD: Block = Block::new(165); -pub const RED_BED_SOUTH_FOOT: Block = Block::new(166); -pub const RED_BED_WEST_HEAD: Block = Block::new(167); -pub const RED_BED_WEST_FOOT: Block = Block::new(168); -pub const GRAY_STAINED_GLASS: Block = Block::new(169); -pub const LIGHT_GRAY_STAINED_GLASS: Block = Block::new(170); -pub const BROWN_STAINED_GLASS: Block = Block::new(171); -pub const TINTED_GLASS: Block = Block::new(172); -pub const OAK_TRAPDOOR: Block = Block::new(173); -pub const BROWN_CONCRETE: Block = Block::new(174); -pub const BLACK_TERRACOTTA: Block = Block::new(175); -pub const BROWN_TERRACOTTA: Block = Block::new(176); -pub const STONE_BRICK_STAIRS: Block = Block::new(177); -pub const MUD_BRICK_STAIRS: Block = Block::new(178); -pub const POLISHED_BLACKSTONE_BRICK_STAIRS: Block = Block::new(179); -pub const BRICK_STAIRS: Block = Block::new(180); -pub const POLISHED_GRANITE_STAIRS: Block = Block::new(181); -pub const END_STONE_BRICK_STAIRS: Block = Block::new(182); -pub const POLISHED_DIORITE_STAIRS: Block = Block::new(183); -pub const SMOOTH_SANDSTONE_STAIRS: Block = Block::new(184); -pub const QUARTZ_STAIRS: Block = Block::new(185); -pub const POLISHED_ANDESITE_STAIRS: Block = Block::new(186); -pub const NETHER_BRICK_STAIRS: Block = Block::new(187); +pub const ACACIA_PLANKS: Block = Block::new("minecraft:acacia_planks"); +pub const AIR: Block = Block::new("minecraft:air"); +pub const ANDESITE: Block = Block::new("minecraft:andesite"); +pub const BIRCH_LEAVES: Block = Block::new("minecraft:birch_leaves"); +pub const BIRCH_LOG: Block = Block::new("minecraft:birch_log"); +pub const BLACK_CONCRETE: Block = Block::new("minecraft:black_concrete"); +pub const BLACKSTONE: Block = Block::new("minecraft:blackstone"); +pub const BLUE_FLOWER: Block = Block::new("minecraft:blue_orchid"); +pub const BLUE_TERRACOTTA: Block = Block::new("minecraft:blue_terracotta"); +pub const BRICK: Block = Block::new("minecraft:bricks"); +pub const CAULDRON: Block = Block::new("minecraft:cauldron"); +pub const CHISELED_STONE_BRICKS: Block = Block::new("minecraft:chiseled_stone_bricks"); +pub const COBBLESTONE_WALL: Block = Block::new("minecraft:cobblestone_wall"); +pub const COBBLESTONE: Block = Block::new("minecraft:cobblestone"); +pub const POLISHED_BLACKSTONE_BRICKS: Block = Block::new("minecraft:polished_blackstone_bricks"); +pub const CRACKED_STONE_BRICKS: Block = Block::new("minecraft:cracked_stone_bricks"); +pub const CRIMSON_PLANKS: Block = Block::new("minecraft:crimson_planks"); +pub const CUT_SANDSTONE: Block = Block::new("minecraft:cut_sandstone"); +pub const CYAN_CONCRETE: Block = Block::new("minecraft:cyan_concrete"); +pub const DARK_OAK_PLANKS: Block = Block::new("minecraft:dark_oak_planks"); +pub const DEEPSLATE_BRICKS: Block = Block::new("minecraft:deepslate_bricks"); +pub const DIORITE: Block = Block::new("minecraft:diorite"); +pub const DIRT: Block = Block::new("minecraft:dirt"); +pub const END_STONE_BRICKS: Block = Block::new("minecraft:end_stone_bricks"); +pub const FARMLAND: Block = Block::new("minecraft:farmland"); +pub const GLASS: Block = Block::new("minecraft:glass"); +pub const GLOWSTONE: Block = Block::new("minecraft:glowstone"); +pub const GRANITE: Block = Block::new("minecraft:granite"); +pub const GRASS_BLOCK: Block = Block::new("minecraft:grass_block"); +pub const GRASS: Block = Block::new("minecraft:short_grass"); +pub const GRAVEL: Block = Block::new("minecraft:gravel"); +pub const GRAY_CONCRETE: Block = Block::new("minecraft:gray_concrete"); +pub const GRAY_TERRACOTTA: Block = Block::new("minecraft:gray_terracotta"); +pub const GREEN_STAINED_HARDENED_CLAY: Block = Block::new("minecraft:green_terracotta"); +pub const GREEN_WOOL: Block = Block::new("minecraft:green_wool"); +pub const HAY_BALE: Block = Block::new("minecraft:hay_block"); +pub const IRON_BARS: Block = Block::new("minecraft:iron_bars"); +pub const IRON_BLOCK: Block = Block::new("minecraft:iron_block"); +pub const JUNGLE_PLANKS: Block = Block::new("minecraft:jungle_planks"); +pub const LADDER: Block = Block::new("minecraft:ladder"); +pub const LIGHT_BLUE_CONCRETE: Block = Block::new("minecraft:light_blue_concrete"); +pub const LIGHT_BLUE_TERRACOTTA: Block = Block::new("minecraft:light_blue_terracotta"); +pub const LIGHT_GRAY_CONCRETE: Block = Block::new("minecraft:light_gray_concrete"); +pub const MOSS_BLOCK: Block = Block::new("minecraft:moss_block"); +pub const MOSSY_COBBLESTONE: Block = Block::new("minecraft:mossy_cobblestone"); +pub const MUD_BRICKS: Block = Block::new("minecraft:mud_bricks"); +pub const NETHER_BRICK: Block = Block::new("minecraft:nether_bricks"); +pub const NETHERITE_BLOCK: Block = Block::new("minecraft:netherite_block"); +pub const OAK_FENCE: Block = Block::new("minecraft:oak_fence"); +pub const OAK_LEAVES: Block = Block::new("minecraft:oak_leaves"); +pub const OAK_LOG: Block = Block::new("minecraft:oak_log"); +pub const OAK_PLANKS: Block = Block::new("minecraft:oak_planks"); +pub const OAK_SLAB: Block = Block::new("minecraft:oak_slab"); +pub const ORANGE_TERRACOTTA: Block = Block::new("minecraft:orange_terracotta"); +pub const PODZOL: Block = Block::new("minecraft:podzol"); +pub const POLISHED_ANDESITE: Block = Block::new("minecraft:polished_andesite"); +pub const POLISHED_BASALT: Block = Block::new("minecraft:polished_basalt"); +pub const QUARTZ_BLOCK: Block = Block::new("minecraft:quartz_block"); +pub const POLISHED_BLACKSTONE: Block = Block::new("minecraft:polished_blackstone"); +pub const POLISHED_DEEPSLATE: Block = Block::new("minecraft:polished_deepslate"); +pub const POLISHED_DIORITE: Block = Block::new("minecraft:polished_diorite"); +pub const POLISHED_GRANITE: Block = Block::new("minecraft:polished_granite"); +pub const PRISMARINE: Block = Block::new("minecraft:prismarine"); +pub const PURPUR_BLOCK: Block = Block::new("minecraft:purpur_block"); +pub const PURPUR_PILLAR: Block = Block::new("minecraft:purpur_pillar"); +pub const QUARTZ_BRICKS: Block = Block::new("minecraft:quartz_bricks"); +pub const RAIL: Block = Block::new("minecraft:rail"); +pub const RED_FLOWER: Block = Block::new("minecraft:poppy"); +pub const RED_NETHER_BRICK: Block = Block::new("minecraft:red_nether_bricks"); +pub const RED_TERRACOTTA: Block = Block::new("minecraft:red_terracotta"); +pub const RED_WOOL: Block = Block::new("minecraft:red_wool"); +pub const SAND: Block = Block::new("minecraft:sand"); +pub const SANDSTONE: Block = Block::new("minecraft:sandstone"); +pub const SCAFFOLDING: Block = Block::new("minecraft:scaffolding"); +pub const SMOOTH_QUARTZ: Block = Block::new("minecraft:smooth_quartz"); +pub const SMOOTH_RED_SANDSTONE: Block = Block::new("minecraft:smooth_red_sandstone"); +pub const SMOOTH_SANDSTONE: Block = Block::new("minecraft:smooth_sandstone"); +pub const SMOOTH_STONE: Block = Block::new("minecraft:smooth_stone"); +pub const SPONGE: Block = Block::new("minecraft:sponge"); +pub const SPRUCE_LOG: Block = Block::new("minecraft:spruce_log"); +pub const SPRUCE_PLANKS: Block = Block::new("minecraft:spruce_planks"); +pub const STONE_BLOCK_SLAB: Block = Block::new("minecraft:stone_slab"); +pub const STONE_BRICK_SLAB: Block = Block::new("minecraft:stone_brick_slab"); +pub const STONE_BRICKS: Block = Block::new("minecraft:stone_bricks"); +pub const STONE: Block = Block::new("minecraft:stone"); +pub const TERRACOTTA: Block = Block::new("minecraft:terracotta"); +pub const WARPED_PLANKS: Block = Block::new("minecraft:warped_planks"); +pub const WATER: Block = Block::new("minecraft:water"); +pub const WHITE_CONCRETE: Block = Block::new("minecraft:white_concrete"); +pub const WHITE_FLOWER: Block = Block::new("minecraft:azure_bluet"); +pub const WHITE_STAINED_GLASS: Block = Block::new("minecraft:white_stained_glass"); +pub const WHITE_TERRACOTTA: Block = Block::new("minecraft:white_terracotta"); +pub const WHITE_WOOL: Block = Block::new("minecraft:white_wool"); +pub const YELLOW_CONCRETE: Block = Block::new("minecraft:yellow_concrete"); +pub const YELLOW_FLOWER: Block = Block::new("minecraft:dandelion"); +pub const YELLOW_WOOL: Block = Block::new("minecraft:yellow_wool"); +pub const LIME_CONCRETE: Block = Block::new("minecraft:lime_concrete"); +pub const CYAN_WOOL: Block = Block::new("minecraft:cyan_wool"); +pub const BLUE_CONCRETE: Block = Block::new("minecraft:blue_concrete"); +pub const PURPLE_CONCRETE: Block = Block::new("minecraft:purple_concrete"); +pub const RED_CONCRETE: Block = Block::new("minecraft:red_concrete"); +pub const MAGENTA_CONCRETE: Block = Block::new("minecraft:magenta_concrete"); +pub const BROWN_WOOL: Block = Block::new("minecraft:brown_wool"); +pub const OXIDIZED_COPPER: Block = Block::new("minecraft:oxidized_copper"); +pub const YELLOW_TERRACOTTA: Block = Block::new("minecraft:yellow_terracotta"); +pub const SNOW_BLOCK: Block = Block::new("minecraft:snow_block"); +pub const SNOW_LAYER: Block = Block::new("minecraft:snow"); +pub const SIGN: Block = Block::new("minecraft:oak_sign"); +pub const ANDESITE_WALL: Block = Block::new("minecraft:andesite_wall"); +pub const STONE_BRICK_WALL: Block = Block::new("minecraft:stone_brick_wall"); +pub const CARROTS: Block = Block::new("minecraft:carrots"); +pub const DARK_OAK_DOOR_LOWER: Block = Block::new("minecraft:dark_oak_door"); +pub const DARK_OAK_DOOR_UPPER: Block = Block::new("minecraft:dark_oak_door"); +pub const POTATOES: Block = Block::new("minecraft:potatoes"); +pub const WHEAT: Block = Block::new("minecraft:wheat"); +pub const BEDROCK: Block = Block::new("minecraft:bedrock"); +pub const RAIL_NORTH_SOUTH: Block = Block::new("minecraft:rail"); +pub const RAIL_EAST_WEST: Block = Block::new("minecraft:rail"); +pub const RAIL_ASCENDING_EAST: Block = Block::new("minecraft:rail"); +pub const RAIL_ASCENDING_WEST: Block = Block::new("minecraft:rail"); +pub const RAIL_ASCENDING_NORTH: Block = Block::new("minecraft:rail"); +pub const RAIL_ASCENDING_SOUTH: Block = Block::new("minecraft:rail"); +pub const RAIL_NORTH_EAST: Block = Block::new("minecraft:rail"); +pub const RAIL_NORTH_WEST: Block = Block::new("minecraft:rail"); +pub const RAIL_SOUTH_EAST: Block = Block::new("minecraft:rail"); +pub const RAIL_SOUTH_WEST: Block = Block::new("minecraft:rail"); +pub const COARSE_DIRT: Block = Block::new("minecraft:coarse_dirt"); +pub const IRON_ORE: Block = Block::new("minecraft:iron_ore"); +pub const COAL_ORE: Block = Block::new("minecraft:coal_ore"); +pub const GOLD_ORE: Block = Block::new("minecraft:gold_ore"); +pub const COPPER_ORE: Block = Block::new("minecraft:copper_ore"); +pub const CLAY: Block = Block::new("minecraft:clay"); +pub const DIRT_PATH: Block = Block::new("minecraft:dirt_path"); +pub const ICE: Block = Block::new("minecraft:ice"); +pub const PACKED_ICE: Block = Block::new("minecraft:packed_ice"); +pub const MUD: Block = Block::new("minecraft:mud"); +pub const DEAD_BUSH: Block = Block::new("minecraft:dead_bush"); +pub const TALL_GRASS_BOTTOM: Block = Block::new("minecraft:tall_grass"); +pub const TALL_GRASS_TOP: Block = Block::new("minecraft:tall_grass"); +pub const CRAFTING_TABLE: Block = Block::new("minecraft:crafting_table"); +pub const FURNACE: Block = Block::new("minecraft:furnace"); +pub const WHITE_CARPET: Block = Block::new("minecraft:white_carpet"); +pub const BOOKSHELF: Block = Block::new("minecraft:bookshelf"); +pub const OAK_PRESSURE_PLATE: Block = Block::new("minecraft:oak_pressure_plate"); +pub const OAK_STAIRS: Block = Block::new("minecraft:oak_stairs"); +pub const CHEST: Block = Block::new("minecraft:chest"); +pub const RED_CARPET: Block = Block::new("minecraft:red_carpet"); +pub const ANVIL: Block = Block::new("minecraft:anvil"); +pub const NOTE_BLOCK: Block = Block::new("minecraft:note_block"); +pub const OAK_DOOR: Block = Block::new("minecraft:oak_door"); +pub const BREWING_STAND: Block = Block::new("minecraft:brewing_stand"); +pub const RED_BED_NORTH_HEAD: Block = Block::new("minecraft:red_bed"); +pub const RED_BED_NORTH_FOOT: Block = Block::new("minecraft:red_bed"); +pub const RED_BED_EAST_HEAD: Block = Block::new("minecraft:red_bed"); +pub const RED_BED_EAST_FOOT: Block = Block::new("minecraft:red_bed"); +pub const RED_BED_SOUTH_HEAD: Block = Block::new("minecraft:red_bed"); +pub const RED_BED_SOUTH_FOOT: Block = Block::new("minecraft:red_bed"); +pub const RED_BED_WEST_HEAD: Block = Block::new("minecraft:red_bed"); +pub const RED_BED_WEST_FOOT: Block = Block::new("minecraft:red_bed"); +pub const GRAY_STAINED_GLASS: Block = Block::new("minecraft:gray_stained_glass"); +pub const LIGHT_GRAY_STAINED_GLASS: Block = Block::new("minecraft:light_gray_stained_glass"); +pub const BROWN_STAINED_GLASS: Block = Block::new("minecraft:brown_stained_glass"); +pub const TINTED_GLASS: Block = Block::new("minecraft:tinted_glass"); +pub const OAK_TRAPDOOR: Block = Block::new("minecraft:oak_trapdoor"); +pub const BROWN_CONCRETE: Block = Block::new("minecraft:brown_concrete"); +pub const BLACK_TERRACOTTA: Block = Block::new("minecraft:black_terracotta"); +pub const BROWN_TERRACOTTA: Block = Block::new("minecraft:brown_terracotta"); +pub const STONE_BRICK_STAIRS: Block = Block::new("minecraft:stone_brick_stairs"); +pub const MUD_BRICK_STAIRS: Block = Block::new("minecraft:mud_brick_stairs"); +pub const POLISHED_BLACKSTONE_BRICK_STAIRS: Block = + Block::new("minecraft:polished_blackstone_brick_stairs"); +pub const BRICK_STAIRS: Block = Block::new("minecraft:brick_stairs"); +pub const POLISHED_GRANITE_STAIRS: Block = Block::new("minecraft:polished_granite_stairs"); +pub const END_STONE_BRICK_STAIRS: Block = Block::new("minecraft:end_stone_brick_stairs"); +pub const POLISHED_DIORITE_STAIRS: Block = Block::new("minecraft:polished_diorite_stairs"); +pub const SMOOTH_SANDSTONE_STAIRS: Block = Block::new("minecraft:smooth_sandstone_stairs"); +pub const QUARTZ_STAIRS: Block = Block::new("minecraft:quartz_stairs"); +pub const POLISHED_ANDESITE_STAIRS: Block = Block::new("minecraft:polished_andesite_stairs"); +pub const NETHER_BRICK_STAIRS: Block = Block::new("minecraft:nether_brick_stairs"); /// Maps a block to its corresponding stair variant #[inline] @@ -1005,3 +700,19 @@ pub fn get_castle_wall_block() -> Block { ]; castle_wall_options[rng.gen_range(0..castle_wall_options.len())] } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn block_new_stores_namespace_qualified_name() { + let block = Block::new("minecraft:oak_planks"); + assert_eq!(block.name(), "minecraft:oak_planks"); + } + + #[test] + fn block_constant_returns_namespaced_name() { + assert_eq!(OAK_PLANKS.name(), "minecraft:oak_planks"); + } +} diff --git a/src/data_processing.rs b/src/data_processing.rs index 8c094698b..b4630e782 100644 --- a/src/data_processing.rs +++ b/src/data_processing.rs @@ -6,7 +6,7 @@ use crate::element_processing::*; use crate::ground::Ground; use crate::osm_parser::ProcessedElement; use crate::progress::emit_gui_progress_update; -use crate::world_editor::WorldEditor; +use crate::world_editor::{format_sign_text, WorldEditor}; use colored::Colorize; use indicatif::{ProgressBar, ProgressStyle}; @@ -203,16 +203,9 @@ pub fn generate_world( } // Set sign for player orientation - /*editor.set_sign( - "↑".to_string(), - "Generated World".to_string(), - "This direction".to_string(), - "".to_string(), - 9, - -61, - 9, - 6, - );*/ + let (line1, line2, line3, line4) = format_sign_text("↑\nGenerated World\nThis direction\n"); + let sign_y = editor.get_absolute_y(9, -61, 9); + editor.set_sign(line1, line2, line3, line4, 9, sign_y, 9); ground_pb.inc(block_counter % batch_size); ground_pb.finish(); diff --git a/src/element_processing/highways.rs b/src/element_processing/highways.rs index 5c94c9f64..323640f9b 100644 --- a/src/element_processing/highways.rs +++ b/src/element_processing/highways.rs @@ -4,7 +4,7 @@ use crate::bresenham::bresenham_line; use crate::coordinate_system::cartesian::XZPoint; use crate::floodfill::flood_fill_area; use crate::osm_parser::{ProcessedElement, ProcessedWay}; -use crate::world_editor::WorldEditor; +use crate::world_editor::WorldEditor; //format_sign_text use std::collections::HashMap; /// Generates highways with elevation support based on layer tags and connectivity analysis @@ -451,6 +451,71 @@ fn generate_highways_internal( } previous_node = Some((node.x, node.z)); } + + /* Add signs for named highways every 200 meters + if let Some(name) = element.tags().get("name") { + let mut prev_node: Option<&crate::osm_parser::ProcessedNode> = None; + let sign_interval = (200.0 * args.scale).max(1.0); + let mut distance_since_sign = 0.0; + let mut sign_placed = false; + + for node in &way.nodes { + if let Some(start) = prev_node { + let dx_seg = node.x - start.x; + let dz_seg = node.z - start.z; + let side_dx = -dz_seg.signum(); + let side_dz = dx_seg.signum(); + + let bres_points = bresenham_line(start.x, 0, start.z, node.x, 0, node.z); + let mut prev_point = (start.x, start.z); + for (x, _, z) in bres_points.into_iter().skip(1) { + let step = (((x - prev_point.0).pow(2) + (z - prev_point.1).pow(2)) + as f64) + .sqrt(); + distance_since_sign += step; + if distance_since_sign >= sign_interval { + let sign_x = x + side_dx * (block_range + 1); + let sign_z = z + side_dz * (block_range + 1); + let (min_x, min_z) = editor.get_min_coords(); + let (max_x, max_z) = editor.get_max_coords(); + let sign_y = editor.get_absolute_y(sign_x, 1, sign_z); + if sign_x >= min_x + && sign_x <= max_x + && sign_z >= min_z + && sign_z <= max_z + { + let (l1, l2, l3, l4) = format_sign_text(name); + editor.set_sign(l1, l2, l3, l4, sign_x, sign_y, sign_z); + sign_placed = true; + } + distance_since_sign = 0.0; + } + prev_point = (x, z); + } + } + prev_node = Some(node); + } + + if !sign_placed { + if let (Some(start), Some(next)) = (way.nodes.first(), way.nodes.get(1)) { + let dx_seg = next.x - start.x; + let dz_seg = next.z - start.z; + let side_dx = -dz_seg.signum(); + let side_dz = dx_seg.signum(); + let sign_x = start.x + side_dx * (block_range + 1); + let sign_z = start.z + side_dz * (block_range + 1); + let (min_x, min_z) = editor.get_min_coords(); + let (max_x, max_z) = editor.get_max_coords(); + let sign_y = editor.get_absolute_y(sign_x, 1, sign_z); + if sign_x >= min_x && sign_x <= max_x && sign_z >= min_z && sign_z <= max_z + { + let (l1, l2, l3, l4) = format_sign_text(name); + editor.set_sign(l1, l2, l3, l4, sign_x, sign_y, sign_z); + } + } + } + } + */ } } } @@ -636,3 +701,130 @@ pub fn generate_aeroway(editor: &mut WorldEditor, way: &ProcessedWay, args: &Arg previous_node = Some((node.x, node.z)); } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::args::Args; + use crate::block_definitions::SIGN; + use crate::coordinate_system::cartesian::XZBBox; + use crate::coordinate_system::geographic::LLBBox; + use crate::osm_parser::{ProcessedElement, ProcessedNode, ProcessedWay}; + use crate::world_editor::WorldEditor; + use std::collections::HashMap; + use tempfile::tempdir; + + #[test] + fn places_signs_every_200_meters() { + let bbox = XZBBox::rect_from_xz_lengths(1010.0, 20.0).unwrap(); + let tmp = tempdir().unwrap(); + let region_dir = tmp.path().join("region"); + std::fs::create_dir(®ion_dir).unwrap(); + let llbbox = LLBBox::new(0., 0., 1., 1.).unwrap(); + let mut editor = WorldEditor::new(region_dir.clone(), &bbox, llbbox); + + let args = Args { + bbox: llbbox, + file: None, + save_json_file: None, + path: tmp.path().to_path_buf(), + downloader: "requests".to_string(), + scale: 1.0, + ground_level: -62, + terrain: false, + interior: true, + roof: true, + fillground: false, + debug: false, + timeout: None, + spawn_point: None, + }; + + let nodes = vec![ + ProcessedNode { + id: 1, + tags: HashMap::new(), + x: 0, + z: 0, + }, + ProcessedNode { + id: 2, + tags: HashMap::new(), + x: 1000, + z: 0, + }, + ]; + let mut tags = HashMap::new(); + tags.insert("highway".to_string(), "primary".to_string()); + tags.insert("name".to_string(), "First St.".to_string()); + let way = ProcessedWay { id: 1, nodes, tags }; + let element = ProcessedElement::Way(way); + + let all = [element.clone()]; + generate_highways(&mut editor, &element, &args, &all); + + for x in [200, 400, 600, 800, 1000] { + assert!(editor.check_for_block(x, 1, 6, Some(&[SIGN]))); + } + assert!(!editor.check_for_block(100, 1, 6, Some(&[SIGN]))); + } + + #[test] + fn short_roads_get_a_sign() { + let bbox = XZBBox::rect_from_xz_lengths(110.0, 20.0).unwrap(); + let tmp = tempdir().unwrap(); + let region_dir = tmp.path().join("region"); + std::fs::create_dir(®ion_dir).unwrap(); + let llbbox = LLBBox::new(0., 0., 1., 1.).unwrap(); + let mut editor = WorldEditor::new(region_dir.clone(), &bbox, llbbox); + + let args = Args { + bbox: llbbox, + file: None, + save_json_file: None, + path: tmp.path().to_path_buf(), + downloader: "requests".to_string(), + scale: 1.0, + ground_level: -62, + terrain: false, + interior: true, + roof: true, + fillground: false, + debug: false, + timeout: None, + spawn_point: None, + }; + + let nodes = vec![ + ProcessedNode { + id: 1, + tags: HashMap::new(), + x: 0, + z: 0, + }, + ProcessedNode { + id: 2, + tags: HashMap::new(), + x: 100, + z: 0, + }, + ]; + let mut tags = HashMap::new(); + tags.insert("highway".to_string(), "primary".to_string()); + tags.insert("name".to_string(), "Short St.".to_string()); + let way = ProcessedWay { id: 1, nodes, tags }; + let element = ProcessedElement::Way(way); + + let all = [element.clone()]; + generate_highways(&mut editor, &element, &args, &all); + + let mut found = false; + for x in 0..=100 { + if editor.check_for_block(x, 1, 6, Some(&[SIGN])) { + found = true; + break; + } + } + assert!(found); + } +} diff --git a/src/gui.rs b/src/gui.rs index 80155f69b..7ed1018c7 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -53,7 +53,7 @@ impl SessionLock { impl Drop for SessionLock { fn drop(&mut self) { // Release the lock and remove the session.lock file - let _ = self.file.unlock(); + let _ = fs2::FileExt::unlock(&self.file); let _ = fs::remove_file(&self.path); } } diff --git a/src/world_editor.rs b/src/world_editor.rs index 6aa174c35..3b9334554 100644 --- a/src/world_editor.rs +++ b/src/world_editor.rs @@ -5,7 +5,7 @@ use crate::ground::Ground; use crate::progress::emit_gui_progress_update; use colored::Colorize; use fastanvil::Region; -use fastnbt::{LongArray, Value}; +use fastnbt::{ByteArray, LongArray, Value}; use fnv::FnvHashMap; use indicatif::{ProgressBar, ProgressStyle}; use rayon::prelude::*; @@ -16,6 +16,62 @@ use std::io::Write; use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; +const DATA_VERSION: i32 = 3700; + +#[inline] +fn canonicalize_name_in_place(s: &mut String) { + if !s.as_bytes().contains(&b':') { + s.insert_str(0, "minecraft:"); + } +} + +#[inline] +fn json_text_message(s: &str) -> String { + let mut out = String::with_capacity(s.len() + 10); + out.push_str("{\"text\":\""); + for ch in s.chars() { + match ch { + '\\' => out.push_str("\\\\"), + '"' => out.push_str("\\\""), + _ => out.push(ch), + } + } + out.push_str("\"}"); + out +} + +/// Formats a single text string into four lines suitable for Minecraft signs. +/// Each line is limited to 15 characters. Excess text is wrapped to the next +/// line and anything beyond four lines is truncated. +pub fn format_sign_text(text: &str) -> (String, String, String, String) { + let mut lines: Vec = text + .split('\n') + .flat_map(|segment| { + let chars: Vec = segment.chars().collect(); + if chars.is_empty() { + vec![String::new()] + } else { + chars + .chunks(15) + .map(|chunk| chunk.iter().collect::()) + .collect::>() + } + }) + .collect(); + + lines.truncate(4); + while lines.len() < 4 { + lines.push(String::new()); + } + + ( + lines[0].clone(), + lines[1].clone(), + lines[2].clone(), + lines[3].clone(), + ) +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct Chunk { @@ -33,6 +89,10 @@ struct Section { block_states: Blockstates, #[serde(rename = "Y")] y: i8, + #[serde(default)] + sky_light: Option, + #[serde(default)] + block_light: Option, #[serde(flatten)] other: FnvHashMap, } @@ -160,6 +220,8 @@ impl SectionToModify { other: FnvHashMap::default(), }, y, + sky_light: None, + block_light: None, other: FnvHashMap::default(), } } @@ -403,6 +465,7 @@ impl<'a> WorldEditor<'a> { self.world.get_block(x, absolute_y, z).is_some() } + /// Places a sign at an absolute Y coordinate. #[allow(clippy::too_many_arguments, dead_code)] pub fn set_sign( &mut self, @@ -413,9 +476,8 @@ impl<'a> WorldEditor<'a> { x: i32, y: i32, z: i32, - _rotation: i8, ) { - let absolute_y = self.get_absolute_y(x, y, z); + let absolute_y = y; let chunk_x = x >> 4; let chunk_z = z >> 4; let region_x = chunk_x >> 5; @@ -423,19 +485,29 @@ impl<'a> WorldEditor<'a> { let mut block_entities = HashMap::new(); - let messages = vec![ - Value::String(format!("\"{line1}\"")), - Value::String(format!("\"{line2}\"")), - Value::String(format!("\"{line3}\"")), - Value::String(format!("\"{line4}\"")), - ]; + let lines = [line1, line2, line3, line4]; + let messages: Vec = lines + .iter() + .map(|l| Value::String(json_text_message(l))) + .collect(); + + let front = Value::List(messages.clone()); + let back = Value::List(messages); + + let mut front_text = HashMap::new(); + front_text.insert("messages".to_string(), front.clone()); + front_text.insert("filtered_messages".to_string(), front); + front_text.insert("color".to_string(), Value::String("black".to_string())); + front_text.insert("has_glowing_text".to_string(), Value::Byte(0)); - let mut text_data = HashMap::new(); - text_data.insert("messages".to_string(), Value::List(messages)); - text_data.insert("color".to_string(), Value::String("black".to_string())); - text_data.insert("has_glowing_text".to_string(), Value::Byte(0)); + let mut back_text = HashMap::new(); + back_text.insert("messages".to_string(), back.clone()); + back_text.insert("filtered_messages".to_string(), back); + back_text.insert("color".to_string(), Value::String("black".to_string())); + back_text.insert("has_glowing_text".to_string(), Value::Byte(0)); - block_entities.insert("front_text".to_string(), Value::Compound(text_data)); + block_entities.insert("front_text".to_string(), Value::Compound(front_text)); + block_entities.insert("back_text".to_string(), Value::Compound(back_text)); block_entities.insert( "id".to_string(), Value::String("minecraft:sign".to_string()), @@ -460,7 +532,24 @@ impl<'a> WorldEditor<'a> { ); } - self.set_block(SIGN, x, y, z, None, None); + // Explicitly set all sign properties. + let mut props = HashMap::new(); + props.insert("rotation".to_string(), Value::String("0".to_string())); + props.insert( + "waterlogged".to_string(), + Value::String("false".to_string()), + ); + let sign_block = BlockWithProperties::new(SIGN, Some(Value::Compound(props))); + + // Ensure that the sign is always placed even if another block + // already occupies the target position. An empty blacklist allows + // overriding any existing block, which is necessary because signs + // are typically placed next to roads where terrain or vegetation + // might have been generated earlier. + if self.world.get_block(x, absolute_y - 1, z).is_none() { + self.set_block_absolute(DIRT, x, absolute_y - 1, z, None, Some(&[])); + } + self.set_block_with_properties_absolute(sign_block, x, absolute_y, z, None, Some(&[])); } /// Sets a block of the specified type at the given coordinates. @@ -486,13 +575,9 @@ impl<'a> WorldEditor<'a> { let should_insert = if let Some(existing_block) = self.world.get_block(x, absolute_y, z) { // Check against whitelist and blacklist if let Some(whitelist) = override_whitelist { - whitelist - .iter() - .any(|whitelisted_block: &Block| whitelisted_block.id() == existing_block.id()) + whitelist.contains(&existing_block) } else if let Some(blacklist) = override_blacklist { - !blacklist - .iter() - .any(|blacklisted_block: &Block| blacklisted_block.id() == existing_block.id()) + !blacklist.contains(&existing_block) } else { false } @@ -524,13 +609,9 @@ impl<'a> WorldEditor<'a> { let should_insert = if let Some(existing_block) = self.world.get_block(x, absolute_y, z) { // Check against whitelist and blacklist if let Some(whitelist) = override_whitelist { - whitelist - .iter() - .any(|whitelisted_block: &Block| whitelisted_block.id() == existing_block.id()) + whitelist.contains(&existing_block) } else if let Some(blacklist) = override_blacklist { - !blacklist - .iter() - .any(|blacklisted_block: &Block| blacklisted_block.id() == existing_block.id()) + !blacklist.contains(&existing_block) } else { false } @@ -562,13 +643,9 @@ impl<'a> WorldEditor<'a> { let should_insert = if let Some(existing_block) = self.world.get_block(x, absolute_y, z) { // Check against whitelist and blacklist if let Some(whitelist) = override_whitelist { - whitelist - .iter() - .any(|whitelisted_block: &Block| whitelisted_block.id() == existing_block.id()) + whitelist.contains(&existing_block) } else if let Some(blacklist) = override_blacklist { - !blacklist - .iter() - .any(|blacklisted_block: &Block| blacklisted_block.id() == existing_block.id()) + !blacklist.contains(&existing_block) } else { false } @@ -664,10 +741,7 @@ impl<'a> WorldEditor<'a> { // Retrieve the chunk modification map if let Some(existing_block) = self.world.get_block(x, absolute_y, z) { if let Some(whitelist) = whitelist { - if whitelist - .iter() - .any(|whitelisted_block: &Block| whitelisted_block.id() == existing_block.id()) - { + if whitelist.contains(&existing_block) { return true; // Block is in the list } } @@ -689,19 +763,13 @@ impl<'a> WorldEditor<'a> { if let Some(existing_block) = self.world.get_block(x, absolute_y, z) { // Check against whitelist and blacklist if let Some(whitelist) = whitelist { - if whitelist - .iter() - .any(|whitelisted_block: &Block| whitelisted_block.id() == existing_block.id()) - { + if whitelist.contains(&existing_block) { return true; // Block is in whitelist } return false; } if let Some(blacklist) = blacklist { - if blacklist - .iter() - .any(|blacklisted_block: &Block| blacklisted_block.id() == existing_block.id()) - { + if blacklist.contains(&existing_block) { return true; // Block is in blacklist } } @@ -738,12 +806,67 @@ impl<'a> WorldEditor<'a> { other: chunk.other, }; - // Create the Level wrapper - let level_data = create_level_wrapper(&chunk_data); + // Build the root NBT structure for the chunk + let sections = Value::List( + chunk_data + .sections + .iter() + .map(|section| { + let mut block_states = HashMap::from([( + "palette".to_string(), + Value::List( + section + .block_states + .palette + .iter() + .map(|item| { + Value::Compound(HashMap::from([( + "Name".to_string(), + Value::String(item.name.clone()), + )])) + }) + .collect(), + ), + )]); + if let Some(data) = §ion.block_states.data { + if !data.is_empty() { + block_states.insert("data".to_string(), Value::LongArray(data.clone())); + } + } + let mut map = HashMap::from([ + ("Y".to_string(), Value::Byte(section.y)), + ("block_states".to_string(), Value::Compound(block_states)), + ]); + if let Some(bl) = §ion.block_light { + map.insert("block_light".to_string(), Value::ByteArray(bl.clone())); + } + if let Some(sl) = §ion.sky_light { + map.insert("sky_light".to_string(), Value::ByteArray(sl.clone())); + } + Value::Compound(map) + }) + .collect(), + ); + + let mut root = HashMap::from([ + ("DataVersion".to_string(), Value::Int(DATA_VERSION)), + ("xPos".to_string(), Value::Int(abs_chunk_x)), + ("zPos".to_string(), Value::Int(abs_chunk_z)), + ("InhabitedTime".to_string(), Value::Long(0)), + ("LastUpdate".to_string(), Value::Long(0)), + ("isLightOn".to_string(), Value::Byte(0)), + ("status".to_string(), Value::String("full".to_string())), + ("sections".to_string(), sections), + ]); + + // Include any additional top-level fields + for (k, v) in chunk_data.other.iter() { + root.insert(k.clone(), v.clone()); + } - // Serialize the chunk with Level wrapper + // Serialize the chunk let mut ser_buffer = Vec::with_capacity(8192); - fastnbt::to_writer(&mut ser_buffer, &level_data).unwrap(); + fastnbt::to_writer(&mut ser_buffer, &root).unwrap(); (ser_buffer, true) } @@ -792,7 +915,9 @@ impl<'a> WorldEditor<'a> { // Parse existing chunk or create new one let mut chunk: Chunk = if !existing_data.is_empty() { - fastnbt::from_bytes(&existing_data).unwrap() + let mut existing: Chunk = fastnbt::from_bytes(&existing_data).unwrap(); + existing.is_light_on = 0; + existing } else { Chunk { sections: Vec::new(), @@ -803,6 +928,13 @@ impl<'a> WorldEditor<'a> { } }; + // Normalize palette block names from NBT + for section in &mut chunk.sections { + for palette_item in &mut section.block_states.palette { + canonicalize_name_in_place(&mut palette_item.name); + } + } + // Update sections while preserving existing data let new_sections: Vec
= chunk_to_modify.sections().collect(); for new_section in new_sections { @@ -813,6 +945,8 @@ impl<'a> WorldEditor<'a> { existing_section.block_states.palette = new_section.block_states.palette; existing_section.block_states.data = new_section.block_states.data; + existing_section.sky_light = new_section.sky_light; + existing_section.block_light = new_section.block_light; } else { // Add new section if it doesn't exist chunk.sections.push(new_section); @@ -859,6 +993,7 @@ impl<'a> WorldEditor<'a> { // Update chunk coordinates and flags chunk.x_pos = chunk_x + (region_x * 32); chunk.z_pos = chunk_z + (region_z * 32); + chunk.is_light_on = 0; // Create Level wrapper and save let level_data = create_level_wrapper(&chunk); @@ -960,67 +1095,141 @@ fn get_entity_coords(entity: &HashMap) -> (i32, i32, i32) { (x, y, z) } -#[inline] fn create_level_wrapper(chunk: &Chunk) -> HashMap { - HashMap::from([( - "Level".to_string(), - Value::Compound(HashMap::from([ - ("xPos".to_string(), Value::Int(chunk.x_pos)), - ("zPos".to_string(), Value::Int(chunk.z_pos)), - ( - "isLightOn".to_string(), - Value::Byte(i8::try_from(chunk.is_light_on).unwrap()), - ), - ( - "sections".to_string(), - Value::List( - chunk - .sections - .iter() - .map(|section| { - let mut block_states = HashMap::from([( - "palette".to_string(), - Value::List( - section - .block_states - .palette - .iter() - .map(|item| { - let mut palette_item = HashMap::from([( - "Name".to_string(), - Value::String(item.name.clone()), - )]); - if let Some(props) = &item.properties { - palette_item.insert( - "Properties".to_string(), - props.clone(), - ); - } - Value::Compound(palette_item) - }) - .collect(), - ), - )]); - - // only add the `data` attribute if it's non-empty - // some software (cough cough dynmap) chokes otherwise - if let Some(data) = §ion.block_states.data { - if !data.is_empty() { - block_states.insert( - "data".to_string(), - Value::LongArray(data.to_owned()), - ); + let sections = Value::List( + chunk + .sections + .iter() + .map(|section| { + let mut block_states = HashMap::from([( + "palette".to_string(), + Value::List( + section + .block_states + .palette + .iter() + .map(|item| { + let mut palette_item = HashMap::from([( + "Name".to_string(), + Value::String(item.name.clone()), + )]); + if let Some(props) = &item.properties { + palette_item.insert("Properties".to_string(), props.clone()); } - } + Value::Compound(palette_item) + }) + .collect(), + ), + )]); + if let Some(data) = §ion.block_states.data { + if !data.is_empty() { + block_states.insert("data".to_string(), Value::LongArray(data.clone())); + } + } + let mut map = HashMap::from([ + ("Y".to_string(), Value::Byte(section.y)), + ("block_states".to_string(), Value::Compound(block_states)), + ]); + if let Some(bl) = §ion.block_light { + map.insert("block_light".to_string(), Value::ByteArray(bl.clone())); + } + if let Some(sl) = §ion.sky_light { + map.insert("sky_light".to_string(), Value::ByteArray(sl.clone())); + } + Value::Compound(map) + }) + .collect(), + ); + + let mut root = HashMap::from([ + ("DataVersion".to_string(), Value::Int(DATA_VERSION)), + ("xPos".to_string(), Value::Int(chunk.x_pos)), + ("zPos".to_string(), Value::Int(chunk.z_pos)), + ("InhabitedTime".to_string(), Value::Long(0)), + ("LastUpdate".to_string(), Value::Long(0)), + ("isLightOn".to_string(), Value::Byte(0)), + ("status".to_string(), Value::String("full".to_string())), + ("sections".to_string(), sections), + ]); + + for (k, v) in chunk.other.iter() { + root.insert(k.clone(), v.clone()); + } + + root +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn format_sign_text_wraps() { + let (l1, l2, l3, l4) = format_sign_text("A very long \"street\" name that needs wrapping"); + assert!(l1.len() <= 15); + assert!(l2.len() <= 15); + assert!(l3.len() <= 15); + assert!(l4.len() <= 15); + } - Value::Compound(HashMap::from([ - ("Y".to_string(), Value::Byte(section.y)), - ("block_states".to_string(), Value::Compound(block_states)), - ])) - }) - .collect(), - ), - ), - ])), - )]) + #[test] + fn format_sign_text_truncates_after_four_lines() { + let input = "0123456789abcde".repeat(5); + let (l1, l2, l3, l4) = format_sign_text(&input); + assert_eq!(l1, "0123456789abcde"); + assert_eq!(l2, "0123456789abcde"); + assert_eq!(l3, "0123456789abcde"); + assert_eq!(l4, "0123456789abcde"); + } + + #[test] + fn palette_item_contains_namespaced_names() { + use crate::block_definitions::OAK_PLANKS; + + let mut section = SectionToModify::default(); + section.set_block(0, 0, 0, OAK_PLANKS); + + let nbt_section = section.to_section(0); + assert!(nbt_section + .block_states + .palette + .iter() + .any(|p| p.name == "minecraft:oak_planks")); + } + + #[test] + fn sign_block_serializes_with_rotation_and_waterlogged() { + use crate::block_definitions::SIGN; + use std::collections::HashMap; + + let mut section = SectionToModify::default(); + + // Build properties starting from the sign's defaults and override rotation. + let mut props = match SIGN.properties() { + Some(Value::Compound(map)) => map, + _ => HashMap::new(), + }; + props.insert("rotation".to_string(), Value::String("4".to_string())); + let sign_block = BlockWithProperties::new(SIGN, Some(Value::Compound(props))); + + section.set_block_with_properties(0, 0, 0, sign_block); + + let nbt_section = section.to_section(0); + let sign_palette = nbt_section + .block_states + .palette + .iter() + .find(|p| p.name == "minecraft:oak_sign") + .expect("sign palette entry"); + + match &sign_palette.properties { + Some(Value::Compound(map)) => { + assert_eq!(map.get("rotation"), Some(&Value::String("4".to_string()))); + assert_eq!( + map.get("waterlogged"), + Some(&Value::String("false".to_string())) + ); + } + _ => panic!("sign properties missing"), + } + } }