586 lines
22 KiB
Rust
586 lines
22 KiB
Rust
use std::{collections::HashMap, io::Write};
|
|
|
|
use pumpkin_data::item::Item;
|
|
use pumpkin_data::item_id_remap::remap_item_id_for_version;
|
|
use pumpkin_data::item_stack::ItemStack;
|
|
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, codec::item_stack_seralizer::ItemStackSerializer,
|
|
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;
|
|
// 1.21.2 - 1.21.11
|
|
const SLOT_DISPLAY_ITEM_LEGACY: i32 = 2;
|
|
const SLOT_DISPLAY_ITEM_STACK_LEGACY: i32 = 3;
|
|
const SLOT_DISPLAY_COMPOSITE_LEGACY: i32 = 7;
|
|
// 26.1+
|
|
const SLOT_DISPLAY_ITEM_26_1: i32 = 4;
|
|
const SLOT_DISPLAY_ITEM_STACK_26_1: i32 = 5;
|
|
const SLOT_DISPLAY_COMPOSITE_26_1: i32 = 10;
|
|
|
|
const ENTRY_FLAG_NOTIFICATION: u8 = 0x01;
|
|
const ENTRY_FLAG_HIGHLIGHT: u8 = 0x02;
|
|
|
|
// 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 slot_display_item_type(version: MinecraftVersion) -> i32 {
|
|
if version >= MinecraftVersion::V_26_1 {
|
|
SLOT_DISPLAY_ITEM_26_1
|
|
} else {
|
|
SLOT_DISPLAY_ITEM_LEGACY
|
|
}
|
|
}
|
|
|
|
fn slot_display_composite_type(version: MinecraftVersion) -> i32 {
|
|
if version >= MinecraftVersion::V_26_1 {
|
|
SLOT_DISPLAY_COMPOSITE_26_1
|
|
} else {
|
|
SLOT_DISPLAY_COMPOSITE_LEGACY
|
|
}
|
|
}
|
|
|
|
fn slot_display_item_stack_type(version: MinecraftVersion) -> i32 {
|
|
if version >= MinecraftVersion::V_26_1 {
|
|
SLOT_DISPLAY_ITEM_STACK_26_1
|
|
} else {
|
|
SLOT_DISPLAY_ITEM_STACK_LEGACY
|
|
}
|
|
}
|
|
|
|
fn write_item_slot_display(
|
|
write: &mut impl Write,
|
|
item: &Item,
|
|
version: MinecraftVersion,
|
|
) -> Result<(), WritingError> {
|
|
write.write_var_int(&VarInt(slot_display_item_type(version)))?;
|
|
write.write_var_int(&VarInt(item_id_versioned(item, version)))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn write_item_stack_slot_display(
|
|
write: &mut impl Write,
|
|
item: &Item,
|
|
count: u8,
|
|
version: MinecraftVersion,
|
|
) -> Result<(), WritingError> {
|
|
write.write_var_int(&VarInt(slot_display_item_stack_type(version)))?;
|
|
let static_item = Item::from_id(item.id)
|
|
.ok_or_else(|| WritingError::Message(format!("item id {} must exist", item.id)))?;
|
|
ItemStackSerializer::from(ItemStack::new(count, static_item))
|
|
.write_with_version(write, &version)
|
|
}
|
|
|
|
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_type(version)))?;
|
|
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_stack_slot_display(write, item, result.count, version)?;
|
|
} else {
|
|
write_empty_slot_display(write)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn write_optional_var_int(write: &mut impl Write, value: Option<i32>) -> Result<(), WritingError> {
|
|
let encoded = value.map_or(Ok(0), |v| {
|
|
v.checked_add(1)
|
|
.ok_or_else(|| WritingError::Message(format!("group id {v} overflow")))
|
|
})?;
|
|
write.write_var_int(&VarInt(encoded))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn resolve_group_id(
|
|
group_ids: &mut HashMap<&'static str, i32>,
|
|
next_group_id: &mut i32,
|
|
group: Option<&'static str>,
|
|
) -> Option<i32> {
|
|
let key = group?;
|
|
Some(*group_ids.entry(key).or_insert_with(|| {
|
|
let id = *next_group_id;
|
|
*next_group_id += 1;
|
|
id
|
|
}))
|
|
}
|
|
|
|
const fn entry_flags(replace: bool, notification: bool, highlight: bool) -> u8 {
|
|
if replace {
|
|
return 0;
|
|
}
|
|
|
|
(if notification {
|
|
ENTRY_FLAG_NOTIFICATION
|
|
} else {
|
|
0
|
|
}) | (if highlight { ENTRY_FLAG_HIGHLIGHT } else { 0 })
|
|
}
|
|
|
|
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,
|
|
group_id: Option<i32>,
|
|
flags: u8,
|
|
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: OptionalVarInt
|
|
write_optional_var_int(write, group_id)?;
|
|
// 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)?;
|
|
};
|
|
write.write_u8(flags)?;
|
|
}
|
|
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: OptionalVarInt
|
|
write_optional_var_int(write, group_id)?;
|
|
// 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)?;
|
|
};
|
|
write.write_u8(flags)?;
|
|
}
|
|
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_optional_var_int(write, group_id)?;
|
|
write.write_var_int(&VarInt(crafting_category(category)))?;
|
|
// craftingRequirements: input + material
|
|
write_crafting_requirements(write, &[Some(input), Some(material)], version)?;
|
|
write.write_u8(flags)?;
|
|
}
|
|
// 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: OptionalVarInt
|
|
write_optional_var_int(write, group_id)?;
|
|
// category
|
|
write.write_var_int(&VarInt(book_category))?;
|
|
// craftingRequirements: the single ingredient
|
|
write_crafting_requirements(write, &[Some(&cooking.ingredient)], version)?;
|
|
write.write_u8(flags)?;
|
|
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")
|
|
.ok_or_else(|| WritingError::Message("crafting_table item must exist".into()))?;
|
|
let furnace = Item::from_registry_key("furnace")
|
|
.ok_or_else(|| WritingError::Message("furnace item must exist".into()))?;
|
|
let blast_furnace = Item::from_registry_key("blast_furnace")
|
|
.ok_or_else(|| WritingError::Message("blast_furnace item must exist".into()))?;
|
|
let smoker = Item::from_registry_key("smoker")
|
|
.ok_or_else(|| WritingError::Message("smoker item must exist".into()))?;
|
|
let campfire = Item::from_registry_key("campfire")
|
|
.ok_or_else(|| WritingError::Message("campfire item must exist".into()))?;
|
|
|
|
// 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;
|
|
let mut group_ids: HashMap<&'static str, i32> = HashMap::new();
|
|
let mut next_group_id: i32 = 0;
|
|
let highlight = !self.replace;
|
|
|
|
// Write crafting recipes
|
|
for recipe in RECIPES_CRAFTING {
|
|
let (group, notification) = match recipe {
|
|
CraftingRecipeTypes::CraftingShaped {
|
|
group,
|
|
show_notification,
|
|
..
|
|
} => (*group, *show_notification),
|
|
CraftingRecipeTypes::CraftingShapeless { group, .. }
|
|
| CraftingRecipeTypes::CraftingTransmute { group, .. } => (*group, true),
|
|
CraftingRecipeTypes::CraftingDecoratedPot { .. }
|
|
| CraftingRecipeTypes::CraftingSpecial => (None, true),
|
|
};
|
|
let group_id = resolve_group_id(&mut group_ids, &mut next_group_id, group);
|
|
let flags = entry_flags(self.replace, notification, highlight);
|
|
let written = write_entry(
|
|
&mut write,
|
|
display_id,
|
|
*version,
|
|
group_id,
|
|
flags,
|
|
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, group) = match recipe {
|
|
CookingRecipeType::Smelting(r) => (
|
|
match r.category {
|
|
RecipeCategoryTypes::Food => CATEGORY_FURNACE_FOOD,
|
|
RecipeCategoryTypes::Blocks => CATEGORY_FURNACE_BLOCKS,
|
|
_ => CATEGORY_FURNACE_MISC,
|
|
},
|
|
r.group,
|
|
),
|
|
CookingRecipeType::Blasting(r) => (
|
|
match r.category {
|
|
RecipeCategoryTypes::Blocks => CATEGORY_BLAST_FURNACE_BLOCKS,
|
|
_ => CATEGORY_BLAST_FURNACE_MISC,
|
|
},
|
|
r.group,
|
|
),
|
|
CookingRecipeType::Smoking(r) => (CATEGORY_SMOKER_FOOD, r.group),
|
|
CookingRecipeType::CampfireCooking(r) => (CATEGORY_CAMPFIRE, r.group),
|
|
};
|
|
let group_id = resolve_group_id(&mut group_ids, &mut next_group_id, group);
|
|
let flags = entry_flags(self.replace, true, highlight);
|
|
write_entry(
|
|
&mut write,
|
|
display_id,
|
|
*version,
|
|
group_id,
|
|
flags,
|
|
crafting_table,
|
|
furnace,
|
|
blast_furnace,
|
|
smoker,
|
|
campfire,
|
|
None,
|
|
Some((recipe, book_category)),
|
|
)?;
|
|
display_id += 1;
|
|
}
|
|
|
|
// replace flag
|
|
write.write_bool(self.replace)?;
|
|
Ok(())
|
|
}
|
|
}
|