From 2f653f63366cd8c7c7b9480cab4f06d867e6ce9f Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sat, 27 Jun 2026 22:16:21 +0200 Subject: [PATCH] feat: bedrock crafting --- .../src/bedrock/client/crafting_data.rs | 193 ++++++++++++++++++ pumpkin-protocol/src/bedrock/client/mod.rs | 2 + pumpkin-protocol/src/bedrock/server/mod.rs | 2 + .../src/bedrock/server/request_ability.rs | 39 ++++ pumpkin/src/net/bedrock/mod.rs | 5 + pumpkin/src/net/bedrock/play.rs | 50 ++++- pumpkin/src/world/mod.rs | 184 +++++++++++++++++ 7 files changed, 473 insertions(+), 2 deletions(-) create mode 100644 pumpkin-protocol/src/bedrock/client/crafting_data.rs create mode 100644 pumpkin-protocol/src/bedrock/server/request_ability.rs diff --git a/pumpkin-protocol/src/bedrock/client/crafting_data.rs b/pumpkin-protocol/src/bedrock/client/crafting_data.rs new file mode 100644 index 000000000..d45a4338e --- /dev/null +++ b/pumpkin-protocol/src/bedrock/client/crafting_data.rs @@ -0,0 +1,193 @@ +use crate::{ + bedrock::network_item::NetworkItemDescriptor, + codec::{var_int::VarInt, var_uint::VarUInt}, + serial::PacketWrite, +}; +use pumpkin_macros::packet; +use std::io::{Error, Write}; + +#[derive(Clone, Debug)] +pub struct ItemDescriptorCount { + pub network_id: i16, + pub metadata_value: i16, + pub count: i32, +} + +impl PacketWrite for ItemDescriptorCount { + fn write(&self, writer: &mut W) -> Result<(), Error> { + if self.network_id == 0 { + // Invalid item descriptor (type 0) + 0u8.write(writer)?; + } else { + // Default item descriptor (type 1) + 1u8.write(writer)?; + self.network_id.write(writer)?; + self.metadata_value.write(writer)?; + } + VarInt(self.count).write(writer)?; + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub struct RecipeUnlockRequirement { + pub context: u8, +} + +impl PacketWrite for RecipeUnlockRequirement { + fn write(&self, writer: &mut W) -> Result<(), Error> { + self.context.write(writer)?; + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub struct BedrockShapelessRecipe { + pub recipe_id: String, + pub input: Vec, + pub output: Vec, + pub uuid: [u8; 16], + pub block: String, + pub priority: VarInt, + pub unlock_requirement: RecipeUnlockRequirement, + pub recipe_network_id: VarUInt, +} + +impl PacketWrite for BedrockShapelessRecipe { + fn write(&self, writer: &mut W) -> Result<(), Error> { + self.recipe_id.write(writer)?; + + // input slice with VarUInt length prefix + VarUInt(self.input.len() as u32).write(writer)?; + for item in &self.input { + item.write(writer)?; + } + + // output slice with VarUInt length prefix + VarUInt(self.output.len() as u32).write(writer)?; + for item in &self.output { + item.write(writer)?; + } + + // uuid + writer.write_all(&self.uuid)?; + + // block + self.block.write(writer)?; + + // priority + self.priority.write(writer)?; + + // unlock_requirement + self.unlock_requirement.write(writer)?; + + // recipe_network_id + self.recipe_network_id.write(writer)?; + + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub struct BedrockShapedRecipe { + pub recipe_id: String, + pub width: VarInt, + pub height: VarInt, + pub input: Vec, + pub output: Vec, + pub uuid: [u8; 16], + pub block: String, + pub priority: VarInt, + pub assume_symmetry: bool, + pub unlock_requirement: RecipeUnlockRequirement, + pub recipe_network_id: VarUInt, +} + +impl PacketWrite for BedrockShapedRecipe { + fn write(&self, writer: &mut W) -> Result<(), Error> { + self.recipe_id.write(writer)?; + self.width.write(writer)?; + self.height.write(writer)?; + + // input slice: width * height elements sequentially (no count prefix) + for item in &self.input { + item.write(writer)?; + } + + // output slice with VarUInt length prefix + VarUInt(self.output.len() as u32).write(writer)?; + for item in &self.output { + item.write(writer)?; + } + + // uuid + writer.write_all(&self.uuid)?; + + // block + self.block.write(writer)?; + + // priority + self.priority.write(writer)?; + + // assume_symmetry + self.assume_symmetry.write(writer)?; + + // unlock_requirement + self.unlock_requirement.write(writer)?; + + // recipe_network_id + self.recipe_network_id.write(writer)?; + + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub enum BedrockRecipe { + Shapeless(BedrockShapelessRecipe), + Shaped(BedrockShapedRecipe), +} + +impl PacketWrite for BedrockRecipe { + fn write(&self, writer: &mut W) -> Result<(), Error> { + match self { + Self::Shapeless(recipe) => { + VarInt(0).write(writer)?; // type 0: Shapeless + recipe.write(writer)?; + } + Self::Shaped(recipe) => { + VarInt(1).write(writer)?; // type 1: Shaped + recipe.write(writer)?; + } + } + Ok(()) + } +} + +#[packet(52)] +pub struct CCraftingData { + pub recipes: Vec, + pub clean_recipes: bool, +} + +impl PacketWrite for CCraftingData { + fn write(&self, writer: &mut W) -> Result<(), Error> { + // recipes slice with VarUInt prefix + VarUInt(self.recipes.len() as u32).write(writer)?; + for recipe in &self.recipes { + recipe.write(writer)?; + } + + // potion recipes count prefix (0) + VarUInt(0).write(writer)?; + // potion container change recipes count prefix (0) + VarUInt(0).write(writer)?; + // material reducers count prefix (0) + VarUInt(0).write(writer)?; + + // clean_recipes + self.clean_recipes.write(writer)?; + + Ok(()) + } +} diff --git a/pumpkin-protocol/src/bedrock/client/mod.rs b/pumpkin-protocol/src/bedrock/client/mod.rs index 1535d0ebc..d95134ef2 100644 --- a/pumpkin-protocol/src/bedrock/client/mod.rs +++ b/pumpkin-protocol/src/bedrock/client/mod.rs @@ -7,6 +7,7 @@ pub mod chunk_radius_update; pub mod common; pub mod container_open; pub mod correct_player_move; +pub mod crafting_data; pub mod creative_content; pub mod disconnect_player; pub mod gamerules_changed; @@ -53,6 +54,7 @@ pub use chunk_radius_update::*; pub use common::*; pub use container_open::*; pub use correct_player_move::*; +pub use crafting_data::*; pub use creative_content::*; pub use disconnect_player::*; pub use gamerules_changed::*; diff --git a/pumpkin-protocol/src/bedrock/server/mod.rs b/pumpkin-protocol/src/bedrock/server/mod.rs index 70bca7a27..706584a8c 100644 --- a/pumpkin-protocol/src/bedrock/server/mod.rs +++ b/pumpkin-protocol/src/bedrock/server/mod.rs @@ -16,6 +16,7 @@ pub mod modal_form_response; pub mod player_action; pub mod player_auth_input; pub mod raknet; +pub mod request_ability; pub mod request_chunk_radius; pub mod request_network_settings; pub mod resource_pack_response; @@ -41,6 +42,7 @@ pub use modal_form_response::*; pub use player_action::{Action as PlayerActionType, SPlayerAction}; pub use player_auth_input::*; pub use raknet::*; +pub use request_ability::*; pub use request_chunk_radius::*; pub use request_network_settings::*; pub use resource_pack_response::*; diff --git a/pumpkin-protocol/src/bedrock/server/request_ability.rs b/pumpkin-protocol/src/bedrock/server/request_ability.rs new file mode 100644 index 000000000..f054a5854 --- /dev/null +++ b/pumpkin-protocol/src/bedrock/server/request_ability.rs @@ -0,0 +1,39 @@ +use crate::{codec::var_int::VarInt, serial::PacketRead}; +use pumpkin_macros::packet; +use std::io::{Error, Read}; + +#[derive(Clone, Debug)] +pub enum AbilityValue { + Bool(bool), + Float(f32), +} + +impl PacketRead for AbilityValue { + fn read(buf: &mut R) -> Result { + let val_type = u8::read(buf)?; + let bool_val = bool::read(buf)?; + let float_val = f32::read(buf)?; + match val_type { + 1 => Ok(Self::Bool(bool_val)), + 2 => Ok(Self::Float(float_val)), + _ => Err(Error::new( + std::io::ErrorKind::InvalidData, + format!("Invalid ability value type: {val_type}"), + )), + } + } +} + +#[packet(102)] +pub struct SRequestAbility { + pub ability: VarInt, + pub value: AbilityValue, +} + +impl PacketRead for SRequestAbility { + fn read(buf: &mut R) -> Result { + let ability = VarInt::read(buf)?; + let value = AbilityValue::read(buf)?; + Ok(Self { ability, value }) + } +} diff --git a/pumpkin/src/net/bedrock/mod.rs b/pumpkin/src/net/bedrock/mod.rs index d0bfbe841..4e64ea47d 100644 --- a/pumpkin/src/net/bedrock/mod.rs +++ b/pumpkin/src/net/bedrock/mod.rs @@ -49,6 +49,7 @@ use pumpkin_protocol::{ open_connection::{SOpenConnectionRequest1, SOpenConnectionRequest2}, unconnected_ping::SUnconnectedPing, }, + request_ability::SRequestAbility, request_chunk_radius::SRequestChunkRadius, request_network_settings::SRequestNetworkSettings, resource_pack_response::SResourcePackResponse, @@ -1121,6 +1122,10 @@ impl BedrockClient { self.handle_block_pick_request(player, SBlockPickRequest::read(reader)?) .await; } + SRequestAbility::PACKET_ID => { + self.handle_request_ability(player, SRequestAbility::read(reader)?) + .await; + } SMobEquipment::PACKET_ID => { self.handle_mob_equipment(player, SMobEquipment::read(reader)?) .await; diff --git a/pumpkin/src/net/bedrock/play.rs b/pumpkin/src/net/bedrock/play.rs index 9e932cb07..18f5ef892 100644 --- a/pumpkin/src/net/bedrock/play.rs +++ b/pumpkin/src/net/bedrock/play.rs @@ -595,8 +595,8 @@ impl BedrockClient { let target_runtime_id = data.target_entity_runtime_id.0 as i32; // TODO: replace with consts, i'm too lazy match data.action_type.0 { - // Interact - 0 => { + // Interact / Item Interact + 0 | 2 => { let world = player.world(); if let Some(target) = world.get_entity_by_id(target_runtime_id) { let held = player.inventory.held_item(); @@ -1188,6 +1188,22 @@ impl BedrockClient { .. } => { if repetitions > 0 { + screen_handler.update_to_client().await; + + let is_player = screen_handler.window_type().is_none(); + let grid_size = if is_player { 4 } else { 9 }; + for i in 0..grid_size { + let grid_slot_index = 1 + i; + let grid_slot = + screen_handler.get_behaviour().slots[grid_slot_index].clone(); + let grid_stack = grid_slot.get_cloned_stack().await; + tracing::info!( + "Crafting Grid slot {i} (slot index {grid_slot_index}): Item ID: {}, Count: {}", + grid_stack.item.id, + grid_stack.item_count + ); + } + let output_slot = screen_handler.get_behaviour().slots[0].clone(); let output_stack = output_slot.get_cloned_stack().await; @@ -1467,6 +1483,36 @@ impl BedrockClient { let equipment = &[(EquipmentSlot::MAIN_HAND, stack)]; player.living_entity.send_equipment_changes(equipment); } + + pub async fn handle_request_ability( + &self, + player: &Arc, + packet: pumpkin_protocol::bedrock::server::request_ability::SRequestAbility, + ) { + player.update_last_action_time(); + let ability_id = packet.ability.0; + match ability_id { + 9 => { + // Flying + if let pumpkin_protocol::bedrock::server::request_ability::AbilityValue::Bool( + requested_flying, + ) = packet.value + { + let mut abilities = player.abilities.lock().await; + if abilities.allow_flying { + abilities.flying = requested_flying; + } else { + abilities.flying = false; + } + drop(abilities); + player.send_abilities_update().await; + } + } + _ => { + debug!("Received RequestAbility packet for unhandled ability {ability_id}"); + } + } + } } #[allow(clippy::too_many_lines)] diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 2b46e07fe..2d7295587 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -1791,6 +1791,11 @@ impl World { ) { static CREATIVE_CONTENT: std::sync::OnceLock<(Vec, Vec)> = std::sync::OnceLock::new(); + + static BEDROCK_CRAFTING_DATA: std::sync::OnceLock< + Vec, + > = std::sync::OnceLock::new(); + let level_info = server.level_info.load(); let weather = self.weather.lock().await; let runtime_id = player.entity_id() as u64; @@ -2009,6 +2014,185 @@ impl World { .send_game_packet(&CCreativeContent { groups, entries }) .await; + let bedrock_recipes = BEDROCK_CRAFTING_DATA.get_or_init(|| { + use pumpkin_data::item::{Item, JavaToBedrockItemMapping}; + use pumpkin_data::recipes::{CraftingRecipeTypes, RecipeIngredientTypes}; + use pumpkin_protocol::bedrock::client::{ + BedrockRecipe, BedrockShapedRecipe, BedrockShapelessRecipe, ItemDescriptorCount, + RecipeUnlockRequirement, + }; + use pumpkin_protocol::bedrock::network_item::NetworkItemDescriptor; + use pumpkin_protocol::codec::{var_int::VarInt, var_uint::VarUInt}; + + let mut mapped_recipes = Vec::new(); + let mut network_id_counter = 1u32; + + for recipe in pumpkin_data::recipes::RECIPES_CRAFTING { + let map_ingredient = |ing: &RecipeIngredientTypes| -> ItemDescriptorCount { + let item_key = match ing { + RecipeIngredientTypes::Simple(name) => Some(*name), + RecipeIngredientTypes::Tagged(tag) => { + let tag_name = tag.strip_prefix('#').unwrap_or(tag); + pumpkin_data::tag::get_tag_ids( + pumpkin_data::tag::RegistryKey::Item, + tag_name, + ) + .and_then(|ids| { + ids.first().and_then(|&first_id| { + Item::from_id(first_id).map(|item| item.registry_key) + }) + }) + } + RecipeIngredientTypes::OneOf(names) => names.first().copied(), + }; + + if let Some(key) = item_key { + let registry_key = key.strip_prefix("minecraft:").unwrap_or(key); + if let Some(item) = Item::from_registry_key(registry_key) + && let Some(mapping) = + JavaToBedrockItemMapping::from_java_item_id(item.id) + { + return ItemDescriptorCount { + network_id: mapping.bedrock_item.id, + metadata_value: mapping.bedrock_data as i16, + count: 1, + }; + } + } + + ItemDescriptorCount { + network_id: 0, + metadata_value: 0, + count: 0, + } + }; + + match recipe { + CraftingRecipeTypes::CraftingShaped { + category: _, + group: _, + show_notification: _, + key, + pattern, + result, + } => { + let height = pattern.len() as i32; + let width = pattern.iter().map(|s| s.len()).max().unwrap_or(0) as i32; + + let mut input = Vec::new(); + for r in 0..height { + let pattern_row = pattern[r as usize]; + for c in 0..width { + let ch = pattern_row.chars().nth(c as usize).unwrap_or(' '); + if ch == ' ' { + input.push(ItemDescriptorCount { + network_id: 0, + metadata_value: 0, + count: 0, + }); + } else { + let mut ingredient = None; + for &(key_ch, ref ing) in *key { + if key_ch == ch { + ingredient = Some(ing); + break; + } + } + if let Some(ing) = ingredient { + input.push(map_ingredient(ing)); + } else { + input.push(ItemDescriptorCount { + network_id: 0, + metadata_value: 0, + count: 0, + }); + } + } + } + } + + let output_item = Item::from_registry_key(result.id); + if let Some(item) = output_item + && let Some(mapping) = + JavaToBedrockItemMapping::from_java_item_id(item.id) + { + let output_descriptor = NetworkItemDescriptor { + id: VarInt::from(mapping.bedrock_item.id), + stack_size: result.count as u16, + aux_value: VarUInt(mapping.bedrock_data), + block_runtime_id: VarInt::from(mapping.bedrock_block_state), + nbt_data: pumpkin_nbt::Nbt::default(), + place_on_blocks: Vec::new(), + destroy_blocks: Vec::new(), + shield_blocking_tick: 0, + }; + + mapped_recipes.push(BedrockRecipe::Shaped(BedrockShapedRecipe { + recipe_id: format!("pumpkin:recipe_{network_id_counter}"), + width: VarInt(width), + height: VarInt(height), + input, + output: vec![output_descriptor], + uuid: [0; 16], + block: "crafting_table".to_string(), + priority: VarInt(1), + assume_symmetry: true, + unlock_requirement: RecipeUnlockRequirement { context: 1 }, + recipe_network_id: VarUInt(network_id_counter), + })); + network_id_counter += 1; + } + } + CraftingRecipeTypes::CraftingShapeless { + category: _, + group: _, + ingredients, + result, + } => { + let input = ingredients.iter().map(map_ingredient).collect::>(); + + let output_item = Item::from_registry_key(result.id); + if let Some(item) = output_item + && let Some(mapping) = + JavaToBedrockItemMapping::from_java_item_id(item.id) + { + let output_descriptor = NetworkItemDescriptor { + id: VarInt::from(mapping.bedrock_item.id), + stack_size: result.count as u16, + aux_value: VarUInt(mapping.bedrock_data), + block_runtime_id: VarInt::from(mapping.bedrock_block_state), + nbt_data: pumpkin_nbt::Nbt::default(), + place_on_blocks: Vec::new(), + destroy_blocks: Vec::new(), + shield_blocking_tick: 0, + }; + + mapped_recipes.push(BedrockRecipe::Shapeless(BedrockShapelessRecipe { + recipe_id: format!("pumpkin:recipe_{network_id_counter}"), + input, + output: vec![output_descriptor], + uuid: [0; 16], + block: "crafting_table".to_string(), + priority: VarInt(1), + unlock_requirement: RecipeUnlockRequirement { context: 1 }, + recipe_network_id: VarUInt(network_id_counter), + })); + network_id_counter += 1; + } + } + _ => {} + } + } + mapped_recipes + }); + + client + .send_game_packet(&pumpkin_protocol::bedrock::client::CCraftingData { + recipes: bedrock_recipes.clone(), + clean_recipes: false, + }) + .await; + client .send_game_packet(&CInventoryContent { container_id: VarUInt(0), // player inventory,