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 / 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],
        });
    }
}