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 / inventory / container.rs

use super::{PlayerInventory, armor_accepts, compatible, merge, take};
use crate::crafting::{self, Grid};
use crate::item::ItemStack;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ContainerKind {
    Inventory,
    Workbench,
    Chest { slots: usize },
    Furnace,
    Dispenser,
}

impl ContainerKind {
    pub fn inventory_start(self) -> usize {
        match self {
            Self::Inventory => 9,
            Self::Workbench => 10,
            _ => self.external_count(),
        }
    }

    pub fn slot_count(self) -> usize {
        self.inventory_start() + 36
    }

    pub fn external_count(self) -> usize {
        match self {
            Self::Chest { slots } => slots,
            Self::Furnace => 3,
            Self::Dispenser => 9,
            _ => 0,
        }
    }
}

#[derive(Clone, Debug)]
pub struct Container {
    pub kind: ContainerKind,
    pub craft: [Option<ItemStack>; 9],
    pub carried: Option<ItemStack>,
}

#[derive(Default, Debug)]
pub struct ClickResult {
    pub item: Option<ItemStack>,
    pub dropped: Option<ItemStack>,
    pub crafted: Option<ItemStack>,
}

#[derive(Clone, Copy)]
enum Slot {
    Result,
    Craft(usize),
    Armor(usize),
    Main(usize),
    External(usize),
}

impl Container {
    pub fn new(kind: ContainerKind) -> Self {
        Self {
            kind,
            craft: [None; 9],
            carried: None,
        }
    }

    fn slot(&self, index: usize) -> Option<Slot> {
        let base = self.kind.inventory_start();
        if index >= self.kind.slot_count() {
            return None;
        }
        if index >= base {
            let index = index - base;
            return Some(Slot::Main(if index < 27 { index + 9 } else { index - 27 }));
        }
        Some(match self.kind {
            ContainerKind::Inventory if index == 0 => Slot::Result,
            ContainerKind::Inventory if index <= 4 => Slot::Craft(index - 1),
            ContainerKind::Inventory => Slot::Armor(8 - index),
            ContainerKind::Workbench if index == 0 => Slot::Result,
            ContainerKind::Workbench => Slot::Craft(index - 1),
            _ => Slot::External(index),
        })
    }

    pub fn result(&self) -> Option<ItemStack> {
        let width = match self.kind {
            ContainerKind::Inventory => 2,
            ContainerKind::Workbench => 3,
            _ => return None,
        };
        crafting::find(&Grid {
            width,
            slots: &self.craft[..width * width],
        })
    }

    pub fn get(
        &self,
        inventory: &PlayerInventory,
        external: &[Option<ItemStack>],
        index: usize,
    ) -> Option<ItemStack> {
        match self.slot(index)? {
            Slot::Result => self.result(),
            Slot::Craft(i) => self.craft[i],
            Slot::Armor(i) => inventory.armor[i],
            Slot::Main(i) => inventory.main[i],
            Slot::External(i) => external.get(i).copied().flatten(),
        }
    }

    pub fn set(
        &mut self,
        inventory: &mut PlayerInventory,
        external: &mut [Option<ItemStack>],
        index: usize,
        value: Option<ItemStack>,
    ) {
        match self.slot(index) {
            Some(Slot::Craft(i)) => self.craft[i] = value,
            Some(Slot::Armor(i)) => inventory.armor[i] = value,
            Some(Slot::Main(i)) => inventory.main[i] = value,
            Some(Slot::External(i)) if i < external.len() => external[i] = value,
            _ => {}
        }
    }

    pub fn contents(
        &self,
        inventory: &PlayerInventory,
        external: &[Option<ItemStack>],
    ) -> Vec<Option<ItemStack>> {
        (0..self.kind.slot_count())
            .map(|i| self.get(inventory, external, i))
            .collect()
    }

    fn accepts(&self, index: usize, stack: ItemStack) -> bool {
        match self.slot(index) {
            None | Some(Slot::Result) => false,
            Some(Slot::Armor(i)) => armor_accepts(i, stack),
            Some(Slot::External(2)) if self.kind == ContainerKind::Furnace => false,
            _ => true,
        }
    }

    fn limit(&self, index: usize) -> i32 {
        if matches!(self.slot(index), Some(Slot::Armor(_))) {
            1
        } else {
            64
        }
    }

    fn consume_recipe(&mut self) {
        for slot in &mut self.craft {
            if let Some(ingredient) = take(slot, 1)
                && let Some(container) = ingredient.item.properties().and_then(|p| p.container)
            {
                *slot = Some(ItemStack::one(container));
            }
        }
    }

    pub fn click(
        &mut self,
        inventory: &mut PlayerInventory,
        external: &mut [Option<ItemStack>],
        index: i16,
        button: i8,
        shift: bool,
    ) -> ClickResult {
        if !(0..=1).contains(&button) {
            return ClickResult::default();
        }
        if index == -999 {
            let dropped = take(&mut self.carried, if button == 0 { i32::MAX } else { 1 });
            return ClickResult {
                dropped,
                ..ClickResult::default()
            };
        }
        let Ok(index) = usize::try_from(index) else {
            return ClickResult::default();
        };
        let Some(slot) = self.slot(index) else {
            return ClickResult::default();
        };
        let before = self.get(inventory, external, index);
        let crafting_slot = matches!(slot, Slot::Result);
        if shift {
            let item = self.transfer(inventory, external, index);
            return ClickResult {
                item,
                crafted: item.filter(|_| crafting_slot),
                ..ClickResult::default()
            };
        }
        if crafting_slot {
            let crafted = before.filter(|crafted| {
                self.carried.is_none_or(|held| {
                    compatible(held, *crafted) && held.count + crafted.count <= held.max_stack()
                })
            });
            if let Some(crafted) = crafted {
                let count = self.carried.map_or(0, |held| held.count) + crafted.count;
                self.carried = Some(ItemStack {
                    count,
                    ..self.carried.unwrap_or(crafted)
                });
                self.consume_recipe();
            }
            return ClickResult {
                item: before,
                crafted,
                ..ClickResult::default()
            };
        }
        let mut value = before;
        match (value, self.carried) {
            (Some(stack), None) => {
                self.carried = take(
                    &mut value,
                    if button == 0 {
                        stack.count
                    } else {
                        (stack.count + 1) / 2
                    },
                )
            }
            (_, Some(mut held)) if self.accepts(index, held) => {
                if value.is_none_or(|other| compatible(other, held)) {
                    let count = if button == 0 { held.count } else { 1 };
                    let mut part = ItemStack::new(held.item, count, held.damage);
                    merge(&mut value, &mut part, self.limit(index));
                    held.count -= count - part.count;
                    self.carried = (held.count > 0).then_some(held);
                } else if held.count <= self.limit(index).min(held.max_stack()) {
                    self.carried = value;
                    value = Some(held);
                }
            }
            (Some(stack), Some(mut held))
                if compatible(stack, held) && stack.count + held.count <= held.max_stack() =>
            {
                held.count += stack.count;
                self.carried = Some(held);
                value = None;
            }
            _ => {}
        }
        self.set(inventory, external, index, value);
        ClickResult {
            item: before,
            ..ClickResult::default()
        }
    }

    fn transfer(
        &mut self,
        inventory: &mut PlayerInventory,
        external: &mut [Option<ItemStack>],
        index: usize,
    ) -> Option<ItemStack> {
        let original = self.get(inventory, external, index)?;
        // `ContainerDispenser` keeps the base `getStackInSlot`, which moves nothing
        if self.kind == ContainerKind::Dispenser {
            return Some(original);
        }
        let mut stack = original;
        let base = self.kind.inventory_start();
        let (start, end, reverse) = if index < base {
            (
                base,
                base + 36,
                matches!(self.slot(index), Some(Slot::Result))
                    || matches!(self.kind, ContainerKind::Chest { .. })
                    || index == 2 && self.kind == ContainerKind::Furnace,
            )
        } else if matches!(self.kind, ContainerKind::Chest { .. }) {
            (0, base, false)
        } else if index < base + 27 {
            (base + 27, base + 36, false)
        } else {
            (base, base + 27, false)
        };
        let crafting = matches!(self.slot(index), Some(Slot::Result));
        // Crafting transfers commit only when the complete output fits
        let mut changes = Vec::new();
        for empty in [false, true] {
            for offset in 0..end - start {
                let target = if reverse {
                    end - 1 - offset
                } else {
                    start + offset
                };
                if !self.accepts(target, stack) {
                    continue;
                }
                let mut value = self.get(inventory, external, target);
                if value.is_none() != empty {
                    continue;
                }
                let before = value;
                merge(&mut value, &mut stack, self.limit(target));
                if value != before {
                    changes.push((target, value));
                }
                if stack.count == 0 {
                    break;
                }
            }
            if stack.count == 0 {
                break;
            }
        }
        if stack.count == original.count || crafting && stack.count != 0 {
            return None;
        }
        for (target, value) in changes {
            self.set(inventory, external, target, value);
        }
        if crafting {
            self.consume_recipe();
        } else {
            self.set(
                inventory,
                external,
                index,
                (stack.count > 0).then_some(stack),
            );
        }
        Some(original)
    }

    pub fn close(&mut self) -> Vec<ItemStack> {
        let mut drops: Vec<_> = self.carried.take().into_iter().collect();
        drops.extend(self.craft.iter_mut().filter_map(Option::take));
        drops
    }
}