1use crate::components::BlockPosition;
8use basalt_types::Slot;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum InventoryType {
16 Generic9x1,
18 Generic9x2,
20 Generic9x3,
22 Generic9x4,
24 Generic9x5,
26 Generic9x6,
28 Generic3x3,
30 Crafter3x3,
32 Anvil,
34 Beacon,
36 BlastFurnace,
38 BrewingStand,
40 Crafting,
42 Enchantment,
44 Furnace,
46 Grindstone,
48 Hopper,
50 Lectern,
52 Loom,
54 Merchant,
56 ShulkerBox,
58 Smithing,
60 Smoker,
62 Cartography,
64 Stonecutter,
66}
67
68impl InventoryType {
69 pub fn protocol_id(&self) -> i32 {
74 match self {
75 Self::Generic9x1 => 0,
76 Self::Generic9x2 => 1,
77 Self::Generic9x3 => 2,
78 Self::Generic9x4 => 3,
79 Self::Generic9x5 => 4,
80 Self::Generic9x6 => 5,
81 Self::Generic3x3 => 6,
82 Self::Crafter3x3 => 7,
83 Self::Anvil => 8,
84 Self::Beacon => 9,
85 Self::BlastFurnace => 10,
86 Self::BrewingStand => 11,
87 Self::Crafting => 12,
88 Self::Enchantment => 13,
89 Self::Furnace => 14,
90 Self::Grindstone => 15,
91 Self::Hopper => 16,
92 Self::Lectern => 17,
93 Self::Loom => 18,
94 Self::Merchant => 19,
95 Self::ShulkerBox => 20,
96 Self::Smithing => 21,
97 Self::Smoker => 22,
98 Self::Cartography => 23,
99 Self::Stonecutter => 24,
100 }
101 }
102
103 pub fn slot_count(&self) -> usize {
108 match self {
109 Self::Generic9x1 => 9,
110 Self::Generic9x2 => 18,
111 Self::Generic9x3 => 27,
112 Self::Generic9x4 => 36,
113 Self::Generic9x5 => 45,
114 Self::Generic9x6 => 54,
115 Self::Generic3x3 | Self::Crafter3x3 => 9,
116 Self::Anvil => 3,
117 Self::Beacon => 1,
118 Self::BlastFurnace => 3,
119 Self::BrewingStand => 5,
120 Self::Crafting => 10,
121 Self::Enchantment => 2,
122 Self::Furnace => 3,
123 Self::Grindstone => 3,
124 Self::Hopper => 5,
125 Self::Lectern => 1,
126 Self::Loom => 4,
127 Self::Merchant => 3,
128 Self::ShulkerBox => 27,
129 Self::Smithing => 4,
130 Self::Smoker => 3,
131 Self::Cartography => 3,
132 Self::Stonecutter => 2,
133 }
134 }
135
136 pub fn is_chest_like(&self) -> bool {
141 matches!(
142 self,
143 Self::Generic9x1
144 | Self::Generic9x2
145 | Self::Generic9x3
146 | Self::Generic9x4
147 | Self::Generic9x5
148 | Self::Generic9x6
149 | Self::ShulkerBox
150 )
151 }
152
153 pub fn has_craft_output(&self) -> bool {
158 matches!(self, Self::Crafting)
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum ContainerBacking {
169 Virtual,
174 Block {
179 position: BlockPosition,
181 },
182}
183
184#[derive(Debug, Clone)]
191pub struct Container {
192 pub inventory_type: InventoryType,
194 pub title: String,
196 pub backing: ContainerBacking,
198 pub initial_slots: Option<Vec<Slot>>,
207}
208
209impl Container {
210 pub fn builder() -> ContainerBuilder {
212 ContainerBuilder::new()
213 }
214}
215
216pub struct ContainerBuilder {
222 inventory_type: InventoryType,
224 title: String,
226 backing: ContainerBacking,
228 initial_slots: Option<Vec<Slot>>,
230}
231
232impl ContainerBuilder {
233 pub fn new() -> Self {
235 Self {
236 inventory_type: InventoryType::Generic9x3,
237 title: String::new(),
238 backing: ContainerBacking::Virtual,
239 initial_slots: None,
240 }
241 }
242
243 pub fn inventory_type(mut self, t: InventoryType) -> Self {
245 self.inventory_type = t;
246 self
247 }
248
249 pub fn title(mut self, title: impl Into<String>) -> Self {
251 self.title = title.into();
252 self
253 }
254
255 pub fn backed_by(mut self, x: i32, y: i32, z: i32) -> Self {
260 self.backing = ContainerBacking::Block {
261 position: BlockPosition { x, y, z },
262 };
263 self
264 }
265
266 pub fn slots(mut self, slots: Vec<Slot>) -> Self {
272 self.initial_slots = Some(slots);
273 self
274 }
275
276 pub fn build(self) -> Container {
280 let expected = self.inventory_type.slot_count();
281 let initial_slots = self.initial_slots.map(|mut s| {
282 s.resize(expected, Slot::empty());
283 s
284 });
285
286 Container {
287 inventory_type: self.inventory_type,
288 title: self.title,
289 backing: self.backing,
290 initial_slots,
291 }
292 }
293}
294
295impl Default for ContainerBuilder {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 const VARIANTS: [(InventoryType, i32, usize); 25] = [
307 (InventoryType::Generic9x1, 0, 9),
308 (InventoryType::Generic9x2, 1, 18),
309 (InventoryType::Generic9x3, 2, 27),
310 (InventoryType::Generic9x4, 3, 36),
311 (InventoryType::Generic9x5, 4, 45),
312 (InventoryType::Generic9x6, 5, 54),
313 (InventoryType::Generic3x3, 6, 9),
314 (InventoryType::Crafter3x3, 7, 9),
315 (InventoryType::Anvil, 8, 3),
316 (InventoryType::Beacon, 9, 1),
317 (InventoryType::BlastFurnace, 10, 3),
318 (InventoryType::BrewingStand, 11, 5),
319 (InventoryType::Crafting, 12, 10),
320 (InventoryType::Enchantment, 13, 2),
321 (InventoryType::Furnace, 14, 3),
322 (InventoryType::Grindstone, 15, 3),
323 (InventoryType::Hopper, 16, 5),
324 (InventoryType::Lectern, 17, 1),
325 (InventoryType::Loom, 18, 4),
326 (InventoryType::Merchant, 19, 3),
327 (InventoryType::ShulkerBox, 20, 27),
328 (InventoryType::Smithing, 21, 4),
329 (InventoryType::Smoker, 22, 3),
330 (InventoryType::Cartography, 23, 3),
331 (InventoryType::Stonecutter, 24, 2),
332 ];
333
334 #[test]
335 fn protocol_id_matches_all_variants() {
336 for (variant, expected_id, _) in &VARIANTS {
337 assert_eq!(
338 variant.protocol_id(),
339 *expected_id,
340 "wrong protocol_id for {:?}",
341 variant
342 );
343 }
344 }
345
346 #[test]
347 fn slot_count_matches_all_variants() {
348 for (variant, _, expected_count) in &VARIANTS {
349 assert_eq!(
350 variant.slot_count(),
351 *expected_count,
352 "wrong slot_count for {:?}",
353 variant
354 );
355 }
356 }
357
358 #[test]
359 fn is_chest_like_generic_and_shulker() {
360 let chest_like = [
361 InventoryType::Generic9x1,
362 InventoryType::Generic9x2,
363 InventoryType::Generic9x3,
364 InventoryType::Generic9x4,
365 InventoryType::Generic9x5,
366 InventoryType::Generic9x6,
367 InventoryType::ShulkerBox,
368 ];
369 for variant in &chest_like {
370 assert!(
371 variant.is_chest_like(),
372 "{:?} should be chest-like",
373 variant
374 );
375 }
376 }
377
378 #[test]
379 fn is_chest_like_false_for_non_chest() {
380 let non_chest = [
381 InventoryType::Generic3x3,
382 InventoryType::Crafter3x3,
383 InventoryType::Anvil,
384 InventoryType::Beacon,
385 InventoryType::BlastFurnace,
386 InventoryType::BrewingStand,
387 InventoryType::Crafting,
388 InventoryType::Enchantment,
389 InventoryType::Furnace,
390 InventoryType::Grindstone,
391 InventoryType::Hopper,
392 InventoryType::Lectern,
393 InventoryType::Loom,
394 InventoryType::Merchant,
395 InventoryType::Smithing,
396 InventoryType::Smoker,
397 InventoryType::Cartography,
398 InventoryType::Stonecutter,
399 ];
400 for variant in &non_chest {
401 assert!(
402 !variant.is_chest_like(),
403 "{:?} should NOT be chest-like",
404 variant
405 );
406 }
407 }
408
409 #[test]
410 fn has_craft_output_only_crafting() {
411 assert!(InventoryType::Crafting.has_craft_output());
412 for (variant, _, _) in &VARIANTS {
413 if *variant != InventoryType::Crafting {
414 assert!(
415 !variant.has_craft_output(),
416 "{:?} should NOT have craft output",
417 variant
418 );
419 }
420 }
421 }
422
423 #[test]
424 fn container_backing_virtual() {
425 let backing = ContainerBacking::Virtual;
426 assert_eq!(backing, ContainerBacking::Virtual);
427 }
428
429 #[test]
430 fn container_backing_block() {
431 let pos = BlockPosition { x: 1, y: 2, z: 3 };
432 let backing = ContainerBacking::Block { position: pos };
433 assert_eq!(
434 backing,
435 ContainerBacking::Block {
436 position: BlockPosition { x: 1, y: 2, z: 3 }
437 }
438 );
439 }
440
441 #[test]
442 fn container_with_initial_slots() {
443 let c = Container {
444 inventory_type: InventoryType::Generic9x3,
445 title: "My Chest".to_string(),
446 backing: ContainerBacking::Virtual,
447 initial_slots: Some(vec![Slot::default(); 27]),
448 };
449 assert_eq!(c.inventory_type.slot_count(), 27);
450 assert_eq!(c.initial_slots.as_ref().unwrap().len(), 27);
451 }
452
453 #[test]
454 fn container_no_initial_slots() {
455 let c = Container {
456 inventory_type: InventoryType::Crafting,
457 title: "Crafting".to_string(),
458 backing: ContainerBacking::Block {
459 position: BlockPosition {
460 x: 10,
461 y: 64,
462 z: -5,
463 },
464 },
465 initial_slots: None,
466 };
467 assert!(c.initial_slots.is_none());
468 assert!(c.inventory_type.has_craft_output());
469 assert_eq!(c.inventory_type.protocol_id(), 12);
470 }
471
472 #[test]
473 fn builder_defaults() {
474 let c = Container::builder().build();
475 assert_eq!(c.inventory_type, InventoryType::Generic9x3);
476 assert!(c.title.is_empty());
477 assert!(matches!(c.backing, ContainerBacking::Virtual));
478 assert!(c.initial_slots.is_none());
479 }
480
481 #[test]
482 fn builder_full_chain() {
483 let c = Container::builder()
484 .inventory_type(InventoryType::Generic9x6)
485 .title("Shop")
486 .backed_by(5, 64, 3)
487 .slots(vec![Slot::empty(); 54])
488 .build();
489 assert_eq!(c.inventory_type, InventoryType::Generic9x6);
490 assert_eq!(c.title, "Shop");
491 assert_eq!(
492 c.backing,
493 ContainerBacking::Block {
494 position: BlockPosition { x: 5, y: 64, z: 3 }
495 }
496 );
497 assert_eq!(c.initial_slots.unwrap().len(), 54);
498 }
499
500 #[test]
501 fn builder_pads_slots() {
502 let c = Container::builder()
503 .inventory_type(InventoryType::Generic9x3)
504 .slots(vec![Slot::new(1, 5)])
505 .build();
506 assert_eq!(c.initial_slots.as_ref().unwrap().len(), 27);
507 }
508
509 #[test]
510 fn builder_truncates_slots() {
511 let c = Container::builder()
512 .inventory_type(InventoryType::Hopper)
513 .slots(vec![Slot::new(1, 1); 10])
514 .build();
515 assert_eq!(c.initial_slots.as_ref().unwrap().len(), 5);
516 }
517
518 #[test]
519 fn container_is_reusable() {
520 let c = Container::builder()
521 .inventory_type(InventoryType::Generic9x6)
522 .build();
523 let c2 = c.clone();
524 assert_eq!(c.inventory_type, c2.inventory_type);
525 assert_eq!(c.title, c2.title);
526 }
527
528 #[test]
529 fn builder_default_trait() {
530 let b = ContainerBuilder::default();
531 let c = b.build();
532 assert_eq!(c.inventory_type, InventoryType::Generic9x3);
533 }
534
535 #[test]
536 fn builder_title_into_string() {
537 let c = Container::builder().title(String::from("Dynamic")).build();
538 assert_eq!(c.title, "Dynamic");
539 }
540}