mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat: send all crafting recipes to players (#1999)
* feat: send all recipes to players * chore: clippy and added config option
This commit is contained in:
@@ -2,6 +2,7 @@ use fun::FunConfig;
|
||||
use logging::LoggingConfig;
|
||||
use pumpkin_util::world_seed::Seed;
|
||||
use pumpkin_util::{Difficulty, GameMode, PermissionLvl, random};
|
||||
use recipe::RecipeConfig;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
|
||||
use std::net::SocketAddr;
|
||||
@@ -11,6 +12,7 @@ use tracing::{debug, warn};
|
||||
pub mod fun;
|
||||
pub mod logging;
|
||||
pub mod networking;
|
||||
pub mod recipe;
|
||||
|
||||
pub mod resource_pack;
|
||||
|
||||
@@ -70,6 +72,8 @@ pub struct AdvancedConfiguration {
|
||||
pub player_data: PlayerDataConfig,
|
||||
/// Optional fun and experimental features.
|
||||
pub fun: FunConfig,
|
||||
/// Recipe-related configuration.
|
||||
pub recipe: RecipeConfig,
|
||||
}
|
||||
|
||||
/// Basic configuration for core server settings.
|
||||
|
||||
15
pumpkin-config/src/recipe.rs
Normal file
15
pumpkin-config/src/recipe.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Recipe-related configuration.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct RecipeConfig {
|
||||
/// Whether recipes are sent to clients, enabling the recipe book.
|
||||
pub send_recipes: bool,
|
||||
}
|
||||
|
||||
impl Default for RecipeConfig {
|
||||
fn default() -> Self {
|
||||
Self { send_recipes: true }
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ query = []
|
||||
|
||||
[dependencies]
|
||||
pumpkin-nbt.workspace = true
|
||||
pumpkin-data = { workspace = true, features = ["packet", "item_id_remap", "entity_id_remap"] }
|
||||
pumpkin-data = { workspace = true, features = ["packet", "item_id_remap", "entity_id_remap", "recipes"] }
|
||||
pumpkin-macros.workspace = true
|
||||
pumpkin-world.workspace = true
|
||||
pumpkin-util.workspace = true
|
||||
|
||||
@@ -49,6 +49,8 @@ mod player_info_update;
|
||||
mod player_position;
|
||||
mod player_remove;
|
||||
mod player_spawn_position;
|
||||
mod recipe_book_add;
|
||||
mod recipe_book_settings;
|
||||
mod remove_entities;
|
||||
mod remove_mob_effect;
|
||||
mod reset_score;
|
||||
@@ -145,6 +147,8 @@ pub use player_info_update::*;
|
||||
pub use player_position::*;
|
||||
pub use player_remove::*;
|
||||
pub use player_spawn_position::*;
|
||||
pub use recipe_book_add::*;
|
||||
pub use recipe_book_settings::*;
|
||||
pub use remove_entities::*;
|
||||
pub use remove_mob_effect::*;
|
||||
pub use reset_score::*;
|
||||
|
||||
471
pumpkin-protocol/src/java/client/play/recipe_book_add.rs
Normal file
471
pumpkin-protocol/src/java/client/play/recipe_book_add.rs
Normal file
@@ -0,0 +1,471 @@
|
||||
use std::io::Write;
|
||||
|
||||
use pumpkin_data::item::Item;
|
||||
use pumpkin_data::item_id_remap::remap_item_id_for_version;
|
||||
use pumpkin_data::packet::clientbound::PLAY_RECIPE_BOOK_ADD;
|
||||
use pumpkin_data::recipes::{
|
||||
CookingRecipeType, CraftingRecipeTypes, RECIPES_COOKING, RECIPES_CRAFTING, RecipeCategoryTypes,
|
||||
RecipeIngredientTypes, RecipeResultStruct,
|
||||
};
|
||||
use pumpkin_macros::java_packet;
|
||||
use pumpkin_util::version::MinecraftVersion;
|
||||
|
||||
use crate::{ClientPacket, VarInt, WritingError, ser::NetworkWriteExt};
|
||||
|
||||
// Recipe Display type IDs
|
||||
const RECIPE_DISPLAY_SHAPELESS: i32 = 0;
|
||||
const RECIPE_DISPLAY_SHAPED: i32 = 1;
|
||||
const RECIPE_DISPLAY_FURNACE: i32 = 2;
|
||||
|
||||
// Slot Display type IDs
|
||||
const SLOT_DISPLAY_EMPTY: i32 = 0;
|
||||
const SLOT_DISPLAY_ANY_FUEL: i32 = 1;
|
||||
const SLOT_DISPLAY_ITEM: i32 = 4;
|
||||
const SLOT_DISPLAY_COMPOSITE: i32 = 10;
|
||||
|
||||
// RecipeBookCategory IDs
|
||||
const CATEGORY_CRAFTING_BUILDING: i32 = 0;
|
||||
const CATEGORY_CRAFTING_REDSTONE: i32 = 1;
|
||||
const CATEGORY_CRAFTING_EQUIPMENT: i32 = 2;
|
||||
const CATEGORY_CRAFTING_MISC: i32 = 3;
|
||||
const CATEGORY_FURNACE_FOOD: i32 = 4;
|
||||
const CATEGORY_FURNACE_BLOCKS: i32 = 5;
|
||||
const CATEGORY_FURNACE_MISC: i32 = 6;
|
||||
const CATEGORY_BLAST_FURNACE_BLOCKS: i32 = 7;
|
||||
const CATEGORY_BLAST_FURNACE_MISC: i32 = 8;
|
||||
const CATEGORY_SMOKER_FOOD: i32 = 9;
|
||||
const CATEGORY_CAMPFIRE: i32 = 12;
|
||||
|
||||
/// Clientbound packet that adds recipes to the client's recipe book.
|
||||
/// `replace = true` means the client replaces its current recipe list.
|
||||
#[java_packet(PLAY_RECIPE_BOOK_ADD)]
|
||||
pub struct CRecipeBookAdd {
|
||||
pub replace: bool,
|
||||
}
|
||||
|
||||
impl CRecipeBookAdd {
|
||||
#[must_use]
|
||||
pub const fn new(replace: bool) -> Self {
|
||||
Self { replace }
|
||||
}
|
||||
}
|
||||
|
||||
fn item_id_versioned(item: &Item, version: MinecraftVersion) -> i32 {
|
||||
remap_item_id_for_version(item.id, version) as i32
|
||||
}
|
||||
|
||||
fn write_item_slot_display(
|
||||
write: &mut impl Write,
|
||||
item: &Item,
|
||||
version: MinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
write.write_var_int(&VarInt(SLOT_DISPLAY_ITEM))?;
|
||||
write.write_var_int(&VarInt(item_id_versioned(item, version)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_empty_slot_display(write: &mut impl Write) -> Result<(), WritingError> {
|
||||
write.write_var_int(&VarInt(SLOT_DISPLAY_EMPTY))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_any_fuel_slot_display(write: &mut impl Write) -> Result<(), WritingError> {
|
||||
write.write_var_int(&VarInt(SLOT_DISPLAY_ANY_FUEL))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_ingredient_slot_display(
|
||||
write: &mut impl Write,
|
||||
ingredient: &RecipeIngredientTypes,
|
||||
version: MinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
match ingredient {
|
||||
RecipeIngredientTypes::Simple(id) => {
|
||||
let key = id.strip_prefix("minecraft:").unwrap_or(id);
|
||||
if let Some(item) = Item::from_registry_key(key) {
|
||||
write_item_slot_display(write, item, version)?;
|
||||
} else {
|
||||
write_empty_slot_display(write)?;
|
||||
}
|
||||
}
|
||||
RecipeIngredientTypes::Tagged(_tag) => {
|
||||
// TODO: We lack registry access here to resolve tags to a TagSlotDisplay.
|
||||
// Sending an empty slot prevents a client DecoderException, but will
|
||||
// result in invisible ingredients in the recipe book.
|
||||
write_empty_slot_display(write)?;
|
||||
}
|
||||
RecipeIngredientTypes::OneOf(ids) => {
|
||||
let mut items: Vec<&Item> = Vec::new();
|
||||
for id in *ids {
|
||||
let key = id.strip_prefix("minecraft:").unwrap_or(id);
|
||||
if let Some(item) = Item::from_registry_key(key) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
if items.is_empty() {
|
||||
write_empty_slot_display(write)?;
|
||||
} else if items.len() == 1 {
|
||||
write_item_slot_display(write, items[0], version)?;
|
||||
} else {
|
||||
write.write_var_int(&VarInt(SLOT_DISPLAY_COMPOSITE))?;
|
||||
write.write_var_int(&VarInt(items.len() as i32))?;
|
||||
for item in &items {
|
||||
write_item_slot_display(write, item, version)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a single Ingredient as a `HolderSet`<Item> for craftingRequirements.
|
||||
///
|
||||
/// Vanilla wire format for `ByteBufCodecs.holderSet(Registries.ITEM)`:
|
||||
/// VarInt(0) -> named tag reference (followed by `ResourceLocation`)
|
||||
/// VarInt(n + 1) -> direct list of n item IDs
|
||||
///
|
||||
/// So an empty/absent ingredient writes VarInt(1), one item writes VarInt(2) + id, etc.
|
||||
fn write_ingredient_holderset(
|
||||
write: &mut impl Write,
|
||||
ingredient: Option<&RecipeIngredientTypes>,
|
||||
version: MinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
match ingredient {
|
||||
// Empty ingredient slot -> direct list of 0 items -> VarInt(0 + 1) = VarInt(1)
|
||||
None => {
|
||||
write.write_var_int(&VarInt(1))?;
|
||||
}
|
||||
Some(RecipeIngredientTypes::Simple(id)) => {
|
||||
let key = id.strip_prefix("minecraft:").unwrap_or(id);
|
||||
if let Some(item) = Item::from_registry_key(key) {
|
||||
// 1 item -> VarInt(1 + 1) = VarInt(2)
|
||||
write.write_var_int(&VarInt(2))?;
|
||||
write.write_var_int(&VarInt(item_id_versioned(item, version)))?;
|
||||
} else {
|
||||
// Item not found -> empty direct list
|
||||
write.write_var_int(&VarInt(1))?;
|
||||
}
|
||||
}
|
||||
Some(RecipeIngredientTypes::Tagged(_tag)) => {
|
||||
// No current recipes use Tagged; write empty direct list.
|
||||
write.write_var_int(&VarInt(1))?;
|
||||
}
|
||||
Some(RecipeIngredientTypes::OneOf(ids)) => {
|
||||
let items: Vec<i32> = ids
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let key = id.strip_prefix("minecraft:").unwrap_or(id);
|
||||
Item::from_registry_key(key).map(|item| item_id_versioned(item, version))
|
||||
})
|
||||
.collect();
|
||||
// n items -> VarInt(n + 1)
|
||||
write.write_var_int(&VarInt(items.len() as i32 + 1))?;
|
||||
for id in &items {
|
||||
write.write_var_int(&VarInt(*id))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write the `craftingRequirements: Option<List<Ingredient>>` field (present).
|
||||
/// Each slot is either `None` (empty grid cell) or `Some(ingredient)`.
|
||||
fn write_crafting_requirements(
|
||||
write: &mut impl Write,
|
||||
slots: &[Option<&RecipeIngredientTypes>],
|
||||
version: MinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
write.write_bool(true)?; // present
|
||||
write.write_var_int(&VarInt(slots.len() as i32))?;
|
||||
for slot in slots {
|
||||
write_ingredient_holderset(write, *slot, version)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_result_slot_display(
|
||||
write: &mut impl Write,
|
||||
result: &RecipeResultStruct,
|
||||
version: MinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
let key = result.id.strip_prefix("minecraft:").unwrap_or(result.id);
|
||||
if let Some(item) = Item::from_registry_key(key) {
|
||||
write_item_slot_display(write, item, version)?;
|
||||
} else {
|
||||
write_empty_slot_display(write)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const fn crafting_category(cat: &RecipeCategoryTypes) -> i32 {
|
||||
match cat {
|
||||
RecipeCategoryTypes::Equipment => CATEGORY_CRAFTING_EQUIPMENT,
|
||||
RecipeCategoryTypes::Building | RecipeCategoryTypes::Blocks => CATEGORY_CRAFTING_BUILDING,
|
||||
RecipeCategoryTypes::Restone => CATEGORY_CRAFTING_REDSTONE,
|
||||
RecipeCategoryTypes::Food | RecipeCategoryTypes::Misc => CATEGORY_CRAFTING_MISC,
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a single `RecipeDisplayEntry` + flags byte.
|
||||
/// Returns `Ok(true)` if written, `Ok(false)` if skipped (special recipe).
|
||||
#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
|
||||
fn write_entry(
|
||||
write: &mut impl Write,
|
||||
display_id: i32,
|
||||
version: MinecraftVersion,
|
||||
crafting_table: &Item,
|
||||
furnace: &Item,
|
||||
blast_furnace: &Item,
|
||||
smoker: &Item,
|
||||
campfire: &Item,
|
||||
crafting_recipe: Option<&CraftingRecipeTypes>,
|
||||
cooking_recipe: Option<(&CookingRecipeType, i32)>,
|
||||
) -> Result<bool, WritingError> {
|
||||
if let Some(recipe) = crafting_recipe {
|
||||
match recipe {
|
||||
CraftingRecipeTypes::CraftingShaped {
|
||||
category,
|
||||
pattern,
|
||||
key,
|
||||
result,
|
||||
..
|
||||
} => {
|
||||
// Compute width and height from pattern
|
||||
let height = pattern.len() as i32;
|
||||
let width = pattern.first().map_or(0, |r| r.len()) as i32;
|
||||
|
||||
// RecipeDisplayId
|
||||
write.write_var_int(&VarInt(display_id))?;
|
||||
// RecipeDisplay type = shaped (1)
|
||||
write.write_var_int(&VarInt(RECIPE_DISPLAY_SHAPED))?;
|
||||
// width, height
|
||||
write.write_var_int(&VarInt(width))?;
|
||||
write.write_var_int(&VarInt(height))?;
|
||||
// ingredients: flat list, row by row
|
||||
write.write_var_int(&VarInt(width * height))?;
|
||||
for row in *pattern {
|
||||
for ch in row.chars() {
|
||||
if ch == ' ' {
|
||||
write_empty_slot_display(write)?;
|
||||
} else if let Some((_, ingredient)) = key.iter().find(|(k, _)| *k == ch) {
|
||||
write_ingredient_slot_display(write, ingredient, version)?;
|
||||
} else {
|
||||
write_empty_slot_display(write)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// result
|
||||
write_result_slot_display(write, result, version)?;
|
||||
// craftingStation
|
||||
write_item_slot_display(write, crafting_table, version)?;
|
||||
// group: absent
|
||||
write.write_bool(false)?;
|
||||
// category
|
||||
write.write_var_int(&VarInt(crafting_category(category)))?;
|
||||
// craftingRequirements: one HolderSet per non-empty grid slot
|
||||
// (Ingredient cannot be empty, so empty slots must be excluded)
|
||||
{
|
||||
let mut slots: Vec<Option<&RecipeIngredientTypes>> = Vec::new();
|
||||
for row in *pattern {
|
||||
for ch in row.chars() {
|
||||
if ch != ' '
|
||||
&& let Some((_, ing)) = key.iter().find(|(k, _)| *k == ch)
|
||||
{
|
||||
slots.push(Some(ing));
|
||||
}
|
||||
}
|
||||
}
|
||||
write_crafting_requirements(write, &slots, version)?;
|
||||
};
|
||||
// flags: 0 (no notification, no highlight)
|
||||
write.write_u8(0)?;
|
||||
}
|
||||
CraftingRecipeTypes::CraftingShapeless {
|
||||
category,
|
||||
ingredients,
|
||||
result,
|
||||
..
|
||||
} => {
|
||||
// RecipeDisplayId
|
||||
write.write_var_int(&VarInt(display_id))?;
|
||||
// RecipeDisplay type = shapeless (0)
|
||||
write.write_var_int(&VarInt(RECIPE_DISPLAY_SHAPELESS))?;
|
||||
// ingredients list
|
||||
write.write_var_int(&VarInt(ingredients.len() as i32))?;
|
||||
for ing in *ingredients {
|
||||
write_ingredient_slot_display(write, ing, version)?;
|
||||
}
|
||||
// result
|
||||
write_result_slot_display(write, result, version)?;
|
||||
// craftingStation
|
||||
write_item_slot_display(write, crafting_table, version)?;
|
||||
// group: absent
|
||||
write.write_bool(false)?;
|
||||
// category
|
||||
write.write_var_int(&VarInt(crafting_category(category)))?;
|
||||
// craftingRequirements: one HolderSet per ingredient
|
||||
{
|
||||
let slots: Vec<Option<&RecipeIngredientTypes>> =
|
||||
ingredients.iter().map(Some).collect();
|
||||
write_crafting_requirements(write, &slots, version)?;
|
||||
};
|
||||
// flags
|
||||
write.write_u8(0)?;
|
||||
}
|
||||
CraftingRecipeTypes::CraftingTransmute {
|
||||
category,
|
||||
input,
|
||||
material,
|
||||
result,
|
||||
..
|
||||
} => {
|
||||
// Transmute shown as shapeless with 2 ingredients
|
||||
write.write_var_int(&VarInt(display_id))?;
|
||||
write.write_var_int(&VarInt(RECIPE_DISPLAY_SHAPELESS))?;
|
||||
// 2 ingredients
|
||||
write.write_var_int(&VarInt(2))?;
|
||||
write_ingredient_slot_display(write, input, version)?;
|
||||
write_ingredient_slot_display(write, material, version)?;
|
||||
write_result_slot_display(write, result, version)?;
|
||||
write_item_slot_display(write, crafting_table, version)?;
|
||||
write.write_bool(false)?;
|
||||
write.write_var_int(&VarInt(crafting_category(category)))?;
|
||||
// craftingRequirements: input + material
|
||||
write_crafting_requirements(write, &[Some(input), Some(material)], version)?;
|
||||
write.write_u8(0)?;
|
||||
}
|
||||
// Skip special/decorated_pot recipes as they have no useful display
|
||||
CraftingRecipeTypes::CraftingDecoratedPot { .. }
|
||||
| CraftingRecipeTypes::CraftingSpecial => {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if let Some((recipe, book_category)) = cooking_recipe {
|
||||
let (cooking, station) = match recipe {
|
||||
CookingRecipeType::Smelting(r) => (r, furnace),
|
||||
CookingRecipeType::Blasting(r) => (r, blast_furnace),
|
||||
CookingRecipeType::Smoking(r) => (r, smoker),
|
||||
CookingRecipeType::CampfireCooking(r) => (r, campfire),
|
||||
};
|
||||
|
||||
write.write_var_int(&VarInt(display_id))?;
|
||||
// RecipeDisplay type = furnace (2)
|
||||
write.write_var_int(&VarInt(RECIPE_DISPLAY_FURNACE))?;
|
||||
// ingredient
|
||||
write_ingredient_slot_display(write, &cooking.ingredient, version)?;
|
||||
// fuel: AnyFuel
|
||||
write_any_fuel_slot_display(write)?;
|
||||
// result
|
||||
write_result_slot_display(write, &cooking.result, version)?;
|
||||
// craftingStation
|
||||
write_item_slot_display(write, station, version)?;
|
||||
// duration
|
||||
write.write_var_int(&VarInt(cooking.cookingtime))?;
|
||||
// experience
|
||||
write.write_f32_be(cooking.experience)?;
|
||||
// group: absent
|
||||
write.write_bool(false)?;
|
||||
// category
|
||||
write.write_var_int(&VarInt(book_category))?;
|
||||
// craftingRequirements: the single ingredient
|
||||
write_crafting_requirements(write, &[Some(&cooking.ingredient)], version)?;
|
||||
// flags
|
||||
write.write_u8(0)?;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
impl ClientPacket for CRecipeBookAdd {
|
||||
fn write_packet_data(
|
||||
&self,
|
||||
write: impl Write,
|
||||
version: &MinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
let mut write = write;
|
||||
|
||||
// Station items (these IDs are stable across all versions we support)
|
||||
let crafting_table =
|
||||
Item::from_registry_key("crafting_table").expect("crafting_table item must exist");
|
||||
let furnace = Item::from_registry_key("furnace").expect("furnace item must exist");
|
||||
let blast_furnace =
|
||||
Item::from_registry_key("blast_furnace").expect("blast_furnace item must exist");
|
||||
let smoker = Item::from_registry_key("smoker").expect("smoker item must exist");
|
||||
let campfire = Item::from_registry_key("campfire").expect("campfire item must exist");
|
||||
|
||||
// First pass - count and skip CraftingSpecial and CraftingDecoratedPot entries.
|
||||
let crafting_count: usize = RECIPES_CRAFTING
|
||||
.iter()
|
||||
.filter(|r| {
|
||||
!matches!(
|
||||
r,
|
||||
CraftingRecipeTypes::CraftingSpecial
|
||||
| CraftingRecipeTypes::CraftingDecoratedPot { .. }
|
||||
)
|
||||
})
|
||||
.count();
|
||||
let total = crafting_count + RECIPES_COOKING.len();
|
||||
|
||||
// Entry count (VarInt)
|
||||
write.write_var_int(&VarInt(total as i32))?;
|
||||
|
||||
let mut display_id: i32 = 0;
|
||||
|
||||
// Write crafting recipes
|
||||
for recipe in RECIPES_CRAFTING {
|
||||
let written = write_entry(
|
||||
&mut write,
|
||||
display_id,
|
||||
*version,
|
||||
crafting_table,
|
||||
furnace,
|
||||
blast_furnace,
|
||||
smoker,
|
||||
campfire,
|
||||
Some(recipe),
|
||||
None,
|
||||
)?;
|
||||
if written {
|
||||
display_id += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Write cooking recipes
|
||||
for recipe in RECIPES_COOKING {
|
||||
let book_category = match recipe {
|
||||
CookingRecipeType::Smelting(r) => match r.category {
|
||||
RecipeCategoryTypes::Food => CATEGORY_FURNACE_FOOD,
|
||||
RecipeCategoryTypes::Blocks => CATEGORY_FURNACE_BLOCKS,
|
||||
_ => CATEGORY_FURNACE_MISC,
|
||||
},
|
||||
CookingRecipeType::Blasting(r) => match r.category {
|
||||
RecipeCategoryTypes::Blocks => CATEGORY_BLAST_FURNACE_BLOCKS,
|
||||
_ => CATEGORY_BLAST_FURNACE_MISC,
|
||||
},
|
||||
CookingRecipeType::Smoking(_) => CATEGORY_SMOKER_FOOD,
|
||||
CookingRecipeType::CampfireCooking(_) => CATEGORY_CAMPFIRE,
|
||||
};
|
||||
write_entry(
|
||||
&mut write,
|
||||
display_id,
|
||||
*version,
|
||||
crafting_table,
|
||||
furnace,
|
||||
blast_furnace,
|
||||
smoker,
|
||||
campfire,
|
||||
None,
|
||||
Some((recipe, book_category)),
|
||||
)?;
|
||||
display_id += 1;
|
||||
}
|
||||
|
||||
// replace flag
|
||||
write.write_bool(self.replace)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use std::io::Write;
|
||||
|
||||
use pumpkin_data::packet::clientbound::PLAY_RECIPE_BOOK_SETTINGS;
|
||||
use pumpkin_macros::java_packet;
|
||||
use pumpkin_util::version::MinecraftVersion;
|
||||
|
||||
use crate::{ClientPacket, WritingError, ser::NetworkWriteExt};
|
||||
|
||||
/// Sent by the server to update the player's recipe book open/filter state.
|
||||
///
|
||||
/// Wire format: 4 `TypeSettings` pairs (crafting, furnace, `blast_furnace`, smoker),
|
||||
/// each pair is (`is_open`: bool, `is_filtering`: bool).
|
||||
#[java_packet(PLAY_RECIPE_BOOK_SETTINGS)]
|
||||
pub struct CRecipeBookSettings {
|
||||
pub crafting_open: bool,
|
||||
pub crafting_filtering: bool,
|
||||
pub furnace_open: bool,
|
||||
pub furnace_filtering: bool,
|
||||
pub blast_furnace_open: bool,
|
||||
pub blast_furnace_filtering: bool,
|
||||
pub smoker_open: bool,
|
||||
pub smoker_filtering: bool,
|
||||
}
|
||||
|
||||
impl CRecipeBookSettings {
|
||||
#[must_use]
|
||||
pub const fn default_closed() -> Self {
|
||||
Self {
|
||||
crafting_open: false,
|
||||
crafting_filtering: false,
|
||||
furnace_open: false,
|
||||
furnace_filtering: false,
|
||||
blast_furnace_open: false,
|
||||
blast_furnace_filtering: false,
|
||||
smoker_open: false,
|
||||
smoker_filtering: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientPacket for CRecipeBookSettings {
|
||||
fn write_packet_data(
|
||||
&self,
|
||||
write: impl Write,
|
||||
_version: &MinecraftVersion,
|
||||
) -> Result<(), WritingError> {
|
||||
let mut write = write;
|
||||
write.write_bool(self.crafting_open)?;
|
||||
write.write_bool(self.crafting_filtering)?;
|
||||
write.write_bool(self.furnace_open)?;
|
||||
write.write_bool(self.furnace_filtering)?;
|
||||
write.write_bool(self.blast_furnace_open)?;
|
||||
write.write_bool(self.blast_furnace_filtering)?;
|
||||
write.write_bool(self.smoker_open)?;
|
||||
write.write_bool(self.smoker_filtering)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ mod move_vehicle;
|
||||
mod paddle_boat;
|
||||
mod pick_item;
|
||||
mod ping_request;
|
||||
mod place_recipe;
|
||||
mod player_abilities;
|
||||
mod player_action;
|
||||
mod player_command;
|
||||
@@ -28,6 +29,8 @@ mod player_position;
|
||||
mod player_position_rotation;
|
||||
mod player_rotation;
|
||||
mod player_session;
|
||||
mod recipe_book_change_settings;
|
||||
mod recipe_book_seen_recipe;
|
||||
mod set_command_block;
|
||||
mod set_creative_slot;
|
||||
mod set_held_item;
|
||||
@@ -56,6 +59,7 @@ pub use move_vehicle::*;
|
||||
pub use paddle_boat::*;
|
||||
pub use pick_item::*;
|
||||
pub use ping_request::*;
|
||||
pub use place_recipe::*;
|
||||
pub use player_abilities::*;
|
||||
pub use player_action::*;
|
||||
pub use player_command::*;
|
||||
@@ -66,6 +70,8 @@ pub use player_position::*;
|
||||
pub use player_position_rotation::*;
|
||||
pub use player_rotation::*;
|
||||
pub use player_session::*;
|
||||
pub use recipe_book_change_settings::*;
|
||||
pub use recipe_book_seen_recipe::*;
|
||||
pub use set_command_block::*;
|
||||
pub use set_creative_slot::*;
|
||||
pub use set_held_item::*;
|
||||
|
||||
13
pumpkin-protocol/src/java/server/play/place_recipe.rs
Normal file
13
pumpkin-protocol/src/java/server/play/place_recipe.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use pumpkin_data::packet::serverbound::PLAY_PLACE_RECIPE;
|
||||
use pumpkin_macros::java_packet;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::VarInt;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[java_packet(PLAY_PLACE_RECIPE)]
|
||||
pub struct SPlaceRecipe {
|
||||
pub container_id: i8,
|
||||
pub recipe_display_id: VarInt,
|
||||
pub use_max_items: bool,
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use pumpkin_data::packet::serverbound::PLAY_RECIPE_BOOK_CHANGE_SETTINGS;
|
||||
use pumpkin_macros::java_packet;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::VarInt;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[java_packet(PLAY_RECIPE_BOOK_CHANGE_SETTINGS)]
|
||||
pub struct SRecipeBookChangeSettings {
|
||||
pub book_type: VarInt,
|
||||
pub is_open: bool,
|
||||
pub is_filtering: bool,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
use pumpkin_data::packet::serverbound::PLAY_RECIPE_BOOK_SEEN_RECIPE;
|
||||
use pumpkin_macros::java_packet;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::VarInt;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[java_packet(PLAY_RECIPE_BOOK_SEEN_RECIPE)]
|
||||
pub struct SRecipeBookSeenRecipe {
|
||||
pub recipe_display_id: VarInt,
|
||||
}
|
||||
@@ -9,10 +9,11 @@ use pumpkin_protocol::java::server::play::{
|
||||
SAttack, SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClickSlot, SClientCommand,
|
||||
SClientInformationPlay, SClientTickEnd, SCloseContainer, SCommandSuggestion, SConfirmTeleport,
|
||||
SCookieResponse as SPCookieResponse, SCustomPayload, SInteract, SKeepAlive, SMoveVehicle,
|
||||
SPaddleBoat, SPickItemFromBlock, SPlayPingRequest, SPlayerAbilities, SPlayerAction,
|
||||
SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition, SPlayerPositionRotation,
|
||||
SPlayerRotation, SPlayerSession, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem,
|
||||
SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem, SUseItemOn,
|
||||
SPaddleBoat, SPickItemFromBlock, SPlaceRecipe, SPlayPingRequest, SPlayerAbilities,
|
||||
SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition,
|
||||
SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings,
|
||||
SRecipeBookSeenRecipe, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, SSetPlayerGround,
|
||||
SSwingArm, SUpdateSign, SUseItem, SUseItemOn,
|
||||
};
|
||||
use pumpkin_protocol::packet::MultiVersionJavaPacket;
|
||||
use pumpkin_protocol::{
|
||||
@@ -856,6 +857,24 @@ impl JavaClient {
|
||||
);
|
||||
server.plugin_manager.fire(event).await;
|
||||
}
|
||||
id if id == SRecipeBookChangeSettings::to_id(version) => {
|
||||
self.handle_recipe_book_change_settings(
|
||||
player,
|
||||
SRecipeBookChangeSettings::read(payload, &version)?,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
id if id == SRecipeBookSeenRecipe::to_id(version) => {
|
||||
self.handle_recipe_book_seen_recipe(
|
||||
player,
|
||||
SRecipeBookSeenRecipe::read(payload, &version)?,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
id if id == SPlaceRecipe::to_id(version) => {
|
||||
self.handle_place_recipe(player, SPlaceRecipe::read(payload, &version)?)
|
||||
.await;
|
||||
}
|
||||
_ => {
|
||||
warn!("Failed to handle player packet id {}", packet.id);
|
||||
}
|
||||
|
||||
@@ -55,10 +55,11 @@ use pumpkin_protocol::java::server::play::{
|
||||
Action, ActionType, CommandBlockMode, FLAG_ON_GROUND, SAttack, SChangeGameMode, SChatCommand,
|
||||
SChatMessage, SChunkBatch, SClientCommand, SClientInformationPlay, SCloseContainer,
|
||||
SCommandSuggestion, SConfirmTeleport, SCookieResponse as SPCookieResponse, SInteract,
|
||||
SKeepAlive, SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPlayPingRequest, SPlayerAbilities,
|
||||
SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerPosition, SPlayerPositionRotation,
|
||||
SPlayerRotation, SPlayerSession, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem,
|
||||
SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem, SUseItemOn, Status,
|
||||
SKeepAlive, SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPlaceRecipe, SPlayPingRequest,
|
||||
SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerPosition,
|
||||
SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings,
|
||||
SRecipeBookSeenRecipe, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, SSetPlayerGround,
|
||||
SSwingArm, SUpdateSign, SUseItem, SUseItemOn, Status,
|
||||
};
|
||||
use pumpkin_util::math::boundingbox::BoundingBox;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
@@ -267,7 +268,7 @@ impl JavaClient {
|
||||
let delta = Vector3::new(pos.x - last_pos.x, pos.y - last_pos.y, pos.z - last_pos.z);
|
||||
let entity_id = player.entity_id();
|
||||
|
||||
// Teleport when more than 8 blocks (-8..=7.999755859375) (checking 8²)
|
||||
// Teleport when more than 8 blocks (-8..=7.999755859375)
|
||||
if delta.length_squared() < 64.0 {
|
||||
return false;
|
||||
}
|
||||
@@ -855,6 +856,300 @@ impl JavaClient {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
pub async fn handle_recipe_book_change_settings(
|
||||
&self,
|
||||
_player: &Arc<Player>,
|
||||
_packet: SRecipeBookChangeSettings,
|
||||
) {
|
||||
// Client is updating its recipe book filter/open state; no server action needed.
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
pub async fn handle_recipe_book_seen_recipe(
|
||||
&self,
|
||||
_player: &Arc<Player>,
|
||||
_packet: SRecipeBookSeenRecipe,
|
||||
) {
|
||||
// Client acknowledged a recipe display; no server action needed.
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn handle_place_recipe(&self, player: &Arc<Player>, packet: SPlaceRecipe) {
|
||||
use pumpkin_data::recipes::{
|
||||
CookingRecipeType, CraftingRecipeTypes, RECIPES_COOKING, RECIPES_CRAFTING,
|
||||
RecipeIngredientTypes,
|
||||
};
|
||||
use pumpkin_data::screen::WindowType;
|
||||
|
||||
// Take `amount` items matching `ingredient` from inventory, committing to one item type.
|
||||
async fn take_n_ingredient(
|
||||
inventory: &PlayerInventory,
|
||||
ingredient: &RecipeIngredientTypes,
|
||||
amount: u8,
|
||||
) -> ItemStack {
|
||||
let mut remaining = amount;
|
||||
let mut result: Option<ItemStack> = None;
|
||||
for slot in &inventory.main_inventory {
|
||||
if remaining == 0 {
|
||||
break;
|
||||
}
|
||||
let mut stack = slot.lock().await;
|
||||
if stack.is_empty() || !ingredient.match_item(stack.item) {
|
||||
continue;
|
||||
}
|
||||
// Commit to the first item type encountered.
|
||||
if result.as_ref().is_some_and(|r| r.item.id != stack.item.id) {
|
||||
continue;
|
||||
}
|
||||
let take = remaining.min(stack.item_count);
|
||||
let taken = stack.split(take);
|
||||
remaining -= taken.item_count;
|
||||
match &mut result {
|
||||
None => result = Some(taken),
|
||||
Some(r) => r.increment(taken.item_count),
|
||||
}
|
||||
}
|
||||
result.unwrap_or_else(|| ItemStack::EMPTY.clone())
|
||||
}
|
||||
|
||||
// Compute the maximum number of times all ingredients can be supplied from inventory.
|
||||
// Each ingredient slot commits to one item type (no mixing within a slot).
|
||||
async fn compute_biggest_craftable(
|
||||
ingredients: &[&RecipeIngredientTypes],
|
||||
inventory: &PlayerInventory,
|
||||
) -> u8 {
|
||||
// Aggregate inventory by item id -> (item, count).
|
||||
let mut available: Vec<(&'static pumpkin_data::item::Item, u32)> = Vec::new();
|
||||
for slot in &inventory.main_inventory {
|
||||
let stack = slot.lock().await;
|
||||
if !stack.is_empty() {
|
||||
if let Some(e) = available.iter_mut().find(|(i, _)| i.id == stack.item.id) {
|
||||
e.1 += u32::from(stack.item_count);
|
||||
} else {
|
||||
available.push((stack.item, u32::from(stack.item_count)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// From 64 down to 1, find the highest amount where every slot can be satisfied.
|
||||
'outer: for amount in (1u32..=64).rev() {
|
||||
let mut budget = available.clone();
|
||||
for ing in ingredients {
|
||||
// Pick the first item type that satisfies this slot and has enough.
|
||||
let Some(idx) = budget
|
||||
.iter()
|
||||
.position(|(item, count)| *count >= amount && ing.match_item(item))
|
||||
else {
|
||||
continue 'outer;
|
||||
};
|
||||
budget[idx].1 -= amount;
|
||||
}
|
||||
return amount as u8;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
let target_id = packet.recipe_display_id.0 as usize;
|
||||
let use_max = packet.use_max_items;
|
||||
|
||||
// Count crafting display IDs.
|
||||
let crafting_display_count = RECIPES_CRAFTING
|
||||
.iter()
|
||||
.filter(|r| {
|
||||
!matches!(
|
||||
r,
|
||||
CraftingRecipeTypes::CraftingSpecial
|
||||
| CraftingRecipeTypes::CraftingDecoratedPot { .. }
|
||||
)
|
||||
})
|
||||
.count();
|
||||
|
||||
let screen_handler_arc = player.current_screen_handler.lock().await.clone();
|
||||
let mut handler = screen_handler_arc.lock().await;
|
||||
|
||||
if target_id < crafting_display_count {
|
||||
// Crafting recipe
|
||||
let mut counter = 0usize;
|
||||
let recipe = RECIPES_CRAFTING.iter().find(|r| {
|
||||
if matches!(
|
||||
r,
|
||||
CraftingRecipeTypes::CraftingSpecial
|
||||
| CraftingRecipeTypes::CraftingDecoratedPot { .. }
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
let found = counter == target_id;
|
||||
counter += 1;
|
||||
found
|
||||
});
|
||||
let Some(recipe) = recipe else { return };
|
||||
|
||||
let grid_width: usize = match handler.window_type() {
|
||||
Some(WindowType::Crafting) => 3,
|
||||
None => 2, // player inventory 2x2
|
||||
_ => return,
|
||||
};
|
||||
let grid_size = grid_width * grid_width;
|
||||
|
||||
// Map each grid position to its required ingredient (None = empty/unused).
|
||||
let mut ingredient_slots: Vec<Option<&RecipeIngredientTypes>> = vec![None; grid_size];
|
||||
match recipe {
|
||||
CraftingRecipeTypes::CraftingShaped { pattern, key, .. } => {
|
||||
for (row, row_str) in pattern.iter().enumerate() {
|
||||
for (col, ch) in row_str.chars().enumerate() {
|
||||
if ch != ' '
|
||||
&& let Some(ing) =
|
||||
key.iter().find_map(|(k, v)| (*k == ch).then_some(v))
|
||||
{
|
||||
ingredient_slots[row * grid_width + col] = Some(ing);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CraftingRecipeTypes::CraftingShapeless { ingredients, .. } => {
|
||||
for (i, ing) in ingredients.iter().enumerate().take(grid_size) {
|
||||
ingredient_slots[i] = Some(ing);
|
||||
}
|
||||
}
|
||||
CraftingRecipeTypes::CraftingTransmute {
|
||||
input, material, ..
|
||||
} => {
|
||||
ingredient_slots[0] = Some(input);
|
||||
ingredient_slots[1] = Some(material);
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
|
||||
let crafting_inv = handler.get_behaviour().slots[1].get_inventory();
|
||||
|
||||
// Check if this exact recipe is already placed (determines stacking vs fresh fill).
|
||||
let recipe_matches = {
|
||||
let mut ok = true;
|
||||
for (idx, ing) in ingredient_slots.iter().enumerate() {
|
||||
let slot_arc = crafting_inv.get_stack(idx).await;
|
||||
let stack = slot_arc.lock().await;
|
||||
match ing {
|
||||
None => {
|
||||
if !stack.is_empty() {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(ingredient) => {
|
||||
if stack.is_empty() || !ingredient.match_item(stack.item) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ok
|
||||
};
|
||||
|
||||
// Read minimum count from occupied slots before clearing (needed for stacking).
|
||||
let current_min = if recipe_matches && !use_max {
|
||||
let mut min = u8::MAX;
|
||||
for (idx, ing) in ingredient_slots.iter().enumerate() {
|
||||
if ing.is_some() {
|
||||
let slot_arc = crafting_inv.get_stack(idx).await;
|
||||
let stack = slot_arc.lock().await;
|
||||
if !stack.is_empty() {
|
||||
min = min.min(stack.item_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
if min == u8::MAX { 0 } else { min }
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Always clear the grid first, returning items to inventory.
|
||||
for i in 0..grid_size {
|
||||
let stack = crafting_inv.remove_stack(i).await;
|
||||
if !stack.is_empty() {
|
||||
player.inventory.offer(stack, false, player.as_ref()).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine how many of each ingredient to place per slot.
|
||||
let active_ingredients: Vec<&RecipeIngredientTypes> =
|
||||
ingredient_slots.iter().flatten().copied().collect();
|
||||
let amount_to_craft = if use_max {
|
||||
compute_biggest_craftable(&active_ingredients, &player.inventory).await
|
||||
} else if recipe_matches {
|
||||
current_min.saturating_add(1)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
if amount_to_craft == 0 {
|
||||
handler.send_content_updates().await;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill each grid slot with exactly `amount_to_craft` matching items.
|
||||
for (idx, ing) in ingredient_slots.iter().enumerate() {
|
||||
let Some(ingredient) = ing else { continue };
|
||||
let taken = take_n_ingredient(&player.inventory, ingredient, amount_to_craft).await;
|
||||
if !taken.is_empty() {
|
||||
crafting_inv.set_stack(idx, taken).await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Cooking recipe
|
||||
let cooking_index = target_id - crafting_display_count;
|
||||
let Some(recipe) = RECIPES_COOKING.get(cooking_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match handler.window_type() {
|
||||
Some(WindowType::Furnace | WindowType::BlastFurnace | WindowType::Smoker) => {}
|
||||
_ => return,
|
||||
}
|
||||
|
||||
let ingredient = match recipe {
|
||||
CookingRecipeType::Smelting(r)
|
||||
| CookingRecipeType::Blasting(r)
|
||||
| CookingRecipeType::Smoking(r)
|
||||
| CookingRecipeType::CampfireCooking(r) => &r.ingredient,
|
||||
};
|
||||
|
||||
let furnace_inv = handler.get_behaviour().slots[0].get_inventory();
|
||||
|
||||
// Check if ingredient already matches (for stacking).
|
||||
let (recipe_matches, current_count) = {
|
||||
let slot_arc = furnace_inv.get_stack(0).await;
|
||||
let stack = slot_arc.lock().await;
|
||||
let matches = !stack.is_empty() && ingredient.match_item(stack.item);
|
||||
let count = if matches { stack.item_count } else { 0 };
|
||||
(matches, count)
|
||||
};
|
||||
|
||||
// Always clear slot 0 first, returning item to inventory.
|
||||
let old = furnace_inv.remove_stack(0).await;
|
||||
if !old.is_empty() {
|
||||
player.inventory.offer(old, false, player.as_ref()).await;
|
||||
}
|
||||
|
||||
let amount_to_craft = if use_max {
|
||||
compute_biggest_craftable(&[ingredient], &player.inventory).await
|
||||
} else if recipe_matches {
|
||||
current_count.saturating_add(1)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
if amount_to_craft > 0 {
|
||||
let taken = take_n_ingredient(&player.inventory, ingredient, amount_to_craft).await;
|
||||
if !taken.is_empty() {
|
||||
furnace_inv.set_stack(0, taken).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handler.send_content_updates().await;
|
||||
}
|
||||
pub async fn handle_swing_arm(&self, player: &Arc<Player>, swing_arm: SSwingArm) {
|
||||
player.update_last_action_time();
|
||||
let Ok(hand) = Hand::try_from(swing_arm.hand.0) else {
|
||||
|
||||
@@ -67,7 +67,9 @@ use pumpkin_protocol::bedrock::client::set_actor_data::{
|
||||
};
|
||||
use pumpkin_protocol::bedrock::client::start_game::{CStartGame, ServerTelemetryData};
|
||||
use pumpkin_protocol::bedrock::frame_set::FrameSet;
|
||||
use pumpkin_protocol::java::client::play::{CPlayerSpawnPosition, CSystemChatMessage};
|
||||
use pumpkin_protocol::java::client::play::{
|
||||
CPlayerSpawnPosition, CRecipeBookAdd, CRecipeBookSettings, CSystemChatMessage,
|
||||
};
|
||||
use pumpkin_protocol::java::client::play::{CSetEntityMetadata, Metadata};
|
||||
use pumpkin_protocol::{
|
||||
BClientPacket, ClientPacket, IdOr, SoundEvent,
|
||||
@@ -2074,6 +2076,17 @@ impl World {
|
||||
player.send_active_effects().await;
|
||||
self.send_player_equipment(player).await;
|
||||
|
||||
if let crate::net::ClientPlatform::Java(java_client) = &player.client
|
||||
&& server.advanced_config.recipe.send_recipes
|
||||
{
|
||||
java_client
|
||||
.send_packet_now(&CRecipeBookSettings::default_closed())
|
||||
.await;
|
||||
java_client
|
||||
.send_packet_now(&CRecipeBookAdd::new(true))
|
||||
.await;
|
||||
}
|
||||
|
||||
let msg_comp = TextComponent::translate(
|
||||
translation::MULTIPLAYER_PLAYER_JOINED,
|
||||
[TextComponent::text(player.gameprofile.name.clone())],
|
||||
|
||||
Reference in New Issue
Block a user