use heck::{ToShoutySnakeCase, ToUpperCamelCase}; use proc_macro2::{Span, TokenStream}; use pumpkin_nbt::deserializer::{NbtReadHelper, NbtReadHelperBedrock}; use pumpkin_util::math::{experience::Experience, vector3::Vector3}; use quote::{ToTokens, format_ident, quote}; use serde::Deserialize; use std::{ collections::{BTreeMap, HashSet}, fs, io::{Cursor, Read, Seek, SeekFrom}, panic, }; use syn::{Ident, LitInt, LitStr}; use crate::{ bitsets::{Bitset, gen_u16_bitset}, loot::LootTableStruct, }; /// Converts a sparse index-value list into a dense array, filling gaps with `None` tokens. /// /// # Arguments /// – `array` – pairs of `(index, value)` where `index` is the position in the output array. fn fill_array(array: Vec<(u16, T)>) -> Vec { let max_index = array.iter().map(|(index, _)| index).max().unwrap(); let mut raw_id_from_state_id_ordered = vec![quote! { None }; (max_index + 1) as usize]; for (state_id, id_lit) in array { raw_id_from_state_id_ordered[state_id as usize] = quote! { #id_lit }; } raw_id_from_state_id_ordered } /// Converts a sparse `(block_ident, state_index, state_id)` list into a dense `TokenStream` array. /// /// # Arguments /// – `array` – triples of `(block_ident, state_index, state_id)` mapping each state ID to the block constant and local index. fn fill_state_array(array: Vec<(Ident, usize, u16)>) -> Vec { let max_index = array.iter().map(|(_, _, index)| index).max().unwrap(); let mut ret = vec![quote! { Missed State }; (max_index + 1) as usize]; for (block, index, state_id) in array { let index_lit = LitInt::new(&index.to_string(), Span::call_site()); ret[state_id as usize] = quote! { &Block::#block.states[#index_lit] }; } ret } /// Converts a block registry name into the SCREAMING_SNAKE_CASE constant identifier. /// /// # Arguments /// – `block` – the lowercase registry name (e.g., `"stone_slab"`). fn const_block_name_from_block_name(block: &str) -> String { block.to_shouty_snake_case() } /// Derives the UpperCamelCase struct name for a block-property group from a derived base name. /// /// # Arguments /// – `name` – a base name such as `"oak_slab_like"` which is converted to e.g., `"OakSlabLikeProperties"`. fn property_group_name_from_derived_name(name: &str) -> String { format!("{name}_properties").to_upper_camel_case() } /// Discriminates between the two runtime representations of a block property. enum PropertyType { /// The property is a simple boolean (`true`/`false`). Bool, /// The property is an integer with an inclusive range. Int { min: u8, max: u8 }, /// The property is an enum with a generated Rust type identified by `name`. Enum { name: String }, } /// Maps a single block-property field to its Rust type representation. struct PropertyVariantMapping { /// The serialized property name as it appears in JSON (e.g., `"facing"`). original_name: String, /// The Rust type used to represent this property in the generated code. property_type: PropertyType, } /// Accumulated data for a group of blocks that share the same set of block properties. struct PropertyCollectionData { /// The ordered list of property-to-type mappings that form the generated struct fields. variant_mappings: Vec, /// All blocks (by name and numeric ID) that belong to this property group. blocks: Vec<(String, u16)>, } impl PropertyCollectionData { /// Registers an additional block that shares this property group. /// /// # Arguments /// – `block_name` – the registry name of the block. /// – `block_id` – the numeric block ID. pub fn add_block(&mut self, block_name: String, block_id: u16) { self.blocks.push((block_name, block_id)); } /// Creates a new `PropertyCollectionData` from an ordered list of property mappings with no blocks yet registered. /// /// # Arguments /// – `variant_mappings` – the property-to-type mappings for this group. pub const fn from_mappings(variant_mappings: Vec) -> Self { Self { variant_mappings, blocks: Vec::new(), } } /// Derives the base name for this property group from the first registered block. pub fn derive_name(&self) -> String { format!("{}_like", self.blocks[0].0) } } /// Deserialized representation of a single block property enum, used to emit the Rust enum definition. #[derive(Deserialize, Clone, Debug)] pub struct PropertyStruct { /// The UpperCamelCase name used for the generated Rust enum (e.g., `"Facing"`). pub name: String, /// The ordered list of variant value strings (e.g., `["north", "south", ...]`). pub values: Vec, } impl ToTokens for PropertyStruct { fn to_tokens(&self, tokens: &mut TokenStream) { if self.values[0] == "true" && self.values[1] == "false" { return; } let name = Ident::new(&self.name, Span::call_site()); let count = self.values.len(); let variant_count = count as u16; let is_number_values = !self.values.is_empty() && self.values.iter().all(|v| v.starts_with('L')) && self.values.iter().any(|v| v == "L1"); let mut variants = Vec::with_capacity(count); let mut literals = Vec::with_capacity(count); let mut indices = Vec::with_capacity(count); for (i, raw_value) in self.values.iter().enumerate() { let ident = Ident::new(&raw_value.to_upper_camel_case(), Span::call_site()); let literal_str = if is_number_values { raw_value.strip_prefix('L').unwrap_or(raw_value) } else { raw_value.as_str() }; variants.push(ident); literals.push(literal_str); indices.push(i as u16); } tokens.extend(quote! { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum #name { #(#variants),* } impl EnumVariants for #name { fn variant_count() -> u16 { #variant_count } fn to_index(&self) -> u16 { match self { #(Self::#variants => #indices),* } } fn from_index(index: u16) -> Self { match index { #(#indices => Self::#variants,)* _ => panic!("Invalid index: {index}"), } } fn to_value(&self) -> &'static str { match self { #(Self::#variants => #literals),* } } fn from_value(value: &str) -> Self { match value { #(#literals => Self::#variants),*, _ => panic!("Invalid value: {value}"), } } } }); } } /// Code-generation wrapper that emits the full `BlockProperties` impl for one property group. struct BlockPropertyStruct { /// The property group data used to generate the struct and its trait implementation. data: PropertyCollectionData, } impl ToTokens for BlockPropertyStruct { fn to_tokens(&self, tokens: &mut TokenStream) { let struct_name = property_group_name_from_derived_name(&self.data.derive_name()); let name = Ident::new(&struct_name, Span::call_site()); // Strukturfelder generieren let fields = self.data.variant_mappings.iter().map(|entry| { let key = Ident::new_raw(&entry.original_name, Span::call_site()); match &entry.property_type { PropertyType::Bool => quote! { pub #key: bool }, PropertyType::Int { .. } => quote! { pub #key: u8 }, PropertyType::Enum { name } => { let value = Ident::new(name, Span::call_site()); quote! { pub #key: #value } } } }); let block_ids = self .data .blocks .iter() .map(|(_, id)| *id) .collect::>(); let to_index_logic = self.data.variant_mappings.iter().rev().map(|entry| { let field = Ident::new_raw(&entry.original_name, Span::call_site()); match &entry.property_type { PropertyType::Bool => quote! { (!self.#field as u16, 2) }, PropertyType::Int { min, max } => { let count = (max - min + 1) as u16; if *min > 0 { quote! { ((self.#field - #min) as u16, #count) } } else { quote! { (self.#field as u16, #count) } } } PropertyType::Enum { name } => { let ty = Ident::new(name, Span::call_site()); quote! { (self.#field.to_index(), #ty::variant_count()) } } } }); let from_index_body = self .data .variant_mappings .iter() .rev() .map(|entry| { let field_name = Ident::new_raw(&entry.original_name, Span::call_site()); match &entry.property_type { PropertyType::Bool => quote! { #field_name: { let value = index % 2; index /= 2; value == 0 } }, PropertyType::Int { min, max } => { let count = (max - min + 1) as u16; let val = if *min > 0 { quote! { value + #min } } else { quote! {value} }; quote! { #field_name: { let value = (index % #count) as u8; index /= #count; #val } } } PropertyType::Enum { name } => { let enum_ident = Ident::new(name, Span::call_site()); quote! { #field_name: { let value = index % #enum_ident::variant_count(); index /= #enum_ident::variant_count(); #enum_ident::from_index(value) } } } } }) .collect::>(); let to_props_entries = self.data.variant_mappings.iter().map(|entry| { let key_str = &entry.original_name; let field = Ident::new_raw(&entry.original_name, Span::call_site()); match &entry.property_type { PropertyType::Bool => quote! { (#key_str, if self.#field { "true" } else { "false" }) }, PropertyType::Int { min, max } => { let mut arms = Vec::new(); for i in *min..=*max { let i_str = i.to_string(); arms.push(quote! { #i => #i_str }); } quote! { (#key_str, match self.#field { #(#arms,)* _ => unreachable!() }) } } PropertyType::Enum { .. } => quote! { (#key_str, self.#field.to_value()) }, } }); let from_props_keys = self .data .variant_mappings .iter() .map(|entry| &entry.original_name); let from_props_values = self.data.variant_mappings.iter().map(|entry| { let field_name = Ident::new_raw(&entry.original_name, Span::call_site()); match &entry.property_type { PropertyType::Bool => quote! { block_props.#field_name = matches!(*value, "true") }, PropertyType::Int { min, max } => { let mut arms = Vec::new(); for i in *min..=*max { let i_str = i.to_string(); arms.push(quote! { #i_str => #i }); } quote! { block_props.#field_name = match *value { #(#arms,)* _ => #min, } } } PropertyType::Enum { name } => { let enum_ident = Ident::new(name, Span::call_site()); quote! { block_props.#field_name = #enum_ident::from_value(value) } } } }); let from_props_loop_body = if self.data.variant_mappings.len() > 1 { quote! { match *key { #(#from_props_keys => #from_props_values),*, _ => {}, // } } } else { let key = from_props_keys.into_iter().next(); let val = from_props_values.into_iter().next(); quote! { if *key == #key { #val } } }; tokens.extend(quote! { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct #name { #(#fields),* } impl BlockProperties for #name { fn to_index(&self) -> u16 { let (index, _) = [#(#to_index_logic),*] .iter() .fold((0, 1), |(curr, mul), &(val, count)| (curr + val * mul, mul * count)); index } #[allow(unused_assignments)] fn from_index(mut index: u16) -> Self { Self { #(#from_index_body),* } } #[inline] #[allow(clippy::manual_range_patterns)] fn handles_block_id(block_id: BlockId) -> bool where Self: Sized { matches!(block_id.as_u16(), #(#block_ids)|*) } fn to_state_id(&self, block: &Block) -> BlockStateId { if !Self::handles_block_id(block.id) { panic!("{} is not a valid block for {}", block.name, #struct_name); } block.states[self.to_index() as usize].id } fn from_state_id(id: BlockStateId, block: &Block) -> Self { debug_assert!( Self::handles_block_id(block.id), "{} is not a valid block for {}", block.name, #struct_name ); let min_id = block.states[0].id.as_u16(); let max_id = block.states.last().map(|s| s.id.as_u16()).unwrap_or(min_id); if (min_id..=max_id).contains(&id.as_u16()) { Self::from_index(id.as_u16() - min_id) } else { #[cfg(debug_assertions)] panic!("State ID {} does not exist for {}", id, block.name); #[cfg(not(debug_assertions))] Self::from_index(0) } } fn default(block: &Block) -> Self { if !Self::handles_block_id(block.id) { panic!("{} is not a valid block for {}", block.name, #struct_name); } Self::from_state_id(block.default_state.id, block) } fn to_props(&self) -> Vec<(&'static str, &'static str)> { vec![ #(#to_props_entries),* ] } #[allow(clippy::manual_range_patterns)] fn from_props(props: &[(&str, &str)], block: &Block) -> Self { #[cfg(debug_assertions)] if !matches!(block.id.as_u16(), #(#block_ids)|*) { panic!("{} is not a valid block for {}", block.name, #struct_name); } let mut block_props = Self::default(block); for (key, value) in props { #from_props_loop_body } block_props } } }); } } /// Deserialized flammability data for a block. #[derive(Deserialize)] pub struct FlammableStruct { /// Chance (0–300) that fire spreads to neighbouring blocks. pub spread_chance: u8, /// Chance (0–300) that the block itself burns away. pub burn_chance: u8, } impl ToTokens for FlammableStruct { fn to_tokens(&self, tokens: &mut TokenStream) { let spread_chance = &self.spread_chance; let burn_chance = &self.burn_chance; tokens.extend(quote! { Flammable { spread_chance: #spread_chance, burn_chance: #burn_chance, } }); } } /// Axis-aligned bounding box used to describe block collision and outline shapes. #[derive(Deserialize, Clone, Copy)] pub struct BoundingBox { /// The minimum corner of the bounding box. pub min: Vector3, /// The maximum corner of the bounding box. pub max: Vector3, } impl ToTokens for BoundingBox { fn to_tokens(&self, tokens: &mut TokenStream) { let min_x = &self.min.x; let min_y = &self.min.y; let min_z = &self.min.z; let max_x = &self.max.x; let max_y = &self.max.y; let max_z = &self.max.z; tokens.extend(quote! { BoundingBox { min: Vector3::new(#min_x, #min_y, #min_z), max: Vector3::new(#max_x, #max_y, #max_z), } }); } } #[derive(Deserialize, Copy, Clone, PartialEq, Eq)] pub struct BlockStateId(pub u16); impl ToTokens for BlockStateId { fn to_tokens(&self, tokens: &mut TokenStream) { let inner = self.0; tokens.extend(quote! { BlockStateId::new(#inner).unwrap() }); } } /// Deserialized representation of a single block state as stored in `blocks.json`. #[derive(Deserialize, Clone)] pub struct BlockState { /// Globally unique numeric ID for this block state. pub id: BlockStateId, /// Bitfield encoding boolean state properties (air, random ticks, etc.). pub state_flags: u16, /// Bitfield encoding which sides of the block are solid. pub side_flags: u8, /// Name of the note-block instrument for this state. TODO: make this an enum pub instrument: String, /// Light level emitted by this state (0–15). pub luminance: u8, /// How pistons interact with this block state. pub piston_behavior: PistonBehavior, /// Mining hardness of this block state. pub hardness: f32, /// Indices into the global shapes array for collision shape segments. pub collision_shapes: Vec, /// Indices into the global shapes array for outline (selection) shape segments. pub outline_shapes: Vec, /// Opacity value for light propagation, if non-full. pub opacity: Option, /// Associated block-entity type ID, if any. pub block_entity_type: Option, } /// Describes how a piston interacts with a block. #[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum PistonBehavior { /// The block can be pushed and pulled normally. Normal, /// The block is destroyed when pushed. Destroy, /// The block prevents piston movement. Block, /// The piston ignores the block entirely. Ignore, /// The block can only be pushed, not pulled. PushOnly, } impl PistonBehavior { fn to_tokens(&self) -> TokenStream { match self { Self::Normal => quote! { PistonBehavior::Normal }, Self::Destroy => quote! { PistonBehavior::Destroy }, Self::Block => quote! { PistonBehavior::Block }, Self::Ignore => quote! { PistonBehavior::Ignore }, Self::PushOnly => quote! { PistonBehavior::PushOnly }, } } } impl BlockState { /// Bit flag indicating this state is an air block. const IS_AIR: u16 = 1 << 0; const IS_LIQUID: u16 = 1 << 5; /// Bit flag indicating this state receives random tick events. const HAS_RANDOM_TICKS: u16 = 1 << 9; /// Returns `true` if this state receives random tick events. const fn has_random_ticks(&self) -> bool { self.state_flags & Self::HAS_RANDOM_TICKS != 0 } /// Returns `true` if this state is an air variant. pub const fn is_air(&self) -> bool { self.state_flags & Self::IS_AIR != 0 } pub const fn is_liquid(&self) -> bool { self.state_flags & Self::IS_LIQUID != 0 } /// Emits the `BlockState { … }` struct literal token stream for code generation. fn to_tokens(&self) -> TokenStream { let mut tokens = TokenStream::new(); let id = self.id; let state_flags = LitInt::new(&self.state_flags.to_string(), Span::call_site()); let side_flags = LitInt::new(&self.side_flags.to_string(), Span::call_site()); let instrument = format_ident!("{}", self.instrument.to_upper_camel_case()); let luminance = LitInt::new(&self.luminance.to_string(), Span::call_site()); let hardness = self.hardness; let opacity = if let Some(opacity) = self.opacity { let opacity = LitInt::new(&opacity.to_string(), Span::call_site()); quote! { #opacity } } else { quote! { 0 } }; let block_entity_type = if let Some(block_entity_type) = self.block_entity_type { let block_entity_type = LitInt::new(&block_entity_type.to_string(), Span::call_site()); quote! { #block_entity_type } } else { quote! { u16::MAX } }; let collision_shapes = self .collision_shapes .iter() .map(|shape_id| LitInt::new(&shape_id.to_string(), Span::call_site())); let outline_shapes = self .outline_shapes .iter() .map(|shape_id| LitInt::new(&shape_id.to_string(), Span::call_site())); let piston_behavior = &self.piston_behavior.to_tokens(); tokens.extend(quote! { BlockState { id: #id, state_flags: #state_flags, side_flags: #side_flags, instrument: NoteblockInstrument::#instrument, luminance: #luminance, piston_behavior: #piston_behavior, hardness: #hardness, collision_shapes: &[#(#collision_shapes),*], outline_shapes: &[#(#outline_shapes),*], opacity: #opacity, block_entity_type: #block_entity_type, } }); tokens } } #[derive(Deserialize, Copy, Clone)] pub struct BlockId(pub u16); impl ToTokens for BlockId { fn to_tokens(&self, tokens: &mut TokenStream) { let inner = self.0; tokens.extend(quote! { BlockId::new(#inner).unwrap() }); } } /// Deserialized representation of a Minecraft block as stored in `blocks.json`. #[derive(Deserialize)] pub struct Block { /// Numeric block ID used in the protocol. pub id: BlockId, /// Registry name without the `minecraft:` namespace prefix. pub name: String, /// Translation key for the block's display name. // pub translation_key: String, /// Mining hardness; affects how long the block takes to break. pub hardness: f32, /// Blast resistance against explosions. pub blast_resistance: f32, pub map_color: u8, /// Numeric ID of the corresponding item, if any. pub item_id: u16, /// Flammability data, present only if the block can catch fire. pub flammable: Option, /// Loot table used when the block is broken, if any. pub loot_table: Option, /// Friction applied to entities walking on this block. pub slipperiness: f32, /// Horizontal velocity multiplier for entities inside this block. pub velocity_multiplier: f32, /// Jump velocity multiplier applied when jumping from this block. pub jump_velocity_multiplier: f32, /// Hash keys referencing the properties defined for this block. pub properties: Vec, /// State ID of the default (canonical) block state. pub default_state_id: BlockStateId, /// All possible states for this block in state-ID order. pub states: Vec, /// Experience points dropped when the block is mined, if any. pub experience: Option, } impl ToTokens for Block { fn to_tokens(&self, tokens: &mut TokenStream) { let id = self.id; let name = LitStr::new(&self.name, Span::call_site()); //let translation_key = LitStr::new(&self.translation_key, Span::call_site()); let hardness = &self.hardness; let blast_resistance = &self.blast_resistance; let map_color = &self.map_color; let item_id = LitInt::new(&self.item_id.to_string(), Span::call_site()); let slipperiness = &self.slipperiness; let velocity_multiplier = &self.velocity_multiplier; let jump_velocity_multiplier = &self.jump_velocity_multiplier; let experience = if let Some(exp) = &self.experience { let exp_tokens = exp.to_token_stream(); quote! { Some(#exp_tokens) } } else { quote! { None } }; // Generate state tokens let states = self.states.iter().map(BlockState::to_tokens); let loot_table = if let Some(table) = &self.loot_table { let table_tokens = table.to_token_stream(); quote! { Some(#table_tokens) } } else { quote! { None } }; let default_state_ref: &BlockState = self .states .iter() .find(|state| state.id == self.default_state_id) .unwrap(); let mut default_state = default_state_ref.clone(); default_state.id = default_state_ref.id; let default_state = default_state.to_tokens(); let flammable = if let Some(flammable) = &self.flammable { let flammable_tokens = flammable.to_token_stream(); quote! { Some(#flammable_tokens) } } else { quote! { None } }; tokens.extend(quote! { Block { id: #id, name: #name, hardness: #hardness, blast_resistance: #blast_resistance, map_color: #map_color, slipperiness: #slipperiness, velocity_multiplier: #velocity_multiplier, jump_velocity_multiplier: #jump_velocity_multiplier, item_id: #item_id, default_state: &#default_state, states: &[#(#states),*], flammable: #flammable, loot_table: #loot_table, experience: #experience, } }); } } /// The underlying data type of generated block property as classified in `properties.json`. #[derive(Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(tag = "type")] pub enum GeneratedPropertyType { /// A simple `true`/`false` boolean property. #[serde(rename = "boolean")] Boolean, /// An integer property with an inclusive range. #[serde(rename = "int")] Int { /// Minimum value (inclusive). min: u8, /// Maximum value (inclusive). max: u8, }, /// A named-variant enum property. #[serde(rename = "enum")] Enum { /// The ordered list of variant strings. values: Vec, }, } /// A single block property entry as deserialized from `properties.json`. #[derive(Deserialize, Clone)] pub struct GeneratedProperty { /// A stable integer hash used to identify this property across blocks. hash_key: i32, /// The name used for the generated Rust enum type. enum_name: String, /// The property name as it appears in block state JSON. serialized_name: String, /// The kind and possible values of this property. #[serde(rename = "type")] #[serde(flatten)] property_type: GeneratedPropertyType, } impl GeneratedProperty { /// Converts this deserialized property into the intermediate `Property` used during code generation. fn to_property(&self) -> Property { let enum_name = match &self.property_type { GeneratedPropertyType::Boolean => "boolean".to_string(), GeneratedPropertyType::Int { min, max } => format!("integer_{min}_to_{max}"), GeneratedPropertyType::Enum { .. } => self.enum_name.clone(), }; let values = match &self.property_type { GeneratedPropertyType::Boolean => { vec!["true".to_string(), "false".to_string()] } GeneratedPropertyType::Int { min, max } => { let mut values = Vec::new(); for i in *min..=*max { values.push(format!("L{i}")); } values } GeneratedPropertyType::Enum { values } => values.clone(), }; Property { enum_name, serialized_name: self.serialized_name.clone(), values, } } } /// Intermediate representation of a block property used while building the property group map. #[derive(Clone)] struct Property { /// The Rust enum type name for this property. enum_name: String, /// The JSON key name for this property. serialized_name: String, /// All possible string values this property can take. values: Vec, } /// Top-level container for all block assets loaded from `blocks.json`. #[derive(Deserialize)] pub struct BlockAssets { /// All blocks with their states and properties. pub blocks: Vec, /// All unique bounding boxes referenced by block states. pub shapes: Vec, /// Registry names of all block entity types. pub block_entity_types: Vec, } /// Reads all block assets and generates the complete block registry `TokenStream`. pub fn build() -> TokenStream { let be_blocks_data = fs::read("../assets/bedrock/block_states.nbt").unwrap(); let mut be_blocks_cursor = Cursor::new(be_blocks_data); let be_blocks = get_be_data_from_nbt(&mut be_blocks_cursor); let geyser_blocks_data = fs::read("../assets/bedrock/blocks.nbt").unwrap(); let geyser_nbt_compound = pumpkin_nbt::nbt_compress::read_gzip_compound_tag(Cursor::new(geyser_blocks_data)).unwrap(); let bedrock_mappings_list = geyser_nbt_compound .get_list("bedrock_mappings") .expect("Missing bedrock_mappings list in Geyser blocks.nbt"); let blocks_assets: BlockAssets = serde_json::from_str(&fs::read_to_string("../assets/blocks.json").unwrap()) .expect("Failed to parse blocks.json"); let generated_properties: Vec = serde_json::from_str(&fs::read_to_string("../assets/properties.json").unwrap()) .expect("Failed to parse properties.json"); let generated_prop_map: BTreeMap = generated_properties .iter() .map(|p| (p.hash_key, p)) .collect(); let mut random_tick_states = Vec::new(); let mut air_states = Vec::new(); let mut liquid_states = Vec::new(); let mut constants_list = Vec::new(); let mut block_id_constants = Vec::new(); let mut block_from_name_entries = Vec::new(); let mut block_from_item_id_arms = Vec::new(); let mut block_state_to_bedrock = Vec::new(); let mut raw_id_from_state_id_array = Vec::new(); let mut type_from_raw_id_array = Vec::new(); let mut state_from_state_id_array = Vec::<(Ident, usize, u16)>::new(); let mut property_enums: BTreeMap = BTreeMap::new(); let mut block_properties: Vec = Vec::new(); let mut property_collection_map: BTreeMap, PropertyCollectionData> = BTreeMap::new(); let mut existing_item_ids: HashSet = HashSet::new(); for block in blocks_assets.blocks { let mut property_collection = HashSet::new(); let mut property_mapping = Vec::new(); for property_hash in &block.properties { let generated_property = generated_prop_map .get(property_hash) .expect("Property hash not found in generated_properties"); property_collection.insert(generated_property.hash_key); let property = generated_property.to_property(); let renamed_property = property.enum_name.to_upper_camel_case(); let property_type = match &generated_property.property_type { GeneratedPropertyType::Boolean => PropertyType::Bool, GeneratedPropertyType::Int { min, max } => PropertyType::Int { min: *min, max: *max, }, GeneratedPropertyType::Enum { .. } => PropertyType::Enum { name: renamed_property.clone(), }, }; if let PropertyType::Enum { name } = &property_type { property_enums .entry(name.clone()) .or_insert_with(|| PropertyStruct { name: name.clone(), values: property.values.clone(), }); } property_mapping.push(PropertyVariantMapping { original_name: property.serialized_name, property_type, }); } let mut multiplier = 1; let mut property_descriptors = Vec::new(); for hash in block.properties.iter().rev() { let gen_prop = generated_prop_map.get(hash).unwrap(); let variant_count = match &gen_prop.property_type { GeneratedPropertyType::Boolean => 2, GeneratedPropertyType::Int { min, max } => (max - min + 1) as u16, GeneratedPropertyType::Enum { values } => values.len() as u16, }; property_descriptors.push(quote! { PropertyDescriptor { hash_key: #hash, multiplier: #multiplier, variant_count: #variant_count, } }); multiplier *= variant_count; } let const_ident = format_ident!("{}", const_block_name_from_block_name(&block.name)); let name_str = &block.name; let item_id = block.item_id; let block_id = block.id; // let mut block_with_descriptors = block.clone(); // block_with_descriptors.property_descriptors = property_descriptors; constants_list.push(quote! { pub const #const_ident: Self = #block; }); block_id_constants.push(quote! { pub const #const_ident: Self = #block_id; }); type_from_raw_id_array.push((block.id.0, quote! { &Block::#const_ident })); block_from_name_entries.push(quote! { #name_str => Block::#const_ident, }); for (i, state) in block.states.iter().enumerate() { if state.has_random_ticks() { let state_id = LitInt::new(&state.id.0.to_string(), Span::call_site()); random_tick_states.push(state_id); } if state.is_air() { let state_id = LitInt::new(&state.id.0.to_string(), Span::call_site()); air_states.push(state_id); } if state.is_liquid() { let state_id = LitInt::new(&state.id.0.to_string(), Span::call_site()); liquid_states.push(state_id); } let mut matched_be_id = 1; if let Some(geyser_entry) = bedrock_mappings_list.get(state.id.0 as usize) { let (bedrock_name, bedrock_props) = parse_geyser_entry(geyser_entry, &block.name); if let Some(be_variants) = be_blocks.get(&bedrock_name) { matched_be_id = be_variants .iter() .find(|(_, be_props)| { bedrock_props .iter() .all(|(k, v)| be_props.get(k).is_some_and(|be_v| be_v == v)) }) .map_or_else( || be_variants.first().map_or(1, |(id, _)| *id), |(id, _)| *id, ); } else if let Some(be_variants) = be_blocks.get(&block.name) { matched_be_id = be_variants.first().map_or(1, |(id, _)| *id); } } else if let Some(be_variants) = be_blocks.get(&block.name) { matched_be_id = be_variants.first().map_or(1, |(id, _)| *id); } block_state_to_bedrock.push((state.id.0, matched_be_id)); raw_id_from_state_id_array.push((state.id.0, quote! { BlockId::#const_ident })); state_from_state_id_array.push((const_ident.clone(), i, state.id.0)); } if !property_collection.is_empty() { let mut property_collection_vec: Vec = property_collection.into_iter().collect(); property_collection_vec.sort_unstable(); property_collection_map .entry(property_collection_vec) .or_insert_with(|| PropertyCollectionData::from_mappings(property_mapping)) .add_block(block.name.clone(), block.id.0); } if existing_item_ids.insert(item_id) { block_from_item_id_arms.push(quote! { #item_id => Some(&Self::#const_ident), }); } } let mut block_properties_from_state_and_block_id_arms = Vec::new(); let mut block_properties_from_props_and_name_arms = Vec::new(); for property_group in property_collection_map.into_values() { let property_name = Ident::new( &property_group_name_from_derived_name(&property_group.derive_name()), Span::call_site(), ); for (block_name, id) in &property_group.blocks { let const_block_name = Ident::new( &const_block_name_from_block_name(block_name), Span::call_site(), ); let id_lit = LitInt::new(&id.to_string(), Span::call_site()); block_properties_from_state_and_block_id_arms.push(quote! { #id_lit => Box::new(#property_name::from_state_id(state_id, &Block::#const_block_name)), }); block_properties_from_props_and_name_arms.push(quote! { #id_lit => Box::new(#property_name::from_props(props, &Block::#const_block_name)), }); } block_properties.push(BlockPropertyStruct { data: property_group, }); } let shapes = blocks_assets.shapes.iter().map(ToTokens::to_token_stream); let air_state_ids = quote! { #(#air_states)|* }; let liquid_state_ids = quote! { #(#liquid_states)|* }; let block_props = block_properties.iter().map(ToTokens::to_token_stream); let properties = property_enums.values().map(ToTokens::to_token_stream); let block_entity_types = blocks_assets .block_entity_types .iter() .map(|entity_type| LitStr::new(entity_type, Span::call_site())); let raw_id_from_state_id_ordered = fill_array(raw_id_from_state_id_array); let max_state_id = raw_id_from_state_id_ordered.len(); let raw_id_from_state_id = quote! { #(#raw_id_from_state_id_ordered),* }; let max_index = block_state_to_bedrock .iter() .map(|(idx, _)| *idx) .max() .unwrap_or(0); let mut state_to_bedrock_tokens = vec![quote! { 1 }; (max_index + 1) as usize]; for (state_id, id_lit) in block_state_to_bedrock { let lit = LitInt::new(&id_lit.to_string(), Span::call_site()); state_to_bedrock_tokens[state_id as usize] = quote! { #lit }; } let block_state_to_bedrock_t = quote! { #(#state_to_bedrock_tokens),* }; let type_from_raw_id_vec = fill_array(type_from_raw_id_array); let max_type_id = type_from_raw_id_vec.len(); let type_from_raw_id_items = quote! { #(#type_from_raw_id_vec),* }; let state_from_state_id_vec = fill_state_array(state_from_state_id_array); let max_state_id_2 = state_from_state_id_vec.len(); let state_from_state_id = quote! { #(#state_from_state_id_vec),* }; assert_eq!(max_state_id, max_state_id_2); let Bitset { items, mod_ident, contains_ident, } = &gen_u16_bitset( "RANDOM_TICKS", &random_tick_states .iter() .map(|it| it.base10_parse().unwrap()) .collect::>(), ); quote! { #[allow(clippy::wildcard_imports, clippy::enum_glob_use, clippy::too_many_lines, clippy::match_same_arms)] use pumpkin_util::math::boundingbox::BoundingBox; use crate::{BlockState, BlockStateId, Block, BlockId, blocks::Flammable}; use crate::block_state::PistonBehavior; use pumpkin_util::math::int_provider::{UniformIntProvider, IntProvider, NormalIntProvider}; use pumpkin_util::loot_table::*; use pumpkin_util::math::experience::Experience; use pumpkin_util::math::vector3::Vector3; use std::collections::BTreeMap; #items #[derive(Clone, Copy, Debug)] pub struct BlockProperty { pub name: &'static str, pub values: &'static [&'static str], } pub trait BlockProperties where Self: 'static { fn to_index(&self) -> u16; fn from_index(index: u16) -> Self where Self: Sized; fn handles_block_id(id: BlockId) -> bool where Self: Sized; fn to_state_id(&self, block: &Block) -> BlockStateId; fn from_state_id(id: BlockStateId, block: &Block) -> Self where Self: Sized; fn default(block: &Block) -> Self where Self: Sized; fn to_props(&self) -> Vec<(&'static str, &'static str)>; fn from_props(props: &[(&str, &str)], block: &Block) -> Self where Self: Sized; } pub trait EnumVariants { fn variant_count() -> u16; fn to_index(&self) -> u16; fn from_index(index: u16) -> Self; fn to_value(&self) -> &'static str; fn from_value(value: &str) -> Self; } pub const COLLISION_SHAPES: &[BoundingBox] = &[ #(#shapes),* ]; pub const BLOCK_ENTITY_TYPES: &[&str] = &[ #(#block_entity_types),* ]; #[inline(always)] #[must_use] pub const fn is_air(id: BlockStateId) -> bool { matches!(id.as_u16(), #air_state_ids) } #[inline(always)] #[must_use] pub const fn is_liquid(id: BlockStateId) -> bool { matches!(id.as_u16(), #liquid_state_ids) } #[inline(always)] #[must_use] pub fn has_random_ticks(id: BlockStateId) -> bool { #mod_ident::#contains_ident(id.as_u16()) } #[must_use] pub const fn blocks_movement(block_state: &BlockState, id: BlockId) -> bool { block_state.is_solid() && !matches!(id, BlockId::COBWEB | BlockId::BAMBOO_SAPLING) } impl BlockState { const STATE_ID_TO_BEDROCK: &[u16] = &[ #block_state_to_bedrock_t ]; /// Get a [`BlockState`] from a [`BlockStateId`]. /// If you need access to the block use `BlockState::from_id_with_block` instead. #[inline] #[must_use] pub const fn from_id(id: BlockStateId) -> &'static Self { // Safety: We always check this condition when creating a BlockStateId. // the u16 field is private and immutable. BlockStateId::STATE_COUNT is a const u16. // If the condition held once, it will always hold. unsafe { std::hint::assert_unchecked(id.as_u16() < BlockStateId::STATE_COUNT) } // This hint guarantees that bound checks can be optimized away in release builds. // Due to debug_assertions forcing -Zub_checks=yes (rust-lang/rust#123499) // bound checks-panics are replaced with ub-checks on profile.dev // https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/ub-checks.html mappings::STATE_FROM_STATE_ID[id.as_u16() as usize] } #[doc = r" Get a block state from a state id and the corresponding block."] #[inline] #[must_use] pub const fn from_id_with_block(id: BlockStateId) -> (&'static Block, &'static Self) { let block = Block::from_state_id(id); let state = Self::from_id(id); (block, state) } #[must_use] pub const fn to_be_network_id(id: BlockStateId) -> u16 { // Safety: We always check this condition when creating a BlockStateId. // the u16 field is private and immutable. BlockStateId::STATE_COUNT is a const u16. // If the condition held once, it will always hold. unsafe { std::hint::assert_unchecked(id.as_u16() < BlockStateId::STATE_COUNT) } // This hint guarantees that bound checks can be optimized away in release builds. // Due to debug_assertions forcing -Zub_checks=yes (rust-lang/rust#123499) // bound checks-panics are replaced with ub-checks on profile.dev // https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/ub-checks.html Self::STATE_ID_TO_BEDROCK[id.as_u16() as usize] } } impl BlockStateId { pub const AIR: Self = Block::AIR.default_state.id; pub(crate) const STATE_COUNT: u16 = mappings::STATE_FROM_STATE_ID.len() as u16; } mod mappings { use crate::{Block, BlockId, BlockState, BlockStateId}; use phf; pub(super) static BLOCK_FROM_NAME_MAP: phf::Map<&'static str, Block> = phf::phf_map!{ #(#block_from_name_entries)* }; pub(super) static BLOCK_ID_FROM_STATE_ID: [BlockId; #max_state_id] = [ #raw_id_from_state_id ]; pub(super) static TYPE_FROM_RAW_ID: [&Block; #max_type_id] = [ #type_from_raw_id_items ]; pub(super) static STATE_FROM_STATE_ID: [&BlockState; #max_state_id] = [ #state_from_state_id ]; } impl Block { #(#constants_list)* #[doc = r" Try to parse a block from a resource location string."] #[inline] #[must_use] pub fn from_registry_key(name: &str) -> Option<&'static Self> { mappings::BLOCK_FROM_NAME_MAP.get(name) } #[doc = r" Try to get a block from a namespace prefixed name."] #[must_use] pub fn from_name(name: &str) -> Option<&'static Self> { let key = name.strip_prefix("minecraft:").unwrap_or(name); mappings::BLOCK_FROM_NAME_MAP.get(key) } /// Get a [`Block`] from a [`BlockId`] #[inline] #[must_use] pub const fn from_id(id: BlockId) -> &'static Self { // Safety: We always check this condition when creating a BlockId. // the u16 field is private and immutable. BlockId::BLOCK_COUNT is a const u16. // If the condition held once, it will always hold. unsafe { std::hint::assert_unchecked(id.as_u16() < BlockId::BLOCK_COUNT) } // This hint guarantees that bound checks can be optimized away in release builds. // Due to debug_assertions forcing -Zub_checks=yes (rust-lang/rust#123499) // bound checks-panics are replaced with ub-checks on profile.dev // https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/ub-checks.html mappings::TYPE_FROM_RAW_ID[id.as_u16() as usize] } /// Get a [`Block`] from a state id #[inline] #[must_use] pub const fn from_state_id(id: BlockStateId) -> &'static Self { Self::from_id(BlockId::from_state_id(id)) } #[doc = r" Try to parse a block from an item id."] #[must_use] pub const fn from_item_id(id: u16) -> Option<&'static Self> { #[allow(unreachable_patterns)] match id { #(#block_from_item_id_arms)* _ => None } } #[track_caller] #[doc = r" Get the properties of the block."] pub fn properties(&self, state_id: BlockStateId) -> Option> { Some(match self.id.as_u16() { #(#block_properties_from_state_and_block_id_arms)* _ => return None, }) } #[track_caller] #[doc = r" Get the properties of the block."] pub fn from_properties(&self, props: &[(&str, &str)]) -> Box { match self.id.as_u16() { #(#block_properties_from_props_and_name_arms)* _ => panic!("Invalid props") } } } impl BlockId { #(#block_id_constants)* pub(crate) const BLOCK_COUNT: u16 = mappings::TYPE_FROM_RAW_ID.len() as u16; /// Get a [`BlockId`] from a [`BlockStateId`] #[inline] #[must_use] pub const fn from_state_id(id: BlockStateId) -> BlockId { // Safety: We always check this condition when creating a BlockStateId. // the u16 field is private and immutable. BlockStateId::STATE_COUNT is a const u16. // If the condition held once, it will always hold. unsafe { std::hint::assert_unchecked(id.as_u16() < BlockStateId::STATE_COUNT) } // This hint guarantees that bound checks can be optimized away in release builds. // Due to debug_assertions forcing -Zub_checks=yes (rust-lang/rust#123499) // bound checks-panics are replaced with ub-checks on profile.dev // https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/ub-checks.html mappings::BLOCK_ID_FROM_STATE_ID[id.as_u16() as usize] } } #(#properties)* #(#block_props)* impl Facing { #[must_use] pub const fn opposite(&self) -> Self { match self { Self::North => Self::South, Self::South => Self::North, Self::East => Self::West, Self::West => Self::East, Self::Up => Self::Down, Self::Down => Self::Up, } } } impl HorizontalFacing { #[must_use] pub fn all() -> [Self; 4] { [ Self::North, Self::South, Self::West, Self::East, ] } #[must_use] pub fn to_offset(&self) -> Vector3 { match self { Self::North => (0, 0, -1), Self::South => (0, 0, 1), Self::West => (-1, 0, 0), Self::East => (1, 0, 0), } .into() } #[must_use] pub const fn to_axis(&self) -> HorizontalAxis { match self { Self::North | Self::South => HorizontalAxis::Z, Self::West | Self::East => HorizontalAxis::X, } } #[must_use] pub const fn to_facing(&self) -> HorizontalFacing { match self { Self::North => HorizontalFacing::North, Self::South => HorizontalFacing::South, Self::West => HorizontalFacing::West, Self::East => HorizontalFacing::East, } } #[must_use] pub const fn opposite(&self) -> Self { match self { Self::North => Self::South, Self::South => Self::North, Self::West => Self::East, Self::East => Self::West, } } #[must_use] pub const fn rotate_clockwise(&self) -> Self { match self { Self::North => Self::East, Self::South => Self::West, Self::West => Self::North, Self::East => Self::South, } } #[must_use] pub const fn rotate_counter_clockwise(&self) -> Self { match self { Self::North => Self::West, Self::South => Self::East, Self::West => Self::South, Self::East => Self::North, } } } impl RailShape { #[must_use] pub const fn is_ascending(&self) -> bool { matches!(self, Self::AscendingEast | Self::AscendingWest | Self::AscendingNorth | Self::AscendingSouth) } } impl RailShapeStraight { #[must_use] pub const fn is_ascending(&self) -> bool { matches!(self, Self::AscendingEast | Self::AscendingWest | Self::AscendingNorth | Self::AscendingSouth) } } } } /// Parses the Bedrock Edition block palette NBT file into a map of block names to their state variants. /// /// # Arguments /// – `reader` – a readable byte source positioned at the start of the NBT data. #[expect(clippy::type_complexity)] fn get_be_data_from_nbt( reader: &mut R, ) -> BTreeMap)>> { let mut block_data: BTreeMap)>> = BTreeMap::new(); let mut current_id = 0; let data_start = reader.stream_position().unwrap(); let data_end = reader.seek(SeekFrom::End(0)).unwrap(); let _ = reader.seek(SeekFrom::Start(data_start)); let nbt_reader = &mut NbtReadHelperBedrock::new(pumpkin_nbt::deserializer::NbtStreamReader(&mut *reader)); loop { if nbt_reader.reader().0.stream_position().unwrap() >= data_end { break; } let nbt = pumpkin_nbt::Nbt::read(nbt_reader).unwrap(); let block_name = { let raw_name = nbt.get_string("name").unwrap(); raw_name .strip_prefix("minecraft:") .unwrap_or(raw_name) .to_string() }; let properties = nbt .get_compound("states") .unwrap() .clone() .into_iter() .map(|(key, val)| { let unpacked = match val { pumpkin_nbt::tag::NbtTag::Byte(v) => { if v == 1 { "true".into() } else { "false".into() } } pumpkin_nbt::tag::NbtTag::Int(v) => v.to_string(), pumpkin_nbt::tag::NbtTag::String(v) => v.into(), _ => { panic!("Unexpected type for {}. Value: {val:?}", key); } }; (key.into(), unpacked) }) .collect::>(); if !block_name.is_empty() { block_data .entry(block_name) .or_default() .push((current_id, properties)); } current_id += 1; } block_data } fn parse_geyser_entry( entry: &pumpkin_nbt::tag::NbtTag, java_block_name: &str, ) -> (String, BTreeMap) { let compound = match entry { pumpkin_nbt::tag::NbtTag::Compound(c) => c, _ => panic!("Expected NbtCompound in Geyser bedrock_mappings list"), }; let raw_identifier = compound .get_string("bedrock_identifier") .filter(|s| !s.is_empty()) .unwrap_or(java_block_name); let bedrock_identifier = raw_identifier .strip_prefix("minecraft:") .unwrap_or(raw_identifier) .to_string(); let mut properties = BTreeMap::new(); if let Some(state_comp) = compound.get_compound("state") { for (key, val) in state_comp.clone() { let unpacked = match val { pumpkin_nbt::tag::NbtTag::Byte(v) => { if v == 1 { "true".to_string() } else { "false".to_string() } } pumpkin_nbt::tag::NbtTag::Int(v) => v.to_string(), pumpkin_nbt::tag::NbtTag::String(v) => v.to_string(), _ => { panic!( "Unexpected NBT type in Geyser state tag for {}. Value: {:?}", key, val ); } }; properties.insert(key.to_string(), unpacked); } } (bedrock_identifier, properties) }