1use std::{fmt, io::Read};
4
5use mcproto_codec::error::{CodecError, CodecKind, InvalidEncodingReason};
6
7use crate::{Float, PrefixedArray, SlotDisplay, TypeCodec, TypeStructCodec, VarInt};
8
9#[derive(Debug, Clone, PartialEq, TypeStructCodec)]
11#[type_struct_codec(kind = RecipeDisplay)]
12pub struct CraftingShapelessRecipeDisplay {
13 pub ingredients: PrefixedArray<SlotDisplay>,
15 pub result: SlotDisplay,
17 pub crafting_station: SlotDisplay,
19}
20
21#[derive(Debug, Clone, PartialEq)]
26pub struct ShapedRecipeGrid {
27 width: u32,
28 height: u32,
29 ingredients: Vec<SlotDisplay>,
30}
31
32impl ShapedRecipeGrid {
33 pub fn new(
35 width: u32,
36 height: u32,
37 ingredients: Vec<SlotDisplay>,
38 ) -> Result<Self, InvalidShapedRecipeGrid> {
39 validate_grid(width, height, ingredients.len())?;
40 Ok(Self {
41 width,
42 height,
43 ingredients,
44 })
45 }
46
47 #[must_use]
49 pub const fn width(&self) -> u32 {
50 self.width
51 }
52
53 #[must_use]
55 pub const fn height(&self) -> u32 {
56 self.height
57 }
58
59 #[must_use]
61 pub const fn dimensions(&self) -> (u32, u32) {
62 (self.width, self.height)
63 }
64
65 #[must_use]
67 pub fn ingredients(&self) -> &[SlotDisplay] {
68 &self.ingredients
69 }
70
71 #[must_use]
73 pub fn into_ingredients(self) -> Vec<SlotDisplay> {
74 self.ingredients
75 }
76}
77
78impl TypeCodec for ShapedRecipeGrid {
79 fn encode(&self, writer: &mut impl std::io::Write) -> Result<(), CodecError> {
80 VarInt(self.width as i32)
81 .encode(writer)
82 .map_err(|error| error.with_context(CodecKind::ShapedRecipeGrid))?;
83 VarInt(self.height as i32)
84 .encode(writer)
85 .map_err(|error| error.with_context(CodecKind::ShapedRecipeGrid))?;
86 VarInt(self.ingredients.len() as i32)
87 .encode(writer)
88 .map_err(|error| error.with_context(CodecKind::ShapedRecipeGrid))?;
89 for ingredient in &self.ingredients {
90 ingredient
91 .encode(writer)
92 .map_err(|error| error.with_context(CodecKind::ShapedRecipeGrid))?;
93 }
94 Ok(())
95 }
96
97 fn decode(reader: &mut impl Read) -> Result<Self, CodecError> {
98 let width = decode_grid_dimension(reader)?;
99 let height = decode_grid_dimension(reader)?;
100 let ingredient_count = decode_ingredient_count(reader)?;
101 validate_decoded_grid(width, height, ingredient_count)?;
102
103 let mut ingredients = Vec::new();
104 for _ in 0..ingredient_count {
105 ingredients.push(
106 SlotDisplay::decode(reader)
107 .map_err(|error| error.with_context(CodecKind::ShapedRecipeGrid))?,
108 );
109 }
110 Ok(Self {
111 width,
112 height,
113 ingredients,
114 })
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, TypeStructCodec)]
120#[type_struct_codec(kind = RecipeDisplay)]
121pub struct CraftingShapedRecipeDisplay {
122 pub grid: ShapedRecipeGrid,
124 pub result: SlotDisplay,
126 pub crafting_station: SlotDisplay,
128}
129
130impl CraftingShapedRecipeDisplay {
131 pub fn new(
133 width: u32,
134 height: u32,
135 ingredients: Vec<SlotDisplay>,
136 result: SlotDisplay,
137 crafting_station: SlotDisplay,
138 ) -> Result<Self, InvalidShapedRecipeGrid> {
139 Ok(Self {
140 grid: ShapedRecipeGrid::new(width, height, ingredients)?,
141 result,
142 crafting_station,
143 })
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, TypeStructCodec)]
149#[type_struct_codec(kind = RecipeDisplay)]
150pub struct FurnaceRecipeDisplay {
151 pub ingredient: SlotDisplay,
153 pub fuel: SlotDisplay,
155 pub result: SlotDisplay,
157 pub crafting_station: SlotDisplay,
159 pub cooking_time: VarInt,
161 pub experience: Float,
163}
164
165#[derive(Debug, Clone, PartialEq, TypeStructCodec)]
167#[type_struct_codec(kind = RecipeDisplay)]
168pub struct StonecutterRecipeDisplay {
169 pub ingredient: SlotDisplay,
171 pub result: SlotDisplay,
173 pub crafting_station: SlotDisplay,
175}
176
177#[derive(Debug, Clone, PartialEq, TypeStructCodec)]
179#[type_struct_codec(kind = RecipeDisplay)]
180pub struct SmithingRecipeDisplay {
181 pub template: SlotDisplay,
183 pub base: SlotDisplay,
185 pub addition: SlotDisplay,
187 pub result: SlotDisplay,
189 pub crafting_station: SlotDisplay,
191}
192
193#[derive(Debug, Clone, PartialEq)]
233pub enum RecipeDisplay {
234 CraftingShapeless(CraftingShapelessRecipeDisplay),
235 CraftingShaped(CraftingShapedRecipeDisplay),
236 Furnace(FurnaceRecipeDisplay),
237 Stonecutter(StonecutterRecipeDisplay),
238 Smithing(SmithingRecipeDisplay),
239}
240
241impl TypeCodec for RecipeDisplay {
242 fn encode(&self, writer: &mut impl std::io::Write) -> Result<(), CodecError> {
243 match self {
244 Self::CraftingShapeless(value) => {
245 encode_display_type(0, writer)?;
246 value.encode(writer)
247 }
248 Self::CraftingShaped(value) => {
249 encode_display_type(1, writer)?;
250 value.encode(writer)
251 }
252 Self::Furnace(value) => {
253 encode_display_type(2, writer)?;
254 value.encode(writer)
255 }
256 Self::Stonecutter(value) => {
257 encode_display_type(3, writer)?;
258 value.encode(writer)
259 }
260 Self::Smithing(value) => {
261 encode_display_type(4, writer)?;
262 value.encode(writer)
263 }
264 }
265 }
266
267 fn decode(reader: &mut impl Read) -> Result<Self, CodecError> {
268 let display_type = VarInt::decode(reader)
269 .map_err(|error| error.with_context(CodecKind::RecipeDisplay))?
270 .0;
271 match display_type {
272 0 => CraftingShapelessRecipeDisplay::decode(reader).map(Self::CraftingShapeless),
273 1 => CraftingShapedRecipeDisplay::decode(reader).map(Self::CraftingShaped),
274 2 => FurnaceRecipeDisplay::decode(reader).map(Self::Furnace),
275 3 => StonecutterRecipeDisplay::decode(reader).map(Self::Stonecutter),
276 4 => SmithingRecipeDisplay::decode(reader).map(Self::Smithing),
277 value => Err(CodecError::invalid_encoding(
278 CodecKind::RecipeDisplay,
279 0,
280 InvalidEncodingReason::InvalidEnumValue {
281 value: i128::from(value),
282 },
283 )),
284 }
285 }
286}
287
288fn encode_display_type(
289 display_type: i32,
290 writer: &mut impl std::io::Write,
291) -> Result<(), CodecError> {
292 VarInt(display_type)
293 .encode(writer)
294 .map_err(|error| error.with_context(CodecKind::RecipeDisplay))
295}
296
297fn decode_grid_dimension(reader: &mut impl Read) -> Result<u32, CodecError> {
298 let value = VarInt::decode(reader)
299 .map_err(|error| error.with_context(CodecKind::ShapedRecipeGrid))?
300 .0;
301 u32::try_from(value).map_err(|_| {
302 CodecError::invalid_encoding(
303 CodecKind::ShapedRecipeGrid,
304 0,
305 InvalidEncodingReason::NegativeLength { value },
306 )
307 })
308}
309
310fn decode_ingredient_count(reader: &mut impl Read) -> Result<usize, CodecError> {
311 let value = VarInt::decode(reader)
312 .map_err(|error| error.with_context(CodecKind::ShapedRecipeGrid))?
313 .0;
314 usize::try_from(value).map_err(|_| {
315 CodecError::invalid_encoding(
316 CodecKind::ShapedRecipeGrid,
317 0,
318 InvalidEncodingReason::NegativeLength { value },
319 )
320 })
321}
322
323fn validate_decoded_grid(
324 width: u32,
325 height: u32,
326 ingredient_count: usize,
327) -> Result<(), CodecError> {
328 let expected = encoded_grid_area(width, height).ok_or_else(|| {
329 CodecError::invalid_encoding(
330 CodecKind::ShapedRecipeGrid,
331 0,
332 InvalidEncodingReason::LengthOutOfRange {
333 max: i32::MAX as usize,
334 actual: usize::MAX,
335 },
336 )
337 })?;
338 if ingredient_count != expected {
339 return Err(CodecError::invalid_encoding(
340 CodecKind::ShapedRecipeGrid,
341 0,
342 InvalidEncodingReason::ArrayLengthMismatch {
343 expected,
344 actual: ingredient_count,
345 },
346 ));
347 }
348 Ok(())
349}
350
351fn validate_grid(
352 width: u32,
353 height: u32,
354 ingredient_count: usize,
355) -> Result<(), InvalidShapedRecipeGrid> {
356 if width > i32::MAX as u32 {
357 return Err(InvalidShapedRecipeGrid::WidthOutOfRange { width });
358 }
359 if height > i32::MAX as u32 {
360 return Err(InvalidShapedRecipeGrid::HeightOutOfRange { height });
361 }
362 let expected = encoded_grid_area(width, height)
363 .ok_or(InvalidShapedRecipeGrid::AreaOutOfRange { width, height })?;
364 if ingredient_count != expected {
365 return Err(InvalidShapedRecipeGrid::IngredientCountMismatch {
366 expected,
367 actual: ingredient_count,
368 });
369 }
370 Ok(())
371}
372
373fn encoded_grid_area(width: u32, height: u32) -> Option<usize> {
374 let area = u64::from(width).checked_mul(u64::from(height))?;
375 if area > i32::MAX as u64 {
376 return None;
377 }
378 Some(area as usize)
379}
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
383pub enum InvalidShapedRecipeGrid {
384 WidthOutOfRange { width: u32 },
386 HeightOutOfRange { height: u32 },
388 AreaOutOfRange { width: u32, height: u32 },
390 IngredientCountMismatch { expected: usize, actual: usize },
392}
393
394impl fmt::Display for InvalidShapedRecipeGrid {
395 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
396 match self {
397 Self::WidthOutOfRange { width } => {
398 write!(
399 formatter,
400 "recipe grid width exceeds a positive VarInt: {width}"
401 )
402 }
403 Self::HeightOutOfRange { height } => {
404 write!(
405 formatter,
406 "recipe grid height exceeds a positive VarInt: {height}"
407 )
408 }
409 Self::AreaOutOfRange { width, height } => write!(
410 formatter,
411 "recipe grid area {width} * {height} exceeds a positive VarInt"
412 ),
413 Self::IngredientCountMismatch { expected, actual } => write!(
414 formatter,
415 "recipe grid requires {expected} ingredients, got {actual}"
416 ),
417 }
418 }
419}
420
421impl std::error::Error for InvalidShapedRecipeGrid {}