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 / server / src / player / container.rs

use common::entity::vehicle::VehicleKind;
use common::inventory::{Container, ContainerKind};
use common::item::{Item, ItemStack};
use common::protocol::Packet;
use common::stats;
use common::world::shape::BlockView;
use common::world::{Block, TileEntity};
use glam::DVec3;

use super::ServerPlayer;
use super::interaction::center;
use crate::entity::Kind;
use crate::level::Level;

impl ServerPlayer {
    fn external_items(&self, level: &Level) -> Vec<Option<ItemStack>> {
        if let Some(id) = self.container_entity {
            return level
                .entities
                .get(id)
                .and_then(|e| match &e.kind {
                    Kind::Cart(cart) => Some(cart.items.to_vec()),
                    _ => None,
                })
                .unwrap_or_default();
        }
        let mut items = Vec::with_capacity(self.container.kind.external_count());
        for &[x, y, z] in &self.container_blocks {
            if let Some(slots) = level.tile_entity(x, y, z).and_then(TileEntity::inventory) {
                items.extend_from_slice(slots);
            }
        }
        items
    }

    pub(super) fn sync_container(&mut self, level: &Level) {
        self.last_contents = self
            .container
            .contents(&self.inventory, &self.external_items(level));
        self.send(Packet::WindowItems {
            window: self.window,
            items: self.last_contents.clone(),
        });
        self.send(Packet::SetSlot {
            window: -1,
            slot: -1,
            item: self.container.carried,
        });
        self.last_bars = [-1; 3];
        self.sync_progress(level);
    }

    pub(super) fn tick_container(&mut self, level: &Level) {
        let contents = self
            .container
            .contents(&self.inventory, &self.external_items(level));
        for (slot, &item) in contents.iter().enumerate() {
            if self.last_contents.get(slot) != Some(&item) {
                self.send(Packet::SetSlot {
                    window: self.window,
                    slot: slot as i16,
                    item,
                });
            }
        }
        self.last_contents = contents;
        self.sync_progress(level);
    }

    fn sync_progress(&mut self, level: &Level) {
        if self.container.kind == ContainerKind::Furnace
            && let Some(&[x, y, z]) = self.container_blocks.first()
            && let Some(TileEntity::Furnace {
                cook_time,
                burn_time,
                item_burn_time,
                ..
            }) = level.tile_entity(x, y, z)
        {
            for (bar, value) in [*cook_time, *burn_time, *item_burn_time]
                .into_iter()
                .enumerate()
            {
                if self.last_bars[bar] != value {
                    self.last_bars[bar] = value;
                    self.send(Packet::UpdateProgressBar {
                        window: self.window,
                        bar: bar as i16,
                        value,
                    });
                }
            }
        }
    }

    pub(super) fn container_usable(&self, level: &Level) -> bool {
        let near = |pos: DVec3| self.position(level).distance_squared(pos) <= 64.0;
        if let Some(id) = self.container_entity {
            return level.entities.get(id).is_some_and(|e| {
                !e.entity.dead
                    && matches!(&e.kind, Kind::Cart(cart) if cart.kind == VehicleKind::StorageCart)
                    && near(e.entity.pos)
            });
        }
        // `TileEntityChest.canInteractWith`: still the tile entity that was opened
        self.container_blocks
            .iter()
            .zip(&self.container_generations)
            .all(|(&position @ [x, y, z], &generation)| {
                let block = BlockView::block(level, x, y, z);
                level.tile_generation(position) == generation
                    && match self.container.kind {
                        ContainerKind::Workbench => block == Block::CRAFTING_TABLE,
                        ContainerKind::Chest { .. } => block == Block::CHEST,
                        ContainerKind::Furnace => {
                            matches!(block, Block::FURNACE | Block::LIT_FURNACE)
                        }
                        ContainerKind::Dispenser => block == Block::DISPENSER,
                        ContainerKind::Inventory => true,
                    }
                    && near(center(position))
            })
    }

    pub(super) fn open_container(&mut self, level: &mut Level, [x, y, z]: [i32; 3]) -> bool {
        let (kind, title, blocks) = match level.block(x, y, z) {
            Block::CRAFTING_TABLE => (ContainerKind::Workbench, "Crafting", vec![[x, y, z]]),
            Block::FURNACE | Block::LIT_FURNACE => {
                (ContainerKind::Furnace, "Furnace", vec![[x, y, z]])
            }
            Block::DISPENSER => (ContainerKind::Dispenser, "Trap", vec![[x, y, z]]),
            Block::CHEST => {
                if level.is_normal_cube(x, y + 1, z) {
                    return true;
                }
                let mut blocks = vec![[x, y, z]];
                for [dx, dz] in [[-1, 0], [1, 0], [0, -1], [0, 1]] {
                    if level.block(x + dx, y, z + dz) == Block::CHEST {
                        if level.is_normal_cube(x + dx, y + 1, z + dz) {
                            return true;
                        }
                        if dx < 0 || dz < 0 {
                            blocks.insert(0, [x + dx, y, z + dz]);
                        } else {
                            blocks.push([x + dx, y, z + dz]);
                        }
                        break;
                    }
                }
                let slots = 27 * blocks.len();
                let title = if blocks.len() == 1 {
                    "Chest"
                } else {
                    "Large chest"
                };
                (ContainerKind::Chest { slots }, title, blocks)
            }
            _ => return false,
        };
        self.open_window(level, kind, title);
        self.container_generations = blocks.iter().map(|&p| level.tile_generation(p)).collect();
        self.container_blocks = blocks;
        self.sync_container(level);
        true
    }

    pub(super) fn open_cart(&mut self, level: &mut Level, id: i32) {
        self.open_window(level, ContainerKind::Chest { slots: 27 }, "Minecart");
        self.container_entity = Some(id);
        self.sync_container(level);
    }

    /// Closes the current window and opens the next one, still without contents.
    fn open_window(&mut self, level: &mut Level, kind: ContainerKind, title: &str) {
        self.close_container(level);
        self.next_window = (self.next_window % 100) + 1;
        self.window = self.next_window;
        self.container = Container::new(kind);
        self.send(Packet::OpenWindow {
            window: self.window,
            kind: match kind {
                ContainerKind::Chest { .. } => 0,
                ContainerKind::Workbench => 1,
                ContainerKind::Dispenser => 3,
                _ => 2,
            },
            title: title.into(),
            slots: kind.external_count() as i8,
        });
    }

    pub fn close_container(&mut self, level: &mut Level) {
        for stack in self.container.close() {
            self.drop_stack(level, stack);
        }
        if self.window != 0 {
            self.send(Packet::CloseWindow {
                window: self.window,
            });
        }
        self.window = 0;
        self.last_action = None;
        self.container = Container::new(ContainerKind::Inventory);
        self.container_blocks.clear();
        self.container_entity = None;
        self.sync_container(level);
    }

    #[allow(clippy::too_many_arguments)]
    pub(super) fn click_container(
        &mut self,
        level: &mut Level,
        window: i8,
        slot: i16,
        button: i8,
        action: i16,
        shift: bool,
        expected: Option<ItemStack>,
    ) {
        if window != self.window {
            return;
        }
        if !self.container_usable(level) {
            return self.close_container(level);
        }
        let external = self.external_items(level);
        let valid = external.len() == self.container.kind.external_count()
            && (0..=1).contains(&button)
            && (slot == -999 || slot >= 0 && (slot as usize) < self.container.kind.slot_count())
            && self
                .last_action
                .is_none_or(|last| action.wrapping_sub(last) > 0);
        let mut inventory = self.inventory.clone();
        let mut container = self.container.clone();
        let mut updated = external.clone();
        let result = if valid {
            container.click(&mut inventory, &mut updated, slot, button, shift)
        } else {
            Default::default()
        };
        let accepted = valid && result.item == expected;
        if valid {
            self.last_action = Some(action);
        }
        if accepted {
            if let Some(stack) = result.crafted {
                self.crafted(stack);
            }
            if self.container.kind == ContainerKind::Furnace && slot == 2 {
                let count = external[2].map_or(0, |s| s.count) - updated[2].map_or(0, |s| s.count);
                if let Some(stack) = external[2]
                    && count > 0
                {
                    self.send_stat(stats::CRAFT + stats::item_id(stack.item), count);
                    if stack.item == Item::IRON_INGOT {
                        self.send_stat(stats::ACHIEVEMENT + 5, 1);
                    }
                    if stack.item == Item::COOKED_FISH {
                        self.send_stat(stats::ACHIEVEMENT + 10, 1);
                    }
                }
            }
            if result.crafted.is_some_and(|stack| stack.item == Item::MAP)
                && let Some(id) = self.create_map(level)
            {
                let map = if shift {
                    inventory
                        .main
                        .iter_mut()
                        .zip(&self.inventory.main)
                        .filter(|(new, old)| **new != **old)
                        .find_map(|(new, _)| new.as_mut().filter(|s| s.item == Item::MAP))
                } else {
                    container.carried.as_mut()
                };
                if let Some(map) = map {
                    map.damage = id;
                }
            }
            self.inventory = inventory;
            self.container = container;
            if external != updated {
                if let Some(id) = self.container_entity
                    && let Some(e) = level.entities.get_mut(id)
                {
                    if let Kind::Cart(cart) = &mut e.kind {
                        cart.items.copy_from_slice(&updated);
                    }
                    let chunk = e.chunk();
                    level.mark_entity_chunk(chunk);
                }
                let mut offset = 0;
                for &[x, y, z] in &self.container_blocks {
                    if let Some(items) = level
                        .tile_entity_mut(x, y, z)
                        .and_then(TileEntity::inventory_mut)
                    {
                        let end = offset + items.len();
                        items.copy_from_slice(&updated[offset..end]);
                        offset = end;
                    }
                }
            }
            if let Some(stack) = result.dropped {
                self.drop_stack(level, stack);
            }
        }
        self.send(Packet::Transaction {
            window,
            action,
            accepted,
        });
        if accepted {
            // The client predicted this click: only the cursor is confirmed
            // (`EntityPlayerMP.isChangingQuantityOnly`)
            self.last_contents = self
                .container
                .contents(&self.inventory, &self.external_items(level));
            self.send(Packet::SetSlot {
                window: -1,
                slot: -1,
                item: self.container.carried,
            });
        } else {
            self.sync_container(level);
        }
    }
}