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