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 / crafting.rs

//! Port of `CraftingManager` with its shaped and shapeless recipes.

use std::cmp::Reverse;
use std::sync::LazyLock;

use crate::item::{Item, ItemKind, ItemStack, ToolClass::*, ToolMaterial::*};
use crate::world::Block;

/// Damage that matches any ingredient damage.
const ANY: i32 = -1;

/// A recipe ingredient as the original stores it: a block matches any damage, an item only damage 0.
#[derive(Clone, Copy)]
enum Ingredient {
    Block(Block),
    Item(Item),
    Stack(Item, i32),
}

impl Ingredient {
    /// The stack the original recipe holds for this ingredient.
    fn stack(self, shaped: bool) -> ItemStack {
        match self {
            Self::Block(block) => {
                ItemStack::new(Item::from_block(block), 1, if shaped { ANY } else { 0 })
            }
            Self::Item(item) => ItemStack::new(item, 1, 0),
            Self::Stack(item, damage) => ItemStack::new(item, 1, damage),
        }
    }
}

fn matches(pattern: &ItemStack, stack: &ItemStack) -> bool {
    pattern.item == stack.item && (pattern.damage == ANY || pattern.damage == stack.damage)
}

struct Recipe {
    /// Width and height of a shaped recipe, `None` for a shapeless one.
    shape: Option<(usize, usize)>,
    items: Vec<Option<ItemStack>>,
    output: ItemStack,
}

/// A crafting grid: `width` columns, row-major.
pub struct Grid<'a> {
    pub width: usize,
    pub slots: &'a [Option<ItemStack>],
}

impl Grid<'_> {
    /// `InventoryCrafting.getStackInRowAndColumn`.
    fn at(&self, x: usize, y: usize) -> Option<&ItemStack> {
        if x >= self.width {
            return None;
        }
        self.slots.get(x + y * self.width)?.as_ref()
    }
}

impl Recipe {
    fn matches(&self, grid: &Grid) -> bool {
        let cells = || (0..3).flat_map(|y| (0..3).map(move |x| (x, y)));
        let Some((width, height)) = self.shape else {
            let mut remaining: Vec<_> = self.items.iter().flatten().collect();
            for stack in cells().filter_map(|(x, y)| grid.at(x, y)) {
                match remaining.iter().position(|pattern| matches(pattern, stack)) {
                    Some(i) => remaining.remove(i),
                    None => return false,
                };
            }
            return remaining.is_empty();
        };
        let fits = |ox: usize, oy: usize, flipped: bool| {
            cells().all(|(x, y)| {
                let inside = x >= ox && y >= oy && x - ox < width && y - oy < height;
                let expected = inside.then(|| {
                    let rx = if flipped { width + ox - 1 - x } else { x - ox };
                    self.items[rx + (y - oy) * width]
                });
                match (expected.flatten(), grid.at(x, y)) {
                    (None, None) => true,
                    (Some(pattern), Some(stack)) => matches(&pattern, stack),
                    _ => false,
                }
            })
        };
        (0..=3 - width)
            .any(|ox| (0..=3 - height).any(|oy| fits(ox, oy, true) || fits(ox, oy, false)))
    }
}

/// `CraftingManager.findMatchingRecipe`.
pub fn find(grid: &Grid) -> Option<ItemStack> {
    RECIPES
        .iter()
        .find(|recipe| recipe.matches(grid))
        .map(|recipe| recipe.output)
}

struct Builder(Vec<Recipe>);

impl Builder {
    fn shaped(&mut self, output: ItemStack, rows: &[&str], keys: &[(char, Ingredient)]) {
        let key = |c| {
            keys.iter()
                .find(|(k, _)| *k == c)
                .map(|(_, i)| i.stack(true))
        };
        self.0.push(Recipe {
            shape: Some((rows.last().map_or(0, |row| row.len()), rows.len())),
            items: rows.concat().chars().map(key).collect(),
            output,
        });
    }

    fn shapeless(&mut self, output: ItemStack, ingredients: &[Ingredient]) {
        self.0.push(Recipe {
            shape: None,
            items: ingredients.iter().map(|i| Some(i.stack(false))).collect(),
            output,
        });
    }
}

/// `r.shaped` with the key list written like the original: `'#' => ingredient`.
macro_rules! shaped {
    ($r:ident, $output:expr, $rows:expr, $($key:literal => $ingredient:expr),+) => {
        $r.shaped($output, $rows, &[$(($key, $ingredient)),+])
    };
}

/// The item of the item table with this kind.
fn item_of(kind: ItemKind) -> Item {
    (256..i16::MAX)
        .map(Item)
        .find(|item| item.properties().is_some_and(|p| p.kind == kind))
        .expect("item kind in the item table")
}

static RECIPES: LazyLock<Vec<Recipe>> = LazyLock::new(|| {
    use Ingredient::{Block as B, Item as I, Stack as S};
    let mut r = Builder(Vec::new());
    let stack = |item: Item, count: i32| ItemStack::new(item, count, 0);
    let block = |block: Block, count: i32| ItemStack::new(Item::from_block(block), count, 0);
    let (planks, cobble, stone) = (B(Block::PLANKS), B(Block::COBBLESTONE), B(Block::STONE));
    let (stick, iron, redstone) = (I(Item::STICK), I(Item::IRON_INGOT), I(Item::REDSTONE));
    let tiers = [Wood, Stone, Iron, Diamond, Gold];
    let materials = [planks, cobble, iron, I(Item::DIAMOND), I(Item::GOLD_INGOT)];

    // RecipesTools
    for (material, tier) in materials.into_iter().zip(tiers) {
        for (rows, kind) in [
            (&["XXX", " # ", " # "][..], ItemKind::Tool(Pickaxe, tier)),
            (&["X", "#", "#"], ItemKind::Tool(Shovel, tier)),
            (&["XX", "X#", " #"], ItemKind::Tool(Axe, tier)),
            (&["XX", " #", " #"], ItemKind::Hoe(tier)),
        ] {
            shaped!(r, stack(item_of(kind), 1), rows, '#' => stick, 'X' => material);
        }
    }
    shaped!(r, stack(Item::SHEARS, 1), &[" #", "# "], '#' => iron);

    // RecipesWeapons
    for (material, tier) in materials.into_iter().zip(tiers) {
        let sword = item_of(ItemKind::Sword(tier));
        shaped!(r, stack(sword, 1), &["X", "X", "#"], '#' => stick, 'X' => material);
    }
    shaped!(r, stack(Item::BOW, 1), &[" #X", "# X", " #X"], 'X' => I(Item::STRING), '#' => stick);
    shaped!(r, stack(Item::ARROW, 4), &["X", "#", "Y"],
        'Y' => I(Item::FEATHER), 'X' => I(Item::FLINT), '#' => stick);

    // RecipesIngots
    for (storage, ingot) in [
        (Block::GOLD_BLOCK, ItemStack::new(Item::GOLD_INGOT, 9, 0)),
        (Block::IRON_BLOCK, ItemStack::new(Item::IRON_INGOT, 9, 0)),
        (Block::DIAMOND_BLOCK, ItemStack::new(Item::DIAMOND, 9, 0)),
        (Block::LAPIS_BLOCK, ItemStack::new(Item::DYE, 9, 4)),
    ] {
        shaped!(r, block(storage, 1), &["###", "###", "###"], '#' => S(ingot.item, ingot.damage));
        shaped!(r, ingot, &["#"], '#' => B(storage));
    }

    // RecipesFood
    for (x, y) in [
        (Block::BROWN_MUSHROOM, Block::RED_MUSHROOM),
        (Block::RED_MUSHROOM, Block::BROWN_MUSHROOM),
    ] {
        shaped!(r, stack(Item::MUSHROOM_STEW, 1), &["Y", "X", "#"],
            'X' => B(x), 'Y' => B(y), '#' => I(Item::BOWL));
    }
    shaped!(r, stack(Item::COOKIE, 8), &["#X#"], 'X' => S(Item::DYE, 3), '#' => I(Item::WHEAT));

    // RecipesCrafting
    shaped!(r, block(Block::CHEST, 1), &["###", "# #", "###"], '#' => planks);
    shaped!(r, block(Block::FURNACE, 1), &["###", "# #", "###"], '#' => cobble);
    shaped!(r, block(Block::CRAFTING_TABLE, 1), &["##", "##"], '#' => planks);
    shaped!(r, block(Block::SANDSTONE, 1), &["##", "##"], '#' => B(Block::SAND));

    // RecipesArmor
    let materials = [
        I(Item::LEATHER),
        B(Block::FIRE),
        iron,
        I(Item::DIAMOND),
        I(Item::GOLD_INGOT),
    ];
    for (material_index, material) in (0..).zip(materials) {
        for (slot, rows) in (0..).zip([
            &["XXX", "X X"][..],
            &["X X", "XXX", "XXX"],
            &["XXX", "X X", "X X"],
            &["X X", "X X"],
        ]) {
            // `ItemArmor` ids run from helmet to boots, material by material
            let armor = Item(Item::LEATHER_HELMET.0 + 4 * material_index + slot);
            shaped!(r, stack(armor, 1), rows, 'X' => material);
        }
    }

    // RecipesDyes
    let dye = |damage: i32| S(Item::DYE, damage);
    let wool = Item::from_block(Block::WOOL);
    for color in 0..16 {
        r.shapeless(
            ItemStack::new(wool, 1, !color & 15),
            &[dye(color), S(wool, 0)],
        );
    }
    r.shapeless(ItemStack::new(Item::DYE, 2, 11), &[B(Block::DANDELION)]);
    r.shapeless(ItemStack::new(Item::DYE, 2, 1), &[B(Block::ROSE)]);
    r.shapeless(ItemStack::new(Item::DYE, 3, 15), &[I(Item::BONE)]);
    for (count, damage, inputs) in [
        (2, 9, &[1, 15][..]),
        (2, 14, &[1, 11]),
        (2, 10, &[2, 15]),
        (2, 8, &[0, 15]),
        (2, 7, &[8, 15]),
        (3, 7, &[0, 15, 15]),
        (2, 12, &[4, 15]),
        (2, 6, &[4, 2]),
        (2, 5, &[4, 1]),
        (2, 13, &[5, 9]),
        (3, 13, &[4, 1, 9]),
        (4, 13, &[4, 1, 1, 15]),
    ] {
        let ingredients: Vec<Ingredient> = inputs.iter().map(|&d| dye(d)).collect();
        r.shapeless(ItemStack::new(Item::DYE, count, damage), &ingredients);
    }

    // CraftingManager
    shaped!(r, stack(Item::PAPER, 3), &["###"], '#' => I(Item::SUGAR_CANE));
    shaped!(r, stack(Item::BOOK, 1), &["#", "#", "#"], '#' => I(Item::PAPER));
    shaped!(r, block(Block::FENCE, 2), &["###", "###"], '#' => stick);
    shaped!(r, block(Block::JUKEBOX, 1), &["###", "#X#", "###"],
        '#' => planks, 'X' => I(Item::DIAMOND));
    shaped!(r, block(Block::NOTE_BLOCK, 1), &["###", "#X#", "###"], '#' => planks, 'X' => redstone);
    shaped!(r, block(Block::BOOKSHELF, 1), &["###", "XXX", "###"],
        '#' => planks, 'X' => I(Item::BOOK));
    shaped!(r, block(Block::SNOW, 1), &["##", "##"], '#' => I(Item::SNOWBALL));
    shaped!(r, block(Block::CLAY, 1), &["##", "##"], '#' => I(Item::CLAY_BALL));
    shaped!(r, block(Block::BRICKS, 1), &["##", "##"], '#' => I(Item::BRICK));
    shaped!(r, block(Block::GLOWSTONE, 1), &["##", "##"], '#' => I(Item::GLOWSTONE_DUST));
    shaped!(r, block(Block::WOOL, 1), &["##", "##"], '#' => I(Item::STRING));
    shaped!(r, block(Block::TNT, 1), &["X#X", "#X#", "X#X"],
        'X' => I(Item::GUNPOWDER), '#' => B(Block::SAND));
    for (damage, material) in [
        (3, cobble),
        (0, stone),
        (1, B(Block::SANDSTONE)),
        (2, planks),
    ] {
        let slab = ItemStack::new(Item::from_block(Block::SLAB), 3, damage);
        shaped!(r, slab, &["###"], '#' => material);
    }
    shaped!(r, block(Block::LADDER, 2), &["# #", "###", "# #"], '#' => stick);
    shaped!(r, stack(Item::WOODEN_DOOR, 1), &["##", "##", "##"], '#' => planks);
    shaped!(r, block(Block::TRAPDOOR, 2), &["###", "###"], '#' => planks);
    shaped!(r, stack(Item::IRON_DOOR, 1), &["##", "##", "##"], '#' => iron);
    shaped!(r, stack(Item::SIGN, 1), &["###", "###", " X "], '#' => planks, 'X' => stick);
    shaped!(r, stack(Item::CAKE, 1), &["AAA", "BEB", "CCC"],
        'A' => I(Item::MILK_BUCKET), 'B' => I(Item::SUGAR),
        'C' => I(Item::WHEAT), 'E' => I(Item::EGG));
    shaped!(r, stack(Item::SUGAR, 1), &["#"], '#' => I(Item::SUGAR_CANE));
    shaped!(r, block(Block::PLANKS, 4), &["#"], '#' => B(Block::LOG));
    shaped!(r, stack(Item::STICK, 4), &["#", "#"], '#' => planks);
    shaped!(r, block(Block::TORCH, 4), &["X", "#"], 'X' => I(Item::COAL), '#' => stick);
    shaped!(r, block(Block::TORCH, 4), &["X", "#"], 'X' => S(Item::COAL, 1), '#' => stick);
    shaped!(r, stack(Item::BOWL, 4), &["# #", " # "], '#' => planks);
    shaped!(r, block(Block::RAIL, 16), &["X X", "X#X", "X X"], 'X' => iron, '#' => stick);
    shaped!(r, block(Block::POWERED_RAIL, 6), &["X X", "X#X", "XRX"],
        'X' => I(Item::GOLD_INGOT), 'R' => redstone, '#' => stick);
    shaped!(r, block(Block::DETECTOR_RAIL, 6), &["X X", "X#X", "XRX"],
        'X' => iron, 'R' => redstone, '#' => B(Block::STONE_PRESSURE_PLATE));
    shaped!(r, stack(Item::MINECART, 1), &["# #", "###"], '#' => iron);
    shaped!(r, block(Block::JACK_O_LANTERN, 1), &["A", "B"],
        'A' => B(Block::PUMPKIN), 'B' => B(Block::TORCH));
    shaped!(r, stack(Item::CHEST_MINECART, 1), &["A", "B"],
        'A' => B(Block::CHEST), 'B' => I(Item::MINECART));
    shaped!(r, stack(Item::FURNACE_MINECART, 1), &["A", "B"],
        'A' => B(Block::FURNACE), 'B' => I(Item::MINECART));
    shaped!(r, stack(Item::BOAT, 1), &["# #", "###"], '#' => planks);
    shaped!(r, stack(Item::BUCKET, 1), &["# #", " # "], '#' => iron);
    shaped!(r, stack(Item::FLINT_AND_STEEL, 1), &["A ", " B"], 'A' => iron, 'B' => I(Item::FLINT));
    shaped!(r, stack(Item::BREAD, 1), &["###"], '#' => I(Item::WHEAT));
    shaped!(r, block(Block::OAK_STAIRS, 4), &["#  ", "## ", "###"], '#' => planks);
    shaped!(r, stack(Item::FISHING_ROD, 1), &["  #", " #X", "# X"],
        '#' => stick, 'X' => I(Item::STRING));
    shaped!(r, block(Block::COBBLESTONE_STAIRS, 4), &["#  ", "## ", "###"], '#' => cobble);
    shaped!(r, stack(Item::PAINTING, 1), &["###", "#X#", "###"],
        '#' => stick, 'X' => B(Block::WOOL));
    shaped!(r, stack(Item::GOLDEN_APPLE, 1), &["###", "#X#", "###"],
        '#' => B(Block::GOLD_BLOCK), 'X' => I(Item::APPLE));
    shaped!(r, block(Block::LEVER, 1), &["X", "#"], '#' => cobble, 'X' => stick);
    shaped!(r, block(Block::REDSTONE_TORCH, 1), &["X", "#"], '#' => stick, 'X' => redstone);
    shaped!(r, stack(Item::REPEATER, 1), &["#X#", "III"],
        '#' => B(Block::REDSTONE_TORCH), 'X' => redstone, 'I' => stone);
    shaped!(r, stack(Item::CLOCK, 1), &[" # ", "#X#", " # "],
        '#' => I(Item::GOLD_INGOT), 'X' => redstone);
    shaped!(r, stack(Item::COMPASS, 1), &[" # ", "#X#", " # "], '#' => iron, 'X' => redstone);
    shaped!(r, stack(Item::MAP, 1), &["###", "#X#", "###"],
        '#' => I(Item::PAPER), 'X' => I(Item::COMPASS));
    shaped!(r, block(Block::BUTTON, 1), &["#", "#"], '#' => stone);
    shaped!(r, block(Block::STONE_PRESSURE_PLATE, 1), &["##"], '#' => stone);
    shaped!(r, block(Block::WOODEN_PRESSURE_PLATE, 1), &["##"], '#' => planks);
    shaped!(r, block(Block::DISPENSER, 1), &["###", "#X#", "#R#"],
        '#' => cobble, 'X' => I(Item::BOW), 'R' => redstone);
    shaped!(r, block(Block::PISTON, 1), &["TTT", "#X#", "#R#"],
        '#' => cobble, 'X' => iron, 'R' => redstone, 'T' => planks);
    shaped!(r, block(Block::STICKY_PISTON, 1), &["S", "P"],
        'S' => I(Item::SLIME_BALL), 'P' => B(Block::PISTON));
    shaped!(r, stack(Item::BED, 1), &["###", "XXX"], '#' => B(Block::WOOL), 'X' => planks);

    // `RecipeSorter`: shaped before shapeless, then by decreasing size; the sort is stable.
    r.0.sort_by_key(|r| (r.shape.is_none(), Reverse(r.items.len())));
    r.0
});