diff --git a/Cargo.lock b/Cargo.lock index 4f66bfda4..5ff8491d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2065,6 +2065,7 @@ dependencies = [ "num-traits", "parking_lot", "pumpkin-core", + "pumpkin-macros", "rand", "rayon", "serde", diff --git a/pumpkin-macros/src/block_id.rs b/pumpkin-macros/src/block_state.rs similarity index 54% rename from pumpkin-macros/src/block_id.rs rename to pumpkin-macros/src/block_state.rs index a71e90df4..3f10f3943 100644 --- a/pumpkin-macros/src/block_id.rs +++ b/pumpkin-macros/src/block_state.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, sync::LazyLock}; +use std::{ + collections::{HashMap, HashSet}, + sync::LazyLock, +}; use itertools::Itertools; use proc_macro::TokenStream; @@ -50,7 +53,108 @@ static BLOCKS: LazyLock> = LazyLock::new(|| { .expect("Could not parse block.json registry.") }); -pub fn block_id_impl(item: TokenStream) -> TokenStream { +fn pascal_case(original: &str) -> String { + let mut pascal = String::new(); + let mut capitalize = true; + for ch in original.chars() { + if ch == '_' { + capitalize = true; + } else if capitalize { + pascal.push(ch.to_ascii_uppercase()); + capitalize = false; + } else { + pascal.push(ch); + } + } + pascal +} + +pub fn block_type_enum_impl() -> TokenStream { + let categories: &HashSet<&str> = &BLOCKS + .values() + .map(|val| val.definition.category.as_str()) + .collect(); + + let original_and_converted_stream = categories.iter().map(|key| { + ( + key, + pascal_case(key.split_once(':').expect("Bad minecraft id").1), + ) + }); + let new_names: proc_macro2::TokenStream = original_and_converted_stream + .clone() + .map(|(_, x)| x) + .join(",\n") + .parse() + .unwrap(); + + let from_string: proc_macro2::TokenStream = original_and_converted_stream + .clone() + .map(|(original, converted)| format!("\"{}\" => BlockCategory::{},", original, converted)) + .join("\n") + .parse() + .unwrap(); + + // I;ve never used macros before so call me out on this lol + quote! { + #[derive(PartialEq, Clone)] + pub enum BlockCategory { + #new_names + } + + impl BlockCategory { + pub fn from_registry_id(id: &str) -> BlockCategory { + match id { + #from_string + _ => panic!("Not a valid block type id"), + } + } + } + } + .into() +} + +pub fn block_enum_impl() -> TokenStream { + let original_and_converted_stream = &BLOCKS.keys().map(|key| { + ( + key, + pascal_case(key.split_once(':').expect("Bad minecraft id").1), + ) + }); + let new_names: proc_macro2::TokenStream = original_and_converted_stream + .clone() + .map(|(_, x)| x) + .join(",\n") + .parse() + .unwrap(); + + let from_string: proc_macro2::TokenStream = original_and_converted_stream + .clone() + .map(|(original, converted)| format!("\"{}\" => Block::{},", original, converted)) + .join("\n") + .parse() + .unwrap(); + + // I;ve never used macros before so call me out on this lol + quote! { + #[derive(PartialEq, Clone)] + pub enum Block { + #new_names + } + + impl Block { + pub fn from_registry_id(id: &str) -> Block { + match id { + #from_string + _ => panic!("Not a valid block id"), + } + } + } + } + .into() +} + +pub fn block_state_impl(item: TokenStream) -> TokenStream { let data = syn::punctuated::Punctuated::::parse_terminated .parse(item) .unwrap(); @@ -61,9 +165,12 @@ pub fn block_id_impl(item: TokenStream) -> TokenStream { let block_name = match block_name { syn::Expr::Lit(lit) => match &lit.lit { syn::Lit::Str(name) => name.value(), - _ => panic!("The first argument should be a string"), + _ => panic!("The first argument should be a string, have: {:?}", lit), }, - _ => panic!("The first argument should be a string"), + _ => panic!( + "The first argument should be a string, have: {:?}", + block_name + ), }; let mut properties = HashMap::new(); @@ -104,7 +211,7 @@ pub fn block_id_impl(item: TokenStream) -> TokenStream { .get(&block_name) .expect("Block with that name does not exist"); - let id = if properties.is_empty() { + let state = if properties.is_empty() { block_info .states .iter() @@ -112,14 +219,13 @@ pub fn block_id_impl(item: TokenStream) -> TokenStream { .expect( "Error inside blocks.json file: Every Block should have at least 1 default state", ) - .id } else { match block_info .states .iter() .find(|state| state.properties == properties) { - Some(state) => state.id, + Some(state) => state, None => panic!( "Could not find block with these properties, the following are valid properties: \n{}", block_info @@ -131,13 +237,24 @@ pub fn block_id_impl(item: TokenStream) -> TokenStream { } }; + let id = state.id; + let category_name = block_info.definition.category.clone(); + if std::env::var("CARGO_PKG_NAME").unwrap() == "pumpkin-world" { quote! { - crate::block::block_id::BlockId::from_id(#id as u16) + crate::block::block_state::BlockState::new_unchecked( + #id as u16, + crate::block::Block::from_registry_id(#block_name), + crate::block::BlockCategory::from_registry_id(#category_name), + ) } } else { quote! { - pumpkin_world::block::block_id::BlockId::from_id(#id as u16) + pumpkin_world::block::block_id::BlockStateId::new_unchecked( + #id as u16, + pumpkin_world::block::Block::from_registry_id(#block_name), + pumpkin_world::block::BlockCategory::from_registry_id(#category_name), + ) } } .into() diff --git a/pumpkin-macros/src/lib.rs b/pumpkin-macros/src/lib.rs index da53c14be..3c27f91a2 100644 --- a/pumpkin-macros/src/lib.rs +++ b/pumpkin-macros/src/lib.rs @@ -23,8 +23,20 @@ pub fn packet(input: TokenStream, item: TokenStream) -> TokenStream { gen.into() } -mod block_id; +mod block_state; #[proc_macro] -pub fn block_id(item: TokenStream) -> TokenStream { - block_id::block_id_impl(item) +pub fn block(item: TokenStream) -> TokenStream { + block_state::block_state_impl(item) +} + +#[proc_macro] +/// Creates an enum for all block types. Should only be used once +pub fn blocks_enum(_item: TokenStream) -> TokenStream { + block_state::block_enum_impl() +} + +#[proc_macro] +/// Creates an enum for all block categories. Should only be used once +pub fn block_categories_enum(_item: TokenStream) -> TokenStream { + block_state::block_type_enum_impl() } diff --git a/pumpkin-world/Cargo.toml b/pumpkin-world/Cargo.toml index 43d335c13..453842903 100644 --- a/pumpkin-world/Cargo.toml +++ b/pumpkin-world/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true [dependencies] pumpkin-core = { path = "../pumpkin-core" } +pumpkin-macros = { path = "../pumpkin-macros" } fastnbt = { git = "https://github.com/owengage/fastnbt.git" } tokio.workspace = true diff --git a/pumpkin-world/src/block/block_id.rs b/pumpkin-world/src/block/block_id.rs deleted file mode 100644 index 747daff2a..000000000 --- a/pumpkin-world/src/block/block_id.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::collections::HashMap; - -use serde::Deserialize; - -use super::block_registry::BLOCKS; -use crate::level::WorldError; - -// 0 is air -> reasonable default -#[derive(Default, Deserialize, Debug, Hash, Clone, Copy, PartialEq, Eq)] -#[serde(transparent)] -pub struct BlockId { - data: u16, -} - -impl BlockId { - pub const AIR: Self = Self::from_id(0); - - pub fn new( - text_id: &str, - properties: Option<&HashMap>, - ) -> Result { - let mut block_states = BLOCKS - .get(text_id) - .ok_or(WorldError::BlockIdentifierNotFound)? - .states - .iter(); - - let block_state = match properties { - Some(properties) => block_states - .find(|state| &state.properties == properties) - .ok_or_else(|| WorldError::BlockStateIdNotFound)?, - None => block_states - .find(|state| state.is_default) - .expect("Every Block should have at least 1 default state"), - }; - - Ok(block_state.id) - } - - pub const fn from_id(id: u16) -> Self { - // TODO: add check if the id is actually valid - Self { data: id } - } - - pub fn is_air(&self) -> bool { - self.data == 0 || self.data == 12959 || self.data == 12958 - } - - pub fn get_id(&self) -> u16 { - self.data - } - - /// An i32 is the way mojang internally represents their Blocks - pub fn get_id_mojang_repr(&self) -> i32 { - self.data as i32 - } -} diff --git a/pumpkin-world/src/block/block_registry.rs b/pumpkin-world/src/block/block_registry.rs index cd06e98de..896efd8b2 100644 --- a/pumpkin-world/src/block/block_registry.rs +++ b/pumpkin-world/src/block/block_registry.rs @@ -2,13 +2,16 @@ use std::{collections::HashMap, sync::LazyLock}; use serde::Deserialize; -use super::block_id::BlockId; +use super::BlockState; pub static BLOCKS: LazyLock> = LazyLock::new(|| { serde_json::from_str(include_str!("../../../assets/blocks.json")) .expect("Could not parse block.json registry.") }); +pumpkin_macros::blocks_enum!(); +pumpkin_macros::block_categories_enum!(); + #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub struct RegistryBlockDefinition { /// e.g. minecraft:door or minecraft:button @@ -48,3 +51,31 @@ pub struct RegistryBlockType { #[serde(default, rename = "properties")] valid_properties: HashMap>, } + +#[derive(Default, Copy, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[serde(transparent)] +pub struct BlockId { + pub data: u16, +} + +impl BlockId { + pub fn is_air(&self) -> bool { + self.data == 0 || self.data == 12959 || self.data == 12958 + } + + pub fn get_id_mojang_repr(&self) -> i32 { + self.data as i32 + } + + pub fn get_id(&self) -> u16 { + self.data + } +} + +impl From for BlockId { + fn from(value: BlockState) -> Self { + Self { + data: value.get_id(), + } + } +} diff --git a/pumpkin-world/src/block/block_state.rs b/pumpkin-world/src/block/block_state.rs new file mode 100644 index 000000000..437349df2 --- /dev/null +++ b/pumpkin-world/src/block/block_state.rs @@ -0,0 +1,73 @@ +use std::collections::HashMap; + +use crate::level::WorldError; + +use super::block_registry::{Block, BlockCategory, BLOCKS}; + +#[derive(Clone)] +pub struct BlockState { + state_id: u16, + block: Block, + category: BlockCategory, +} + +impl BlockState { + pub const AIR: BlockState = BlockState { + state_id: 0, + block: Block::Air, + category: BlockCategory::Air, + }; + + pub fn new( + registry_id: &str, + properties: Option<&HashMap>, + ) -> Result { + let block_registry = BLOCKS + .get(registry_id) + .ok_or(WorldError::BlockIdentifierNotFound)?; + let mut block_states = block_registry.states.iter(); + + let block_state = match properties { + Some(properties) => block_states + .find(|state| &state.properties == properties) + .ok_or_else(|| WorldError::BlockStateIdNotFound)?, + None => block_states + .find(|state| state.is_default) + .expect("Every Block should have at least 1 default state"), + }; + + Ok(Self { + state_id: block_state.id.data, + block: Block::from_registry_id(registry_id), + category: BlockCategory::from_registry_id(&block_registry.definition.category), + }) + } + + pub const fn new_unchecked(state_id: u16, block: Block, category: BlockCategory) -> Self { + Self { + state_id, + block, + category, + } + } + + pub fn is_air(&self) -> bool { + self.category == BlockCategory::Air + } + + pub fn get_id(&self) -> u16 { + self.state_id + } + + pub fn get_id_mojang_repr(&self) -> i32 { + self.state_id as i32 + } + + pub fn of_block(&self, block: Block) -> bool { + self.block == block + } + + pub fn of_category(&self, category: BlockCategory) -> bool { + self.category == category + } +} diff --git a/pumpkin-world/src/block/mod.rs b/pumpkin-world/src/block/mod.rs index f5fbab664..8a838dedc 100644 --- a/pumpkin-world/src/block/mod.rs +++ b/pumpkin-world/src/block/mod.rs @@ -1,11 +1,13 @@ use num_derive::FromPrimitive; -pub mod block_id; mod block_registry; +pub mod block_state; -pub use block_id::BlockId; use pumpkin_core::math::vector3::Vector3; +pub use block_registry::{Block, BlockCategory, BlockId}; +pub use block_state::BlockState; + #[derive(FromPrimitive)] pub enum BlockFace { Bottom = 0, diff --git a/pumpkin-world/src/chunk.rs b/pumpkin-world/src/chunk.rs index dd5aa643b..2037bf5fa 100644 --- a/pumpkin-world/src/chunk.rs +++ b/pumpkin-world/src/chunk.rs @@ -7,7 +7,7 @@ use pumpkin_core::math::vector2::Vector2; use serde::{Deserialize, Serialize}; use crate::{ - block::BlockId, + block::{BlockId, BlockState}, coordinates::{ChunkRelativeBlockCoordinates, Height}, level::{ChunkNotGeneratedError, WorldError}, WORLD_HEIGHT, @@ -215,7 +215,12 @@ impl ChunkData { let palette = block_states .palette .iter() - .map(|entry| BlockId::new(&entry.name, entry.properties.as_ref())) + .map( + |entry| match BlockState::new(&entry.name, entry.properties.as_ref()) { + Err(e) => Err(e), + Ok(state) => Ok(state.into()), + }, + ) .collect::, _>>()?; let block_data = match block_states.data { diff --git a/pumpkin-world/src/world_gen/generator.rs b/pumpkin-world/src/world_gen/generator.rs index 0a7d043d6..36f3f9553 100644 --- a/pumpkin-world/src/world_gen/generator.rs +++ b/pumpkin-world/src/world_gen/generator.rs @@ -3,7 +3,7 @@ use pumpkin_core::math::vector2::Vector2; use static_assertions::assert_obj_safe; use crate::biome::Biome; -use crate::block::BlockId; +use crate::block::block_state::BlockState; use crate::chunk::ChunkData; use crate::coordinates::{BlockCoordinates, XZBlockCoordinates}; use crate::world_gen::Seed; @@ -26,12 +26,12 @@ pub(crate) trait TerrainGenerator: Sync + Send { fn prepare_chunk(&self, at: &Vector2); /// Is static - fn generate_block(&self, at: BlockCoordinates, biome: Biome) -> BlockId; + fn generate_block(&self, at: BlockCoordinates, biome: Biome) -> BlockState; } pub(crate) trait PerlinTerrainGenerator: Sync + Send { fn prepare_chunk(&self, at: &Vector2, perlin: &Perlin); /// Dependens on the perlin noise height - fn generate_block(&self, at: BlockCoordinates, chunk_height: i16, biome: Biome) -> BlockId; + fn generate_block(&self, at: BlockCoordinates, chunk_height: i16, biome: Biome) -> BlockState; } diff --git a/pumpkin-world/src/world_gen/generic_generator.rs b/pumpkin-world/src/world_gen/generic_generator.rs index 8bdc17135..1f0a17657 100644 --- a/pumpkin-world/src/world_gen/generic_generator.rs +++ b/pumpkin-world/src/world_gen/generic_generator.rs @@ -62,11 +62,13 @@ impl WorldGenerator for GenericGen blocks.set_block( coordinates, - self.terrain_generator.generate_block( - coordinates.with_chunk_coordinates(at), - chunk_height as i16, - biome, - ), + self.terrain_generator + .generate_block( + coordinates.with_chunk_coordinates(at), + chunk_height as i16, + biome, + ) + .into(), ); } } diff --git a/pumpkin-world/src/world_gen/implementation/overworld/biome/plains.rs b/pumpkin-world/src/world_gen/implementation/overworld/biome/plains.rs index cb3f3e981..001c71be7 100644 --- a/pumpkin-world/src/world_gen/implementation/overworld/biome/plains.rs +++ b/pumpkin-world/src/world_gen/implementation/overworld/biome/plains.rs @@ -3,7 +3,7 @@ use pumpkin_core::math::vector2::Vector2; use crate::{ biome::Biome, - block::BlockId, + block::block_state::BlockState, coordinates::{BlockCoordinates, XZBlockCoordinates}, world_gen::{ generator::{BiomeGenerator, GeneratorInit, PerlinTerrainGenerator}, @@ -40,21 +40,21 @@ impl GeneratorInit for PlainsTerrainGenerator { impl PerlinTerrainGenerator for PlainsTerrainGenerator { fn prepare_chunk(&self, _at: &Vector2, _perlin: &Perlin) {} // TODO allow specifying which blocks should be at which height in the config. - fn generate_block(&self, at: BlockCoordinates, chunk_height: i16, _: Biome) -> BlockId { + fn generate_block(&self, at: BlockCoordinates, chunk_height: i16, _: Biome) -> BlockState { let begin_stone_height = chunk_height - 5; let begin_dirt_height = chunk_height - 1; let y = *at.y; if y == -64 { - BlockId::from_id(79) // BEDROCK + pumpkin_macros::block!("minecraft:bedrock") } else if y >= -63 && y <= begin_stone_height { - return BlockId::from_id(1); // STONE + pumpkin_macros::block!("minecraft:stone") } else if y >= begin_stone_height && y < begin_dirt_height { - return BlockId::from_id(10); // DIRT; + pumpkin_macros::block!("minecraft:dirt") } else if y == chunk_height - 1 { - return BlockId::from_id(9); // GRASS BLOCK + pumpkin_macros::block!("minecraft:grass_block") } else { - BlockId::AIR + BlockState::AIR } } } diff --git a/pumpkin-world/src/world_gen/implementation/superflat.rs b/pumpkin-world/src/world_gen/implementation/superflat.rs index 3acf32495..872c31515 100644 --- a/pumpkin-world/src/world_gen/implementation/superflat.rs +++ b/pumpkin-world/src/world_gen/implementation/superflat.rs @@ -1,8 +1,9 @@ use pumpkin_core::math::vector2::Vector2; +use pumpkin_macros::block; use crate::{ biome::Biome, - block::BlockId, + block::block_state::BlockState, coordinates::{BlockCoordinates, XZBlockCoordinates}, world_gen::{ generator::{BiomeGenerator, GeneratorInit, TerrainGenerator}, @@ -40,12 +41,12 @@ impl GeneratorInit for SuperflatTerrainGenerator { impl TerrainGenerator for SuperflatTerrainGenerator { fn prepare_chunk(&self, _at: &Vector2) {} // TODO allow specifying which blocks should be at which height in the config. - fn generate_block(&self, at: BlockCoordinates, _: Biome) -> BlockId { + fn generate_block(&self, at: BlockCoordinates, _: Biome) -> BlockState { match *at.y { - -64 => BlockId::from_id(79), // Bedrock - -63..=-62 => BlockId::from_id(10), // Dirt - -61 => BlockId::from_id(9), // Grass - _ => BlockId::AIR, + -64 => block!("minecraft:bedrock"), + -63..=-62 => block!("minecraft:dirt"), + -61 => block!("minecraft:grass_block"), + _ => BlockState::AIR, } } } diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index 420d3589b..5e2e48da7 100644 --- a/pumpkin/src/client/player_packet.rs +++ b/pumpkin/src/client/player_packet.rs @@ -29,7 +29,7 @@ use pumpkin_protocol::{ SUseItemOn, Status, }, }; -use pumpkin_world::block::{BlockFace, BlockId}; +use pumpkin_world::block::{BlockFace, BlockState}; use pumpkin_world::global_registry; use super::PlayerConfig; @@ -517,7 +517,7 @@ impl Player { item.item_id, ) .expect("All item ids are in the global registry"); - if let Ok(block_state_id) = BlockId::new(minecraft_id, None) { + if let Ok(block_state_id) = BlockState::new(minecraft_id, None) { let entity = &self.entity; let world = &entity.world; world.broadcast_packet_all(&CBlockUpdate::new(