Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
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
Download file

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));
        }
    }
}