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 / 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())
}