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 placement.rs
//! What `Item.onItemUse` and `Item.onItemRightClick` return on the client, which decides
//! whether the hand swings or the held item dips.
use common::interaction::{adjacent, look};
use common::item::{ItemKind, ItemStack};
use common::math::floor_f64;
use common::physics::{Aabb, BlockHit};
use common::world::shape::{self, BlockView};
use common::world::{Block, bed};
use crate::player::LocalPlayer;
use crate::world::ClientWorld;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
Ignored,
/// `onItemUse` returned true: the hand swings.
Used,
/// The stack also shrank, which dips the item in hand.
Spent,
}
/// `ItemStack.useItem` on `hit` as the original client runs it.
pub fn use_on_block(
world: &ClientWorld,
stack: ItemStack,
player: &LocalPlayer,
hit: &BlockHit,
obstructed: impl Fn(&[Aabb]) -> bool,
) -> Outcome {
let Some(properties) = stack.item.properties() else {
return Outcome::Ignored;
};
let position @ [x, y, z] = [hit.x, hit.y, hit.z];
let side = hit.side;
let target = world.block(x, y, z);
let air_above = world.block(x, y + 1, z).is_air();
let spent = |done| {
if done {
Outcome::Spent
} else {
Outcome::Ignored
}
};
let placed = |block| {
let metadata = if properties.has_subtypes {
stack.damage as u8
} else {
0
};
let (position, side) = if target == Block::SNOW_LAYER {
(Some(position), 0)
} else {
(adjacent(position, side), side)
};
position.is_some_and(|position| {
fits(world, block, position, side)
&& !obstructed(&shape::placement_boxes(world, block, metadata, position))
})
};
match properties.kind {
ItemKind::Block => spent(stack.item.block().is_some_and(placed)),
// `ItemReed` reports success even when the block does not fit
ItemKind::Places(block) if stack.count > 0 => {
if placed(block) {
Outcome::Spent
} else {
Outcome::Used
}
}
ItemKind::Sign => spent(super::predict_sign(world, *hit).is_some()),
ItemKind::Door(_) => spent(side == 1 && door_fits(world, [x, y + 1, z])),
ItemKind::Bed => spent(side == 1 && bed_fits(world, [x, y + 1, z], player.entity.yaw)),
ItemKind::Seeds => spent(side == 1 && target == Block::FARMLAND && air_above),
ItemKind::Hoe(_)
if matches!(target, Block::GRASS | Block::DIRT) && side != 0 && air_above =>
{
Outcome::Used
}
ItemKind::FlintAndSteel => Outcome::Used,
ItemKind::Redstone => {
let wire = if target == Block::SNOW_LAYER {
Some(position)
} else {
adjacent(position, side).filter(|&[x, y, z]| world.block(x, y, z).is_air())
};
match wire {
Some([x, y, z]) if world.block(x, y - 1, z).is_normal_cube() => Outcome::Spent,
Some(_) => Outcome::Used,
None => Outcome::Ignored,
}
}
// Whether the wall holds a painting depends on the entities there
ItemKind::Painting if side >= 2 => Outcome::Used,
ItemKind::Dye => spent(
stack.damage == 15 && matches!(target, Block::SAPLING | Block::WHEAT | Block::GRASS),
),
ItemKind::Record(_) => spent(target == Block::JUKEBOX && world.metadata(x, y, z) == 0),
ItemKind::Minecart(_) => spent(common::entity::vehicle::is_rail(target)),
_ => Outcome::Ignored,
}
}
/// `PlayerController.sendUseItem`: true when `onItemRightClick` changes the stack in hand.
pub fn uses_in_air(world: &ClientWorld, stack: ItemStack, player: &LocalPlayer) -> bool {
let Some(properties) = stack.item.properties() else {
return false;
};
let eye = player.eye(1.0);
let trace = |liquids| {
let end = eye + look(player.entity.yaw, player.entity.pitch) * 5.0;
shape::ray_trace(world, eye, end, liquids, false)
};
match properties.kind {
ItemKind::Food { .. } | ItemKind::Soup { .. } | ItemKind::Snowball | ItemKind::Egg => true,
ItemKind::Boat => trace(true).is_some(),
ItemKind::Bucket(None) => trace(false).is_some(),
ItemKind::Bucket(Some(Block::AIR)) => trace(true).is_some_and(|hit| {
world.block(hit.x, hit.y, hit.z).material().is_liquid()
&& world.metadata(hit.x, hit.y, hit.z) == 0
}),
ItemKind::Bucket(Some(_)) => trace(false).is_some_and(|hit| {
adjacent([hit.x, hit.y, hit.z], hit.side)
.is_some_and(|[x, y, z]| !world.block(x, y, z).material().is_solid())
}),
_ => false,
}
}
/// `World.canBlockBePlacedAt` without the entity check: `canPlaceBlockOnSide` of `block`.
fn fits(world: &ClientWorld, block: Block, position @ [x, y, z]: [i32; 3], side: u8) -> bool {
if !(0..128).contains(&y)
|| y == 127 && block.material().is_solid()
|| !world.block(x, y, z).material().is_ground_cover()
{
return false;
}
let normal = |dx, dy, dz| world.block(x + dx, y + dy, z + dz).is_normal_cube();
let walls = normal(-1, 0, 0) || normal(1, 0, 0) || normal(0, 0, -1) || normal(0, 0, 1);
let below = world.block(x, y - 1, z);
let solid = |dx, dz| world.block(x + dx, y, z + dz).material().is_solid();
match block {
Block::LEVER | Block::BUTTON => match side {
1 => block == Block::LEVER && normal(0, -1, 0),
2 => normal(0, 0, 1),
3 => normal(0, 0, -1),
4 => normal(1, 0, 0),
5 => normal(-1, 0, 0),
_ => false,
},
Block::TORCH | Block::REDSTONE_TORCH => walls || normal(0, -1, 0) || below == Block::FENCE,
Block::TRAPDOOR => {
side >= 2
&& adjacent(position, side ^ 1)
.is_some_and(|[x, y, z]| world.block(x, y, z).is_normal_cube())
}
Block::LADDER => walls,
Block::BROWN_MUSHROOM | Block::RED_MUSHROOM => below.is_opaque_cube(),
Block::DEAD_BUSH => below == Block::SAND,
Block::SAPLING | Block::DANDELION | Block::ROSE | Block::TALL_GRASS => {
matches!(below, Block::GRASS | Block::DIRT | Block::FARMLAND)
}
Block::CACTUS => {
!(solid(-1, 0) || solid(1, 0) || solid(0, -1) || solid(0, 1))
&& matches!(below, Block::CACTUS | Block::SAND)
}
Block::SNOW_LAYER => below.is_opaque_cube() && below.material().is_solid(),
Block::PUMPKIN
| Block::JACK_O_LANTERN
| Block::STONE_PRESSURE_PLATE
| Block::WOODEN_PRESSURE_PLATE
| Block::RAIL
| Block::POWERED_RAIL
| Block::DETECTOR_RAIL => normal(0, -1, 0),
Block::FENCE => below == Block::FENCE || below.material().is_solid(),
Block::CHEST => chest_fits(world, position),
_ => true,
}
}
/// `BlockChest.canPlaceBlockAt`: beside at most one chest, which has no other neighbor.
fn chest_fits(world: &ClientWorld, position: [i32; 3]) -> bool {
let chests = |[x, y, z]: [i32; 3]| {
(2..6)
.filter_map(|side| adjacent([x, y, z], side))
.filter(|&[x, y, z]| world.block(x, y, z) == Block::CHEST)
.collect::<Vec<_>>()
};
let neighbors = chests(position);
neighbors.len() <= 1
&& neighbors
.iter()
.all(|&neighbor| chests(neighbor).is_empty())
}
/// `BlockDoor.canPlaceBlockAt` for the lower half at `[x, y, z]`.
fn door_fits(world: &ClientWorld, [x, y, z]: [i32; 3]) -> bool {
let cover = |y| world.block(x, y, z).material().is_ground_cover();
(1..127).contains(&y) && world.block(x, y - 1, z).is_normal_cube() && cover(y) && cover(y + 1)
}
/// `ItemBed.onItemUse`: room for both halves, each on a normal cube.
fn bed_fits(world: &ClientWorld, [x, y, z]: [i32; 3], yaw: f32) -> bool {
let facing = (floor_f64(f64::from(yaw * 4.0 / 360.0) + 0.5) & 3) as usize;
let [dx, dz] = bed::HEAD_OFFSETS[facing];
[[x, z], [x + dx, z + dz]]
.into_iter()
.all(|[x, z]| world.block(x, y, z).is_air() && world.block(x, y - 1, z).is_normal_cube())
}