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 interaction.rs
use common::entity::vehicle::VehicleKind;
use common::interaction::{adjacent, look};
use common::inventory::take;
use common::item::{Item, ItemKind, ItemStack};
use common::protocol::Packet;
use common::world::shape::{self, BlockView};
use common::world::{Block, ChunkPos, Material};
use glam::{DVec3, Vec3};
use super::ServerPlayer;
use crate::blocks;
use crate::level::{DroppedItem, Level};
pub(super) struct Mining {
position: [i32; 3],
block: Block,
ticks: u32,
finishing: bool,
}
/// The middle of a block.
pub(super) fn center(position: [i32; 3]) -> DVec3 {
DVec3::from_array(position.map(f64::from)) + 0.5
}
/// Items with a use of their own on right click, pointed at a block or not.
fn uses_in_air(item: Item) -> bool {
item.properties().is_some_and(|p| {
matches!(
p.kind,
ItemKind::Food { .. }
| ItemKind::Soup { .. }
| ItemKind::Bucket(_)
| ItemKind::Boat
| ItemKind::Bow
| ItemKind::FishingRod
| ItemKind::Snowball
| ItemKind::Egg
)
})
}
/// The sound of a block being placed.
fn place_sound(level: &mut Level, block: Block, pos: [f64; 3]) {
let sound = block.sound();
level.events.push(Packet::Sound {
name: sound.step().into(),
pos,
volume: (sound.volume() + 1.0) / 2.0,
pitch: sound.pitch() * 0.8,
});
}
impl ServerPlayer {
fn alive(&self, level: &Level) -> bool {
self.entity(level)
.is_some_and(|e| !e.dead && e.living.as_ref().is_some_and(|l| l.health > 0))
}
pub(super) fn eye(&self, level: &Level) -> DVec3 {
self.position(level) + DVec3::Y * f64::from(1.62_f32)
}
fn reachable(&self, level: &Level, position @ [x, y, z]: [i32; 3]) -> bool {
self.alive(level)
&& (0..128).contains(&y)
&& level.is_loaded(ChunkPos::containing(x, z))
&& self.position(level).distance_squared(center(position)) <= 36.0
}
/// `WorldServer.canMineBlock`, inverted: non-operators may not change the spawn area.
fn protected(&self, level: &Level, x: i32, z: i32) -> bool {
!self.op
&& level
.rules
.dedicated_spawn
.is_some_and(|[sx, sz]| (x - sx).abs().max((z - sz).abs()) <= 16)
}
/// `WorldServer.disableSpawnProtection` is set outside the Overworld.
fn spawn_protected(&self, level: &Level, [x, _, z]: [i32; 3]) -> bool {
level.dimension() == 0 && self.protected(level, x, z)
}
fn sync_block(&mut self, level: &Level, [x, y, z]: [i32; 3]) {
if (0..128).contains(&y) && level.is_loaded(ChunkPos::containing(x, z)) {
self.send(Packet::BlockChange {
x,
y: y as u8,
z,
block: BlockView::block(level, x, y, z).0,
metadata: BlockView::metadata(level, x, y, z),
});
}
}
fn progress(&self, level: &Level, block: Block) -> f32 {
let entity = self.entity(level);
self.inventory.mining_progress(
block,
entity.is_some_and(|e| e.eyes_in(level, Material::Water)),
entity.is_some_and(|e| e.on_ground),
)
}
pub(super) fn dig(&mut self, level: &mut Level, status: u8, position: [i32; 3], face: u8) {
if !self.alive(level) {
return self.sync_block(level, position);
}
if status == 4 {
if let Some(stack) = take(self.inventory.held_mut(), 1) {
self.drop_stack(level, stack);
}
return self.sync_container(level);
}
if status == 1 {
if let Some(mining) = self.mining.take() {
self.sync_block(level, mining.position);
}
return;
}
if face > 5 || !self.reachable(level, position) {
return self.sync_block(level, position);
}
let [x, y, z] = position;
match status {
0 if self.spawn_protected(level, position) => self.sync_block(level, position),
0 => {
if let Some(mining) = self.mining.take() {
self.sync_block(level, mining.position);
}
if let Some(fire @ [fx, fy, fz]) = adjacent(position, face)
&& BlockView::block(level, fx, fy, fz) == Block::FIRE
{
level.aux_effect(1004, fire, 0);
level.set_block_notify(fx, fy, fz, Block::AIR);
}
self.click_block(level, position);
let block = level.block(x, y, z);
let progress = self.progress(level, block);
self.mining = Some(Mining {
position,
block,
ticks: 0,
finishing: false,
});
if progress >= 1.0 {
self.harvest(level, position);
}
}
2 => {
let block = BlockView::block(level, x, y, z);
let progress = self.progress(level, block);
if let Some(mining) = &mut self.mining
&& mining.position == position
&& mining.block == block
{
mining.finishing = true;
if progress * (mining.ticks + 1) as f32 >= 0.7 {
return self.harvest(level, position);
}
}
self.sync_block(level, position);
}
3 => self.sync_block(level, position),
_ => {}
}
}
pub fn tick_interaction(&mut self, level: &mut Level) {
if !self.container_usable(level) {
self.close_container(level);
}
self.tick_container(level);
let Some(mining) = self.mining.take() else {
return;
};
let [x, y, z] = mining.position;
if !self.reachable(level, mining.position)
|| BlockView::block(level, x, y, z) != mining.block
{
return self.sync_block(level, mining.position);
}
let ticks = mining.ticks.saturating_add(1);
let progress = self.progress(level, mining.block) * (ticks + 1) as f32;
if mining.finishing && progress >= 1.0 {
self.harvest(level, mining.position);
} else {
self.mining = Some(Mining { ticks, ..mining });
}
}
fn harvest(&mut self, level: &mut Level, [x, y, z]: [i32; 3]) {
self.mining = None;
let block = level.block(x, y, z);
let metadata = level.metadata(x, y, z);
let harvest = self.inventory.can_harvest(block);
if block.is_air() || !level.set_block_notify(x, y, z, Block::AIR) {
return;
}
let kind = self
.inventory
.held()
.and_then(|stack| stack.item.properties())
.map(|p| p.kind);
let wear = match kind {
Some(ItemKind::Sword(_)) => 2,
Some(ItemKind::Tool(..)) => 1,
Some(ItemKind::Shears) if matches!(block, Block::LEAVES | Block::WEB) => 1,
_ => 0,
};
if matches!(kind, Some(ItemKind::Sword(_) | ItemKind::Tool(..)))
&& let Some(stack) = self.inventory.held()
{
self.used_item(stack);
}
self.damage_held(wear);
if harvest {
self.send_stat(common::stats::MINE + common::stats::block_id(block), 1);
if block == Block::TNT && metadata & 1 != 0 {
level.prime_tnt(center([x, y, z]), false);
} else if kind == Some(ItemKind::Shears) && block == Block::LEAVES {
let leaves = ItemStack::new(Item::from_block(block), 1, i32::from(metadata & 3));
level.drop_item(x, y, z, leaves);
} else if block == Block::SNOW_LAYER {
level.drop_item(x, y, z, ItemStack::one(Item::SNOWBALL));
} else {
blocks::drop_as_item(level, block, x, y, z, metadata);
let below = level.material(x, y - 1, z);
if block == Block::ICE && (below.is_solid() || below.is_liquid()) {
level.set_block_notify(x, y, z, Block::FLOWING_WATER);
}
}
}
self.send(Packet::BlockChange {
x,
y: y as u8,
z,
block: 0,
metadata: 0,
});
level.events_except.push((
self.entity_id,
Packet::AuxEffect {
effect: 2001,
x,
y: y as i8,
z,
data: i32::from(block.0) | (i32::from(metadata) << 8),
},
));
self.sync_container(level);
}
pub(crate) fn drop_stack(&mut self, level: &mut Level, stack: ItemStack) {
use common::math::{cos, sin};
let dedicated = level.rules.dedicated_spawn.is_some();
let Some(stored) = level.entities.get_mut(self.entity_id) else {
return;
};
let entity = &mut stored.entity;
let random = &mut entity.rand;
let motion = if self.died {
let speed = random.next_float() * 0.5;
let angle = random.next_float() * std::f32::consts::PI * 2.0;
Vec3::new(-sin(angle) * speed, 0.2, cos(angle) * speed).as_dvec3()
} else {
let yaw = entity.yaw / 180.0 * std::f32::consts::PI;
let pitch = entity.pitch / 180.0 * std::f32::consts::PI;
let mut motion = DVec3::new(
f64::from(-sin(yaw) * cos(pitch) * 0.3),
f64::from(-sin(pitch) * 0.3 + 0.1),
f64::from(cos(yaw) * cos(pitch) * 0.3),
);
let angle = f64::from(random.next_float() * std::f32::consts::PI * 2.0);
let spread = f64::from(0.02 * random.next_float());
motion.x += angle.cos() * spread;
motion.y += f64::from((random.next_float() - random.next_float()) * 0.1);
motion.z += angle.sin() * spread;
motion
};
let pos = entity.pos;
let extra_eye = if dedicated { 0.0 } else { f64::from(0.12_f32) };
level.dropped_items.push(DroppedItem {
x: pos.x,
// Singleplayer adds `EntityPlayerSP`'s eye height on top of its eye-level position
y: pos.y + f64::from(1.62_f32) - f64::from(0.3_f32) + extra_eye,
z: pos.z,
stack,
motion: Some(motion),
impulse: DVec3::ZERO,
pickup_delay: 40,
});
}
pub(super) fn place(&mut self, level: &mut Level, position: [i32; 3], side: u8) {
if !self.alive(level) {
return self.sync_block(level, position);
}
if side == 255 {
self.use_item(level);
} else if side < 6
&& self.reachable(level, position)
&& !self.spawn_protected(level, position)
&& !self.open_container(level, position)
&& !self.activate(level, position)
{
self.place_item(level, position, side);
}
if side < 6 {
self.sync_block(level, position);
if let Some(next) = adjacent(position, side) {
self.sync_block(level, next);
}
}
self.sync_container(level);
}
/// `Block.onBlockClicked`: the effect of starting to dig a block.
fn click_block(&mut self, level: &mut Level, position @ [x, y, z]: [i32; 3]) {
match level.block(x, y, z) {
Block::WOODEN_DOOR | Block::TRAPDOOR | Block::CAKE => {
self.activate(level, position);
}
Block::TNT
if self
.inventory
.held()
.is_some_and(|stack| stack.item == Item::FLINT_AND_STEEL) =>
{
level.set_metadata(x, y, z, 1);
}
Block::REDSTONE_ORE | Block::LIT_REDSTONE_ORE => {
blocks::glow_redstone_ore(level, x, y, z)
}
Block::LEVER => blocks::flip_lever(level, x, y, z),
Block::BUTTON => blocks::press_button(level, x, y, z),
Block::NOTE_BLOCK => blocks::trigger_note(level, x, y, z),
_ => {}
}
}
fn activate(&mut self, level: &mut Level, [x, y, z]: [i32; 3]) -> bool {
match level.block(x, y, z) {
Block::BED => self.use_bed(level, [x, y, z]),
Block::REDSTONE_ORE | Block::LIT_REDSTONE_ORE => {
blocks::glow_redstone_ore(level, x, y, z);
return false;
}
Block::LEVER => blocks::flip_lever(level, x, y, z),
Block::BUTTON => blocks::press_button(level, x, y, z),
Block::REPEATER | Block::POWERED_REPEATER => blocks::cycle_repeater(level, x, y, z),
Block::NOTE_BLOCK => blocks::tune_note(level, x, y, z),
Block::IRON_DOOR => {}
Block::JUKEBOX if level.metadata(x, y, z) != 0 => blocks::eject_record(level, x, y, z),
Block::MOVING_PISTON if level.tile_entity(x, y, z).is_none() => {
level.set_block_notify(x, y, z, Block::AIR);
}
Block::WOODEN_DOOR => {
let y = y - i32::from(level.metadata(x, y, z) & 8 != 0);
if level.block(x, y, z) == Block::WOODEN_DOOR {
let meta = level.metadata(x, y, z);
blocks::toggle_door(level, Block::WOODEN_DOOR, x, y, z, meta);
}
}
Block::TRAPDOOR => {
let meta = level.metadata(x, y, z) ^ 4;
level.set_metadata_notify(x, y, z, meta);
level.aux_effect(1003, [x, y, z], 0);
}
Block::CAKE => {
if let Some(entity) = level
.entities
.get_mut(self.entity_id)
.map(|e| &mut e.entity)
&& entity.living.as_ref().is_some_and(|l| l.health < 20)
{
entity.heal(3);
let slices = level.metadata(x, y, z) + 1;
if slices >= 6 {
level.set_block_notify(x, y, z, Block::AIR);
} else {
level.set_metadata_notify(x, y, z, slices);
}
}
}
_ => return false,
}
true
}
pub(super) fn use_item(&mut self, level: &mut Level) {
let Some(stack) = self.inventory.held() else {
return;
};
match stack.item.properties().map(|p| p.kind) {
Some(ItemKind::Boat) => {
if let Some(source) = self.entity(level) {
let eye = source.pos + DVec3::Y * (1.62 - f64::from(source.y_offset));
let end = eye + source.look() * 5.0;
if let Some(hit) = shape::ray_trace(level, eye, end, true, false) {
let y = hit.y
- i32::from(level.block(hit.x, hit.y, hit.z) == Block::SNOW_LAYER);
let at = Vec3::new(hit.x as f32 + 0.5, y as f32 + 1.0, hit.z as f32 + 0.5);
level.spawn_boat(at.as_dvec3());
take(self.inventory.held_mut(), 1);
}
}
self.sync_container(level);
}
Some(ItemKind::FishingRod) => {
if let Some(owner) = self.entity(level).cloned() {
let wear = level.fish(&owner);
self.damage_held(wear);
level.events.push(Packet::Animation {
entity: self.entity_id,
animation: 1,
});
}
self.sync_container(level);
}
Some(ItemKind::Bow) => {
if let Some(slot) = self
.inventory
.main
.iter_mut()
.find(|slot| slot.is_some_and(|stack| stack.item == Item::ARROW))
{
take(slot, 1);
if let Some(mut source) = level.entities.take_for_tick(self.entity_id) {
level.shoot_arrow(&mut source.entity, None);
level.entities.finish_tick(source);
}
}
self.sync_container(level);
}
Some(kind @ (ItemKind::Snowball | ItemKind::Egg)) => {
take(self.inventory.held_mut(), 1);
let Some(thrower) = self.entity(level).cloned() else {
return;
};
let pitch = 0.4 / (level.rand.next_float() * 0.4 + 0.8);
common::entity::EntityWorld::play_sound(level, &thrower, "random.bow", 0.5, pitch);
level.throw_from(kind == ItemKind::Egg, &thrower);
}
Some(ItemKind::Food { heal, .. } | ItemKind::Soup { heal }) => {
let health = level.entities.get_mut(self.entity_id).and_then(|e| {
e.entity.heal(heal);
e.entity.living.as_ref().map(|l| l.health as i16)
});
if let Some(health) = health {
self.send(Packet::UpdateHealth { health });
}
take(self.inventory.held_mut(), 1);
if stack.item == Item::MUSHROOM_STEW {
*self.inventory.held_mut() = Some(ItemStack::one(Item::BOWL));
}
}
Some(ItemKind::Bucket(fluid)) => {
let Some(entity) = self.entity(level) else {
return;
};
let eye = self.eye(level);
let end = eye + look(entity.yaw, entity.pitch) * 5.0;
let Some(hit) = shape::ray_trace(level, eye, end, fluid == Some(Block::AIR), false)
else {
return;
};
let position = [hit.x, hit.y, hit.z];
if !level.is_loaded(ChunkPos::containing(hit.x, hit.z))
|| self.protected(level, hit.x, hit.z)
{
return;
}
if fluid == Some(Block::AIR) {
let item = match level.block(hit.x, hit.y, hit.z).material() {
Material::Water => Item::WATER_BUCKET,
Material::Lava => Item::LAVA_BUCKET,
_ => return,
};
if level.metadata(hit.x, hit.y, hit.z) == 0
&& level.set_block_notify(hit.x, hit.y, hit.z, Block::AIR)
{
*self.inventory.held_mut() = Some(ItemStack::one(item));
}
return;
}
if let Some(block) = fluid {
let Some([x, y, z]) = adjacent(position, hit.side) else {
return;
};
if !(0..128).contains(&y)
|| !level.is_loaded(ChunkPos::containing(x, z))
|| level.block(x, y, z).material().is_solid()
{
return;
}
if block == Block::FLOWING_WATER && !level.has_sky() {
vaporize(level, eye, [x, y, z]);
} else if !level.set_block_with_metadata_notify(x, y, z, block, 0) {
return;
}
}
*self.inventory.held_mut() = Some(ItemStack::one(Item::BUCKET));
}
_ => {}
}
}
pub(super) fn place_item(&mut self, level: &mut Level, position: [i32; 3], side: u8) {
let before = self.inventory.held();
self.place_item_inner(level, position, side);
if let Some(stack) = before
&& !uses_in_air(stack.item)
&& self.inventory.held() != before
{
self.used_item(stack);
}
}
fn place_item_inner(&mut self, level: &mut Level, position: [i32; 3], side: u8) {
let Some(stack) = self.inventory.held() else {
return;
};
let Some(properties) = stack.item.properties() else {
return;
};
// Items without a use on blocks are used as in the air (`Minecraft.clickMouse`)
if uses_in_air(stack.item) {
return self.use_item(level);
}
let [x, y, z] = position;
let (pos, yaw) = self
.entity(level)
.map_or((DVec3::ZERO, 0.0), |e| (e.pos, e.yaw));
let used = match properties.kind {
ItemKind::Dye if stack.damage == 15 => {
match level.block(x, y, z) {
Block::SAPLING => blocks::grow_tree(level, x, y, z),
Block::WHEAT => level.set_metadata_notify(x, y, z, 7),
Block::GRASS => blocks::bone_meal_grass(level, x, y, z),
_ => return,
}
true
}
ItemKind::Hoe(_) => {
let block = level.block(x, y, z);
if matches!(block, Block::GRASS | Block::DIRT)
&& side != 0
&& level.is_air(x, y + 1, z)
{
place_sound(
level,
Block::FARMLAND,
position.map(|v| f64::from(v as f32 + 0.5)),
);
level.set_block_notify(x, y, z, Block::FARMLAND);
self.damage_held(1);
}
false
}
ItemKind::Minecart(kind) => {
let rail = common::entity::vehicle::is_rail(level.block(x, y, z));
if rail {
let kind = match kind {
1 => VehicleKind::StorageCart,
2 => VehicleKind::PoweredCart,
_ => VehicleKind::Minecart,
};
let at = Vec3::new(x as f32, y as f32, z as f32) + 0.5;
level.spawn_cart(at.as_dvec3(), kind);
}
rail
}
ItemKind::Painting => level.place_painting(position, side),
ItemKind::Sign => {
let sign = blocks::place_sign(level, position, side, yaw);
if let Some(sign) = sign {
level.editable_signs.insert(sign, self.entity_id);
}
sign.is_some()
}
ItemKind::Bed => side == 1 && blocks::place_bed(level, position, yaw),
ItemKind::Door(block) => side == 1 && blocks::place_door(level, position, block, yaw),
ItemKind::Record(_) => blocks::insert_record(level, x, y, z, stack.item),
ItemKind::Seeds => {
let plant = side == 1
&& level.block(x, y, z) == Block::FARMLAND
&& level.is_air(x, y + 1, z);
if plant {
level.set_block_notify(x, y + 1, z, Block::WHEAT);
}
plant
}
ItemKind::FlintAndSteel => {
if let Some(target @ [fx, fy, fz]) = adjacent(position, side)
&& level.is_air(fx, fy, fz)
{
let pitch = level.rand.next_float() * 0.4 + 0.8;
level.events.push(Packet::Sound {
name: "fire.ignite".into(),
pos: target.map(|v| f64::from(v) + 0.5),
volume: 1.0,
pitch,
});
level.set_block_notify(fx, fy, fz, Block::FIRE);
}
self.damage_held(1);
false
}
ItemKind::Redstone => {
let [tx, ty, tz] = if level.block(x, y, z) == Block::SNOW_LAYER {
position
} else {
match adjacent(position, side) {
Some(next @ [nx, ny, nz]) if level.is_air(nx, ny, nz) => next,
_ => return,
}
};
let supported = level.is_normal_cube(tx, ty - 1, tz);
if supported {
level.set_block_notify(tx, ty, tz, Block::REDSTONE_WIRE);
}
supported
}
kind @ (ItemKind::Block | ItemKind::Places(_)) => {
let block = match kind {
ItemKind::Places(block) => block,
_ => match stack.item.block() {
Some(block) => block,
None => return,
},
};
let (position, side) = if level.block(x, y, z) == Block::SNOW_LAYER {
(position, 0)
} else {
let Some(next) = adjacent(position, side) else {
return;
};
(next, side)
};
let placed =
blocks::place(level, position, block, stack.damage as u8, side, (pos, yaw));
if placed {
place_sound(level, block, position.map(|v| f64::from(v) + 0.5));
}
placed
}
_ => false,
};
if used {
take(self.inventory.held_mut(), 1);
}
}
}
/// Water poured out in the Nether evaporates with a hiss.
fn vaporize(level: &mut Level, eye: DVec3, [x, y, z]: [i32; 3]) {
let pitch = 2.6 + (level.rand.next_float() - level.rand.next_float()) * 0.8;
level.events.push(Packet::Sound {
name: "random.fizz".into(),
pos: (eye + 0.5).to_array(),
volume: 0.5,
pitch,
});
for _ in 0..8 {
let pos = [x, y, z].map(|v| f64::from(v) + level.rand.next_double());
level.events.push(Packet::Particle {
name: "largesmoke".into(),
pos,
velocity: [0.0; 3],
});
}
}