alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
root crates common src item.rs
//! Item ids and stacks.
use crate::nbt::{Compound, Tag};
/// Item id: block ids below 256, `Item.shiftedIndex` above.
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
#[repr(transparent)]
pub struct Item(pub i16);
/// `EnumToolMaterial`.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ToolMaterial {
Wood,
Stone,
Iron,
Diamond,
Gold,
}
impl ToolMaterial {
/// `harvestLevel` (which equals `damageVsEntity`), `maxUses` and `efficiencyOnProperMaterial`.
const fn stats(self) -> (i32, i16, f32) {
match self {
Self::Wood => (0, 59, 2.0),
Self::Stone => (1, 131, 4.0),
Self::Iron => (2, 250, 6.0),
Self::Diamond => (3, 1561, 8.0),
Self::Gold => (0, 32, 12.0),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ToolClass {
Shovel,
Pickaxe,
Axe,
}
/// What an item does, standing in for the `Item` subclasses.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ItemKind {
Generic,
/// `ItemBlock` and its subtyped variants.
Block,
Tool(ToolClass, ToolMaterial),
Sword(ToolMaterial),
Hoe(ToolMaterial),
/// `ItemArmor`: slot 0 helmet to 3 boots, render index, armor material tier.
Armor {
slot: u8,
render: u8,
tier: u8,
},
Food {
heal: i32,
wolf_food: bool,
},
Soup {
heal: i32,
},
/// `ItemBucket`: the fluid it holds, `None` for milk, `Some(AIR)` when empty.
Bucket(Option<crate::world::Block>),
Minecart(u8),
Door(crate::world::Block),
/// `ItemReed`: places a block (sugar cane, cake, repeater).
Places(crate::world::Block),
Seeds,
Record(&'static str),
FlintAndSteel,
Bow,
Coal,
Dye,
Painting,
Sign,
Saddle,
Redstone,
Snowball,
Egg,
Boat,
FishingRod,
Bed,
Map,
Shears,
}
#[derive(Clone, Copy, Debug)]
pub struct ItemProperties {
/// The translation key suffix of `setItemName`.
pub name: &'static str,
/// Index into `gui/items.png`.
pub icon: u8,
pub max_stack: i8,
pub max_damage: i16,
pub has_subtypes: bool,
/// `isFull3D`: held like a tool.
pub full_3d: bool,
pub kind: ItemKind,
/// `getContainerItem`: what remains after crafting.
pub container: Option<Item>,
}
impl ItemProperties {
const fn new(name: &'static str, x: u8, y: u8, kind: ItemKind) -> Self {
let (max_stack, max_damage, full_3d) = match kind {
ItemKind::Tool(_, m) | ItemKind::Sword(m) | ItemKind::Hoe(m) => (1, m.stats().1, true),
ItemKind::Armor { slot, tier, .. } => {
let base = [11, 16, 15, 13][slot as usize];
(1, (base * 3) << tier, false)
}
ItemKind::FlintAndSteel | ItemKind::FishingRod => {
(1, 64, matches!(kind, ItemKind::FishingRod))
}
ItemKind::Shears => (1, 238, false),
ItemKind::Snowball | ItemKind::Egg => (16, 0, false),
ItemKind::Food { .. }
| ItemKind::Soup { .. }
| ItemKind::Bucket(_)
| ItemKind::Minecart(_)
| ItemKind::Door(_)
| ItemKind::Record(_)
| ItemKind::Bow
| ItemKind::Sign
| ItemKind::Saddle
| ItemKind::Boat
| ItemKind::Map => (1, 0, false),
_ => (64, 0, false),
};
Self {
name,
icon: x + y * 16,
max_stack,
max_damage,
has_subtypes: matches!(kind, ItemKind::Coal | ItemKind::Dye | ItemKind::Map),
full_3d,
kind,
container: None,
}
}
const fn stack(mut self, max_stack: i8) -> Self {
self.max_stack = max_stack;
self
}
const fn full_3d(mut self) -> Self {
self.full_3d = true;
self
}
const fn container(mut self, item: Item) -> Self {
self.container = Some(item);
self
}
}
macro_rules! items {
($($index:literal $const_name:ident = $props:expr;)*) => {
impl Item {
$(pub const $const_name: Self = Self(256 + $index);)*
}
static ITEMS: [Option<ItemProperties>; 2304] = {
let mut table = [None; 2304];
$(table[256 + $index] = Some($props);)*
table
};
};
}
use crate::world::Block as B;
use ItemKind as K;
use ItemProperties as P;
use ToolClass::*;
use ToolMaterial::*;
items! {
0 IRON_SHOVEL = P::new("shovelIron", 2, 5, K::Tool(Shovel, Iron));
1 IRON_PICKAXE = P::new("pickaxeIron", 2, 6, K::Tool(Pickaxe, Iron));
2 IRON_AXE = P::new("hatchetIron", 2, 7, K::Tool(Axe, Iron));
3 FLINT_AND_STEEL = P::new("flintAndSteel", 5, 0, K::FlintAndSteel);
4 APPLE = P::new("apple", 10, 0, K::Food { heal: 4, wolf_food: false });
5 BOW = P::new("bow", 5, 1, K::Bow);
6 ARROW = P::new("arrow", 5, 2, K::Generic);
7 COAL = P::new("coal", 7, 0, K::Coal);
8 DIAMOND = P::new("emerald", 7, 3, K::Generic);
9 IRON_INGOT = P::new("ingotIron", 7, 1, K::Generic);
10 GOLD_INGOT = P::new("ingotGold", 7, 2, K::Generic);
11 IRON_SWORD = P::new("swordIron", 2, 4, K::Sword(Iron));
12 WOODEN_SWORD = P::new("swordWood", 0, 4, K::Sword(Wood));
13 WOODEN_SHOVEL = P::new("shovelWood", 0, 5, K::Tool(Shovel, Wood));
14 WOODEN_PICKAXE = P::new("pickaxeWood", 0, 6, K::Tool(Pickaxe, Wood));
15 WOODEN_AXE = P::new("hatchetWood", 0, 7, K::Tool(Axe, Wood));
16 STONE_SWORD = P::new("swordStone", 1, 4, K::Sword(Stone));
17 STONE_SHOVEL = P::new("shovelStone", 1, 5, K::Tool(Shovel, Stone));
18 STONE_PICKAXE = P::new("pickaxeStone", 1, 6, K::Tool(Pickaxe, Stone));
19 STONE_AXE = P::new("hatchetStone", 1, 7, K::Tool(Axe, Stone));
20 DIAMOND_SWORD = P::new("swordDiamond", 3, 4, K::Sword(Diamond));
21 DIAMOND_SHOVEL = P::new("shovelDiamond", 3, 5, K::Tool(Shovel, Diamond));
22 DIAMOND_PICKAXE = P::new("pickaxeDiamond", 3, 6, K::Tool(Pickaxe, Diamond));
23 DIAMOND_AXE = P::new("hatchetDiamond", 3, 7, K::Tool(Axe, Diamond));
24 STICK = P::new("stick", 5, 3, K::Generic).full_3d();
25 BOWL = P::new("bowl", 7, 4, K::Generic);
26 MUSHROOM_STEW = P::new("mushroomStew", 8, 4, K::Soup { heal: 10 });
27 GOLDEN_SWORD = P::new("swordGold", 4, 4, K::Sword(Gold));
28 GOLDEN_SHOVEL = P::new("shovelGold", 4, 5, K::Tool(Shovel, Gold));
29 GOLDEN_PICKAXE = P::new("pickaxeGold", 4, 6, K::Tool(Pickaxe, Gold));
30 GOLDEN_AXE = P::new("hatchetGold", 4, 7, K::Tool(Axe, Gold));
31 STRING = P::new("string", 8, 0, K::Generic);
32 FEATHER = P::new("feather", 8, 1, K::Generic);
33 GUNPOWDER = P::new("sulphur", 8, 2, K::Generic);
34 WOODEN_HOE = P::new("hoeWood", 0, 8, K::Hoe(Wood));
35 STONE_HOE = P::new("hoeStone", 1, 8, K::Hoe(Stone));
36 IRON_HOE = P::new("hoeIron", 2, 8, K::Hoe(Iron));
37 DIAMOND_HOE = P::new("hoeDiamond", 3, 8, K::Hoe(Diamond));
38 GOLDEN_HOE = P::new("hoeGold", 4, 8, K::Hoe(Gold));
39 SEEDS = P::new("seeds", 9, 0, K::Seeds);
40 WHEAT = P::new("wheat", 9, 1, K::Generic);
41 BREAD = P::new("bread", 9, 2, K::Food { heal: 5, wolf_food: false });
42 LEATHER_HELMET = P::new("helmetCloth", 0, 0, K::Armor { slot: 0, render: 0, tier: 0 });
43 LEATHER_CHESTPLATE = P::new("chestplateCloth", 0, 1, K::Armor { slot: 1, render: 0, tier: 0 });
44 LEATHER_LEGGINGS = P::new("leggingsCloth", 0, 2, K::Armor { slot: 2, render: 0, tier: 0 });
45 LEATHER_BOOTS = P::new("bootsCloth", 0, 3, K::Armor { slot: 3, render: 0, tier: 0 });
46 CHAIN_HELMET = P::new("helmetChain", 1, 0, K::Armor { slot: 0, render: 1, tier: 1 });
47 CHAIN_CHESTPLATE = P::new("chestplateChain", 1, 1, K::Armor { slot: 1, render: 1, tier: 1 });
48 CHAIN_LEGGINGS = P::new("leggingsChain", 1, 2, K::Armor { slot: 2, render: 1, tier: 1 });
49 CHAIN_BOOTS = P::new("bootsChain", 1, 3, K::Armor { slot: 3, render: 1, tier: 1 });
50 IRON_HELMET = P::new("helmetIron", 2, 0, K::Armor { slot: 0, render: 2, tier: 2 });
51 IRON_CHESTPLATE = P::new("chestplateIron", 2, 1, K::Armor { slot: 1, render: 2, tier: 2 });
52 IRON_LEGGINGS = P::new("leggingsIron", 2, 2, K::Armor { slot: 2, render: 2, tier: 2 });
53 IRON_BOOTS = P::new("bootsIron", 2, 3, K::Armor { slot: 3, render: 2, tier: 2 });
54 DIAMOND_HELMET = P::new("helmetDiamond", 3, 0, K::Armor { slot: 0, render: 3, tier: 3 });
55 DIAMOND_CHESTPLATE = P::new("chestplateDiamond", 3, 1, K::Armor { slot: 1, render: 3, tier: 3 });
56 DIAMOND_LEGGINGS = P::new("leggingsDiamond", 3, 2, K::Armor { slot: 2, render: 3, tier: 3 });
57 DIAMOND_BOOTS = P::new("bootsDiamond", 3, 3, K::Armor { slot: 3, render: 3, tier: 3 });
58 GOLDEN_HELMET = P::new("helmetGold", 4, 0, K::Armor { slot: 0, render: 4, tier: 1 });
59 GOLDEN_CHESTPLATE = P::new("chestplateGold", 4, 1, K::Armor { slot: 1, render: 4, tier: 1 });
60 GOLDEN_LEGGINGS = P::new("leggingsGold", 4, 2, K::Armor { slot: 2, render: 4, tier: 1 });
61 GOLDEN_BOOTS = P::new("bootsGold", 4, 3, K::Armor { slot: 3, render: 4, tier: 1 });
62 FLINT = P::new("flint", 6, 0, K::Generic);
63 RAW_PORKCHOP = P::new("porkchopRaw", 7, 5, K::Food { heal: 3, wolf_food: true });
64 COOKED_PORKCHOP = P::new("porkchopCooked", 8, 5, K::Food { heal: 8, wolf_food: true });
65 PAINTING = P::new("painting", 10, 1, K::Painting);
66 GOLDEN_APPLE = P::new("appleGold", 11, 0, K::Food { heal: 42, wolf_food: false });
67 SIGN = P::new("sign", 10, 2, K::Sign);
68 WOODEN_DOOR = P::new("doorWood", 11, 2, K::Door(B::WOODEN_DOOR));
69 BUCKET = P::new("bucket", 10, 4, K::Bucket(Some(B::AIR)));
70 WATER_BUCKET = P::new("bucketWater", 11, 4, K::Bucket(Some(B::FLOWING_WATER))).container(Item::BUCKET);
71 LAVA_BUCKET = P::new("bucketLava", 12, 4, K::Bucket(Some(B::FLOWING_LAVA))).container(Item::BUCKET);
72 MINECART = P::new("minecart", 7, 8, K::Minecart(0));
73 SADDLE = P::new("saddle", 8, 6, K::Saddle);
74 IRON_DOOR = P::new("doorIron", 12, 2, K::Door(B::IRON_DOOR));
75 REDSTONE = P::new("redstone", 8, 3, K::Redstone);
76 SNOWBALL = P::new("snowball", 14, 0, K::Snowball);
77 BOAT = P::new("boat", 8, 8, K::Boat);
78 LEATHER = P::new("leather", 7, 6, K::Generic);
79 MILK_BUCKET = P::new("milk", 13, 4, K::Bucket(None)).container(Item::BUCKET);
80 BRICK = P::new("brick", 6, 1, K::Generic);
81 CLAY_BALL = P::new("clay", 9, 3, K::Generic);
82 SUGAR_CANE = P::new("reeds", 11, 1, K::Places(B::SUGAR_CANE));
83 PAPER = P::new("paper", 10, 3, K::Generic);
84 BOOK = P::new("book", 11, 3, K::Generic);
85 SLIME_BALL = P::new("slimeball", 14, 1, K::Generic);
86 CHEST_MINECART = P::new("minecartChest", 7, 9, K::Minecart(1));
87 FURNACE_MINECART = P::new("minecartFurnace", 7, 10, K::Minecart(2));
88 EGG = P::new("egg", 12, 0, K::Egg);
89 COMPASS = P::new("compass", 6, 3, K::Generic);
90 FISHING_ROD = P::new("fishingRod", 5, 4, K::FishingRod);
91 CLOCK = P::new("clock", 6, 4, K::Generic);
92 GLOWSTONE_DUST = P::new("yellowDust", 9, 4, K::Generic);
93 RAW_FISH = P::new("fishRaw", 9, 5, K::Food { heal: 2, wolf_food: false });
94 COOKED_FISH = P::new("fishCooked", 10, 5, K::Food { heal: 5, wolf_food: false });
95 DYE = P::new("dyePowder", 14, 4, K::Dye);
96 BONE = P::new("bone", 12, 1, K::Generic).full_3d();
97 SUGAR = P::new("sugar", 13, 0, K::Generic).full_3d();
98 CAKE = P::new("cake", 13, 1, K::Places(B::CAKE)).stack(1);
99 BED = P::new("bed", 13, 2, K::Bed).stack(1);
100 REPEATER = P::new("diode", 6, 5, K::Places(B::REPEATER));
101 COOKIE = P::new("cookie", 12, 5, K::Food { heal: 1, wolf_food: false }).stack(8);
102 MAP = P::new("map", 12, 3, K::Map);
103 SHEARS = P::new("shears", 13, 5, K::Shears);
2000 RECORD_13 = P::new("record", 0, 15, K::Record("13"));
2001 RECORD_CAT = P::new("record", 1, 15, K::Record("cat"));
}
impl Item {
/// The record `ItemRecord` plays, for record items.
pub fn record_name(self) -> Option<&'static str> {
match self.properties()?.kind {
ItemKind::Record(name) => Some(name),
_ => None,
}
}
pub const fn from_block(block: crate::world::Block) -> Self {
Self(block.0 as i16)
}
/// The block an `ItemBlock` places.
pub fn block(self) -> Option<crate::world::Block> {
u8::try_from(self.0)
.ok()
.map(crate::world::Block)
.filter(|block| block.properties().is_some())
}
pub fn properties(self) -> Option<ItemProperties> {
if let Some(block) = self.block() {
let properties = block.properties()?;
return Some(ItemProperties {
name: properties.name,
icon: properties.texture,
has_subtypes: matches!(block, B::WOOL | B::LOG | B::LEAVES | B::SAPLING | B::SLAB),
..ItemProperties::new("", 0, 0, ItemKind::Block)
});
}
usize::try_from(self.0)
.ok()
.and_then(|i| ITEMS.get(i).copied().flatten())
}
pub fn max_stack(self) -> i8 {
self.properties().map_or(64, |p| p.max_stack)
}
pub fn max_damage(self) -> i16 {
self.properties().map_or(0, |p| p.max_damage)
}
/// `Item.isDamagable`.
pub fn is_damageable(self) -> bool {
self.properties()
.is_some_and(|p| p.max_damage > 0 && !p.has_subtypes)
}
}
/// `TileEntityFurnace.getItemBurnTime`.
pub fn burn_time(item: Item) -> i16 {
match item {
Item::STICK => 100,
Item::COAL => 1600,
Item::LAVA_BUCKET => 20000,
_ if item == Item::from_block(B::SAPLING) => 100,
_ => match item.block() {
Some(block) if block.material() == crate::world::Material::Wood => 300,
_ => 0,
},
}
}
/// Port of `ItemStack`. Counts and damage are `int` like the original and only narrowed
/// when written to NBT or the network.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct ItemStack {
pub item: Item,
pub count: i32,
pub damage: i32,
}
/// `ItemDye.dyeColorNames`.
const DYE_COLORS: [&str; 16] = [
"black",
"red",
"green",
"brown",
"blue",
"purple",
"cyan",
"silver",
"gray",
"pink",
"lime",
"yellow",
"lightBlue",
"magenta",
"orange",
"white",
];
impl ItemStack {
/// `ItemStack.getItemName`: the translation key, which some items vary by their damage.
pub fn translation_key(self) -> Option<String> {
let color = |index: i32| DYE_COLORS[(index & 15) as usize];
if let Some(block) = self.item.block() {
let name = block.properties()?.name;
return Some(match block {
B::WOOL => format!("tile.{name}.{}", color(!self.damage)),
B::SLAB => {
let kind = ["stone", "sand", "wood", "cobble"].get(self.damage as usize)?;
format!("tile.{name}.{kind}")
}
_ => format!("tile.{name}"),
});
}
let name = self.item.properties()?.name;
Some(match self.item {
Item::DYE => format!("item.{name}.{}", color(self.damage)),
Item::COAL if self.damage == 1 => "item.charcoal".into(),
_ => format!("item.{name}"),
})
}
pub const fn new(item: Item, count: i32, damage: i32) -> Self {
Self {
item,
count,
damage,
}
}
pub const fn one(item: Item) -> Self {
Self::new(item, 1, 0)
}
/// `ItemStack.writeToNBT`.
pub fn write_nbt(&self, nbt: &mut Compound) {
nbt.set_short("id", self.item.0);
nbt.set_byte("Count", self.count as i8);
nbt.set_short("Damage", self.damage as i16);
}
/// `new ItemStack(NBTTagCompound)`.
pub fn read_nbt(nbt: &Compound) -> Self {
Self::new(
Item(nbt.short("id")),
i32::from(nbt.byte("Count")),
i32::from(nbt.short("Damage")),
)
}
/// `splitStack`: takes `count` off this stack.
pub fn split(&mut self, count: i32) -> Self {
self.count -= count;
Self::new(self.item, count, self.damage)
}
pub fn max_stack(&self) -> i32 {
i32::from(self.item.max_stack())
}
pub fn max_damage(&self) -> i32 {
i32::from(self.item.max_damage())
}
pub fn is_damageable(&self) -> bool {
self.max_damage() > 0
}
pub fn is_damaged(&self) -> bool {
self.is_damageable() && self.damage > 0
}
pub fn has_subtypes(&self) -> bool {
self.item.properties().is_some_and(|p| p.has_subtypes)
}
/// `isStackable`.
pub fn is_stackable(&self) -> bool {
self.max_stack() > 1 && !self.is_damaged()
}
/// `isItemEqual`: same item and damage.
pub fn same_item(&self, other: &Self) -> bool {
self.item == other.item && self.damage == other.damage
}
/// `damageItem`. Returns true when the item broke.
pub fn damage_item(&mut self, amount: i32) -> bool {
if !self.is_damageable() {
return false;
}
self.damage += amount;
if self.damage > self.max_damage() {
self.count = (self.count - 1).max(0);
self.damage = 0;
return true;
}
false
}
/// `getStrVsBlock`: the mining speed factor against a block.
pub fn strength_against(&self, block: crate::world::Block) -> f32 {
match self.item.properties().map(|p| p.kind) {
Some(ItemKind::Tool(class, material)) if tool_effective(class, block) => {
material.stats().2
}
Some(ItemKind::Sword(_)) if block == B::WEB => 15.0,
Some(ItemKind::Sword(_)) => 1.5,
Some(ItemKind::Shears) if matches!(block, B::WEB | B::LEAVES) => 15.0,
Some(ItemKind::Shears) if block == B::WOOL => 5.0,
_ => 1.0,
}
}
/// `canHarvestBlock`: whether the item can collect a block that needs a tool.
pub fn can_harvest(&self, block: crate::world::Block) -> bool {
use crate::world::Material;
match self.item.properties().map(|p| p.kind) {
Some(ItemKind::Tool(ToolClass::Pickaxe, material)) => {
let level = material.stats().0;
match block {
B::OBSIDIAN => level == 3,
B::DIAMOND_BLOCK | B::DIAMOND_ORE | B::GOLD_BLOCK | B::GOLD_ORE => level >= 2,
B::IRON_BLOCK | B::IRON_ORE | B::LAPIS_BLOCK | B::LAPIS_ORE => level >= 1,
B::REDSTONE_ORE | B::LIT_REDSTONE_ORE => level >= 2,
_ => matches!(block.material(), Material::Rock | Material::Iron),
}
}
Some(ItemKind::Tool(ToolClass::Shovel, _)) => matches!(block, B::SNOW_LAYER | B::SNOW),
Some(ItemKind::Sword(_) | ItemKind::Shears) => block == B::WEB,
_ => false,
}
}
/// `getDamageVsEntity`.
pub fn damage_vs_entity(&self) -> i32 {
match self.item.properties().map(|p| p.kind) {
Some(ItemKind::Tool(class, material)) => {
let base = match class {
ToolClass::Shovel => 1,
ToolClass::Pickaxe => 2,
ToolClass::Axe => 3,
};
base + material.stats().0
}
Some(ItemKind::Sword(material)) => 4 + material.stats().0 * 2,
_ => 1,
}
}
}
/// `ItemTool.blocksEffectiveAgainst` of shovels, pickaxes and axes.
fn tool_effective(class: ToolClass, block: crate::world::Block) -> bool {
match class {
ToolClass::Shovel => matches!(
block,
B::GRASS
| B::DIRT
| B::SAND
| B::GRAVEL
| B::SNOW_LAYER
| B::SNOW
| B::CLAY
| B::FARMLAND
),
ToolClass::Pickaxe => matches!(
block,
B::COBBLESTONE
| B::DOUBLE_SLAB
| B::SLAB
| B::STONE
| B::SANDSTONE
| B::MOSSY_COBBLESTONE
| B::IRON_ORE
| B::IRON_BLOCK
| B::COAL_ORE
| B::GOLD_BLOCK
| B::GOLD_ORE
| B::DIAMOND_ORE
| B::DIAMOND_BLOCK
| B::ICE
| B::NETHERRACK
| B::LAPIS_ORE
| B::LAPIS_BLOCK
),
ToolClass::Axe => matches!(block, B::PLANKS | B::BOOKSHELF | B::LOG | B::CHEST),
}
}
/// A slot list as written by containers: compounds with a `Slot` byte.
pub fn write_slots(slots: &[Option<ItemStack>]) -> Vec<Tag> {
slot_tags(slots.iter().enumerate())
}
/// Compounds of the filled slots with their `Slot` byte.
pub(crate) fn slot_tags<'a>(
slots: impl Iterator<Item = (usize, &'a Option<ItemStack>)>,
) -> Vec<Tag> {
slots
.filter_map(|(slot, stack)| {
let stack = stack.as_ref()?;
let mut nbt = Compound::default();
nbt.set_byte("Slot", slot as i8);
stack.write_nbt(&mut nbt);
Some(Tag::Compound(nbt))
})
.collect()
}
/// Reads a slot list into `slots`, ignoring out-of-range slots like the original.
pub fn read_slots(nbt: &Compound, name: &str, slots: &mut [Option<ItemStack>]) {
for entry in nbt.compounds(name) {
let slot = usize::from(entry.byte("Slot") as u8);
if let Some(target) = slots.get_mut(slot) {
*target = Some(ItemStack::read_nbt(entry));
}
}
}