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 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));
}
}
}