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 / blocks / piston.rs

//! Pistons: `BlockPistonBase`, `BlockPistonExtension`, `BlockPistonMoving` and `TileEntityPiston`.

use common::protocol::Packet;
use common::world::shape::{self, SIDE_OFFSETS};
use common::world::{Block, Material, TileEntity};
use glam::DVec3;

use super::break_block;
use crate::level::Level;

const OPPOSITE: [u8; 6] = [1, 0, 3, 2, 5, 4];

/// The facing, extending and source flags of a moving block.
type Motion = (u8, bool, bool);

fn step([x, y, z]: [i32; 3], facing: u8, distance: i32) -> [i32; 3] {
    let [dx, dy, dz] = SIDE_OFFSETS[usize::from(facing)];
    [x + dx * distance, y + dy * distance, z + dz * distance]
}

fn is_piston(block: Block) -> bool {
    matches!(block, Block::PISTON | Block::STICKY_PISTON)
}

/// `Block.getMobilityFlag`: 0 pushable, 1 broken when pushed, 2 immovable.
pub fn mobility(block: Block) -> u8 {
    match block {
        Block::BED | Block::WOODEN_DOOR | Block::IRON_DOOR => 1,
        Block::STONE_PRESSURE_PLATE | Block::WOODEN_PRESSURE_PLATE => 1,
        Block::ICE | Block::RAIL | Block::POWERED_RAIL | Block::DETECTOR_RAIL => 0,
        _ => match block.material() {
            Material::Water
            | Material::Lava
            | Material::Leaves
            | Material::Plants
            | Material::Fire
            | Material::Circuits
            | Material::Snow
            | Material::Cactus
            | Material::Pumpkin
            | Material::Cake
            | Material::Web => 1,
            Material::Portal | Material::Piston => 2,
            _ => 0,
        },
    }
}

/// `BlockPistonBase.canPushBlock`.
fn can_push(level: &mut Level, block: Block, [x, y, z]: [i32; 3], breaking: bool) -> bool {
    if block == Block::OBSIDIAN {
        return false;
    }
    let movable = if is_piston(block) {
        level.metadata(x, y, z) & 8 == 0
    } else {
        block.hardness() != -1.0 && mobility(block) != 2 && (breaking || mobility(block) != 1)
    };
    movable && !block.is_container()
}

/// The end of the line of blocks a piston pushes, and whether it can be pushed at all
/// (`canExtend`); with `breaking` the block that gets in the way is destroyed (`tryExtend`).
fn push_end(level: &mut Level, piston: [i32; 3], facing: u8, breaking: bool) -> Option<[i32; 3]> {
    let mut position = step(piston, facing, 1);
    for distance in 0..13 {
        let [x, y, z] = position;
        if y <= 0 || y >= 127 {
            return None;
        }
        let block = level.block(x, y, z);
        if block.is_air() {
            break;
        }
        if !can_push(level, block, position, true) {
            return None;
        }
        if mobility(block) == 1 {
            if breaking {
                break_block(level, block, x, y, z);
            }
            break;
        }
        if distance == 12 {
            return None;
        }
        position = step(position, facing, 1);
    }
    Some(position)
}

/// `determineOrientation`: towards the placer, vertically when close by.
pub fn orientation(placer: DVec3, yaw: f32, [x, y, z]: [i32; 3]) -> u8 {
    if (placer.x as f32 - x as f32).abs() < 2.0 && (placer.z as f32 - z as f32).abs() < 2.0 {
        let eye = placer.y + 1.82;
        if eye - f64::from(y) > 2.0 {
            return 1;
        }
        if f64::from(y) - eye > 0.0 {
            return 0;
        }
    }
    [2, 5, 3, 4][super::placement::facing(yaw)]
}

/// `isPowered`: from any side but the front, or from around the block above.
fn powered(level: &Level, position @ [x, y, z]: [i32; 3], facing: u8) -> bool {
    (0..6).filter(|&side| side != facing).any(|side| {
        let [nx, ny, nz] = step(position, side, 1);
        level.power_from(nx, ny, nz, side)
    }) || level.powered(x, y + 1, z)
}

/// `updatePistonState`: starts extending or retracting when the power changed.
pub fn update(level: &mut Level, block: Block, x: i32, y: i32, z: i32) {
    if level.piston_ignore[usize::from(block == Block::STICKY_PISTON)] {
        return;
    }
    let meta = level.metadata(x, y, z);
    let facing = meta & 7;
    if meta == 7 || facing > 5 {
        return;
    }
    let power = powered(level, [x, y, z], facing);
    let extended = meta & 8 != 0;
    if power && !extended {
        if push_end(level, [x, y, z], facing, false).is_some() {
            level.set_metadata(x, y, z, facing | 8);
            level.play_note([x, y, z], 0, facing);
        }
    } else if !power && extended {
        level.set_metadata(x, y, z, facing);
        level.play_note([x, y, z], 1, facing);
    }
}

/// `onBlockAdded`: a piston that is not the end of a move checks its power.
pub fn added(level: &mut Level, block: Block, x: i32, y: i32, z: i32) {
    if level.tile_entity(x, y, z).is_none() {
        update(level, block, x, y, z);
    }
}

fn sound(level: &mut Level, [x, y, z]: [i32; 3], name: &str, pitch: f32) {
    level.events.push(Packet::Sound {
        name: name.into(),
        pos: [x, y, z].map(|v| f64::from(v) + 0.5),
        volume: 0.5,
        pitch,
    });
}

/// Puts a moving block at `position` (`setBlockAndMetadata` with its tile entity).
/// Clients draw the moving block from its tile entity.
fn set_moving(level: &mut Level, [x, y, z]: [i32; 3], block: Block, metadata: u8, motion: Motion) {
    let (facing, extending, source) = motion;
    let tile = TileEntity::piston(block, metadata, facing, extending, source);
    level.set_block_with_metadata(x, y, z, Block::MOVING_PISTON, metadata);
    level.events.push(Packet::TileEntity {
        x,
        y,
        z,
        data: Box::new(tile.clone()),
    });
    level.set_tile_entity(x, y, z, tile);
}

/// `BlockPistonBase.playBlock`: the extension (0) or retraction (1) itself.
pub fn play(level: &mut Level, piston: [i32; 3], action: u8, facing: u8) {
    if facing > 5 {
        return;
    }
    let [x, y, z] = piston;
    let block = level.block(x, y, z);
    let sticky = block == Block::STICKY_PISTON;
    let ignore = usize::from(sticky);
    level.piston_ignore[ignore] = true;
    if action == 0 {
        if extend(level, piston, facing, sticky) {
            level.set_metadata_notify(x, y, z, facing | 8);
            let pitch = level.rand.next_float() * 0.25 + 0.6;
            sound(level, piston, "tile.piston.out", pitch);
        }
    } else if action == 1 {
        let head = step(piston, facing, 1);
        clear(level, head);
        set_moving(level, piston, block, facing, (facing, false, true));
        let mut remove_head = true;
        if sticky {
            let pulled @ [px, py, pz] = step(piston, facing, 2);
            let mut target = level.block(px, py, pz);
            let mut meta = level.metadata(px, py, pz);
            let mut finished = false;
            if target == Block::MOVING_PISTON
                && let Some(&TileEntity::Piston {
                    block: stored,
                    metadata,
                    facing: moving,
                    extending: true,
                    ..
                }) = level.tile_entity(px, py, pz)
                && moving == i32::from(facing)
            {
                clear(level, pulled);
                target = Block(stored as u8);
                meta = metadata as u8;
                finished = true;
            }
            let pullable = !finished
                && !target.is_air()
                && can_push(level, target, pulled, false)
                && (mobility(target) == 0 || is_piston(target));
            if pullable {
                level.piston_ignore[ignore] = false;
                level.set_block_notify(px, py, pz, Block::AIR);
                level.piston_ignore[ignore] = true;
                set_moving(level, head, target, meta, (facing, false, false));
            }
            remove_head = !pullable && !finished;
        }
        if remove_head {
            let [hx, hy, hz] = head;
            level.piston_ignore[ignore] = false;
            level.set_block_notify(hx, hy, hz, Block::AIR);
            level.piston_ignore[ignore] = true;
        }
        let pitch = level.rand.next_float() * 0.15 + 0.6;
        sound(level, piston, "tile.piston.in", pitch);
    }
    level.piston_ignore[ignore] = false;
}

/// `tryExtend`: shifts the line of blocks in front one step, as moving blocks.
fn extend(level: &mut Level, piston: [i32; 3], facing: u8, sticky: bool) -> bool {
    let Some(mut position) = push_end(level, piston, facing, true) else {
        return false;
    };
    while position != piston {
        let previous @ [px, py, pz] = step(position, facing, -1);
        let mut block = level.block(px, py, pz);
        let mut meta = level.metadata(px, py, pz);
        if previous == piston && is_piston(block) {
            (block, meta) = (Block::PISTON_HEAD, facing | if sticky { 8 } else { 0 });
        }
        set_moving(level, position, block, meta, (facing, true, false));
        position = previous;
    }
    true
}

/// `TileEntityPiston.clearPistonTileEntity`: finishes a move at once.
pub fn clear(level: &mut Level, [x, y, z]: [i32; 3]) {
    let Some(&TileEntity::Piston {
        block,
        metadata,
        last_progress,
        ..
    }) = level.tile_entity(x, y, z)
    else {
        return;
    };
    if last_progress < 1.0 {
        level.remove_tile_entity(x, y, z);
        if level.block(x, y, z) == Block::MOVING_PISTON {
            level.set_block_with_metadata_notify(x, y, z, Block(block as u8), metadata as u8);
        }
    }
}

/// `TileEntityPiston.updateEntity`: half a block per tick, pushing entities along.
pub fn tick(level: &mut Level, [x, y, z]: [i32; 3]) {
    let Some(TileEntity::Piston {
        block,
        metadata,
        facing,
        progress,
        last_progress,
        extending,
        ..
    }) = level.chunk_tile_entity_mut(x, y, z)
    else {
        return;
    };
    *last_progress = *progress;
    let (block, metadata, facing, extending) = (
        Block(*block as u8),
        *metadata as u8,
        *facing as u8,
        *extending,
    );
    if *last_progress >= 1.0 {
        push_entities(level, [x, y, z], block, facing, 0.0, 0.25);
        level.remove_tile_entity(x, y, z);
        if level.block(x, y, z) == Block::MOVING_PISTON {
            level.set_block_with_metadata_notify(x, y, z, block, metadata);
        }
    } else {
        let previous = *last_progress;
        *progress = (*progress + 0.5).min(1.0);
        let current = *progress;
        if extending {
            let distance = current - previous + 1.0 / 16.0;
            push_entities(level, [x, y, z], block, facing, 1.0 - current, distance);
        }
    }
}

/// `updatePushedObjects`: moves the entities in the way of the moving block, which is
/// `remaining` blocks short of its destination.
fn push_entities(
    level: &mut Level,
    [x, y, z]: [i32; 3],
    block: Block,
    facing: u8,
    remaining: f32,
    distance: f32,
) {
    let Some(area) = shape::moving_collision_box(&*level, block, x, y, z, facing, remaining) else {
        return;
    };
    let ids: Vec<i32> = level
        .entities
        .iter()
        .filter(|e| e.entity.bb.intersects(&area))
        .map(|e| e.entity.id)
        .collect();
    let offset = SIDE_OFFSETS[usize::from(facing)];
    let motion = DVec3::from(offset.map(|d| f64::from(distance * d as f32)));
    for id in ids {
        if let Some(mut stored) = level.entities.take_for_tick(id) {
            // `EntityPainting.moveEntity`: any push breaks it
            if matches!(stored.kind, crate::entity::Kind::Painting { .. }) {
                if !stored.entity.dead && motion != DVec3::ZERO {
                    stored.entity.dead = true;
                    level.drop_painting(&stored.entity);
                }
            } else {
                stored.entity.move_entity(level, motion);
            }
            level.entities.finish_tick(stored);
        }
    }
}

// ---- Extension ----------------------------------------------

/// The base an extension with this facing belongs to.
fn base(level: &mut Level, [x, y, z]: [i32; 3]) -> Option<[i32; 3]> {
    let facing = level.metadata(x, y, z) & 7;
    (facing <= 5).then(|| step([x, y, z], OPPOSITE[usize::from(facing)], 1))
}

/// `BlockPistonExtension.onBlockRemoval`: an extended base without its head breaks.
pub fn head_removed(level: &mut Level, x: i32, y: i32, z: i32) {
    if let Some([bx, by, bz]) = base(level, [x, y, z]) {
        let base = level.block(bx, by, bz);
        if is_piston(base) && level.metadata(bx, by, bz) & 8 != 0 {
            break_block(level, base, bx, by, bz);
        }
    }
}

/// `BlockPistonExtension.onNeighborBlockChange`: passes the change on to its base.
pub fn head_neighbor_changed(level: &mut Level, x: i32, y: i32, z: i32) {
    if let Some([bx, by, bz]) = base(level, [x, y, z]) {
        let base = level.block(bx, by, bz);
        if is_piston(base) {
            update(level, base, bx, by, bz);
        } else {
            level.set_block_notify(x, y, z, Block::AIR);
        }
    }
}

impl Level {
    /// The moving piston's carried block, for drops (`BlockPistonMoving.dropBlockAsItemWithChance`).
    pub(crate) fn moving_block(&self, x: i32, y: i32, z: i32) -> Option<(Block, u8)> {
        match self.tile_entity(x, y, z)? {
            &TileEntity::Piston {
                block, metadata, ..
            } => Some((Block(block as u8), metadata as u8)),
            _ => None,
        }
    }
}