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 client src interaction mod.rs
mod placement;
use common::interaction::{REACH, look};
use common::inventory::{Container, ContainerKind, PlayerInventory};
use common::item::{Item, ItemStack};
use common::physics::{Aabb, BlockHit};
use common::protocol::{Connection, Packet};
use common::world::shape::{self, BlockView, SIDE_OFFSETS};
use common::world::{Block, Material};
use glam::DVec3;
use placement::Outcome;
use crate::player::LocalPlayer;
use crate::world::ClientWorld;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Screen {
Inventory,
Death,
}
struct Mining {
position: [i32; 3],
block: Block,
damage: f32,
previous: f32,
held: Option<ItemStack>,
sound_ticks: u32,
/// `PlayerControllerSP` spends the first tick on a block only recording it.
fresh: bool,
}
impl Mining {
fn at(&self, position: [i32; 3], block: Block, held: Option<ItemStack>) -> bool {
self.position == position && self.block == block && self.held == held
}
}
fn dig(status: u8, [x, y, z]: [i32; 3], face: u8) -> Packet {
let y = y as u8;
Packet::BlockDig {
status,
x,
y,
z,
face,
}
}
/// The swing of the hand, which the server shows to others.
fn swing(player: &mut LocalPlayer, connection: &Connection) {
player.state.swing();
connection.send(Packet::Animation {
entity: player.entity.id,
animation: 1,
});
}
pub struct Interaction {
/// `ItemStack.animationsToGo` of the hotbar: picked up stacks pop for five ticks.
pub pops: [u8; 9],
/// Connected to a remote server, which leaves predicted sounds to the client.
pub remote: bool,
pub inventory: PlayerInventory,
pub container: Container,
pub external: Vec<Option<ItemStack>>,
pub window: i8,
pub screen: Option<Screen>,
pub title: String,
pub bars: [i16; 3],
pub cursor: [f32; 2],
pub shift: bool,
pub attack: bool,
pub use_item: bool,
pub target: Option<BlockHit>,
/// The block under the crosshair as drawn this frame (`EntityRenderer.getMouseOver`).
pub shown: Option<BlockHit>,
pub rendered_item: Option<ItemStack>,
pub equipped: [f32; 2],
mining: Option<Mining>,
wait: i32,
repeat_wait: i32,
miss_wait: i32,
clicks: Vec<bool>,
action: i16,
statistics: Option<crate::stats::Shared>,
/// A sign just placed, whose text the player writes next (`displayGUIEditSign`).
pub placed_sign: Option<PlacedSign>,
}
/// Where a sign went and which kind it is.
#[derive(Clone, Copy)]
pub struct PlacedSign {
pub position: [i32; 3],
pub block: Block,
}
impl Default for Interaction {
fn default() -> Self {
Self {
pops: [0; 9],
remote: false,
inventory: PlayerInventory::default(),
container: Container::new(ContainerKind::Inventory),
external: Vec::new(),
window: 0,
screen: None,
title: String::new(),
bars: [0; 3],
cursor: [0.0; 2],
shift: false,
attack: false,
use_item: false,
target: None,
shown: None,
rendered_item: None,
equipped: [0.0; 2],
mining: None,
wait: 0,
repeat_wait: 0,
miss_wait: 0,
clicks: Vec::new(),
action: 0,
statistics: None,
placed_sign: None,
}
}
}
impl Interaction {
pub fn new(statistics: crate::stats::Shared, remote: bool) -> Self {
Self {
statistics: Some(statistics),
remote,
..Default::default()
}
}
fn record_drops(&self, count: i32) {
if let Some(stats) = &self.statistics {
stats.lock().unwrap().add(2011, count);
}
}
pub fn drop_selected(&mut self, connection: &Connection) {
if common::inventory::take(self.inventory.held_mut(), 1).is_some() {
self.record_drops(1);
}
connection.send(dig(4, [0; 3], 0));
}
pub fn receive(&mut self, packet: &Packet) -> bool {
match packet {
Packet::OpenWindow {
window,
kind,
title,
slots,
} => {
let kind = match kind {
0 if matches!(*slots, 27 | 54) => ContainerKind::Chest {
slots: *slots as usize,
},
1 => ContainerKind::Workbench,
2 => ContainerKind::Furnace,
3 => ContainerKind::Dispenser,
_ => return false,
};
self.placed_sign = None;
self.window = *window;
self.container = Container::new(kind);
self.external = vec![None; kind.external_count()];
self.screen = Some(Screen::Inventory);
self.title.clone_from(title);
self.action = 0;
self.bars = [0; 3];
}
Packet::CloseWindow { window } if *window == self.window => self.reset_window(),
Packet::WindowItems { window, items } if *window == self.window => {
for (i, &item) in items
.iter()
.take(self.container.kind.slot_count())
.enumerate()
{
self.container
.set(&mut self.inventory, &mut self.external, i, item);
}
}
Packet::SetSlot {
window: -1,
slot: -1,
item,
} => self.container.carried = *item,
// Window 0 is the player's own inventory, which stays up to date behind other windows
Packet::WindowItems { window: 0, items } => {
let mut own = Container::new(ContainerKind::Inventory);
for (i, &item) in items.iter().take(own.kind.slot_count()).enumerate() {
if (36..45).contains(&i) {
self.pop(i - 36, item);
}
own.set(&mut self.inventory, &mut [], i, item);
}
}
Packet::SetSlot {
window: 0,
slot: slot @ 36..45,
item,
} => {
self.pop(*slot as usize - 36, *item);
Container::new(ContainerKind::Inventory).set(
&mut self.inventory,
&mut [],
*slot as usize,
*item,
);
}
Packet::SetSlot { window, slot, item } if *window == self.window && *slot >= 0 => {
self.container.set(
&mut self.inventory,
&mut self.external,
*slot as usize,
*item,
);
}
Packet::UpdateProgressBar { window, bar, value } if *window == self.window => {
if let Some(target) = self.bars.get_mut(*bar as usize) {
*target = *value;
}
}
Packet::Transaction { .. } => {}
_ => return false,
}
true
}
fn reset_window(&mut self) {
self.window = 0;
self.container = Container::new(ContainerKind::Inventory);
self.external.clear();
self.screen = None;
self.action = 0;
}
pub fn close(&mut self, connection: &Connection) {
if self.screen == Some(Screen::Inventory) {
let drops = self.container.close();
self.record_drops(drops.len() as i32);
connection.send(Packet::CloseWindow {
window: self.window,
});
}
self.reset_window();
}
pub fn cancel(&mut self, connection: &Connection) {
self.attack = false;
self.use_item = false;
self.wait = 0;
self.repeat_wait = 0;
self.miss_wait = 0;
self.clicks.clear();
self.stop_mining(connection);
}
/// Traces the outlined block from the camera of this frame, between game ticks.
pub fn refresh_shown(
&mut self,
world: &ClientWorld,
player: &LocalPlayer,
entities: &crate::entities::Entities,
partial: f32,
) {
let entity = &player.entity;
let yaw = entity.prev_yaw + (entity.yaw - entity.prev_yaw) * partial;
let pitch = entity.prev_pitch + (entity.pitch - entity.prev_pitch) * partial;
let eye = player.eye(partial);
let direction = look(yaw, pitch);
self.shown = shape::ray_trace(world, eye, eye + direction * REACH, false, false);
let distance = self.shown.map_or(REACH, |hit| hit.point.distance(eye));
if entities.target(eye, direction, distance).is_some() {
self.shown = None;
}
}
/// `Minecraft.clickMiddleMouseButton`: selects the first stack of the targeted block if it
/// is in the hotbar.
pub fn pick_block(&mut self, world: &ClientWorld, connection: &Connection) {
let Some(hit) = self.target else {
return;
};
let block = match BlockView::block(world, hit.x, hit.y, hit.z) {
Block::GRASS => Block::DIRT,
Block::DOUBLE_SLAB => Block::SLAB,
Block::BEDROCK => Block::STONE,
block => block,
};
let item = Item::from_block(block);
if let Some(slot) = self
.inventory
.main
.iter()
.position(|stack| stack.is_some_and(|stack| stack.item == item))
.filter(|&slot| slot < 9)
{
self.select(slot, connection);
}
}
pub fn select(&mut self, slot: usize, connection: &Connection) {
self.inventory.selected = slot % 9;
connection.send(Packet::BlockItemSwitch {
slot: self.inventory.selected as i16,
});
}
pub fn click_slot(&mut self, index: i16, button: i8, connection: &Connection) {
self.action = self.action.wrapping_add(1);
let result = self.container.click(
&mut self.inventory,
&mut self.external,
index,
button,
self.shift,
);
if result.dropped.is_some() {
self.record_drops(1);
}
connection.send(Packet::WindowClick {
window: self.window,
slot: index,
button,
action: self.action,
shift: self.shift,
item: result.item,
});
}
pub fn damage(&self, partial: f32) -> f32 {
self.mining
.as_ref()
.map_or(0.0, |m| m.previous + (m.damage - m.previous) * partial)
}
/// `handleSetSlot`: a hotbar stack that appears or grows pops.
fn pop(&mut self, slot: usize, item: Option<ItemStack>) {
let current = self.inventory.main[slot];
if item.is_some_and(|new| current.is_none_or(|old| old.count < new.count)) {
self.pops[slot] = 5;
}
}
pub fn tick(
&mut self,
world: &mut ClientWorld,
player: &mut LocalPlayer,
entities: &crate::entities::Entities,
connection: &Connection,
) {
for pop in &mut self.pops {
*pop = pop.saturating_sub(1);
}
self.equipped[0] = self.equipped[1];
let held = self.inventory.held();
let same = match (held, self.rendered_item) {
(None, None) => true,
(Some(a), Some(b)) => {
a.item == b.item && (a.item.is_damageable() || a.damage == b.damage)
}
_ => false,
};
self.equipped[1] += (f32::from(same) - self.equipped[1]).clamp(-0.4, 0.4);
if self.equipped[1] < 0.1 {
self.rendered_item = held;
}
let (eye, direction) = (
player.eye(1.0),
look(player.entity.yaw, player.entity.pitch),
);
self.target = shape::ray_trace(world, eye, eye + direction * REACH, false, false);
let distance = self.target.map_or(REACH, |hit| hit.point.distance(eye));
let target = entities.target(eye, direction, distance);
if target.is_some() {
self.target = None;
}
if self.screen.is_some() || !player.started {
self.cancel(connection);
return;
}
self.repeat_wait = (self.repeat_wait - 1).max(0);
self.miss_wait = (self.miss_wait - 1).max(0);
if let Some(mining) = &mut self.mining {
mining.previous = mining.damage;
}
for attack in std::mem::take(&mut self.clicks) {
self.click(attack, target, world, player, entities, connection);
self.repeat_wait = 5;
}
if self.repeat_wait == 0 && (self.attack || self.use_item) {
self.click(self.attack, target, world, player, entities, connection);
self.repeat_wait = 5;
}
if !self.attack {
self.stop_mining(connection);
self.wait = 0;
self.miss_wait = 0;
return;
}
if self.miss_wait > 0 {
return;
}
let Some(hit) = self.target else {
self.stop_mining(connection);
self.wait = 0;
return;
};
if self.wait > 0 {
self.wait -= 1;
return;
}
let block = BlockView::block(world, hit.x, hit.y, hit.z);
if block.is_air() {
self.stop_mining(connection);
return;
}
let position = [hit.x, hit.y, hit.z];
let held = self.inventory.held();
if !self
.mining
.as_ref()
.is_some_and(|m| m.at(position, block, held))
{
self.start_mining(hit, world, player, connection);
return;
}
let progress = self.inventory.mining_progress(
block,
player.entity.eyes_in(world, Material::Water),
player.entity.on_ground,
);
if let Some(mining) = &mut self.mining
&& !std::mem::take(&mut mining.fresh)
{
mining.damage += progress;
if mining.sound_ticks % 4 == 0 {
let sound = block.sound();
world.sounds.push((
sound.step().into(),
DVec3::new(hit.x as f64 + 0.5, hit.y as f64 + 0.5, hit.z as f64 + 0.5),
(sound.volume() + 1.0) / 8.0,
sound.pitch() * 0.5,
));
}
mining.sound_ticks += 1;
}
let metadata = BlockView::metadata(world, hit.x, hit.y, hit.z);
let bounds = shape::block_bounds(world, block, hit.x, hit.y, hit.z);
let tint = world.debris_tint(block, metadata, position);
world
.particles
.hit_block(block, metadata, &hit, bounds, tint);
if self.mining.as_ref().is_some_and(|m| m.damage >= 1.0) {
connection.send(dig(2, position, hit.side));
world.break_effect(position, block, metadata);
world.set_block(hit.x, hit.y, hit.z, Block::AIR, 0);
self.mining = None;
self.wait = 5;
}
}
pub fn attack_button(&mut self, pressed: bool) {
self.attack = pressed;
if pressed {
self.clicks.push(true);
}
}
pub fn use_button(&mut self, pressed: bool) {
self.use_item = pressed;
if pressed {
self.clicks.push(false);
}
}
fn click(
&mut self,
attack: bool,
target: Option<i32>,
world: &mut ClientWorld,
player: &mut LocalPlayer,
entities: &crate::entities::Entities,
connection: &Connection,
) {
if attack {
if self.miss_wait > 0 {
return;
}
swing(player, connection);
if let Some(target) = target {
self.stop_mining(connection);
connection.send(Packet::UseEntity {
player: player.entity.id,
target,
left_click: true,
});
} else if let Some(hit) = self.target {
if self.wait == 0 {
self.start_mining(hit, world, player, connection);
}
} else {
self.stop_mining(connection);
self.miss_wait = 10;
}
} else if let Some(target) = target {
// `Minecraft.clickMouse`: the interaction, then the item's own use
connection.send(Packet::UseEntity {
player: player.entity.id,
target,
left_click: false,
});
if self.inventory.held().is_some() {
connection.send(Packet::Place {
x: -1,
y: 255,
z: -1,
direction: 255,
item: self.inventory.held(),
});
}
} else {
if let Some(hit) = self.target
&& self
.inventory
.held()
.is_some_and(|stack| stack.item == Item::SIGN)
{
self.placed_sign = predict_sign(world, hit);
}
let (x, y, z, direction) = self.target.map_or((-1, 255, -1, 255), |hit| {
(hit.x, hit.y as u8, hit.z, hit.side)
});
connection.send(Packet::Place {
x,
y,
z,
direction,
item: self.inventory.held(),
});
// `PlayerControllerMP.sendPlaceBlock` acts at once and swings when it succeeds
let before = self.inventory.held();
let activated = self
.target
.is_some_and(|hit| activates(world, [hit.x, hit.y, hit.z]));
let outcome = if activated {
Outcome::Used
} else {
self.target
.zip(before)
.map_or(Outcome::Ignored, |(hit, stack)| {
placement::use_on_block(world, stack, player, &hit, |boxes| {
entities.obstruct(boxes)
|| boxes.iter().any(|b| b.intersects(&player.entity.bb))
})
})
};
if outcome != Outcome::Ignored {
if let Some(hit) = self.target
&& outcome == Outcome::Spent
{
predict_block(world, &mut self.inventory, player, hit, self.remote);
}
swing(player, connection);
// `ItemRenderer.resetEquippedProgressOnPlace` unless the last one went
if outcome == Outcome::Spent && before.is_some_and(|s| s.count > 1) {
self.equipped[1] = 0.0;
}
} else if let Some(stack) = before {
if stack.item == Item::FISHING_ROD {
player.state.swing();
}
// `ItemRenderer.resetEquippedProgressOnUse`
if placement::uses_in_air(world, stack, player) {
self.equipped[1] = 0.0;
// `ItemSnowball.onItemRightClick` on the client: one is thrown
if matches!(stack.item, Item::SNOWBALL | Item::EGG) {
common::inventory::take(self.inventory.held_mut(), 1);
}
}
}
}
}
fn start_mining(
&mut self,
hit: BlockHit,
world: &mut ClientWorld,
player: &LocalPlayer,
connection: &Connection,
) {
let block = BlockView::block(world, hit.x, hit.y, hit.z);
let position = [hit.x, hit.y, hit.z];
let held = self.inventory.held();
if block.is_air()
|| self
.mining
.as_ref()
.is_some_and(|m| m.at(position, block, held))
{
return;
}
self.stop_mining(connection);
connection.send(dig(0, position, hit.side));
let progress = self.inventory.mining_progress(
block,
player.entity.eyes_in(world, Material::Water),
player.entity.on_ground,
);
if progress >= 1.0 {
let metadata = BlockView::metadata(world, hit.x, hit.y, hit.z);
world.break_effect(position, block, metadata);
world.set_block(hit.x, hit.y, hit.z, Block::AIR, 0);
} else {
self.mining = Some(Mining {
position,
block,
damage: 0.0,
previous: 0.0,
held,
sound_ticks: 0,
fresh: !self.remote,
});
}
}
fn stop_mining(&mut self, connection: &Connection) {
if let Some(mining) = self.mining.take() {
connection.send(dig(1, mining.position, 0));
}
}
}
/// Whether `blockActivated` succeeds on the client.
fn activates(world: &ClientWorld, [x, y, z]: [i32; 3]) -> bool {
match BlockView::block(world, x, y, z) {
Block::JUKEBOX => BlockView::metadata(world, x, y, z) != 0,
Block::MOVING_PISTON => BlockView::tile_entity(world, x, y, z).is_none(),
block => matches!(
block,
Block::CHEST
| Block::FURNACE
| Block::LIT_FURNACE
| Block::DISPENSER
| Block::CRAFTING_TABLE
| Block::WOODEN_DOOR
| Block::IRON_DOOR
| Block::TRAPDOOR
| Block::LEVER
| Block::BUTTON
| Block::NOTE_BLOCK
| Block::BED
| Block::REPEATER
| Block::POWERED_REPEATER
| Block::CAKE
),
}
}
/// `ItemBlock.onItemUse` on the client for plain cubes, whose placement needs no orientation
/// or support: the block appears and the stack shrinks before the server confirms.
fn predict_block(
world: &mut ClientWorld,
inventory: &mut PlayerInventory,
player: &LocalPlayer,
hit: BlockHit,
sound: bool,
) -> bool {
let Some(stack) = inventory.held() else {
return false;
};
let Some(block) = stack.item.block().filter(|block| {
block.is_normal_cube()
&& !block.is_container()
&& !matches!(
*block,
Block::PUMPKIN | Block::JACK_O_LANTERN | Block::PISTON | Block::STICKY_PISTON
)
}) else {
return false;
};
let mut position = [hit.x, hit.y, hit.z];
if world.block(hit.x, hit.y, hit.z) != Block::SNOW_LAYER {
let [dx, dy, dz] = SIDE_OFFSETS[usize::from(hit.side.min(5))];
position = [hit.x + dx, hit.y + dy, hit.z + dz];
}
let [x, y, z] = position;
let current = world.block(x, y, z);
let corner = DVec3::from_array(position.map(f64::from));
let cell = Aabb::new(corner, corner + 1.0);
if !(0..127).contains(&y)
|| !(current.is_air() || current.material().is_ground_cover())
|| cell.intersects(&player.entity.bb)
{
return false;
}
let metadata = stack
.item
.properties()
.filter(|properties| properties.has_subtypes)
.map_or(0, |_| stack.damage as u8);
world.set_block(x, y, z, block, metadata);
let step = block.sound();
if sound {
world.sounds.push((
step.step().into(),
corner + 0.5,
(step.volume() + 1.0) / 2.0,
step.pitch() * 0.8,
));
}
common::inventory::take(inventory.held_mut(), 1);
true
}
/// `ItemSign.onItemUse` as the client predicts it, so it can open the sign editor.
fn predict_sign(world: &ClientWorld, hit: BlockHit) -> Option<PlacedSign> {
if hit.side == 0 || !world.block(hit.x, hit.y, hit.z).material().is_solid() {
return None;
}
let [dx, dy, dz] = SIDE_OFFSETS[usize::from(hit.side.min(5))];
let position @ [x, y, z] = [hit.x + dx, hit.y + dy, hit.z + dz];
let current = world.block(x, y, z);
if !(current.is_air() || current.material().is_ground_cover()) {
return None;
}
let block = if hit.side == 1 {
Block::SIGN
} else {
Block::WALL_SIGN
};
Some(PlacedSign { position, block })
}