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 / mod.rs

//! Block behaviors: the `Block` subclass callbacks the world invokes on changes and ticks.

mod devices;
mod falling;
mod fire;
mod fluid;
mod growth;
mod piston;
mod placement;
mod plants;
pub(crate) mod portal;
mod rail;
mod redstone;

use common::item::{Item, ItemStack};
use common::world::{Block, TileEntity, bed};
use glam::DVec3;

use crate::level::{DroppedItem, Level};

pub use devices::{eject_record, insert_record, toggle_door, trigger_note, tune_note};
pub use fire::supported as fire_fits;
pub use fluid::FlowState;
pub use growth::{bone_meal_grass, grow_tree, random_tick};
pub use piston::tick as tick_piston;
pub use placement::{place, place_bed, place_door, place_sign};
pub use plants::{cactus_can_stay, can_stay, pumpkin_can_place, snow_fits, sugar_cane_can_stay};
pub use redstone::{
    TorchUpdates, UpdateSet, cycle_repeater, flip_lever, press_button, update_plate,
};

/// `x - 1`, `x + 1`, `z - 1`, `z + 1`, the order of the original's side loops.
const SIDES: [[i32; 2]; 4] = [[-1, 0], [1, 0], [0, -1], [0, 1]];

/// The blocks behind wall-mounted blocks facing 2 to 5 (north, south, west, east).
const WALLS: [[i32; 2]; 4] = [[0, 1], [0, -1], [1, 0], [-1, 0]];

/// `BlockDetectorRail.onEntityCollidedWithBlock`. The entity being moved is out of the
/// registry, so a cart counts itself.
pub fn detector_touched(level: &mut Level, [x, y, z]: [i32; 3], entity: &common::entity::Entity) {
    let cart = !entity.dead
        && entity
            .vehicle
            .is_some_and(|kind| kind != common::entity::vehicle::VehicleKind::Boat)
        && entity.bb.intersects(&redstone::plate_area(x, y, z));
    rail::update_detector(level, x, y, z, cart);
}

/// `Block.onBlockAdded`.
pub fn on_added(level: &mut Level, block: Block, x: i32, y: i32, z: i32) {
    match block {
        Block::FIRE => fire::added(level, x, y, z),
        Block::FLOWING_WATER | Block::FLOWING_LAVA => {
            fluid::on_flowing_added(level, block, x, y, z)
        }
        Block::WATER | Block::LAVA => fluid::check_for_harden(level, block, x, y, z),
        Block::SAND | Block::GRAVEL => falling::schedule(level, block, x, y, z),
        // `BlockStep.onBlockAdded`: a slab on a matching slab joins it
        Block::SLAB => {
            let metadata = level.metadata(x, y, z);
            if level.block(x, y - 1, z) == Block::SLAB && level.metadata(x, y - 1, z) == metadata {
                level.set_block_notify(x, y, z, Block::AIR);
                level.set_block_with_metadata_notify(x, y - 1, z, Block::DOUBLE_SLAB, metadata);
            }
        }
        Block::SIGN | Block::WALL_SIGN => {
            level.set_tile_entity(x, y, z, TileEntity::Sign(Default::default()));
        }
        Block::CHEST => level.set_tile_entity(x, y, z, TileEntity::chest()),
        Block::FURNACE | Block::LIT_FURNACE => {
            level.set_tile_entity(x, y, z, TileEntity::furnace());
            devices::default_facing(level, x, y, z);
        }
        Block::DISPENSER => {
            level.set_tile_entity(x, y, z, TileEntity::dispenser());
            devices::default_facing(level, x, y, z);
        }
        Block::SPAWNER => level.set_tile_entity(x, y, z, TileEntity::mob_spawner()),
        Block::NOTE_BLOCK => level.set_tile_entity(x, y, z, TileEntity::note()),
        Block::JUKEBOX => level.set_tile_entity(x, y, z, TileEntity::Jukebox(0)),
        Block::TORCH | Block::REDSTONE_TORCH | Block::UNLIT_REDSTONE_TORCH => {
            if level.metadata(x, y, z) == 0 {
                placement::orient_torch(level, block, x, y, z);
            }
            redstone::torch_changed(level, block, x, y, z);
        }
        Block::REDSTONE_WIRE => redstone::wire_added(level, x, y, z),
        Block::REPEATER | Block::POWERED_REPEATER => {
            redstone::repeater_added(level, block, x, y, z)
        }
        Block::TNT => devices::tnt_check(level, x, y, z, None),
        Block::RAIL | Block::POWERED_RAIL | Block::DETECTOR_RAIL => rail::added(level, x, y, z),
        Block::PISTON | Block::STICKY_PISTON => piston::added(level, block, x, y, z),
        _ => {}
    }
}

/// `Block.onBlockRemoval`, called after the block was already replaced.
pub fn on_removed(level: &mut Level, block: Block, x: i32, y: i32, z: i32) {
    match block {
        Block::LOG => growth::mark_leaves(level, x, y, z, 4, true),
        Block::LEAVES => growth::mark_leaves(level, x, y, z, 1, false),
        Block::CHEST | Block::FURNACE | Block::LIT_FURNACE | Block::DISPENSER => {
            if let Some(tile) = level.remove_tile_entity(x, y, z)
                && let Some(items) = tile.inventory()
            {
                spill_chest(level, x, y, z, items);
            }
        }
        Block::REDSTONE_WIRE => redstone::wire_removed(level, x, y, z),
        Block::REDSTONE_TORCH => redstone::torch_changed(level, block, x, y, z),
        Block::LEVER | Block::BUTTON => redstone::switch_removed(level, block, x, y, z),
        Block::STONE_PRESSURE_PLATE | Block::WOODEN_PRESSURE_PLATE => {
            redstone::plate_removed(level, block, x, y, z)
        }
        Block::PISTON_HEAD => piston::head_removed(level, x, y, z),
        Block::JUKEBOX => {
            devices::eject_record(level, x, y, z);
            level.remove_tile_entity(x, y, z);
        }
        _ if block.is_container() => {
            level.remove_tile_entity(x, y, z);
        }
        _ => {}
    }
}

/// `Block.onNeighborBlockChange`.
pub fn on_neighbor_changed(
    level: &mut Level,
    block: Block,
    x: i32,
    y: i32,
    z: i32,
    neighbor: Block,
) {
    match block {
        Block::FIRE if !fire::supported(level, x, y, z) => {
            level.set_block_notify(x, y, z, Block::AIR);
        }
        Block::FLOWING_WATER | Block::FLOWING_LAVA => {
            fluid::check_for_harden(level, block, x, y, z)
        }
        Block::WATER | Block::LAVA => fluid::on_still_neighbor_changed(level, block, x, y, z),
        Block::SAND | Block::GRAVEL => falling::schedule(level, block, x, y, z),
        Block::SAPLING
        | Block::TALL_GRASS
        | Block::DEAD_BUSH
        | Block::DANDELION
        | Block::ROSE
        | Block::BROWN_MUSHROOM
        | Block::RED_MUSHROOM
        | Block::WHEAT
        | Block::SUGAR_CANE
        | Block::CACTUS => plants::check_stay(level, block, x, y, z),
        Block::SNOW_LAYER => plants::check_snow(level, x, y, z),
        Block::BED => bed_neighbor_changed(level, x, y, z),
        Block::SIGN | Block::WALL_SIGN => placement::check_sign(level, block, x, y, z),
        Block::PORTAL => portal::neighbor_changed(level, x, y, z),
        Block::CAKE => plants::check_cake(level, x, y, z),
        Block::FARMLAND if level.material(x, y + 1, z).is_solid() => {
            level.set_block_notify(x, y, z, Block::DIRT);
        }
        Block::TORCH | Block::LADDER => placement::check_support(level, block, x, y, z),
        Block::REDSTONE_TORCH | Block::UNLIT_REDSTONE_TORCH => {
            placement::check_support(level, block, x, y, z);
            level.schedule_tick(x, y, z, block, 2);
        }
        Block::WOODEN_DOOR | Block::IRON_DOOR => {
            devices::door_neighbor_changed(level, block, x, y, z, neighbor)
        }
        Block::TRAPDOOR => devices::trapdoor_neighbor_changed(level, x, y, z, neighbor),
        Block::REDSTONE_WIRE => redstone::wire_neighbor_changed(level, x, y, z),
        Block::REPEATER | Block::POWERED_REPEATER => {
            redstone::repeater_neighbor_changed(level, block, x, y, z)
        }
        Block::LEVER | Block::BUTTON => redstone::switch_neighbor_changed(level, block, x, y, z),
        Block::STONE_PRESSURE_PLATE | Block::WOODEN_PRESSURE_PLATE
            if !level.is_normal_cube(x, y - 1, z) =>
        {
            break_block(level, block, x, y, z)
        }
        Block::TNT => devices::tnt_check(level, x, y, z, Some(neighbor)),
        Block::NOTE_BLOCK => devices::note_neighbor_changed(level, x, y, z, neighbor),
        Block::DISPENSER => devices::dispenser_neighbor_changed(level, x, y, z, neighbor),
        Block::RAIL | Block::POWERED_RAIL | Block::DETECTOR_RAIL => {
            rail::neighbor_changed(level, block, x, y, z, neighbor)
        }
        Block::PISTON | Block::STICKY_PISTON => piston::update(level, block, x, y, z),
        Block::PISTON_HEAD => piston::head_neighbor_changed(level, x, y, z),
        _ => {}
    }
}

/// `BlockRedstoneOre.glow`: sparkles on uncovered faces, and lights up unlit ore.
pub fn glow_redstone_ore(level: &mut Level, x: i32, y: i32, z: i32) {
    const OUT: f64 = 1.0 / 16.0;
    let block = [x, y, z];
    for (axis, offset) in [(1, 1), (1, -1), (2, 1), (2, -1), (0, 1), (0, -1)] {
        let mut point = block.map(|v| f64::from(v as f32 + level.rand.next_float()));
        let mut neighbor = block;
        neighbor[axis] += offset;
        let [nx, ny, nz] = neighbor;
        if !level.block(nx, ny, nz).is_opaque_cube() {
            point[axis] = f64::from(block[axis] + i32::from(offset > 0)) + f64::from(offset) * OUT;
        }
        // The original compares the height against 0 instead of the block's bottom
        let low = [f64::from(x), 0.0, f64::from(z)];
        if (0..3).any(|a| point[a] < low[a] || point[a] > f64::from(block[a] + 1)) {
            level.events.push(common::protocol::Packet::Particle {
                name: "reddust".into(),
                pos: point,
                velocity: [0.0; 3],
            });
        }
    }
    if level.block(x, y, z) == Block::REDSTONE_ORE {
        level.set_block_notify(x, y, z, Block::LIT_REDSTONE_ORE);
    }
}

/// `BlockBed.onNeighborBlockChange`: a half without its other half breaks.
fn bed_neighbor_changed(level: &mut Level, x: i32, y: i32, z: i32) {
    let metadata = level.metadata(x, y, z);
    let [dx, dz] = bed::HEAD_OFFSETS[bed::direction(metadata)];
    if bed::is_head(metadata) {
        if level.block(x - dx, y, z - dz) != Block::BED {
            level.set_block_notify(x, y, z, Block::AIR);
        }
    } else if level.block(x + dx, y, z + dz) != Block::BED {
        level.set_block_notify(x, y, z, Block::AIR);
        drop_as_item(level, Block::BED, x, y, z, metadata);
    }
}

/// `Block.updateTick` for scheduled ticks.
pub fn update_tick(level: &mut Level, block: Block, x: i32, y: i32, z: i32) {
    match block {
        Block::FIRE => fire::tick(level, x, y, z),
        Block::FLOWING_WATER | Block::FLOWING_LAVA => fluid::update_flowing(level, block, x, y, z),
        Block::SAND | Block::GRAVEL => falling::try_to_fall(level, block, x, y, z),
        Block::REDSTONE_TORCH | Block::UNLIT_REDSTONE_TORCH => {
            redstone::torch_tick(level, block, x, y, z)
        }
        Block::REPEATER | Block::POWERED_REPEATER => redstone::repeater_tick(level, block, x, y, z),
        Block::BUTTON => redstone::release_button(level, x, y, z),
        Block::STONE_PRESSURE_PLATE | Block::WOODEN_PRESSURE_PLATE
            if level.metadata(x, y, z) != 0 =>
        {
            redstone::update_plate(level, block, x, y, z, None)
        }
        Block::DETECTOR_RAIL if level.metadata(x, y, z) & 8 != 0 => {
            rail::update_detector(level, x, y, z, false)
        }
        Block::DISPENSER => devices::dispense(level, x, y, z),
        _ => {}
    }
}

/// `Block.tickRate`.
pub fn tick_rate(block: Block) -> i64 {
    match block {
        Block::FLOWING_WATER | Block::WATER => 5,
        Block::FLOWING_LAVA | Block::LAVA => 30,
        Block::SAND | Block::GRAVEL => 3,
        Block::REDSTONE_TORCH | Block::UNLIT_REDSTONE_TORCH => 2,
        Block::DISPENSER => 4,
        Block::BUTTON | Block::STONE_PRESSURE_PLATE | Block::WOODEN_PRESSURE_PLATE => 20,
        Block::DETECTOR_RAIL => 20,
        _ => 10,
    }
}

// ---- Drops --------------------------------------------------

/// `Block.dropBlockAsItem`: rolls the block's drops with the world random.
pub fn drop_as_item(level: &mut Level, block: Block, x: i32, y: i32, z: i32, metadata: u8) {
    drop_with_chance(level, block, [x, y, z], metadata, 1.0);
}

pub fn drop_with_chance(
    level: &mut Level,
    block: Block,
    [x, y, z]: [i32; 3],
    metadata: u8,
    chance: f32,
) {
    if block == Block::MOVING_PISTON {
        if let Some((stored, metadata)) = level.moving_block(x, y, z) {
            drop_with_chance(level, stored, [x, y, z], metadata, chance);
        }
        return;
    }
    for _ in 0..quantity_dropped(level, block) {
        if level.rand.next_float() > chance {
            continue;
        }
        if let Some(item) = id_dropped(level, block, metadata) {
            let stack = ItemStack::new(item, 1, damage_dropped(block, metadata));
            level.drop_item(x, y, z, stack);
        }
    }
    if block == Block::WHEAT {
        drop_seeds(level, x, y, z, metadata);
    }
}

fn quantity_dropped(level: &mut Level, block: Block) -> i32 {
    match block {
        Block::FLOWING_WATER
        | Block::WATER
        | Block::FLOWING_LAVA
        | Block::LAVA
        | Block::SNOW_LAYER
        | Block::ICE
        | Block::GLASS
        | Block::MOVING_PISTON
        | Block::FIRE => 0,
        Block::BOOKSHELF | Block::CAKE | Block::SPAWNER | Block::PORTAL => 0,
        Block::REDSTONE_ORE | Block::LIT_REDSTONE_ORE => 4 + level.rand.next_int(2),
        Block::GLOWSTONE => 2 + level.rand.next_int(3),
        Block::DOUBLE_SLAB => 2,
        Block::SNOW => 4,
        Block::LEAVES => i32::from(level.rand.next_int(20) == 0),
        Block::LAPIS_ORE => 4 + level.rand.next_int(5),
        Block::CLAY => 4,
        _ => 1,
    }
}

fn id_dropped(level: &mut Level, block: Block, metadata: u8) -> Option<Item> {
    let item = match block {
        Block::STONE => Item::from_block(Block::COBBLESTONE),
        Block::GRASS | Block::FARMLAND => Item::from_block(Block::DIRT),
        Block::OAK_STAIRS => Item::from_block(Block::PLANKS),
        Block::COBBLESTONE_STAIRS => Item::from_block(Block::COBBLESTONE),
        Block::DOUBLE_SLAB => Item::from_block(Block::SLAB),
        Block::LIT_FURNACE => Item::from_block(Block::FURNACE),
        Block::REDSTONE_ORE | Block::LIT_REDSTONE_ORE | Block::REDSTONE_WIRE => Item::REDSTONE,
        Block::REPEATER | Block::POWERED_REPEATER => Item::REPEATER,
        Block::PISTON_HEAD | Block::MOVING_PISTON => return None,
        Block::GLOWSTONE => Item::GLOWSTONE_DUST,
        Block::UNLIT_REDSTONE_TORCH => Item::from_block(Block::REDSTONE_TORCH),
        Block::WOODEN_DOOR | Block::IRON_DOOR | Block::BED if metadata & 8 != 0 => return None,
        Block::BED => Item::BED,
        Block::WOODEN_DOOR => Item::WOODEN_DOOR,
        Block::IRON_DOOR => Item::IRON_DOOR,
        Block::WEB => Item::STRING,
        Block::SIGN | Block::WALL_SIGN => Item::SIGN,
        Block::GRAVEL if level.rand.next_int(10) == 0 => Item::FLINT,
        Block::COAL_ORE => Item::COAL,
        Block::DIAMOND_ORE => Item::DIAMOND,
        Block::LAPIS_ORE => Item::DYE,
        Block::CLAY => Item::CLAY_BALL,
        Block::LEAVES => Item::from_block(Block::SAPLING),
        Block::TALL_GRASS if level.rand.next_int(8) == 0 => Item::SEEDS,
        Block::TALL_GRASS | Block::DEAD_BUSH => return None,
        Block::SUGAR_CANE => Item::SUGAR_CANE,
        Block::SNOW_LAYER | Block::SNOW => Item::SNOWBALL,
        Block::WHEAT if metadata == 7 => Item::WHEAT,
        Block::WHEAT => return None,
        _ => Item::from_block(block),
    };
    Some(item)
}

fn damage_dropped(block: Block, metadata: u8) -> i32 {
    match block {
        Block::LOG | Block::WOOL | Block::SLAB | Block::DOUBLE_SLAB => i32::from(metadata),
        Block::SAPLING | Block::LEAVES => i32::from(metadata & 3),
        Block::LAPIS_ORE => 4,
        _ => 0,
    }
}

/// Drops the block as items, then removes it.
fn break_block(level: &mut Level, block: Block, x: i32, y: i32, z: i32) {
    let metadata = level.metadata(x, y, z);
    drop_as_item(level, block, x, y, z, metadata);
    level.set_block_notify(x, y, z, Block::AIR);
}

/// Spawns an item entity. Items thrown with a `motion` can be picked up at once, others after
/// 10 ticks.
fn spawn_item(level: &mut Level, [x, y, z]: [f64; 3], stack: ItemStack, motion: Option<DVec3>) {
    level.dropped_items.push(DroppedItem {
        x,
        y,
        z,
        stack,
        motion,
        impulse: DVec3::ZERO,
        pickup_delay: if motion.is_some() { 0 } else { 10 },
    });
}

/// The extra seeds of `BlockCrops.dropBlockAsItemWithChance`, positioned in float precision.
fn drop_seeds(level: &mut Level, x: i32, y: i32, z: i32, metadata: u8) {
    for _ in 0..3 {
        if level.rand.next_int(15) <= i32::from(metadata) {
            let mut offset = || level.rand.next_float() * 0.7 + (1.0 - 0.7) * 0.5;
            let pos = [x, y, z].map(|v| f64::from(v as f32 + offset()));
            spawn_item(level, pos, ItemStack::one(Item::SEEDS), None);
        }
    }
}

/// `BlockChest.onBlockRemoval` (and the furnace's and dispenser's): scatters the contents in
/// stacks of 10 to 30.
fn spill_chest(level: &mut Level, x: i32, y: i32, z: i32, items: &[Option<ItemStack>]) {
    for stack in items.iter().flatten() {
        let mut offset = || level.block_rand.next_float() * 0.8 + 0.1;
        let pos = [x, y, z].map(|v| f64::from(v as f32 + offset()));
        let mut remaining = stack.count;
        while remaining > 0 {
            let count = (level.block_rand.next_int(21) + 10).min(remaining);
            remaining -= count;
            let mut scatter =
                |lift: f32| f64::from(level.block_rand.next_gaussian() as f32 * 0.05 + lift);
            let motion = DVec3::new(scatter(0.0), scatter(0.2), scatter(0.0));
            let part = ItemStack::new(stack.item, count, stack.damage);
            spawn_item(level, pos, part, Some(motion));
        }
    }
}