feat: bedrock crafting

This commit is contained in:
Alexander Medvedev 2026-06-27 22:16:21 +02:00
parent 0eb7cb1a79
commit 2f653f6336
7 changed files with 473 additions and 2 deletions

View File

@ -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<W: 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<W: 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<ItemDescriptorCount>,
pub output: Vec<NetworkItemDescriptor>,
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<W: 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<ItemDescriptorCount>,
pub output: Vec<NetworkItemDescriptor>,
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<W: 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<W: 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<BedrockRecipe>,
pub clean_recipes: bool,
}
impl PacketWrite for CCraftingData {
fn write<W: 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(())
}
}

View File

@ -7,6 +7,7 @@ pub mod chunk_radius_update;
pub mod common; pub mod common;
pub mod container_open; pub mod container_open;
pub mod correct_player_move; pub mod correct_player_move;
pub mod crafting_data;
pub mod creative_content; pub mod creative_content;
pub mod disconnect_player; pub mod disconnect_player;
pub mod gamerules_changed; pub mod gamerules_changed;
@ -53,6 +54,7 @@ pub use chunk_radius_update::*;
pub use common::*; pub use common::*;
pub use container_open::*; pub use container_open::*;
pub use correct_player_move::*; pub use correct_player_move::*;
pub use crafting_data::*;
pub use creative_content::*; pub use creative_content::*;
pub use disconnect_player::*; pub use disconnect_player::*;
pub use gamerules_changed::*; pub use gamerules_changed::*;

View File

@ -16,6 +16,7 @@ pub mod modal_form_response;
pub mod player_action; pub mod player_action;
pub mod player_auth_input; pub mod player_auth_input;
pub mod raknet; pub mod raknet;
pub mod request_ability;
pub mod request_chunk_radius; pub mod request_chunk_radius;
pub mod request_network_settings; pub mod request_network_settings;
pub mod resource_pack_response; 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_action::{Action as PlayerActionType, SPlayerAction};
pub use player_auth_input::*; pub use player_auth_input::*;
pub use raknet::*; pub use raknet::*;
pub use request_ability::*;
pub use request_chunk_radius::*; pub use request_chunk_radius::*;
pub use request_network_settings::*; pub use request_network_settings::*;
pub use resource_pack_response::*; pub use resource_pack_response::*;

View File

@ -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<R: Read>(buf: &mut R) -> Result<Self, Error> {
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<R: Read>(buf: &mut R) -> Result<Self, Error> {
let ability = VarInt::read(buf)?;
let value = AbilityValue::read(buf)?;
Ok(Self { ability, value })
}
}

View File

@ -49,6 +49,7 @@ use pumpkin_protocol::{
open_connection::{SOpenConnectionRequest1, SOpenConnectionRequest2}, open_connection::{SOpenConnectionRequest1, SOpenConnectionRequest2},
unconnected_ping::SUnconnectedPing, unconnected_ping::SUnconnectedPing,
}, },
request_ability::SRequestAbility,
request_chunk_radius::SRequestChunkRadius, request_chunk_radius::SRequestChunkRadius,
request_network_settings::SRequestNetworkSettings, request_network_settings::SRequestNetworkSettings,
resource_pack_response::SResourcePackResponse, resource_pack_response::SResourcePackResponse,
@ -1121,6 +1122,10 @@ impl BedrockClient {
self.handle_block_pick_request(player, SBlockPickRequest::read(reader)?) self.handle_block_pick_request(player, SBlockPickRequest::read(reader)?)
.await; .await;
} }
SRequestAbility::PACKET_ID => {
self.handle_request_ability(player, SRequestAbility::read(reader)?)
.await;
}
SMobEquipment::PACKET_ID => { SMobEquipment::PACKET_ID => {
self.handle_mob_equipment(player, SMobEquipment::read(reader)?) self.handle_mob_equipment(player, SMobEquipment::read(reader)?)
.await; .await;

View File

@ -595,8 +595,8 @@ impl BedrockClient {
let target_runtime_id = data.target_entity_runtime_id.0 as i32; let target_runtime_id = data.target_entity_runtime_id.0 as i32;
// TODO: replace with consts, i'm too lazy // TODO: replace with consts, i'm too lazy
match data.action_type.0 { match data.action_type.0 {
// Interact // Interact / Item Interact
0 => { 0 | 2 => {
let world = player.world(); let world = player.world();
if let Some(target) = world.get_entity_by_id(target_runtime_id) { if let Some(target) = world.get_entity_by_id(target_runtime_id) {
let held = player.inventory.held_item(); let held = player.inventory.held_item();
@ -1188,6 +1188,22 @@ impl BedrockClient {
.. ..
} => { } => {
if repetitions > 0 { 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_slot = screen_handler.get_behaviour().slots[0].clone();
let output_stack = output_slot.get_cloned_stack().await; let output_stack = output_slot.get_cloned_stack().await;
@ -1467,6 +1483,36 @@ impl BedrockClient {
let equipment = &[(EquipmentSlot::MAIN_HAND, stack)]; let equipment = &[(EquipmentSlot::MAIN_HAND, stack)];
player.living_entity.send_equipment_changes(equipment); player.living_entity.send_equipment_changes(equipment);
} }
pub async fn handle_request_ability(
&self,
player: &Arc<Player>,
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)] #[allow(clippy::too_many_lines)]

View File

@ -1791,6 +1791,11 @@ impl World {
) { ) {
static CREATIVE_CONTENT: std::sync::OnceLock<(Vec<Group>, Vec<Entry>)> = static CREATIVE_CONTENT: std::sync::OnceLock<(Vec<Group>, Vec<Entry>)> =
std::sync::OnceLock::new(); std::sync::OnceLock::new();
static BEDROCK_CRAFTING_DATA: std::sync::OnceLock<
Vec<pumpkin_protocol::bedrock::client::BedrockRecipe>,
> = std::sync::OnceLock::new();
let level_info = server.level_info.load(); let level_info = server.level_info.load();
let weather = self.weather.lock().await; let weather = self.weather.lock().await;
let runtime_id = player.entity_id() as u64; let runtime_id = player.entity_id() as u64;
@ -2009,6 +2014,185 @@ impl World {
.send_game_packet(&CCreativeContent { groups, entries }) .send_game_packet(&CCreativeContent { groups, entries })
.await; .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::<Vec<_>>();
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 client
.send_game_packet(&CInventoryContent { .send_game_packet(&CInventoryContent {
container_id: VarUInt(0), // player inventory, container_id: VarUInt(0), // player inventory,