diff --git a/Cargo.toml b/Cargo.toml index 545bff2d..7e767d0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ name = "minecraft-protocol" version = "0.1.0" authors = ["Mubelotix "] edition = "2018" +build = "build/build.rs" [dependencies] minecraft-packet-derive = {path="../minecraft-packet-derive"} diff --git a/build.rs b/build.rs deleted file mode 100644 index 5b782ff6..00000000 --- a/build.rs +++ /dev/null @@ -1,719 +0,0 @@ -use convert_case::{Case, Casing}; -use serde::{Deserialize, Serialize}; -use std::io::{ErrorKind, Read, Write}; -use std::{collections::HashMap, fs::File}; - -/// Changing this is not enough, please also change static urls in main() since -const VERSION: &str = "1.16.5"; - -fn get_data(url: &str, cache: &str) -> serde_json::Value { - match File::open(cache) { - // The cache file is ready - Ok(mut file) => { - let mut data = Vec::new(); - if let Err(e) = file.read_to_end(&mut data) { - panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded and cached to `{}`. Unfortunately, this file cannot be read. Error: {}", cache, e) - } - - let json_text = match String::from_utf8(data) { - Ok(json_text) => json_text, - Err(e) => panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded and cached to `{}`. Unfortunately, this file appears to contain invalid text data. Error: {}\nNote: Deleting the file will allow the library to download it again.", cache, e), - }; - - let json = match serde_json::from_str(&json_text) { - Ok(json) => json, - Err(e) => panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded and cached to `{}`. Unfortunately, this file appears to contain invalid json data. Error: {}\nNote: Deleting the file will allow the library to download it again.", cache, e), - }; - - json - } - // The cache file needs to be downloaded - Err(e) if e.kind() == ErrorKind::NotFound => { - let response = match minreq::get(url).send() { - Ok(response) => response, - Err(e) => panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded from `{}`. Unfortunately, we can't access this URL. Error: {}", url, e) - }; - - let json_text = match response.as_str() { - Ok(json_text) => json_text, - Err(e) => panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded from `{}`. Unfortunately, this file appears to contain invalid data. Error: {}", url, e), - }; - - let mut file = match File::create(cache) { - Ok(file) => file, - Err(e) => panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded and cached to `{}`. Unfortunately, we can't access this path. Error: {}", cache, e), - }; - - if let Err(e) = file.write_all(json_text.as_bytes()) { - panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded and cached to `{}`. Unfortunately, we can't write to this path. Error: {}", cache, e) - }; - - let json = match serde_json::from_str(json_text) { - Ok(json) => json, - Err(e) => panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded and cached to `{}`. Unfortunately, this file appears to contain invalid json data. Error: {}\nNote: Deleting the file will allow the library to download it again.", cache, e), - }; - - json - } - - // The cache file cannot be accessed - Err(e) => { - panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded and cached to `{}`. Unfortunately, we can't access this path. Error: {}", cache, e); - } - } -} - -mod blocks { - use super::*; - - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - struct Block { - id: u32, - #[serde(rename = "name")] - text_id: String, - display_name: String, - hardness: f32, - resistance: f32, - diggable: bool, - transparent: bool, - filter_light: u8, - emit_light: u8, - default_state: u32, - min_state_id: u32, - max_state_id: u32, - drops: Vec, - material: Option, - #[serde(default)] - harvest_tools: HashMap, - } - - #[allow(clippy::explicit_counter_loop)] - pub fn generate_block_enum(data: serde_json::Value) { - let mut blocks: Vec = serde_json::from_value(data).expect("Invalid block data"); - blocks.sort_by_key(|block| block.id); - - // Look for missing blocks in the array - let mut expected = 0; - for block in &blocks { - if block.id != expected { - panic!("The block with id {} is missing.", expected) - } - expected += 1; - } - - // Process a few fields - let mut raw_harvest_tools: Vec> = Vec::new(); - let mut raw_materials: Vec = Vec::new(); - for block in &blocks { - raw_harvest_tools.push( - block - .harvest_tools - .clone() - .into_iter() - .map(|(k, _v)| k) - .collect(), - ); - raw_materials.push( - block - .material - .clone() - .unwrap_or_else(|| "unknown_material".to_string()) - .from_case(Case::Snake) - .to_case(Case::UpperCamel), - ); - } - - // Generate the MaterialBlock enum and array - let mut different_materials = raw_materials.clone(); - different_materials.sort(); - different_materials.dedup(); - let mut material_variants = String::new(); - for material in different_materials { - material_variants.push_str(&format!("\t{},\n", material)); - } - let mut materials = String::new(); - materials.push('['); - for material in raw_materials { - materials.push_str("Some(BlockMaterial::"); - materials.push_str(&material); - materials.push_str("), "); - } - materials.push(']'); - - // Generate the HARVEST_TOOLS array - let mut harvest_tools = String::new(); - harvest_tools.push('['); - for block_harvest_tools in raw_harvest_tools { - harvest_tools.push_str("&["); - for harvest_tool in block_harvest_tools { - harvest_tools.push_str(&harvest_tool.to_string()); - harvest_tools.push_str(", "); - } - harvest_tools.push_str("], "); - } - harvest_tools.push(']'); - - // Enumerate the air blocks - let mut air_blocks = vec![false; expected as usize]; - for air_block in &[ - "air", - "cave_air", - "grass", - "torch", - "wall_torch", - "wheat", - "soul_torch", - "soul_wall_torch", - "carrots", - "potatoes", - ] { - let mut success = false; - for block in &blocks { - if &block.text_id.as_str() == air_block { - air_blocks[block.id as usize] = true; - success = true; - break; - } - } - if !success { - panic!("Could not find block {} in the block array", air_block); - } - } - - // Generate the variants of the Block enum - let mut variants = String::new(); - for block in &blocks { - let name = block - .text_id - .from_case(Case::Snake) - .to_case(Case::UpperCamel); - variants.push_str(&format!("\t{} = {},\n", name, block.id)); - } - - // Generate the `match` of state ids - let mut state_id_match_arms = String::new(); - for block in &blocks { - let name = block - .text_id - .from_case(Case::Snake) - .to_case(Case::UpperCamel); - let start = block.min_state_id; - let stop = block.max_state_id; - if start != stop { - state_id_match_arms.push_str(&format!( - "\t\t\t{}..={} => Some(Block::{}),\n", - start, stop, name - )); - } else { - state_id_match_arms - .push_str(&format!("\t\t\t{} => Some(Block::{}),\n", start, name)); - } - } - - // Generate the code - let code = format!( - r#"use crate::*; - -/// See [implementations](#implementations) for useful methods. -#[repr(u32)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Block {{ -{variants} -}} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum BlockMaterial {{ -{material_variants} -}} - -impl Block {{ - #[inline] - pub fn from_id(id: u32) -> Option {{ - if id < {max_value} {{ - Some(unsafe{{std::mem::transmute(id)}}) - }} else {{ - None - }} - }} - - pub fn from_state_id(state_id: u32) -> Option {{ - match state_id {{ -{state_id_match_arms} - _ => None, - }} - }} - - /// Get the textual identifier of this block. - #[inline] - pub fn get_text_id(self) -> &'static str {{ - unsafe {{*TEXT_IDS.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_default_state_id(self) -> u32 {{ - unsafe {{*DEFAULT_STATE_IDS.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_id(self) -> u32 {{ - self as u32 - }} - - /// This returns the item that will be dropped if you break the block. - /// If the item is Air, there is actually no drop. - #[inline] - pub fn get_associated_item_id(self) -> u32 {{ - unsafe {{*ITEM_IDS.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_resistance(self) -> f32 {{ - unsafe {{*RESISTANCES.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_hardness(self) -> f32 {{ - unsafe {{*HARDNESSES.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_material(self) -> Option {{ - unsafe {{*MATERIALS.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_display_name(self) -> &'static str {{ - unsafe {{*DISPLAY_NAMES.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_state_id_range(self) -> std::ops::Range {{ - unsafe {{STATE_ID_RANGES.get_unchecked((self as u32) as usize).clone()}} - }} - - #[inline] - pub fn is_diggable(self) -> bool {{ - unsafe {{*DIGGABLE.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn is_transparent(self) -> bool {{ - unsafe {{*TRANSPARENT.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_compatible_harvest_tools(self) -> &'static [u32] {{ - unsafe {{*HARVEST_TOOLS.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_light_emissions(self) -> u8 {{ - unsafe {{*LIGHT_EMISSIONS.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_light_absorption(self) -> u8 {{ - unsafe {{*LIGHT_ABSORPTION.get_unchecked((self as u32) as usize)}} - }} - - /// A "air block" is a block on which a player cannot stand, like air, wheat, torch... - /// Fire is excluded since you may not want your clients to walk trought fire by default. - /// The list of air blocks is maintained by hand. - /// It could not be exhaustive. - /// See also [Block::is_blocking]. - #[inline] - pub fn is_air_block(self) -> bool {{ - unsafe {{*AIR_BLOCKS.get_unchecked((self as u32) as usize)}} - }} - - /// The opposite of [Block::is_air_block]. - /// Fire is included since you may not want your clients to walk trought fire by default. - /// The list of blocking blocks is maintained by hand. - /// It could not be exhaustive. - #[inline] - pub fn is_blocking(self) -> bool {{ - unsafe {{!(*AIR_BLOCKS.get_unchecked((self as u32) as usize))}} - }} -}} - -impl<'a> MinecraftPacketPart<'a> for Block {{ - #[inline] - fn serialize_minecraft_packet_part(self, output: &mut Vec) -> Result<(), &'static str> {{ - VarInt((self as u32) as i32).serialize_minecraft_packet_part(output) - }} - - #[inline] - fn deserialize_minecraft_packet_part(input: &'a[u8]) -> Result<(Self, &'a[u8]), &'static str> {{ - let (id, input) = VarInt::deserialize_minecraft_packet_part(input)?; - let id = std::cmp::max(id.0, 0) as u32; - let block = Block::from_id(id).ok_or("No block corresponding to the specified numeric ID.")?; - Ok((block, input)) - }} -}} - -const TEXT_IDS: [&str; {max_value}] = {text_ids:?}; -const DISPLAY_NAMES: [&str; {max_value}] = {display_names:?}; -const STATE_ID_RANGES: [std::ops::Range; {max_value}] = {state_id_ranges:?}; -const DEFAULT_STATE_IDS: [u32; {max_value}] = {default_state_ids:?}; -const ITEM_IDS: [u32; {max_value}] = {item_ids:?}; -const RESISTANCES: [f32; {max_value}] = {resistances:?}; -const MATERIALS: [Option; {max_value}] = {materials}; -const HARVEST_TOOLS: [&[u32]; {max_value}] = {harvest_tools}; -const HARDNESSES: [f32; {max_value}] = {hardnesses:?}; -const LIGHT_EMISSIONS: [u8; {max_value}] = {light_emissions:?}; -const LIGHT_ABSORPTION: [u8; {max_value}] = {light_absorption:?}; -const DIGGABLE: [bool; {max_value}] = {diggable:?}; -const TRANSPARENT: [bool; {max_value}] = {transparent:?}; -const AIR_BLOCKS: [bool; {max_value}] = {air_blocks:?}; -"#, - variants = variants, - material_variants = material_variants, - max_value = expected, - state_id_match_arms = state_id_match_arms, - text_ids = blocks.iter().map(|b| &b.text_id).collect::>(), - display_names = blocks.iter().map(|b| &b.display_name).collect::>(), - state_id_ranges = blocks - .iter() - .map(|b| b.min_state_id..b.max_state_id + 1) - .collect::>(), - default_state_ids = blocks.iter().map(|b| b.default_state).collect::>(), - item_ids = blocks - .iter() - .map(|b| b.drops.get(0).copied().unwrap_or(0)) - .collect::>(), - materials = materials, - resistances = blocks.iter().map(|b| b.resistance).collect::>(), - harvest_tools = harvest_tools, - hardnesses = blocks.iter().map(|b| b.hardness).collect::>(), - light_emissions = blocks.iter().map(|b| b.emit_light).collect::>(), - light_absorption = blocks.iter().map(|b| b.filter_light).collect::>(), - diggable = blocks.iter().map(|b| b.diggable).collect::>(), - transparent = blocks.iter().map(|b| b.transparent).collect::>(), - air_blocks = air_blocks, - ); - - File::create("src/ids/blocks.rs") - .unwrap() - .write_all(code.as_bytes()) - .unwrap() - } -} - -mod items { - use super::*; - - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - struct Item { - id: u32, - display_name: String, - #[serde(rename = "name")] - text_id: String, - stack_size: u8, - durability: Option, - } - - #[allow(clippy::explicit_counter_loop)] - pub fn generate_item_enum(data: serde_json::Value) { - let mut items: Vec = serde_json::from_value(data).expect("Invalid block data"); - items.sort_by_key(|item| item.id); - - // Look for missing items in the array - let mut expected = 0; - for item in &items { - if item.id != expected { - panic!("The item with id {} is missing.", expected) - } - expected += 1; - } - - // Generate the variants of the Item enum - let mut variants = String::new(); - for item in &items { - let name = item - .text_id - .from_case(Case::Snake) - .to_case(Case::UpperCamel); - variants.push_str(&format!("\t{} = {},\n", name, item.id)); - } - - // Generate the code - let code = format!( - r#"use crate::*; - -/// See [implementations](#implementations) for useful methods. -#[repr(u32)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Item {{ -{variants} -}} - -impl Item {{ - #[inline] - pub fn from_id(id: u32) -> Option {{ - if id < {max_value} {{ - Some(unsafe{{std::mem::transmute(id)}}) - }} else {{ - None - }} - }} - - #[inline] - pub fn get_text_id(self) -> &'static str {{ - unsafe {{*TEXT_IDS.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_display_name(self) -> &'static str {{ - unsafe {{*DISPLAY_NAMES.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_max_stack_size(self) -> u8 {{ - unsafe {{*STACK_SIZES.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_durability(self) -> Option {{ - unsafe {{*DURABILITIES.get_unchecked((self as u32) as usize)}} - }} -}} - -impl<'a> MinecraftPacketPart<'a> for Item {{ - #[inline] - fn serialize_minecraft_packet_part(self, output: &mut Vec) -> Result<(), &'static str> {{ - VarInt((self as u32) as i32).serialize_minecraft_packet_part(output) - }} - - #[inline] - fn deserialize_minecraft_packet_part(input: &'a[u8]) -> Result<(Self, &'a[u8]), &'static str> {{ - let (id, input) = VarInt::deserialize_minecraft_packet_part(input)?; - let id = std::cmp::max(id.0, 0) as u32; - let item = Item::from_id(id).ok_or("No item corresponding to the specified numeric ID.")?; - Ok((item, input)) - }} -}} - -const STACK_SIZES: [u8; {max_value}] = {max_stack_sizes:?}; - -const DURABILITIES: [Option; {max_value}] = {durabilities:?}; - -const DISPLAY_NAMES: [&str; {max_value}] = {display_names:?}; - -const TEXT_IDS: [&str; {max_value}] = {text_ids:?}; -"#, - variants = variants, - max_value = expected, - max_stack_sizes = items.iter().map(|i| i.stack_size).collect::>(), - durabilities = items.iter().map(|i| i.durability).collect::>(), - display_names = items.iter().map(|i| &i.display_name).collect::>(), - text_ids = items.iter().map(|i| &i.text_id).collect::>(), - ); - - File::create("src/ids/items.rs") - .unwrap() - .write_all(code.as_bytes()) - .unwrap() - } -} - -mod entities { - use super::*; - - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - struct Entity { - id: u32, - #[serde(rename = "name")] - text_id: String, - display_name: String, - width: f32, - height: f32, - category: String, - } - - pub fn generate_entity_enum(data: serde_json::Value) { - let mut entities: Vec = serde_json::from_value(data).expect("Invalid block data"); - entities.sort_by_key(|entity| entity.id); - - // Look for missing items in the array - let mut expected = 0; - for entity in &entities { - if entity.id != expected { - panic!("The entity with id {} is missing.", expected) - } - expected += 1; - } - - // Generate the categories array - let mut categories = String::new(); - categories.push('['); - for entity in &entities { - let variant_name = match entity.category.as_str() { - "Passive mobs" => "Passive", - "Hostile mobs" => "Hostile", - "Vehicles" => "Vehicle", - "Immobile" => "Immobile", - "Projectiles" => "Projectile", - "Drops" => "Drop", - "Blocks" => "Block", - "UNKNOWN" => "Unknown", - unknown_category => panic!("Unknown entity category {}", unknown_category), - }; - categories.push_str("EntityCategory::"); - categories.push_str(&variant_name); - categories.push_str(", "); - } - categories.push(']'); - - // Generate the variants of the Item enum - let mut variants = String::new(); - for entity in &entities { - let name = entity - .text_id - .from_case(Case::Snake) - .to_case(Case::UpperCamel); - variants.push_str(&format!("\t{} = {},\n", name, entity.id)); - } - - // Generate the code - let code = format!( - r#"use crate::*; - -/// See [implementations](#implementations) for useful methods. -#[repr(u32)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Entity {{ -{variants} -}} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum EntityCategory {{ - Passive, - Hostile, - Projectile, - Immobile, - Vehicle, - Drop, - Block, - Unknown -}} - -impl Entity {{ - #[inline] - pub fn from_id(id: u32) -> Option {{ - if id < {max_value} {{ - Some(unsafe{{std::mem::transmute(id)}}) - }} else {{ - None - }} - }} - - #[inline] - pub fn get_text_id(self) -> &'static str {{ - unsafe {{*TEXT_IDS.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_display_name(self) -> &'static str {{ - unsafe {{*DISPLAY_NAMES.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_category(self) -> EntityCategory {{ - unsafe {{*CATEGORIES.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_height(self) -> f32 {{ - unsafe {{*HEIGHTS.get_unchecked((self as u32) as usize)}} - }} - - #[inline] - pub fn get_width(self) -> f32 {{ - unsafe {{*WIDTHS.get_unchecked((self as u32) as usize)}} - }} -}} - -impl<'a> MinecraftPacketPart<'a> for Entity {{ - #[inline] - fn serialize_minecraft_packet_part(self, output: &mut Vec) -> Result<(), &'static str> {{ - VarInt((self as u32) as i32).serialize_minecraft_packet_part(output) - }} - - #[inline] - fn deserialize_minecraft_packet_part(input: &'a[u8]) -> Result<(Self, &'a[u8]), &'static str> {{ - let (id, input) = VarInt::deserialize_minecraft_packet_part(input)?; - let id = std::cmp::max(id.0, 0) as u32; - let entity = Entity::from_id(id).ok_or("No entity corresponding to the specified numeric ID.")?; - Ok((entity, input)) - }} -}} - -const HEIGHTS: [f32; {max_value}] = {heights:?}; - -const WIDTHS: [f32; {max_value}] = {widths:?}; - -const DISPLAY_NAMES: [&str; {max_value}] = {display_names:?}; - -const TEXT_IDS: [&str; {max_value}] = {text_ids:?}; - -const CATEGORIES: [EntityCategory; {max_value}] = {categories}; -"#, - variants = variants, - max_value = expected, - heights = entities.iter().map(|e| e.height).collect::>(), - widths = entities.iter().map(|e| e.width).collect::>(), - display_names = entities.iter().map(|e| &e.display_name).collect::>(), - text_ids = entities.iter().map(|e| &e.text_id).collect::>(), - categories = categories, - ); - - File::create("src/ids/entities.rs") - .unwrap() - .write_all(code.as_bytes()) - .unwrap() - } -} - -fn main() { - //println!("cargo:rerun-if-changed=target/burger-cache-{}.json", VERSION); - let mut file_locations = get_data( - "https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/dataPaths.json", - "target/cache-file-location.json", - ); - let file_locations = file_locations.get_mut("pc").unwrap().take(); - let file_locations: HashMap> = - serde_json::from_value(file_locations).unwrap(); - let file_locations = file_locations - .get(VERSION) - .expect("There is no generated data for this minecraft version yet"); - - let blocks_url = format!( - "https://github.com/PrismarineJS/minecraft-data/raw/master/data/{}/blocks.json", - file_locations.get("blocks").unwrap() - ); - let block_data = get_data( - &blocks_url, - &format!("target/cache-blocks-{}.json", VERSION), - ); - blocks::generate_block_enum(block_data); - - let items_url = format!( - "https://github.com/PrismarineJS/minecraft-data/raw/master/data/{}/items.json", - file_locations.get("items").unwrap() - ); - let items_data = get_data(&items_url, &format!("target/cache-items-{}.json", VERSION)); - items::generate_item_enum(items_data); - - let entities_url = format!( - "https://github.com/PrismarineJS/minecraft-data/raw/master/data/{}/entities.json", - file_locations.get("entities").unwrap() - ); - let entities_data = get_data( - &entities_url, - &format!("target/cache-entities-{}.json", VERSION), - ); - entities::generate_entity_enum(entities_data); -} diff --git a/build/blocks.rs b/build/blocks.rs new file mode 100644 index 00000000..a858847b --- /dev/null +++ b/build/blocks.rs @@ -0,0 +1,828 @@ +use super::*; + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +struct BlockState { + name: String, + #[serde(rename = "type")] + ty: String, + num_values: usize, + values: Option>, +} + +impl BlockState { + fn ty(&self, block_name: &str, competing_definitions: bool) -> String { + match self.ty.as_str() { + "int" => { + let values: Vec = self + .values + .as_ref() + .expect("No values for int block state") + .iter() + .map(|v| v.parse().expect("Invalid block state value: expected int")) + .collect(); + let mut min_value: i128 = *values.first().unwrap_or(&0); + let mut max_value: i128 = *values.first().unwrap_or(&0); + + for value in values { + if value < min_value { + min_value = value; + } + if value > max_value { + max_value = value; + } + } + + if min_value >= u8::MIN as i128 && max_value <= u8::MAX as i128 { + return String::from("u8"); + } + if min_value >= i8::MIN as i128 && max_value <= i8::MAX as i128 { + return String::from("i8"); + } + if min_value >= u16::MIN as i128 && max_value <= u16::MAX as i128 { + return String::from("u16"); + } + if min_value >= i16::MIN as i128 && max_value <= i16::MAX as i128 { + return String::from("i16"); + } + if min_value >= u32::MIN as i128 && max_value <= u32::MAX as i128 { + return String::from("u32"); + } + if min_value >= i32::MIN as i128 && max_value <= i32::MAX as i128 { + return String::from("i32"); + } + if min_value >= u64::MIN as i128 && max_value <= u64::MAX as i128 { + return String::from("u64"); + } + if min_value >= i64::MIN as i128 && max_value <= i64::MAX as i128 { + return String::from("i64"); + } + String::from("i128") + } + "enum" => match competing_definitions { + true => format!("{}_{}", block_name, self.name), + false => self.name.to_string(), + } + .from_case(Case::Snake) + .to_case(Case::UpperCamel), + "bool" => String::from("bool"), + _ => unimplemented!(), + } + } + + fn define_enum(&self, block_name: &str, competing_definitions: bool) -> String { + if self.ty.as_str() != "enum" { + panic!("Called defined enum on non-enum"); + } + + let mut variants = String::new(); + for (i, value) in self + .values + .as_ref() + .expect("Expecting values in enum (state id)") + .iter() + .enumerate() + { + variants.push_str(&format!( + "\n\t{} = {},", + value.from_case(Case::Snake).to_case(Case::UpperCamel), + i + )); + } + + format!( + r#"#[derive(Debug, Clone, Copy)] +#[repr(u8)] +pub enum {} {{{} +}}"#, + self.ty(block_name, competing_definitions), + variants + ) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Block { + id: u32, + #[serde(rename = "name")] + text_id: String, + display_name: String, + hardness: f32, + resistance: f32, + diggable: bool, + transparent: bool, + filter_light: u8, + emit_light: u8, + default_state: u32, + min_state_id: u32, + max_state_id: u32, + drops: Vec, + material: Option, + #[serde(default)] + harvest_tools: HashMap, + states: Vec, +} + +#[allow(clippy::explicit_counter_loop)] +pub fn generate_block_enum(data: serde_json::Value) { + let mut blocks: Vec = serde_json::from_value(data).expect("Invalid block data"); + blocks.sort_by_key(|block| block.id); + + // Look for missing blocks in the array + let mut expected = 0; + for block in &blocks { + if block.id != expected { + panic!("The block with id {} is missing.", expected) + } + expected += 1; + } + + // Process a few fields + let mut raw_harvest_tools: Vec> = Vec::new(); + let mut raw_materials: Vec = Vec::new(); + for block in &blocks { + raw_harvest_tools.push( + block + .harvest_tools + .clone() + .into_iter() + .map(|(k, _v)| k) + .collect(), + ); + let mut material = block + .material + .clone() + .unwrap_or_else(|| "unknown_material".to_string()) + .split(';') + .next() + .unwrap() + .to_string(); + if material.starts_with("mineable") { + material = "unknown_material".to_string(); + } + raw_materials.push(material.from_case(Case::Snake).to_case(Case::UpperCamel)); + } + + // Generate the MaterialBlock enum and array + let mut different_materials = raw_materials.clone(); + different_materials.sort(); + different_materials.dedup(); + let mut material_variants = String::new(); + for material in different_materials { + material_variants.push_str(&format!("\t{},\n", material)); + } + let mut materials = String::new(); + materials.push('['); + for material in raw_materials { + materials.push_str("Some(BlockMaterial::"); + materials.push_str(&material); + materials.push_str("), "); + } + materials.push(']'); + + // Generate the HARVEST_TOOLS array + let mut harvest_tools = String::new(); + harvest_tools.push('['); + for block_harvest_tools in raw_harvest_tools { + harvest_tools.push_str("&["); + for harvest_tool in block_harvest_tools { + harvest_tools.push_str(&harvest_tool.to_string()); + harvest_tools.push_str(", "); + } + harvest_tools.push_str("], "); + } + harvest_tools.push(']'); + + // Enumerate the air blocks + let mut air_blocks = vec![false; expected as usize]; + for air_block in &[ + "air", + "cave_air", + "grass", + "torch", + "wall_torch", + "wheat", + "soul_torch", + "soul_wall_torch", + "carrots", + "potatoes", + ] { + let mut success = false; + for block in &blocks { + if &block.text_id.as_str() == air_block { + air_blocks[block.id as usize] = true; + success = true; + break; + } + } + if !success { + panic!("Could not find block {} in the block array", air_block); + } + } + + // Generate the variants of the Block enum + let mut variants = String::new(); + for block in &blocks { + let name = block + .text_id + .from_case(Case::Snake) + .to_case(Case::UpperCamel); + variants.push_str(&format!("\t{} = {},\n", name, block.id)); + } + + // Generate the `match` of state ids + let mut state_id_match_arms = String::new(); + for block in &blocks { + let name = block + .text_id + .from_case(Case::Snake) + .to_case(Case::UpperCamel); + let start = block.min_state_id; + let stop = block.max_state_id; + if start != stop { + state_id_match_arms.push_str(&format!( + "\t\t\t{}..={} => Some(Block::{}),\n", + start, stop, name + )); + } else { + state_id_match_arms.push_str(&format!("\t\t\t{} => Some(Block::{}),\n", start, name)); + } + } + + // Generate the code + let code = format!( + r#"use crate::*; + +/// See [implementations](#implementations) for useful methods. +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Block {{ +{variants} +}} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum BlockMaterial {{ +{material_variants} +}} + +impl Block {{ + #[inline] + pub fn from_id(id: u32) -> Option {{ + if id < {max_value} {{ + Some(unsafe{{std::mem::transmute(id)}}) + }} else {{ + None + }} + }} + + pub fn from_state_id(state_id: u32) -> Option {{ + match state_id {{ +{state_id_match_arms} + _ => None, + }} + }} + + /// Get the textual identifier of this block. + #[inline] + pub fn text_id(self) -> &'static str {{ + unsafe {{*TEXT_IDS.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn default_state_id(self) -> u32 {{ + unsafe {{*DEFAULT_STATE_IDS.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn id(self) -> u32 {{ + self as u32 + }} + + /// This returns the item that will be dropped if you break the block. + /// If the item is Air, there is actually no drop. + #[inline] + pub fn associated_item_id(self) -> u32 {{ + unsafe {{*ITEM_IDS.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn resistance(self) -> f32 {{ + unsafe {{*RESISTANCES.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn hardness(self) -> f32 {{ + unsafe {{*HARDNESSES.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn material(self) -> Option {{ + unsafe {{*MATERIALS.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn display_name(self) -> &'static str {{ + unsafe {{*DISPLAY_NAMES.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn state_id_range(self) -> std::ops::Range {{ + unsafe {{STATE_ID_RANGES.get_unchecked((self as u32) as usize).clone()}} + }} + + #[inline] + pub fn is_diggable(self) -> bool {{ + unsafe {{*DIGGABLE.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn is_transparent(self) -> bool {{ + unsafe {{*TRANSPARENT.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn compatible_harvest_tools(self) -> &'static [u32] {{ + unsafe {{*HARVEST_TOOLS.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn light_emissions(self) -> u8 {{ + unsafe {{*LIGHT_EMISSIONS.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn light_absorption(self) -> u8 {{ + unsafe {{*LIGHT_ABSORPTION.get_unchecked((self as u32) as usize)}} + }} + + /// A "air block" is a block on which a player cannot stand, like air, wheat, torch... + /// Fire is excluded since you may not want your clients to walk trought fire by default. + /// The list of air blocks is maintained by hand. + /// It could not be exhaustive. + /// See also [Block::is_blocking]. + #[inline] + pub fn is_air_block(self) -> bool {{ + unsafe {{*AIR_BLOCKS.get_unchecked((self as u32) as usize)}} + }} + + /// The opposite of [Block::is_air_block]. + /// Fire is included since you may not want your clients to walk trought fire by default. + /// The list of blocking blocks is maintained by hand. + /// It could not be exhaustive. + #[inline] + pub fn is_blocking(self) -> bool {{ + unsafe {{!(*AIR_BLOCKS.get_unchecked((self as u32) as usize))}} + }} +}} + +impl From for Block {{ + #[inline] + fn from(block_with_state: super::block_states::BlockWithState) -> Block {{ + unsafe {{std::mem::transmute(block_with_state.block_id())}} + }} +}} + +impl<'a> MinecraftPacketPart<'a> for Block {{ + #[inline] + fn serialize_minecraft_packet_part(self, output: &mut Vec) -> Result<(), &'static str> {{ + VarInt((self as u32) as i32).serialize_minecraft_packet_part(output) + }} + + #[inline] + fn deserialize_minecraft_packet_part(input: &'a[u8]) -> Result<(Self, &'a[u8]), &'static str> {{ + let (id, input) = VarInt::deserialize_minecraft_packet_part(input)?; + let id = std::cmp::max(id.0, 0) as u32; + let block = Block::from_id(id).ok_or("No block corresponding to the specified numeric ID.")?; + Ok((block, input)) + }} +}} + +const TEXT_IDS: [&str; {max_value}] = {text_ids:?}; +const DISPLAY_NAMES: [&str; {max_value}] = {display_names:?}; +const STATE_ID_RANGES: [std::ops::Range; {max_value}] = {state_id_ranges:?}; +const DEFAULT_STATE_IDS: [u32; {max_value}] = {default_state_ids:?}; +const ITEM_IDS: [u32; {max_value}] = {item_ids:?}; +const RESISTANCES: [f32; {max_value}] = {resistances:?}; +const MATERIALS: [Option; {max_value}] = {materials}; +const HARVEST_TOOLS: [&[u32]; {max_value}] = {harvest_tools}; +const HARDNESSES: [f32; {max_value}] = {hardnesses:?}; +const LIGHT_EMISSIONS: [u8; {max_value}] = {light_emissions:?}; +const LIGHT_ABSORPTION: [u8; {max_value}] = {light_absorption:?}; +const DIGGABLE: [bool; {max_value}] = {diggable:?}; +const TRANSPARENT: [bool; {max_value}] = {transparent:?}; +const AIR_BLOCKS: [bool; {max_value}] = {air_blocks:?}; +"#, + variants = variants, + material_variants = material_variants, + max_value = expected, + state_id_match_arms = state_id_match_arms, + text_ids = blocks.iter().map(|b| &b.text_id).collect::>(), + display_names = blocks.iter().map(|b| &b.display_name).collect::>(), + state_id_ranges = blocks + .iter() + .map(|b| b.min_state_id..b.max_state_id + 1) + .collect::>(), + default_state_ids = blocks.iter().map(|b| b.default_state).collect::>(), + item_ids = blocks + .iter() + .map(|b| b.drops.get(0).copied().unwrap_or(0)) + .collect::>(), + materials = materials, + resistances = blocks.iter().map(|b| b.resistance).collect::>(), + harvest_tools = harvest_tools, + hardnesses = blocks.iter().map(|b| b.hardness).collect::>(), + light_emissions = blocks.iter().map(|b| b.emit_light).collect::>(), + light_absorption = blocks.iter().map(|b| b.filter_light).collect::>(), + diggable = blocks.iter().map(|b| b.diggable).collect::>(), + transparent = blocks.iter().map(|b| b.transparent).collect::>(), + air_blocks = air_blocks, + ); + + File::create("src/ids/blocks.rs") + .unwrap() + .write_all(code.as_bytes()) + .unwrap() +} + +#[allow(clippy::explicit_counter_loop)] +pub fn generate_block_with_state_enum(data: serde_json::Value) { + let mut blocks: Vec = serde_json::from_value(data).expect("Invalid block data"); + blocks.sort_by_key(|block| block.min_state_id); + + // Look for missing blocks in the array + let mut expected = 0; + for block in &blocks { + if block.id != expected { + panic!("The block with id {} is missing.", expected) + } + expected += 1; + } + + // Generate the enum definitions + let mut enum_definitions = Vec::new(); + let mut enum_definitions_string = String::new(); + let mut already_defined_enums = Vec::new(); + for block in &blocks { + for state in &block.states { + if state.ty.as_str() == "enum" { + enum_definitions.push((&block.text_id, state)); + } + } + } + for (block_name, enum_definition) in &enum_definitions { + let mut competing_definitions = false; + for (_, enum_definition2) in &enum_definitions { + if enum_definition.name == enum_definition2.name + && enum_definition.values != enum_definition2.values + { + competing_definitions = true; + break; + } + } + if !already_defined_enums.contains(&enum_definition.ty(block_name, competing_definitions)) { + enum_definitions_string + .push_str(&enum_definition.define_enum(block_name, competing_definitions)); + enum_definitions_string.push('\n'); + enum_definitions_string.push('\n'); + + already_defined_enums.push(enum_definition.ty(block_name, competing_definitions)); + } + } + + // Generate the variants of the Block enum + let mut variants = String::new(); + for block in &blocks { + let name = block + .text_id + .from_case(Case::Snake) + .to_case(Case::UpperCamel); + let mut fields = String::new(); + for state in &block.states { + let name = match state.name.as_str() == "type" { + true => "ty", + false => state.name.as_str(), + }; + let competing_definitions = + already_defined_enums.contains(&state.ty(&block.text_id, true)); + let doc = if state.ty == "int" { + let values: Vec = state + .values + .as_ref() + .expect("No values for int block state") + .iter() + .map(|v| v.parse().expect("Invalid block state value: expected int")) + .collect(); + + let mut expected = values[0]; + let mut standard = true; + for value in &values { + if value != &expected { + standard = false; + break; + } + expected += 1; + } + + match standard { + true => format!("\t\t/// Valid if {} <= {} <= {}\n", values[0], name, values.last().unwrap()), + false => format!("\t\t/// Valid if {} ∈ {:?}\n", name, values), + } + } else { + String::new() + }; + fields.push_str(&format!( + "{}\t\t{}: {},\n", + doc, + name, + state.ty(&block.text_id, competing_definitions) + )); + } + if fields.is_empty() { + variants.push_str(&format!("\t{},\n", name)); + } else { + variants.push_str(&format!("\t{} {{\n{}\t}},\n", name, fields)); + } + } + + // Generate the `match` of state ids + let mut state_id_match_arms = String::new(); + let mut state_id_rebuild_arms = String::new(); + for block in &blocks { + let name = block + .text_id + .from_case(Case::Snake) + .to_case(Case::UpperCamel); + let start = block.min_state_id; + let stop = block.max_state_id; + + if block.states.is_empty() { + state_id_match_arms.push_str(&format!( + "\n\t\t\t{} => Some(BlockWithState::{}),", + start, name + )); + state_id_rebuild_arms.push_str(&format!( + "\n\t\t\tBlockWithState::{} => Some({}),", + name, start + )); + continue; + } + + let mut state_calculations = String::new(); + let mut fields = String::new(); + for (i, state) in block.states.iter().enumerate().rev() { + let competing_definitions = + already_defined_enums.contains(&state.ty(&block.text_id, true)); + let ty = state.ty(&block.text_id, competing_definitions); + let name = match state.name.as_str() { + "type" => "ty", + _ => &state.name, + }; + fields.push_str(&format!("{}, ", name)); + + if i == 0 { + state_calculations.push_str("\n\t\t\t\tlet field_value = state_id;"); + } else { + state_calculations.push_str(&format!( + "\n\t\t\t\tlet field_value = state_id.rem_euclid({});\ + \n\t\t\t\tstate_id -= field_value;\ + \n\t\t\t\tstate_id /= {};", + state.num_values, state.num_values + )); + } + + match state.ty.as_str() { + "enum" => { + state_calculations.push_str(&format!( + "\n\t\t\t\tlet {}: {} = unsafe{{std::mem::transmute(field_value as u8)}};\n", + name, ty + )); + } + "int" => { + let values: Vec = state + .values + .as_ref() + .expect("No values for int block state") + .iter() + .map(|v| v.parse().expect("Invalid block state value: expected int")) + .collect(); + + let mut expected = values[0]; + let mut standard = true; + for value in &values { + if value != &expected { + standard = false; + break; + } + expected += 1; + } + + if standard && values[0] == 0 { + state_calculations.push_str(&format!( + "\n\t\t\t\tlet {}: {} = field_value as {};\n", + name, ty, ty + )); + } else if standard { + state_calculations.push_str(&format!( + "\n\t\t\t\tlet {}: {} = {} + field_value as {};\n", + name, ty, values[0], ty + )); + } else { + state_calculations.push_str(&format!( + "\n\t\t\t\tlet {}: {} = {:?}[field_value as usize];\n", + name, ty, values + )); + } + } + "bool" => { + state_calculations.push_str(&format!( + "\n\t\t\t\tlet {}: bool = field_value == 0;\n", + name + )); + } + other => panic!("Unknown {} type", other), + } + } + + let mut state_reformation = String::new(); + for (i, state) in block.states.iter().enumerate() { + let name = match state.name.as_str() { + "type" => "ty", + _ => &state.name, + }; + + match state.ty.as_str() { + "enum" => { + state_reformation.push_str(&format!( + "\n\t\t\t\tlet field_value = (*{} as u8) as u32;", + name + )); + } + "int" => { + let values: Vec = state + .values + .as_ref() + .expect("No values for int block state") + .iter() + .map(|v| v.parse().expect("Invalid block state value: expected int")) + .collect(); + + let mut expected = values[0]; + let mut standard = true; + for value in &values { + if value != &expected { + standard = false; + break; + } + expected += 1; + } + + if standard && values[0] == 0 { + state_reformation.push_str(&format!( + "\n\t\t\t\tif *{name} > {max} {{ return None }}\ + \n\t\t\t\tlet field_value = *{name} as u32;", + name = name, + max = values.last().unwrap() + )); + } else if standard { + state_reformation.push_str(&format!( + "\n\t\t\t\tif *{name} < {min} || *{name} > {max} {{ return None }}\ + \n\t\t\t\tlet field_value = ({name} - {min}) as u32;", + name = name, + min = values[0], + max = values.last().unwrap() + )); + } else { + state_reformation.push_str(&format!( + "\n\t\t\t\tlet field_value = {:?}.find({})?;", + values, name + )); + } + } + "bool" => { + state_reformation.push_str(&format!( + "\n\t\t\t\tlet field_value = if *{} {{0}} else {{1}};", + name + )); + } + other => panic!("Unknown {} type", other), + } + + if i == 0 { + state_reformation.push_str("\n\t\t\t\tlet mut state_id = field_value;\n"); + } else { + state_reformation.push_str(&format!( + "\n\t\t\t\tstate_id *= {};\ + \n\t\t\t\tstate_id += field_value;\n", + state.num_values + )); + } + } + + state_id_match_arms.push_str(&format!( + " + {}..={} => {{ + state_id -= {}; + {} + Some(BlockWithState::{}{{ {}}}) + }},", + start, stop, start, state_calculations, name, fields + )); + state_id_rebuild_arms.push_str(&format!( + " + BlockWithState::{}{{ {}}} => {{ + {} + state_id += {}; + Some(state_id) + }},", + name, fields, state_reformation, start + )); + } + + // Generate the code + let code = format!( + r#"//! Contains the [BlockWithState] enum to help with block state IDs. + +use crate::*; + +{enum_definitions} + +/// Can be converted for free to [super::blocks::Block] which implements [useful methods](super::blocks::Block#implementations). +#[derive(Debug, Clone)] +#[repr(u32)] +pub enum BlockWithState {{ +{variants} +}} + +impl BlockWithState {{ + #[inline] + pub fn from_state_id(mut state_id: u32) -> Option {{ + match state_id {{ +{state_id_match_arms} + _ => None, + }} + }} + + /// Returns the block id, **not the block state id**. + #[inline] + pub fn block_id(&self) -> u32 {{ + unsafe {{std::mem::transmute(std::mem::discriminant(self))}} + }} + + /// Returns the block state id. + /// Returns None in case of error (invalid field value). + #[inline] + pub fn block_state_id(&self) -> Option {{ + match self {{ +{state_id_rebuild_arms} + }} + }} +}} + +impl From for BlockWithState {{ + #[inline] + fn from(block: super::blocks::Block) -> BlockWithState {{ + BlockWithState::from_state_id(block.default_state_id()).unwrap() // TODO: unwrap unchecked + }} +}} + +impl<'a> MinecraftPacketPart<'a> for BlockWithState {{ + #[inline] + fn serialize_minecraft_packet_part(self, _output: &mut Vec) -> Result<(), &'static str> {{ + unimplemented!("Cannot serialize BlockWithState yet"); + }} + + #[inline] + fn deserialize_minecraft_packet_part(input: &'a[u8]) -> Result<(Self, &'a[u8]), &'static str> {{ + let (id, input) = VarInt::deserialize_minecraft_packet_part(input)?; + let id = std::cmp::max(id.0, 0) as u32; + let block_with_state = BlockWithState::from_state_id(id).ok_or("No block corresponding to the specified block state ID.")?; + Ok((block_with_state, input)) + }} +}} + +#[cfg(test)] +mod tests {{ + use super::*; + + #[test] + fn test_block_states() {{ + for id in 0..={max_block_state_id} {{ + let block = BlockWithState::from_state_id(id).unwrap(); + let id_from_block = block.block_state_id().unwrap(); + assert_eq!(id, id_from_block); + }} + }} +}} +"#, + enum_definitions = enum_definitions_string, + state_id_match_arms = state_id_match_arms, + state_id_rebuild_arms = state_id_rebuild_arms, + variants = variants, + max_block_state_id = blocks.last().unwrap().max_state_id + ); + + File::create("src/ids/block_states.rs") + .unwrap() + .write_all(code.as_bytes()) + .unwrap() +} diff --git a/build/build.rs b/build/build.rs new file mode 100644 index 00000000..ab3b38c0 --- /dev/null +++ b/build/build.rs @@ -0,0 +1,127 @@ +mod blocks; +mod entities; +mod items; +mod recipes; + +use convert_case::{Case, Casing}; +use serde::{Deserialize, Serialize}; +use std::io::{ErrorKind, Read, Write}; +use std::{collections::HashMap, fs::File}; + +const VERSION: &str = "1.17.1"; + +fn get_data(url: &str, cache: &str) -> serde_json::Value { + match File::open(cache) { + // The cache file is ready + Ok(mut file) => { + let mut data = Vec::new(); + if let Err(e) = file.read_to_end(&mut data) { + panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded and cached to `{}`. Unfortunately, this file cannot be read. Error: {}", cache, e) + } + + let json_text = match String::from_utf8(data) { + Ok(json_text) => json_text, + Err(e) => panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded and cached to `{}`. Unfortunately, this file appears to contain invalid text data. Error: {}\nNote: Deleting the file will allow the library to download it again.", cache, e), + }; + + let json = match serde_json::from_str(&json_text) { + Ok(json) => json, + Err(e) => panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded and cached to `{}`. Unfortunately, this file appears to contain invalid json data. Error: {}\nNote: Deleting the file will allow the library to download it again.", cache, e), + }; + + json + } + // The cache file needs to be downloaded + Err(e) if e.kind() == ErrorKind::NotFound => { + let response = match minreq::get(url).send() { + Ok(response) => response, + Err(e) => panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded from `{}`. Unfortunately, we can't access this URL. Error: {}", url, e) + }; + + let json_text = match response.as_str() { + Ok(json_text) => json_text, + Err(e) => panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded from `{}`. Unfortunately, this file appears to contain invalid data. Error: {}", url, e), + }; + + let mut file = match File::create(cache) { + Ok(file) => file, + Err(e) => panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded and cached to `{}`. Unfortunately, we can't access this path. Error: {}", cache, e), + }; + + if let Err(e) = file.write_all(json_text.as_bytes()) { + panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded and cached to `{}`. Unfortunately, we can't write to this path. Error: {}", cache, e) + }; + + let json = match serde_json::from_str(json_text) { + Ok(json) => json, + Err(e) => panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded and cached to `{}`. Unfortunately, this file appears to contain invalid json data. Error: {}\nNote: Deleting the file will allow the library to download it again.", cache, e), + }; + + json + } + + // The cache file cannot be accessed + Err(e) => { + panic!("The minecraft-format library uses a build script to generate data structures from extracted data. The extracted data is downloaded and cached to `{}`. Unfortunately, we can't access this path. Error: {}", cache, e); + } + } +} + +fn main() { + println!( + "cargo:rerun-if-changed=target/cache-file-location-{}.json", + VERSION + ); + println!( + "cargo:rerun-if-changed=build" + ); + + let mut file_locations = get_data( + "https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/dataPaths.json", + &format!("target/cache-file-location-{}.json", VERSION), + ); + let file_locations = file_locations.get_mut("pc").unwrap().take(); + let file_locations: HashMap> = + serde_json::from_value(file_locations).unwrap(); + let file_locations = file_locations + .get(VERSION) + .expect("There is no generated data for this minecraft version yet"); + + let blocks_url = format!( + "https://github.com/PrismarineJS/minecraft-data/raw/master/data/{}/blocks.json", + file_locations.get("blocks").unwrap() + ); + let block_data = get_data( + &blocks_url, + &format!("target/cache-blocks-{}.json", VERSION), + ); + blocks::generate_block_enum(block_data.clone()); + blocks::generate_block_with_state_enum(block_data); + + let items_url = format!( + "https://github.com/PrismarineJS/minecraft-data/raw/master/data/{}/items.json", + file_locations.get("items").unwrap() + ); + let items_data = get_data(&items_url, &format!("target/cache-items-{}.json", VERSION)); + let items = items::generate_item_enum(items_data); + + let entities_url = format!( + "https://github.com/PrismarineJS/minecraft-data/raw/master/data/{}/entities.json", + file_locations.get("entities").unwrap() + ); + let entities_data = get_data( + &entities_url, + &format!("target/cache-entities-{}.json", VERSION), + ); + entities::generate_entity_enum(entities_data); + + let recipes_url = format!( + "https://github.com/PrismarineJS/minecraft-data/raw/master/data/{}/recipes.json", + file_locations.get("recipes").unwrap() + ); + let recipes_data = get_data( + &recipes_url, + &format!("target/cache-recipes-{}.json", VERSION), + ); + recipes::generate_recipes(recipes_data, items); +} diff --git a/build/entities.rs b/build/entities.rs new file mode 100644 index 00000000..57707ebb --- /dev/null +++ b/build/entities.rs @@ -0,0 +1,161 @@ +use super::*; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Entity { + id: u32, + #[serde(rename = "name")] + text_id: String, + display_name: String, + width: f32, + height: f32, + #[serde(rename = "type")] + category: String, +} + +pub fn generate_entity_enum(data: serde_json::Value) { + let mut entities: Vec = serde_json::from_value(data).expect("Invalid entity data"); + entities.sort_by_key(|entity| entity.id); + + // Look for missing items in the array + let mut expected = 0; + for entity in &entities { + if entity.id != expected { + panic!("The entity with id {} is missing.", expected) + } + expected += 1; + } + + // Generate the categories array + let mut categories = String::new(); + categories.push('['); + for entity in &entities { + let variant_name = match entity.category.as_str() { + "other" => "Other", + "living" => "Living", + "projectile" => "Projectile", + "animal" => "Animal", + "ambient" => "Ambient", + "hostile" => "Hostile", + "water_creature" => "WaterCreature", + "mob" => "Mob", + "passive" => "Passive", + "player" => "Player", + unknown_category => panic!("Unknown entity category {}", unknown_category), + }; + categories.push_str("EntityCategory::"); + categories.push_str(variant_name); + categories.push_str(", "); + } + categories.push(']'); + + // Generate the variants of the Item enum + let mut variants = String::new(); + for entity in &entities { + let name = entity + .text_id + .from_case(Case::Snake) + .to_case(Case::UpperCamel); + variants.push_str(&format!("\t{} = {},\n", name, entity.id)); + } + + // Generate the code + let code = format!( + r#"use crate::*; + +/// See [implementations](#implementations) for useful methods. +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Entity {{ +{variants} +}} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum EntityCategory {{ + Other, + Living, + Projectile, + Animal, + Ambient, + Hostile, + WaterCreature, + Mob, + Passive, + Player, +}} + +impl Entity {{ + #[inline] + pub fn from_id(id: u32) -> Option {{ + if id < {max_value} {{ + Some(unsafe{{std::mem::transmute(id)}}) + }} else {{ + None + }} + }} + + #[inline] + pub fn text_id(self) -> &'static str {{ + unsafe {{*TEXT_IDS.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn display_name(self) -> &'static str {{ + unsafe {{*DISPLAY_NAMES.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn category(self) -> EntityCategory {{ + unsafe {{*CATEGORIES.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn height(self) -> f32 {{ + unsafe {{*HEIGHTS.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn width(self) -> f32 {{ + unsafe {{*WIDTHS.get_unchecked((self as u32) as usize)}} + }} +}} + +impl<'a> MinecraftPacketPart<'a> for Entity {{ + #[inline] + fn serialize_minecraft_packet_part(self, output: &mut Vec) -> Result<(), &'static str> {{ + VarInt((self as u32) as i32).serialize_minecraft_packet_part(output) + }} + + #[inline] + fn deserialize_minecraft_packet_part(input: &'a[u8]) -> Result<(Self, &'a[u8]), &'static str> {{ + let (id, input) = VarInt::deserialize_minecraft_packet_part(input)?; + let id = std::cmp::max(id.0, 0) as u32; + let entity = Entity::from_id(id).ok_or("No entity corresponding to the specified numeric ID.")?; + Ok((entity, input)) + }} +}} + +const HEIGHTS: [f32; {max_value}] = {heights:?}; + +const WIDTHS: [f32; {max_value}] = {widths:?}; + +const DISPLAY_NAMES: [&str; {max_value}] = {display_names:?}; + +const TEXT_IDS: [&str; {max_value}] = {text_ids:?}; + +const CATEGORIES: [EntityCategory; {max_value}] = {categories}; +"#, + variants = variants, + max_value = expected, + heights = entities.iter().map(|e| e.height).collect::>(), + widths = entities.iter().map(|e| e.width).collect::>(), + display_names = entities.iter().map(|e| &e.display_name).collect::>(), + text_ids = entities.iter().map(|e| &e.text_id).collect::>(), + categories = categories, + ); + + File::create("src/ids/entities.rs") + .unwrap() + .write_all(code.as_bytes()) + .unwrap() +} diff --git a/build/items.rs b/build/items.rs new file mode 100644 index 00000000..fb47cabe --- /dev/null +++ b/build/items.rs @@ -0,0 +1,136 @@ +use super::*; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Item { + pub id: u32, + display_name: String, + #[serde(rename = "name")] + pub text_id: String, + stack_size: u8, + max_durability: Option, +} + +#[allow(clippy::explicit_counter_loop)] +pub fn generate_item_enum(data: serde_json::Value) -> Vec { + let mut items: Vec = serde_json::from_value(data).expect("Invalid block data"); + items.sort_by_key(|item| item.id); + + // Patch the missing Air + if items.first().map(|i| i.id) != Some(0) { + items.insert( + 0, + Item { + id: 0, + display_name: String::from("Air"), + text_id: String::from("air"), + stack_size: 64, + max_durability: None, + }, + ); + } + + // Look for missing items in the array + let mut expected = 0; + for item in &items { + if item.id != expected { + panic!("The item with id {} is missing.", expected) + } + expected += 1; + } + + // Generate the variants of the Item enum + let mut variants = String::new(); + for item in &items { + let name = item + .text_id + .from_case(Case::Snake) + .to_case(Case::UpperCamel); + variants.push_str(&format!("\t{} = {},\n", name, item.id)); + } + + // Generate the code + let code = format!( + r#"use crate::*; + +/// See [implementations](#implementations) for useful methods. +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Item {{ +{variants} +}} + +impl Item {{ + #[inline] + pub fn from_id(id: u32) -> Option {{ + if id < {max_value} {{ + Some(unsafe{{std::mem::transmute(id)}}) + }} else {{ + None + }} + }} + + #[inline] + pub fn text_id(self) -> &'static str {{ + unsafe {{*TEXT_IDS.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn display_name(self) -> &'static str {{ + unsafe {{*DISPLAY_NAMES.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn max_stack_size(self) -> u8 {{ + unsafe {{*STACK_SIZES.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn durability(self) -> Option {{ + unsafe {{*DURABILITIES.get_unchecked((self as u32) as usize)}} + }} + + #[inline] + pub fn crafting_recipes(&self) -> &'static [crate::ids::recipes::Recipe] {{ + crate::ids::recipes::Recipe::get_recipes_for_item(*self) + }} +}} + +impl<'a> MinecraftPacketPart<'a> for Item {{ + #[inline] + fn serialize_minecraft_packet_part(self, output: &mut Vec) -> Result<(), &'static str> {{ + VarInt((self as u32) as i32).serialize_minecraft_packet_part(output) + }} + + #[inline] + fn deserialize_minecraft_packet_part(input: &'a[u8]) -> Result<(Self, &'a[u8]), &'static str> {{ + let (id, input) = VarInt::deserialize_minecraft_packet_part(input)?; + let id = std::cmp::max(id.0, 0) as u32; + let item = Item::from_id(id).ok_or("No item corresponding to the specified numeric ID.")?; + Ok((item, input)) + }} +}} + +const STACK_SIZES: [u8; {max_value}] = {max_stack_sizes:?}; + +const DURABILITIES: [Option; {max_value}] = {durabilities:?}; + +const DISPLAY_NAMES: [&str; {max_value}] = {display_names:?}; + +const TEXT_IDS: [&str; {max_value}] = {text_ids:?}; +"#, + variants = variants, + max_value = expected, + max_stack_sizes = items.iter().map(|i| i.stack_size).collect::>(), + durabilities = items.iter().map(|i| i.max_durability).collect::>(), + display_names = items.iter().map(|i| &i.display_name).collect::>(), + text_ids = items.iter().map(|i| &i.text_id).collect::>(), + ); + + File::create("src/ids/items.rs") + .unwrap() + .write_all(code.as_bytes()) + .unwrap(); + + items +} diff --git a/build/recipes.rs b/build/recipes.rs new file mode 100644 index 00000000..d7310ca4 --- /dev/null +++ b/build/recipes.rs @@ -0,0 +1,446 @@ +use convert_case::{Case, Casing}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs::File; +use std::io::prelude::*; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +enum CountedItem { + IDAndMetadataAndCount { id: u32, metadata: u32, count: u8 }, + IDAndMetadata { id: u32, metadata: u32 }, + IDAndCount { id: u32, count: u8 }, + ID(u32), +} + +impl CountedItem { + fn to_id_and_count(&self) -> (u32, u8) { + match self { + CountedItem::IDAndMetadataAndCount { .. } => panic!("Metadata not handled"), + CountedItem::IDAndMetadata { .. } => panic!("Metadata not handled"), + CountedItem::IDAndCount { id, count } => (*id, *count), + CountedItem::ID(id) => (*id, 1), + } + } + + fn format(&self, items: &[super::items::Item]) -> String { + let (id, count) = self.to_id_and_count(); + let item_ident = item_id_to_item(id, items); + format!( + "CountedItem {{item: Item::{}, count: {}}}", + item_ident, count + ) + } + + fn format_count1(&self, items: &[super::items::Item]) -> String { + let (id, count) = self.to_id_and_count(); + assert!(count == 1); + let item_ident = item_id_to_item(id, items); + format!( + "Item::{}", + item_ident + ) + } +} + +#[allow(dead_code)] +fn format_option_item(item: &Option, items: &[super::items::Item]) -> String { + match item { + Some(item) => format!("Some({})", item.format(items)), + None => "None".to_string(), + } +} + +fn format_option_item_count1(item: &Option, items: &[super::items::Item]) -> String { + match item { + Some(item) => format!("Some({})", item.format_count1(items)), + None => "None".to_string(), + } +} + +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +enum Shape { + ThreeByThree([[Option; 3]; 3]), + ThreeByTwo([[Option; 3]; 2]), + ThreeByOne([[Option; 3]; 1]), + TwoByThree([[Option; 2]; 3]), + TwoByTwo([[Option; 2]; 2]), + TwoByOne([[Option; 2]; 1]), + OneByThree([[Option; 1]; 3]), + OneByTwo([[Option; 1]; 2]), + OneByOne([[Option; 1]; 1]), +} + +impl Shape { + #[allow(dead_code)] + fn format(&self, i: &[super::items::Item]) -> String { + match self { + Shape::ThreeByThree([[v1, v2, v3], [v4, v5, v6], [v7, v8, v9]]) => { + format!( + "Shape::ThreeByThree([[{}, {}, {}], [{}, {}, {}], [{}, {}, {}]])", + format_option_item(v1, i), + format_option_item(v2, i), + format_option_item(v3, i), + format_option_item(v4, i), + format_option_item(v5, i), + format_option_item(v6, i), + format_option_item(v7, i), + format_option_item(v8, i), + format_option_item(v9, i) + ) + } + Shape::ThreeByTwo([[v1, v2, v3], [v4, v5, v6]]) => { + format!( + "Shape::ThreeByTwo([[{}, {}, {}], [{}, {}, {}]])", + format_option_item(v1, i), + format_option_item(v2, i), + format_option_item(v3, i), + format_option_item(v4, i), + format_option_item(v5, i), + format_option_item(v6, i) + ) + } + Shape::ThreeByOne([[v1, v2, v3]]) => { + format!( + "Shape::ThreeByOne([[{}, {}, {}]])", + format_option_item(v1, i), + format_option_item(v2, i), + format_option_item(v3, i) + ) + } + Shape::TwoByThree([[v1, v2], [v3, v4], [v5, v6]]) => { + format!( + "Shape::TwoByThree([[{}, {}], [{}, {}], [{}, {}]])", + format_option_item(v1, i), + format_option_item(v2, i), + format_option_item(v3, i), + format_option_item(v4, i), + format_option_item(v5, i), + format_option_item(v6, i) + ) + } + Shape::TwoByTwo([[v1, v2], [v3, v4]]) => { + format!( + "Shape::TwoByTwo([[{}, {}], [{}, {}]])", + format_option_item(v1, i), + format_option_item(v2, i), + format_option_item(v3, i), + format_option_item(v4, i) + ) + } + Shape::TwoByOne([[v1, v2]]) => { + format!( + "Shape::TwoByOne([[{}, {}]])", + format_option_item(v1, i), + format_option_item(v2, i) + ) + } + Shape::OneByThree([[v1], [v2], [v3]]) => { + format!( + "Shape::OneByThree([[{}], [{}], [{}]])", + format_option_item(v1, i), + format_option_item(v2, i), + format_option_item(v3, i) + ) + } + Shape::OneByTwo([[v1], [v2]]) => { + format!( + "Shape::OneByTwo([[{}], [{}]])", + format_option_item(v1, i), + format_option_item(v2, i) + ) + } + Shape::OneByOne([[v1]]) => { + format!("Shape::OneByOne([[{}]])", format_option_item(v1, i)) + } + } + } + + fn format_count1(&self, i: &[super::items::Item]) -> String { + match self { + Shape::ThreeByThree([[v1, v2, v3], [v4, v5, v6], [v7, v8, v9]]) => { + format!( + "Shape::ThreeByThree([[{}, {}, {}], [{}, {}, {}], [{}, {}, {}]])", + format_option_item_count1(v1, i), + format_option_item_count1(v2, i), + format_option_item_count1(v3, i), + format_option_item_count1(v4, i), + format_option_item_count1(v5, i), + format_option_item_count1(v6, i), + format_option_item_count1(v7, i), + format_option_item_count1(v8, i), + format_option_item_count1(v9, i) + ) + } + Shape::ThreeByTwo([[v1, v2, v3], [v4, v5, v6]]) => { + format!( + "Shape::ThreeByTwo([[{}, {}, {}], [{}, {}, {}]])", + format_option_item_count1(v1, i), + format_option_item_count1(v2, i), + format_option_item_count1(v3, i), + format_option_item_count1(v4, i), + format_option_item_count1(v5, i), + format_option_item_count1(v6, i) + ) + } + Shape::ThreeByOne([[v1, v2, v3]]) => { + format!( + "Shape::ThreeByOne([[{}, {}, {}]])", + format_option_item_count1(v1, i), + format_option_item_count1(v2, i), + format_option_item_count1(v3, i) + ) + } + Shape::TwoByThree([[v1, v2], [v3, v4], [v5, v6]]) => { + format!( + "Shape::TwoByThree([[{}, {}], [{}, {}], [{}, {}]])", + format_option_item_count1(v1, i), + format_option_item_count1(v2, i), + format_option_item_count1(v3, i), + format_option_item_count1(v4, i), + format_option_item_count1(v5, i), + format_option_item_count1(v6, i) + ) + } + Shape::TwoByTwo([[v1, v2], [v3, v4]]) => { + format!( + "Shape::TwoByTwo([[{}, {}], [{}, {}]])", + format_option_item_count1(v1, i), + format_option_item_count1(v2, i), + format_option_item_count1(v3, i), + format_option_item_count1(v4, i) + ) + } + Shape::TwoByOne([[v1, v2]]) => { + format!( + "Shape::TwoByOne([[{}, {}]])", + format_option_item_count1(v1, i), + format_option_item_count1(v2, i) + ) + } + Shape::OneByThree([[v1], [v2], [v3]]) => { + format!( + "Shape::OneByThree([[{}], [{}], [{}]])", + format_option_item_count1(v1, i), + format_option_item_count1(v2, i), + format_option_item_count1(v3, i) + ) + } + Shape::OneByTwo([[v1], [v2]]) => { + format!( + "Shape::OneByTwo([[{}], [{}]])", + format_option_item_count1(v1, i), + format_option_item_count1(v2, i) + ) + } + Shape::OneByOne([[v1]]) => { + format!("Shape::OneByOne([[{}]])", format_option_item_count1(v1, i)) + } + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +enum Recipe { + #[serde(rename_all = "camelCase")] + DoubleShaped { + result: CountedItem, + in_shape: Shape, + out_shape: Shape, + }, + #[serde(rename_all = "camelCase")] + Shaped { in_shape: Shape, result: CountedItem }, + #[serde(rename_all = "camelCase")] + ShapeLess { + result: CountedItem, + ingredients: Vec, + }, +} + +fn item_id_to_item(id: u32, items: &[super::items::Item]) -> String { + for item in items { + if item.id == id { + return item + .text_id + .from_case(Case::Snake) + .to_case(Case::UpperCamel); + } + } + + panic!("Item ID from recipe not found") +} + +pub fn generate_recipes(data: serde_json::Value, items: Vec) { + let item_recipes: HashMap> = + serde_json::from_value(data).expect("Invalid recipes"); + + // Count recipes + let mut recipes_count = 0; + for recipes in item_recipes.values() { + recipes_count += recipes.len(); + for recipe in recipes { + if matches!(recipe, Recipe::DoubleShaped{..}) { + panic!("Contains a double shaped recipe, which support has been removed as an optimization. It needs to be enabled again if required by future minecraft updates.") + } + } + } + + // Generate recipes + let mut recipes_data = String::new(); + for recipes in item_recipes.values() { + for recipe in recipes { + match recipe { + Recipe::ShapeLess { + result, + ingredients, + } => { + let mut ingredients_string = String::new(); + for ingredient in ingredients { + ingredients_string.push_str(&ingredient.format_count1(&items)); + ingredients_string.push_str(", "); + } + + recipes_data.push_str(&format!( + "\tRecipe::ShapeLess {{ result: {}, ingredients: &[{}] }},\n", + result.format(&items), + ingredients_string, + )); + } + Recipe::Shaped { result, in_shape } => { + recipes_data.push_str(&format!( + "\tRecipe::Shaped {{ result: {}, in_shape: {} }},\n", + result.format(&items), + in_shape.format_count1(&items), + )); + } + Recipe::DoubleShaped { + result, + in_shape, + out_shape, + } => { + recipes_data.push_str(&format!( + "\tRecipe::DoubleShaped {{ result: {}, in_shape: {}, out_shape: {} }},\n", + result.format(&items), + in_shape.format_count1(&items), + out_shape.format_count1(&items), + )); + } + } + } + } + + // Generate shortcuts + let mut idx_in_array = 0; + let mut shortcuts = Vec::new(); + for item_id in 0..items.len() { + let vec_default = Vec::new(); + let recipes = item_recipes.get(&(item_id as u32)).unwrap_or(&vec_default); + shortcuts.push((idx_in_array, idx_in_array + recipes.len())); + idx_in_array += recipes.len(); + } + + #[allow(clippy::useless_format)] + let code = format!( + r#"//! All crafting recipes + +use crate::ids::items::Item; + +/// An [Item](crate::ids::items::Item) associated with a count of this item +#[derive(Debug, Clone)] +pub struct CountedItem {{ + pub item: Item, + pub count: u8, +}} + +#[derive(Debug, Clone)] +pub enum Shape {{ + ThreeByThree([[Option; 3]; 3]), + ThreeByTwo([[Option; 3]; 2]), + ThreeByOne([[Option; 3]; 1]), + TwoByThree([[Option; 2]; 3]), + TwoByTwo([[Option; 2]; 2]), + TwoByOne([[Option; 2]; 1]), + OneByThree([[Option; 1]; 3]), + OneByTwo([[Option; 1]; 2]), + OneByOne([[Option; 1]; 1]), +}} + +impl Shape {{ + /// Returns the size of the shape. + /// (width, height) + pub const fn size(&self) -> (u8, u8) {{ + match self {{ + Shape::ThreeByThree(_) => (3, 3), + Shape::ThreeByTwo(_) => (3, 2), + Shape::ThreeByOne(_) => (3, 1), + Shape::TwoByThree(_) => (2, 3), + Shape::TwoByTwo(_) => (2, 2), + Shape::TwoByOne(_) => (2, 1), + Shape::OneByThree(_) => (1, 3), + Shape::OneByTwo(_) => (1, 2), + Shape::OneByOne(_) => (1, 1), + }} + }} +}} + +#[derive(Debug, Clone)] +pub enum Recipe {{ + Shaped {{ in_shape: Shape, result: CountedItem }}, + ShapeLess {{ ingredients: &'static [Item], result: CountedItem }}, +}} + +impl Recipe {{ + /// Returns all the recipes for an item + #[inline] + pub fn get_recipes_for_item(item: Item) -> &'static [Recipe] {{ + unsafe {{ + let (start, end) = SHORTCUTS.get_unchecked((item as u32) as usize); + RECIPES.get_unchecked(*start..*end) + }} + }} + + #[inline] + pub const fn result(&self) -> &CountedItem {{ + match self {{ + Recipe::Shaped {{ result, .. }} => result, + Recipe::ShapeLess {{ result, .. }} => result, + }} + }} + + #[inline] + pub const fn in_shape(&self) -> Option<&Shape> {{ + match self {{ + Recipe::Shaped {{ in_shape, .. }} => Some(in_shape), + Recipe::ShapeLess {{ .. }} => None, + }} + }} + + #[inline] + pub const fn ingredients(&self) -> Option<&'static [Item]> {{ + match self {{ + Recipe::Shaped {{ .. }} => None, + Recipe::ShapeLess {{ ingredients, .. }} => Some(ingredients), + }} + }} +}} + +const RECIPES: [Recipe; {recipes_count}] = [ +{recipes_data} +]; + +const SHORTCUTS: [(usize, usize); {item_count}] = {shortcuts:?}; +"#, + recipes_count = recipes_count, + recipes_data = recipes_data, + item_count = items.len(), + shortcuts = shortcuts, + ); + + File::create("src/ids/recipes.rs") + .unwrap() + .write_all(code.as_bytes()) + .unwrap() +} diff --git a/src/components/chat.rs b/src/components/chat.rs index dad351e8..36f43efa 100644 --- a/src/components/chat.rs +++ b/src/components/chat.rs @@ -20,30 +20,3 @@ pub enum ChatMode { CommandsOnly, Hidden, } - -#[derive(Debug, MinecraftPacketPart)] -pub enum TitleAction<'a> { - SetTitle { - title: Chat<'a>, - }, - SetSubtitle { - subtitle: Chat<'a>, - }, - SetActionBar { - /// Displays a message above the hotbar (the same as [Position::GameInfo] in [ClientboundPacket::ChatMessage], except that it correctly renders formatted chat; see MC-119145 for more information). - action_bar_text: Chat<'a>, - }, - SetTimes { - /// Ticks to spend fading in - fade_int: i32, - /// Ticks to keep the title displayed - stay: i32, - /// Ticks to spend out, not when to start fading out - fade_out: i32, - }, - /// Sending [TitleAction::Hide] once makes the text disappear. - /// Sending another time will make the text reappear. - Hide, - /// Erases the text - Reset, -} diff --git a/src/components/chunk.rs b/src/components/chunk.rs index cc35590f..79d9400e 100644 --- a/src/components/chunk.rs +++ b/src/components/chunk.rs @@ -9,15 +9,16 @@ pub struct ChunkData<'a> { pub chunk_x: i32, /// Chunk coordinate (block coordinate divided by 16, rounded down). pub chunk_z: i32, - /// Bitmask with bits set to 1 for every 16×16×16 chunk section whose data is included in Data. - /// The least significant bit represents the chunk section at the bottom of the chunk column (from y=0 to y=15). - pub primary_bit_mask: VarInt, - /// Compound containing one long array named `MOTION_BLOCKING`, which is a heightmap for the highest solid block at each position in the chunk (as a compacted long array with 256 entries at 9 bits per entry totaling 36 longs). The Notchian server also adds a `WORLD_SURFACE` long array, the purpose of which is unknown, but it's not required for the chunk to be accepted. + /// BitSet with bits (world height in blocks / 16) set to 1 for every 16×16×16 chunk section whose data is included in Data. + /// The least significant bit represents the chunk section at the bottom of the chunk column (from the lowest y to 15 blocks above). + pub primary_bit_mask: Array<'a, u64, VarInt>, + /// Compound containing one long array named `MOTION_BLOCKING`, which is a heightmap for the highest solid block at each position in the chunk (as a compacted long array with 256 entries at 9 bits per entry totaling 36 longs). + /// The Notchian server also adds a `WORLD_SURFACE` long array, the purpose of which is unknown, but it's not required for the chunk to be accepted. pub heightmaps: NbtTag, /// 1024 biome IDs, ordered by x then z then y, in 4×4×4 blocks. /// Biomes cannot be changed unless a chunk is re-sent. /// The structure is an array of 1024 integers, each representing a [Biome ID](http://minecraft.gamepedia.com/Biome/ID) (it is recommended that "Void" is used if there is no set biome - its default id is 127). The array is ordered by x then z then y, in 4×4×4 blocks. The array is indexed by `((y >> 2) & 63) << 4 | ((z >> 2) & 3) << 2 | ((x >> 2) & 3)`. - pub biomes: Option>, + pub biomes: Array<'a, VarInt, VarInt>, /// The data section of the packet contains most of the useful data for the chunk. /// The number of elements in the array is equal to the number of bits set in [ChunkData::primary_bit_mask]. /// Sections are sent bottom-to-top, i.e. the first section, if sent, extends from Y=0 to Y=15. @@ -34,15 +35,10 @@ impl<'a> MinecraftPacketPart<'a> for ChunkData<'a> { fn serialize_minecraft_packet_part(self, output: &mut Vec) -> Result<(), &'static str> { self.chunk_x.serialize_minecraft_packet_part(output)?; self.chunk_z.serialize_minecraft_packet_part(output)?; - self.biomes - .is_some() - .serialize_minecraft_packet_part(output)?; self.primary_bit_mask .serialize_minecraft_packet_part(output)?; self.heightmaps.serialize_minecraft_packet_part(output)?; - if let Some(biomes) = self.biomes { - biomes.serialize_minecraft_packet_part(output)?; - } + self.biomes.serialize_minecraft_packet_part(output)?; VarInt(self.data.len() as i32).serialize_minecraft_packet_part(output)?; output.extend_from_slice(self.data); self.entities.serialize_minecraft_packet_part(output)?; @@ -54,18 +50,11 @@ impl<'a> MinecraftPacketPart<'a> for ChunkData<'a> { ) -> Result<(Self, &'a [u8]), &'static str> { let (chunk_x, input) = MinecraftPacketPart::deserialize_minecraft_packet_part(input)?; let (chunk_z, input) = MinecraftPacketPart::deserialize_minecraft_packet_part(input)?; - let (full_chunk, input) = MinecraftPacketPart::deserialize_minecraft_packet_part(input)?; let (primary_bit_mask, input) = MinecraftPacketPart::deserialize_minecraft_packet_part(input)?; let (heightmaps, input) = MinecraftPacketPart::deserialize_minecraft_packet_part(input)?; - let (biomes, input) = match full_chunk { - false => (None, input), - true => { - let (biomes, input) = + let (biomes, input) = >::deserialize_minecraft_packet_part(input)?; - (Some(biomes), input) - } - }; let (data_len, input) = VarInt::deserialize_minecraft_packet_part(input)?; let data_len = std::cmp::max(data_len.0, 0) as usize; let (data, input) = input.split_at(data_len); @@ -109,9 +98,12 @@ impl<'a> ChunkData<'a> { &mut self, ) -> Result<[Option; 16], &'static str> { let mut input = self.data; - let primary_bit_mask: u32 = unsafe { + if self.primary_bit_mask.items.len() != 1 { + return Err("ChunkData::deserialize_chunk_sections: primary_bit_mask.items.len() != 1"); + } + let primary_bit_mask: u64 = unsafe { // We don't care the type since we only want to check the bits - std::mem::transmute(self.primary_bit_mask.0) + *self.primary_bit_mask.items.get_unchecked(0) }; let mut chunk_sections: [Option; 16] = [ @@ -120,7 +112,7 @@ impl<'a> ChunkData<'a> { ]; let mut mask = 0b1; for y in 0..16 { - chunk_sections[y] = if primary_bit_mask & mask != 0 { + chunk_sections[y] = if primary_bit_mask & mask != 0 { let (block_count, new_input) = i16::deserialize_minecraft_packet_part(input)?; let (mut bits_per_block, new_input) = u8::deserialize_minecraft_packet_part(new_input)?; @@ -217,51 +209,13 @@ impl<'a> ChunkData<'a> { } } -#[derive(Debug, MinecraftPacketPart)] -#[discriminant(VarInt)] -pub enum WorldBorderAction { - SetSize { - /// Length of a single side of the world border, in meters - diameter: f64, - }, - LerpSize { - /// Current length of a single side of the world border, in meters - old_diameter: f64, - /// Target length of a single side of the world border, in meters - new_diameter: f64, - /// Number of real-time milliseconds until New Diameter is reached. - /// It appears that Notchian server does not sync world border speed to game ticks, so it gets out of sync with server lag. - /// If the world border is not moving, this is set to 0. - speed: VarLong, - }, - SetCenter { - x: f64, - z: f64, - }, - Initialize { - x: f64, - z: f64, - /// Current length of a single side of the world border, in meters - old_diameter: f64, - /// Target length of a single side of the world border, in meters - new_diameter: f64, - /// Number of real-time milliseconds until New Diameter is reached. - /// It appears that Notchian server does not sync world border speed to game ticks, so it gets out of sync with server lag. - /// If the world border is not moving, this is set to 0. - speed: VarLong, - /// Resulting coordinates from a portal teleport are limited to ±value. Usually 29999984. - portal_teleport_value: VarInt, - /// In meters - warning_blocks: VarInt, - /// In seconds as set by `/worldborder warning time` - warning_time: VarInt, - }, - SetWarningTime { - /// In seconds as set by `/worldborder warning time` - warning_time: VarInt, - }, - SetWarningBlocks { - /// In meters - warning_blocks: VarInt, - }, +#[cfg(test)] +#[test] +fn test() { + let chunk_data = &include_bytes!("../../test_data/chunk.mc_packet")[1..]; + + let mut chunk_data_deserialized = ChunkData::deserialize_uncompressed_minecraft_packet(chunk_data).unwrap(); + let _blocks = chunk_data_deserialized.deserialize_chunk_sections().unwrap(); + + //println!("{:?}", chunk_data_deserialized); } diff --git a/src/components/combat.rs b/src/components/combat.rs deleted file mode 100644 index 6ace7dcf..00000000 --- a/src/components/combat.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::*; - -#[derive(Debug, MinecraftPacketPart)] -#[discriminant(VarInt)] -pub enum CombatEvent<'a> { - EnterCombat, - EndCombat { - /// Length of the combat in ticks - duration: VarInt, - /// ID of the primary opponent of the ended combat, or -1 if there is no obvious primary opponent - opponent_entity_id: i32, - }, - EntityDead { - /// Entity ID of the player that died (should match the client's entity ID) - dead_entity_id: VarInt, - /// The killing entity's ID, or -1 if there is no obvious killer - killer_entity_id: i32, - death_message: Chat<'a>, - }, -} diff --git a/src/components/entity.rs b/src/components/entity.rs index ba015140..3ce11d44 100644 --- a/src/components/entity.rs +++ b/src/components/entity.rs @@ -28,6 +28,7 @@ pub enum EntityAttributeModifierOperation { /// `value = base_value * modifier` Multiply, } + #[derive(Debug, MinecraftPacketPart)] #[discriminant(VarInt)] pub enum EntityInteraction { @@ -69,6 +70,7 @@ pub enum Pose { SpinAttack, Sneaking, Dying, + LongJumping, } #[derive(Debug, Clone)] @@ -180,8 +182,12 @@ mod tests { #[test] fn test_entity_metadata() { - let t1 = [/*68, 160, 129, 2, */0, 0, 0, 2, 5, 0, 6, 18, 0, 4, 7, 0, 15, 13, 10, 14, 0, 0, 12, 1, 0, 13, 10, 0, 8, 2, 66, 32, 0, 0, 9, 1, 0, 11, 1, 0, 10, 7, 0, 1, 1, 172, 2, 3, 7, 0, 7, 0, 0, 5, 7, 0, 16, 7, 0, 17, 7, 0, 255]; - let t2 = [/*68, 219, 242, 1, */15, 13, 68, 255]; + let t1 = [ + /*68, 160, 129, 2, */ 0, 0, 0, 2, 5, 0, 6, 18, 0, 4, 7, 0, 15, 13, 10, 14, 0, 0, + 12, 1, 0, 13, 10, 0, 8, 2, 66, 32, 0, 0, 9, 1, 0, 11, 1, 0, 10, 7, 0, 1, 1, 172, 2, 3, + 7, 0, 7, 0, 0, 5, 7, 0, 16, 7, 0, 17, 7, 0, 255, + ]; + let t2 = [/*68, 219, 242, 1, */ 15, 13, 68, 255]; EntityMetadata::deserialize_uncompressed_minecraft_packet(&t1).unwrap(); EntityMetadata::deserialize_uncompressed_minecraft_packet(&t2).unwrap(); diff --git a/src/components/mod.rs b/src/components/mod.rs index fd393158..f682d6aa 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -5,7 +5,6 @@ pub mod blocks; pub mod boss_bar; pub mod chat; pub mod chunk; -pub mod combat; pub mod command_block; pub mod difficulty; pub mod effect; @@ -19,5 +18,6 @@ pub mod recipes; pub mod resource_pack; pub mod slots; pub mod sound; +pub mod tags; pub mod teams; pub mod trades; diff --git a/src/components/slots.rs b/src/components/slots.rs index feb89727..399b6207 100644 --- a/src/components/slots.rs +++ b/src/components/slots.rs @@ -104,6 +104,35 @@ impl<'a> MinecraftPacketPart<'a> for EquipmentSlotArray { } } +#[minecraft_enum(VarInt)] +#[derive(Debug)] +pub enum WindowType { + OneRow, + TwoRows, + ThreeRows, + FourRows, + FiveRows, + SixRows, + ThreeByThree, + Anvil, + Beacon, + BlastFurnace, + BrewingStand, + Crafting, + Enchantment, + Furnace, + Grindstone, + Hopper, + Lectern, + Loom, + Merchant, + ShulkerBox, + Smithing, + Smoker, + Cartography, + Stonecutter, +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/components/tags.rs b/src/components/tags.rs new file mode 100644 index 00000000..fef324bb --- /dev/null +++ b/src/components/tags.rs @@ -0,0 +1,11 @@ +use crate::*; + +/// A tag +/// +/// More information on tags is available at: https://minecraft.gamepedia.com/Tag +/// And a list of all tags is here: https://minecraft.gamepedia.com/Tag#List_of_tags +#[derive(Debug, MinecraftPacketPart)] +pub struct Tag<'a> { + pub tag_name: Identifier<'a>, + pub data: Array<'a, VarInt, VarInt>, +} diff --git a/src/ids/.gitignore b/src/ids/.gitignore index d137a50b..aff77a29 100644 --- a/src/ids/.gitignore +++ b/src/ids/.gitignore @@ -1,3 +1,6 @@ blocks.rs +block_states.rs items.rs entities.rs +recipes.rs + diff --git a/src/ids/mod.rs b/src/ids/mod.rs index 55e45f7b..8acfb997 100644 --- a/src/ids/mod.rs +++ b/src/ids/mod.rs @@ -4,3 +4,5 @@ pub mod blocks; pub mod entities; pub mod items; +pub mod block_states; +pub mod recipes; diff --git a/src/network.rs b/src/network.rs index e1f9aa58..e8ea02e9 100644 --- a/src/network.rs +++ b/src/network.rs @@ -97,7 +97,7 @@ mod tests { send_packet( &mut stream, crate::packets::handshake::ServerboundPacket::Hello { - protocol_version: 754.into(), + protocol_version: 756.into(), server_address: "127.0.0.1", server_port: 25565, next_state: crate::packets::ConnectionState::Login, diff --git a/src/packets/mod.rs b/src/packets/mod.rs index cfd70204..56e2828f 100644 --- a/src/packets/mod.rs +++ b/src/packets/mod.rs @@ -84,6 +84,17 @@ pub struct Array<'a, T: MinecraftPacketPart<'a> + std::fmt::Debug, U: MinecraftP pub items: Vec, } +impl<'a, T: std::fmt::Debug + MinecraftPacketPart<'a>, U: MinecraftPacketPart<'a>> From> + for Array<'a, T, U> +{ + fn from(value: Vec) -> Self { + Array { + _len_prefix: std::marker::PhantomData, + items: value, + } + } +} + #[derive(Debug)] pub struct Map< 'a, @@ -95,6 +106,21 @@ pub struct Map< pub items: std::collections::BTreeMap, } +impl< + 'a, + K: std::fmt::Debug + MinecraftPacketPart<'a>, + V: std::fmt::Debug + MinecraftPacketPart<'a>, + U: MinecraftPacketPart<'a>, + > From> for Map<'a, K, V, U> +{ + fn from(value: std::collections::BTreeMap) -> Self { + Map { + _len_prefix: std::marker::PhantomData, + items: value, + } + } +} + /// The possible packets are different for each state. #[minecraft_enum(VarInt)] #[derive(Debug)] diff --git a/src/packets/play_clientbound.rs b/src/packets/play_clientbound.rs index 743d3d91..6d6fb437 100644 --- a/src/packets/play_clientbound.rs +++ b/src/packets/play_clientbound.rs @@ -1,10 +1,10 @@ #[allow(unused_imports)] use super::play_serverbound::ServerboundPacket; -use crate::nbt::NbtTag; +use super::*; use crate::components::*; -use crate::ids::*; use crate::ids::blocks; -use super::*; +use crate::ids::*; +use crate::nbt::NbtTag; #[derive(Debug, MinecraftPacketPart)] #[discriminant(VarInt)] @@ -88,6 +88,15 @@ pub enum ClientboundPacket<'a> { pitch: Angle, }, + /// Shows a permanent particle. + SculkVibrationSignal { + /// Source position for the vibration + source_position: Position, + /// Identifier of the destination codec type + destination_identifier: Identifier<'a>, + rest: RawBytes<'a>, + }, + /// Sent whenever an entity should change animation EntityAnimation { id: VarInt, @@ -105,8 +114,7 @@ pub enum ClientboundPacket<'a> { /// Position where the digging was happening location: Position, /// Block state ID of the block that should be at that position now. - /// Use [Block::from_state_id](blocks::Block::from_state_id) to get the corresponding [Block](blocks::Block). - block: VarInt, + block: block_states::BlockWithState, status: crate::components::blocks::PartialDiggingState, /// True if the digging succeeded; false if the client should undo any changes it made locally. successful: bool, @@ -156,9 +164,8 @@ pub enum ClientboundPacket<'a> { BlockChange { /// Block Coordinates location: Position, - /// The new block state ID for the block as given in the [global palette](http://minecraft.gamepedia.com/Data_values%23Block_IDs). See that section for more information. - /// Use [Block::from_state_id](blocks::Block::from_state_id) to get the corresponding [Block](blocks::Block). - block_state: VarInt, + /// The new block state ID for the block + block_state: block_states::BlockWithState, }, BossBar { @@ -186,6 +193,11 @@ pub enum ClientboundPacket<'a> { sender: UUID, }, + /// Clears the client's current title information, with the option to also reset it. + ClearTitles { + reset: bool, + }, + /// The server responds with a list of auto-completions of the last word sent to it. /// In the case of regular chat, this is a player username. /// Command names and parameters are also supported. @@ -211,19 +223,6 @@ pub enum ClientboundPacket<'a> { data: RawBytes<'a>, }, - /// A packet from the server indicating whether a request from the client was accepted, or whether there was a conflict (due to lag). - /// If the packet was not accepted, the client must respond with a serverbound window confirmation packet. - /// - /// *Request for [ServerboundPacket::WindowConfirmation]* - WindowConfirmation { - /// The ID of the window that the action occurred in. - window_id: i8, - /// Every action that is to be accepted has a unique ID. This number is an incrementing integer (starting at 0) with separate counts for each window ID. - action_id: i16, - /// Whether the action was accepted. - accepted: bool, - }, - /// This packet is sent from the server to the client when a window is forcibly closed, such as when a chest is destroyed while it's open. CloseWindow { /// This is the ID of the window that was closed. 0 for inventory. @@ -235,9 +234,13 @@ pub enum ClientboundPacket<'a> { WindowItems { /// The ID of window which items are being sent for. 0 for player inventory. window_id: i8, + /// A state id required for future [ServerboundPacket::ClickWindowSlot] + state_id: VarInt, /// The [slots::Slot]s in this window. /// See [inventory windows](https://wiki.vg/Inventory#Windows) for further information about how slots are indexed. - slots: Array<'a, slots::Slot, i16>, + slots: Array<'a, slots::Slot, VarInt>, + /// Item held by player + carried_item: slots::Slot, }, /// This packet is used to inform the client that part of a GUI window should be updated. @@ -264,6 +267,8 @@ pub enum ClientboundPacket<'a> { /// This packet will only be sent for the currently opened window while the player is performing actions, even if it affects the player inventory. /// After the window is closed, a number of these packets are sent to update the player's inventory window (0). window_id: i8, + /// A state id required for future [ServerboundPacket::ClickWindowSlot] + state_id: VarInt, /// The slot that should be updated. slot_index: i16, slot_value: slots::Slot, @@ -374,6 +379,27 @@ pub enum ClientboundPacket<'a> { entity_id: i32, }, + /// The Notchian client determines how solid to display the warning by comparing to whichever is higher, the warning distance or whichever is lower, the distance from the current diameter to the target diameter or the place the border will be after warningTime seconds. + IntitializeWorldBorder { + x: f64, + y: f64, + /// Current length of a single side of the world border, in meters. + old_diameter: f64, + /// Target length of a single side of the world border, in meters. + new_diameter: f64, + /// Number of real-time milliseconds until New Diameter is reached. + /// It appears that Notchian server does not sync world border speed to game ticks, so it gets out of sync with server lag. + /// If the world border is not moving, this is set to 0. + speed: VarLong, + /// Resulting coordinates from a portal teleport are limited to ±value. + /// Usually 29999984. + portal_teleport_boundary: VarInt, + /// In meters + warning_blocks: VarInt, + /// In seconds as set by `/worldborder warning time` + warning_time: VarInt, + }, + /// The server will frequently send out a keep-alive, each containing a random ID. /// The client must respond with the same payload (see [serverbound Keep Alive](https://wiki.vg/Protocol#Keep_Alive_.28serverbound.29)). /// If the client does not respond to them for over 30 seconds, the server kicks the client. @@ -533,14 +559,6 @@ pub enum ClientboundPacket<'a> { on_ground: bool, }, - /// This packet may be used to initialize an entity. - /// - /// For player entities, either this packet or any move/look packet is sent every game tick. - /// So the meaning of this packet is basically that the entity did not move/look since the last such packet. - EntityMovement { - entity_id: VarInt, - }, - /// Note that all fields use absolute positioning and do not allow for relative positioning. VehicleMove { /// Absolute position (X coordinate) @@ -569,8 +587,7 @@ pub enum ClientboundPacket<'a> { /// Notchian server implementation is a counter, starting at 1. window_id: VarInt, /// The window type to use for display. - /// TODO: replace by an enum - window_type: VarInt, + window_type: slots::WindowType, /// The title of the window window_title: Chat<'a>, }, @@ -583,6 +600,12 @@ pub enum ClientboundPacket<'a> { location: Position, }, + /// Unknown what this packet does just yet, not used by the Notchian server or client. + /// Most likely added as a replacement to the removed window confirmation packet. + UselessPacket { + id: i32, + }, + // Todo make add doc links /// Response to the serverbound packet (Craft Recipe Request), with the same recipe ID. /// Appears to be used to notify the UI. @@ -602,10 +625,27 @@ pub enum ClientboundPacket<'a> { field_of_view_modifier: f32, }, - /// Originally used for metadata for twitch streaming circa 1.8. - /// Now only used to display the game over screen (with enter combat and end combat completely ignored by the Notchain client) - CombatEvent { - event: combat::CombatEvent<'a>, + /// Unused by the Notchain client. + /// This data was once used for twitch.tv metadata circa 1.8. + EndCombatEvent { + /// Length of the combat in ticks. + duration: VarInt, + /// ID of the primary opponent of the ended combat, or -1 if there is no obvious primary opponent. + entity_id: i32, + }, + + /// Unused by the Notchain client. + /// This data was once used for twitch.tv metadata circa 1.8. + EnterCombatEvent, + + /// Used to send a respawn screen. + DeathCombatEvent { + /// Entity ID of the player that died (should match the client's entity ID) + player_id: VarInt, + /// The killing entity's ID, or -1 if there is no obvious killer + entity_id: i32, + /// The death message + message: Chat<'a>, }, /// Sent by the server to update the user list ( in the client). @@ -652,14 +692,16 @@ pub enum ClientboundPacket<'a> { pitch: f32, flags: u8, teleport_id: VarInt, + /// True if the player should dismount their vehicle + dismount_vehicle: bool, }, UnlockRecipes { - action: recipes::UnlockRecipesAction<'a>, + action: crate::components::recipes::UnlockRecipesAction<'a>, }, /// Sent by the server when a list of entities is to be destroyed on the client - DestoryEntities { + DestroyEntities { entity_ids: Array<'a, VarInt, VarInt>, }, @@ -733,8 +775,40 @@ pub enum ClientboundPacket<'a> { identifier: Option>, }, - WorldBorder { - action: chunk::WorldBorderAction, + /// Displays a message above the hotbar (the same as position 2 in Chat Message (clientbound). + ActionBar { + action_bar_text: Chat<'a>, + }, + + WorldBorderCenter { + x: f64, + y: f64, + }, + + WorldBorderLerpSize { + /// Current length of a single side of the world border, in meters + old_diameter: f64, + /// Target length of a single side of the world border, in meters + new_diameter: f64, + /// Number of real-time milliseconds until New Diameter is reached. + /// It appears that Notchian server does not sync world border speed to game ticks, so it gets out of sync with server lag. + /// If the world border is not moving, this is set to 0. + speed: VarLong, + }, + + WorldBorderSize { + /// Length of a single side of the world border, in meters + diameter: f64, + }, + + WorldBorderWarningDelay { + /// In seconds as set by `/worldborder warning time` + warning_time: VarInt, + }, + + WorldBorderWarningReach { + /// In meters + warning_blocks: VarInt, }, /// Sets the entity that the player renders from. @@ -780,6 +854,8 @@ pub enum ClientboundPacket<'a> { /// It can be sent at any time to update the point compasses point at. SpawnPosition { location: Position, + /// The angle at which to respawn at + angle: f32, }, /// This is sent to the client when it should display a scoreboard @@ -873,6 +949,10 @@ pub enum ClientboundPacket<'a> { score_action: teams::ScoreboardScoreAction<'a>, }, + SetTitleSubTitle { + subtitle_text: Chat<'a>, + }, + /// Time is based on ticks, where 20 ticks happen every second. /// There are 24000 ticks in a day, making Minecraft days exactly 20 minutes long. /// The time of day is based on the timestamp modulo 24000. 0 is sunrise, 6000 is noon, 12000 is sunset, and 18000 is midnight. @@ -885,8 +965,17 @@ pub enum ClientboundPacket<'a> { time_of_day: i64, }, - Title { - action: chat::TitleAction<'a>, + SetTitleText { + title_text: Chat<'a>, + }, + + SetTitleTimes { + /// Ticks to spend fading in + fade_in: i32, + /// Ticks to keep the title displayed + stay: i32, + /// Ticks to spend out, not when to start fading out + fade_out: i32, }, /// Plays a sound effect from an entity @@ -929,7 +1018,7 @@ pub enum ClientboundPacket<'a> { /// This packet may be used by custom servers to display additional information above/below the player list. /// It is never sent by the Notchian server. - PlayerListSetHeaderAndFooter { + PlayerListHeaderAndFooter { /// To remove the header, send a empty text component: `{"text":""}` header: Chat<'a>, /// To remove the footer, send a empty text component: `{"text":""}` @@ -982,8 +1071,7 @@ pub enum ClientboundPacket<'a> { advancement_mapping: Map<'a, Identifier<'a>, advancements::Advancement<'a>, VarInt>, /// The identifiers of the advancements that should be removed advancements_to_remove: Array<'a, Identifier<'a>, VarInt>, - progress_mapping: - Map<'a, Identifier<'a>, advancements::AdvancementProgress<'a>, VarInt>, + progress_mapping: Map<'a, Identifier<'a>, advancements::AdvancementProgress<'a>, VarInt>, }, /// Sets [attributes](https://minecraft.fandom.com/wiki/Attribute) on the given entity @@ -993,7 +1081,7 @@ pub enum ClientboundPacket<'a> { /// [Attributes](entity::EntityAttribute) also have [Attributes](entity::EntityAttributeModifier) that adjust the strength of their effect. /// /// [More information](https://minecraft.fandom.com/wiki/Attribute) - attributes: Map<'a, Identifier<'a>, entity::EntityAttribute<'a>, i32>, + attributes: Map<'a, Identifier<'a>, entity::EntityAttribute<'a>, VarInt>, }, EntityEffect { @@ -1015,13 +1103,8 @@ pub enum ClientboundPacket<'a> { }, Tags { - /// A map linking block tags to an array of corresponding IDs. - block_tags: Map<'a, Identifier<'a>, Array<'a, VarInt, VarInt>, VarInt>, - /// A map linking item tags to an array of corresponding IDs. - item_tags: Map<'a, Identifier<'a>, Array<'a, VarInt, VarInt>, VarInt>, - /// A map linking fluid tags to an array of corresponding IDs. - fluid_tags: Map<'a, Identifier<'a>, Array<'a, VarInt, VarInt>, VarInt>, - /// A map linking entity tags to an array of corresponding IDs. - entity_tags: Map<'a, Identifier<'a>, Array<'a, VarInt, VarInt>, VarInt>, + /// More information on tags is available at: https://minecraft.gamepedia.com/Tag + /// And a list of all tags is here: https://minecraft.gamepedia.com/Tag#List_of_tags + tags: Map<'a, Identifier<'a>, Array<'a, tags::Tag<'a>, VarInt>, VarInt>, }, } diff --git a/src/packets/play_serverbound.rs b/src/packets/play_serverbound.rs index 592f511d..927c3a29 100644 --- a/src/packets/play_serverbound.rs +++ b/src/packets/play_serverbound.rs @@ -1,7 +1,7 @@ #[allow(unused_imports)] use super::play_clientbound::ClientboundPacket; -use crate::components::*; use super::*; +use crate::components::*; #[derive(Debug, MinecraftPacketPart)] #[discriminant(VarInt)] @@ -37,9 +37,7 @@ pub enum ServerboundPacket<'a> { }, /// *Request for [ClientboundPacket::Statistics]* - ClientStatus { - action: game_state::ClientStatus, - }, + ClientStatus { action: game_state::ClientStatus }, /// Sent when the player connects, or when settings are changed ClientSettings { @@ -53,6 +51,9 @@ pub enum ServerboundPacket<'a> { /// Bit mask, see [the wiki](https://wiki.vg/Protocol#Client_Settings) displayed_skin_parts: u8, main_hand: slots::MainHand, + /// Disables filtering of text on signs and written book titles. + /// Currently always true (i.e. the filtering is disabled) + disable_text_filtering: bool, }, /// *Request for [ClientboundPacket::TabComplete]* @@ -62,20 +63,6 @@ pub enum ServerboundPacket<'a> { text: &'a str, }, - /// The server may reject client actions by sending [ClientboundPacket::WindowConfirmation] with the `accepted` field set to `false`. - /// When this happens, the client must send this packet to apologize (as with movement), otherwise the server ignores any successive confirmations. - /// - /// *Response to [ClientboundPacket::WindowConfirmation]* - WindowConfirmation { - /// The ID of the window that the action occurred in - window_id: i8, - /// Every action that is to be accepted has a unique id. - /// This id is an incrementing integer (starting at 1) with separate counts for each window ID. - action_id: i16, - /// Whether the action was accepted - accepted: bool, - }, - /// Used when clicking on window buttons ClickWindowButton { /// The ID of the window sent by [ClientboundPacket::OpenWindow]. @@ -90,15 +77,19 @@ pub enum ServerboundPacket<'a> { ClickWindowSlot { /// The ID of the window which was clicked. 0 for player inventory. window_id: i8, + /// The last received State ID from either a [ClientboundPacket::SetSlot] or a [ClientboundPacket::WindowItems] packet + state_id: VarInt, /// The clicked slot number, see [the wiki](https://wiki.vg/Protocol#Click_Window) slot: i16, /// The button used in the click, see [the wiki](https://wiki.vg/Protocol#Click_Window) button: u8, - /// A unique number for the action, implemented by Notchian as a counter, starting at 1 (different counter for every window ID). Used by the server to send back a [ClientboundPacket::WindowConfirmation]. - action_id: i16, /// Inventory operation mode, see [the wiki](https://wiki.vg/Protocol#Click_Window) mode: VarInt, - /// The clicked slot. Has to be empty (item ID = -1) for drop mode. (TODO: check this) + /// New values for affected slots + new_slot_values: Map<'a, i16, slots::Slot, VarInt>, + /// The clicked slot + /// Has to be empty (item ID = -1) for drop mode. (TODO: check this) + /// Is always empty for mode 2 and mode 5 packets. clicked_item: slots::Slot, }, @@ -308,6 +299,11 @@ pub enum ServerboundPacket<'a> { flags: u8, }, + /// A response to the ping packet sync to the main thread. + /// Unknown what this is used for, this is ignored by the Notchian client and server. + /// Most likely added as a replacement to the removed window confirmation packet. + UselessPacket { id: i32 }, + /// Replaces Recipe Book Data, type 1. SetRecipeBookState { book: recipes::RecipeBook, diff --git a/test_data/chunk.mc_packet b/test_data/chunk.mc_packet new file mode 100644 index 00000000..9956f91f Binary files /dev/null and b/test_data/chunk.mc_packet differ