mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
1560 lines
58 KiB
Rust
1560 lines
58 KiB
Rust
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<T: Clone + ToTokens>(array: Vec<(u16, T)>) -> Vec<TokenStream> {
|
||
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<TokenStream> {
|
||
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<PropertyVariantMapping>,
|
||
/// 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<PropertyVariantMapping>) -> 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<String>,
|
||
}
|
||
|
||
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::<Vec<_>>();
|
||
|
||
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::<Vec<_>>();
|
||
|
||
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<f64>,
|
||
/// The maximum corner of the bounding box.
|
||
pub max: Vector3<f64>,
|
||
}
|
||
|
||
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<u16>,
|
||
/// Indices into the global shapes array for outline (selection) shape segments.
|
||
pub outline_shapes: Vec<u16>,
|
||
/// Opacity value for light propagation, if non-full.
|
||
pub opacity: Option<u8>,
|
||
/// Associated block-entity type ID, if any.
|
||
pub block_entity_type: Option<u16>,
|
||
}
|
||
|
||
/// 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<FlammableStruct>,
|
||
/// Loot table used when the block is broken, if any.
|
||
pub loot_table: Option<LootTableStruct>,
|
||
/// 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<i32>,
|
||
/// 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<BlockState>,
|
||
/// Experience points dropped when the block is mined, if any.
|
||
pub experience: Option<Experience>,
|
||
}
|
||
|
||
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<String>,
|
||
},
|
||
}
|
||
|
||
/// 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<String>,
|
||
}
|
||
|
||
/// 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<Block>,
|
||
/// All unique bounding boxes referenced by block states.
|
||
pub shapes: Vec<BoundingBox>,
|
||
/// Registry names of all block entity types.
|
||
pub block_entity_types: Vec<String>,
|
||
}
|
||
|
||
/// 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<GeneratedProperty> =
|
||
serde_json::from_str(&fs::read_to_string("../assets/properties.json").unwrap())
|
||
.expect("Failed to parse properties.json");
|
||
|
||
let generated_prop_map: BTreeMap<i32, &GeneratedProperty> = 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<String, PropertyStruct> = BTreeMap::new();
|
||
let mut block_properties: Vec<BlockPropertyStruct> = Vec::new();
|
||
let mut property_collection_map: BTreeMap<Vec<i32>, PropertyCollectionData> = BTreeMap::new();
|
||
let mut existing_item_ids: HashSet<u16> = 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<i32> = 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::<Vec<u16>>(),
|
||
);
|
||
|
||
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<Box<dyn BlockProperties>> {
|
||
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<dyn BlockProperties> {
|
||
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<i32> {
|
||
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<R: Read + Seek>(
|
||
reader: &mut R,
|
||
) -> BTreeMap<String, Vec<(u32, BTreeMap<String, String>)>> {
|
||
let mut block_data: BTreeMap<String, Vec<(u32, BTreeMap<String, String>)>> = 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::<BTreeMap<_, _>>();
|
||
|
||
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<String, String>) {
|
||
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)
|
||
}
|