use std::{fmt, io::Read};
use mcproto_codec::error::{CodecError, CodecKind, InvalidEncodingReason};
use crate::{Float, PrefixedArray, SlotDisplay, TypeCodec, TypeStructCodec, VarInt};
#[derive(Debug, Clone, PartialEq, TypeStructCodec)]
#[type_struct_codec(kind = RecipeDisplay)]
pub struct CraftingShapelessRecipeDisplay {
pub ingredients: PrefixedArray<SlotDisplay>,
pub result: SlotDisplay,
pub crafting_station: SlotDisplay,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ShapedRecipeGrid {
width: u32,
height: u32,
ingredients: Vec<SlotDisplay>,
}
impl ShapedRecipeGrid {
pub fn new(
width: u32,
height: u32,
ingredients: Vec<SlotDisplay>,
) -> Result<Self, InvalidShapedRecipeGrid> {
validate_grid(width, height, ingredients.len())?;
Ok(Self {
width,
height,
ingredients,
})
}
#[must_use]
pub const fn width(&self) -> u32 {
self.width
}
#[must_use]
pub const fn height(&self) -> u32 {
self.height
}
#[must_use]
pub const fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
#[must_use]
pub fn ingredients(&self) -> &[SlotDisplay] {
&self.ingredients
}
#[must_use]
pub fn into_ingredients(self) -> Vec<SlotDisplay> {
self.ingredients
}
}
impl TypeCodec for ShapedRecipeGrid {
fn encode(&self, writer: &mut impl std::io::Write) -> Result<(), CodecError> {
VarInt(self.width as i32)
.encode(writer)
.map_err(|error| error.with_context(CodecKind::ShapedRecipeGrid))?;
VarInt(self.height as i32)
.encode(writer)
.map_err(|error| error.with_context(CodecKind::ShapedRecipeGrid))?;
VarInt(self.ingredients.len() as i32)
.encode(writer)
.map_err(|error| error.with_context(CodecKind::ShapedRecipeGrid))?;
for ingredient in &self.ingredients {
ingredient
.encode(writer)
.map_err(|error| error.with_context(CodecKind::ShapedRecipeGrid))?;
}
Ok(())
}
fn decode(reader: &mut impl Read) -> Result<Self, CodecError> {
let width = decode_grid_dimension(reader)?;
let height = decode_grid_dimension(reader)?;
let ingredient_count = decode_ingredient_count(reader)?;
validate_decoded_grid(width, height, ingredient_count)?;
let mut ingredients = Vec::new();
for _ in 0..ingredient_count {
ingredients.push(
SlotDisplay::decode(reader)
.map_err(|error| error.with_context(CodecKind::ShapedRecipeGrid))?,
);
}
Ok(Self {
width,
height,
ingredients,
})
}
}
#[derive(Debug, Clone, PartialEq, TypeStructCodec)]
#[type_struct_codec(kind = RecipeDisplay)]
pub struct CraftingShapedRecipeDisplay {
pub grid: ShapedRecipeGrid,
pub result: SlotDisplay,
pub crafting_station: SlotDisplay,
}
impl CraftingShapedRecipeDisplay {
pub fn new(
width: u32,
height: u32,
ingredients: Vec<SlotDisplay>,
result: SlotDisplay,
crafting_station: SlotDisplay,
) -> Result<Self, InvalidShapedRecipeGrid> {
Ok(Self {
grid: ShapedRecipeGrid::new(width, height, ingredients)?,
result,
crafting_station,
})
}
}
#[derive(Debug, Clone, PartialEq, TypeStructCodec)]
#[type_struct_codec(kind = RecipeDisplay)]
pub struct FurnaceRecipeDisplay {
pub ingredient: SlotDisplay,
pub fuel: SlotDisplay,
pub result: SlotDisplay,
pub crafting_station: SlotDisplay,
pub cooking_time: VarInt,
pub experience: Float,
}
#[derive(Debug, Clone, PartialEq, TypeStructCodec)]
#[type_struct_codec(kind = RecipeDisplay)]
pub struct StonecutterRecipeDisplay {
pub ingredient: SlotDisplay,
pub result: SlotDisplay,
pub crafting_station: SlotDisplay,
}
#[derive(Debug, Clone, PartialEq, TypeStructCodec)]
#[type_struct_codec(kind = RecipeDisplay)]
pub struct SmithingRecipeDisplay {
pub template: SlotDisplay,
pub base: SlotDisplay,
pub addition: SlotDisplay,
pub result: SlotDisplay,
pub crafting_station: SlotDisplay,
}
#[derive(Debug, Clone, PartialEq)]
pub enum RecipeDisplay {
CraftingShapeless(CraftingShapelessRecipeDisplay),
CraftingShaped(CraftingShapedRecipeDisplay),
Furnace(FurnaceRecipeDisplay),
Stonecutter(StonecutterRecipeDisplay),
Smithing(SmithingRecipeDisplay),
}
impl TypeCodec for RecipeDisplay {
fn encode(&self, writer: &mut impl std::io::Write) -> Result<(), CodecError> {
match self {
Self::CraftingShapeless(value) => {
encode_display_type(0, writer)?;
value.encode(writer)
}
Self::CraftingShaped(value) => {
encode_display_type(1, writer)?;
value.encode(writer)
}
Self::Furnace(value) => {
encode_display_type(2, writer)?;
value.encode(writer)
}
Self::Stonecutter(value) => {
encode_display_type(3, writer)?;
value.encode(writer)
}
Self::Smithing(value) => {
encode_display_type(4, writer)?;
value.encode(writer)
}
}
}
fn decode(reader: &mut impl Read) -> Result<Self, CodecError> {
let display_type = VarInt::decode(reader)
.map_err(|error| error.with_context(CodecKind::RecipeDisplay))?
.0;
match display_type {
0 => CraftingShapelessRecipeDisplay::decode(reader).map(Self::CraftingShapeless),
1 => CraftingShapedRecipeDisplay::decode(reader).map(Self::CraftingShaped),
2 => FurnaceRecipeDisplay::decode(reader).map(Self::Furnace),
3 => StonecutterRecipeDisplay::decode(reader).map(Self::Stonecutter),
4 => SmithingRecipeDisplay::decode(reader).map(Self::Smithing),
value => Err(CodecError::invalid_encoding(
CodecKind::RecipeDisplay,
0,
InvalidEncodingReason::InvalidEnumValue {
value: i128::from(value),
},
)),
}
}
}
fn encode_display_type(
display_type: i32,
writer: &mut impl std::io::Write,
) -> Result<(), CodecError> {
VarInt(display_type)
.encode(writer)
.map_err(|error| error.with_context(CodecKind::RecipeDisplay))
}
fn decode_grid_dimension(reader: &mut impl Read) -> Result<u32, CodecError> {
let value = VarInt::decode(reader)
.map_err(|error| error.with_context(CodecKind::ShapedRecipeGrid))?
.0;
u32::try_from(value).map_err(|_| {
CodecError::invalid_encoding(
CodecKind::ShapedRecipeGrid,
0,
InvalidEncodingReason::NegativeLength { value },
)
})
}
fn decode_ingredient_count(reader: &mut impl Read) -> Result<usize, CodecError> {
let value = VarInt::decode(reader)
.map_err(|error| error.with_context(CodecKind::ShapedRecipeGrid))?
.0;
usize::try_from(value).map_err(|_| {
CodecError::invalid_encoding(
CodecKind::ShapedRecipeGrid,
0,
InvalidEncodingReason::NegativeLength { value },
)
})
}
fn validate_decoded_grid(
width: u32,
height: u32,
ingredient_count: usize,
) -> Result<(), CodecError> {
let expected = encoded_grid_area(width, height).ok_or_else(|| {
CodecError::invalid_encoding(
CodecKind::ShapedRecipeGrid,
0,
InvalidEncodingReason::LengthOutOfRange {
max: i32::MAX as usize,
actual: usize::MAX,
},
)
})?;
if ingredient_count != expected {
return Err(CodecError::invalid_encoding(
CodecKind::ShapedRecipeGrid,
0,
InvalidEncodingReason::ArrayLengthMismatch {
expected,
actual: ingredient_count,
},
));
}
Ok(())
}
fn validate_grid(
width: u32,
height: u32,
ingredient_count: usize,
) -> Result<(), InvalidShapedRecipeGrid> {
if width > i32::MAX as u32 {
return Err(InvalidShapedRecipeGrid::WidthOutOfRange { width });
}
if height > i32::MAX as u32 {
return Err(InvalidShapedRecipeGrid::HeightOutOfRange { height });
}
let expected = encoded_grid_area(width, height)
.ok_or(InvalidShapedRecipeGrid::AreaOutOfRange { width, height })?;
if ingredient_count != expected {
return Err(InvalidShapedRecipeGrid::IngredientCountMismatch {
expected,
actual: ingredient_count,
});
}
Ok(())
}
fn encoded_grid_area(width: u32, height: u32) -> Option<usize> {
let area = u64::from(width).checked_mul(u64::from(height))?;
if area > i32::MAX as u64 {
return None;
}
Some(area as usize)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InvalidShapedRecipeGrid {
WidthOutOfRange { width: u32 },
HeightOutOfRange { height: u32 },
AreaOutOfRange { width: u32, height: u32 },
IngredientCountMismatch { expected: usize, actual: usize },
}
impl fmt::Display for InvalidShapedRecipeGrid {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::WidthOutOfRange { width } => {
write!(
formatter,
"recipe grid width exceeds a positive VarInt: {width}"
)
}
Self::HeightOutOfRange { height } => {
write!(
formatter,
"recipe grid height exceeds a positive VarInt: {height}"
)
}
Self::AreaOutOfRange { width, height } => write!(
formatter,
"recipe grid area {width} * {height} exceeds a positive VarInt"
),
Self::IngredientCountMismatch { expected, actual } => write!(
formatter,
"recipe grid requires {expected} ingredients, got {actual}"
),
}
}
}
impl std::error::Error for InvalidShapedRecipeGrid {}