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 level mod.rs
//! The server's world: chunk loading and population, block access with the original
//! update semantics, scheduled ticks and deferred lighting. Port of `World` and `ChunkProvider`.
mod random_ticks;
mod ticks;
mod tiles;
mod updates;
use std::sync::Arc;
use common::JavaRandom;
use common::item::ItemStack;
use common::nbt::Compound;
use common::protocol::Packet;
use common::world::chunk::{self, HEIGHT};
use common::world::{Block, Chunk, ChunkPos, Material, TileEntity};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::blocks;
use crate::entity::Entities;
use crate::populate;
use crate::save::{ChunkStorage, LoadedChunk, SaveError};
use crate::terrain::Generator;
pub use common::world::light::LightKind;
use common::world::light::{self, LightQueue};
use ticks::{TickList, TickTarget};
pub use updates::BlockUpdates;
/// Horizontal world border of the original block accessors.
const LIMIT: i32 = 32_000_000;
/// The neighbors in the order of `World.notifyBlocksOfNeighborChange`.
pub(crate) const NEIGHBORS: [(i32, i32, i32); 6] = [
(-1, 0, 0),
(1, 0, 0),
(0, -1, 0),
(0, 1, 0),
(0, 0, -1),
(0, 0, 1),
];
/// A chunk read from disk or generated ahead of time, with its biomes, waiting to be loaded.
pub enum Prefetched {
Failed(SaveError),
Loaded(Box<LoadedChunk>, Box<[common::biome::Biome]>),
/// Lit by its columns, with `hellRNG` as Nether generation left it.
Generated(Box<Chunk>, Option<JavaRandom>, Box<[common::biome::Biome]>),
}
impl Prefetched {
/// Reads the chunk from `storage`, generating its terrain if it was never saved.
pub fn fetch(generator: &Generator, storage: Option<&ChunkStorage>, pos: ChunkPos) -> Self {
let has_sky = generator.dimension() == 0;
let sky = |chunk: Chunk| if has_sky { chunk } else { chunk.without_sky() };
let biomes = || {
generator
.biomes(pos.min_block_x(), pos.min_block_z(), 16, 16)
.into_boxed_slice()
};
match storage.map_or(Ok(None), |storage| storage.load(pos)) {
Ok(Some(mut loaded)) => {
loaded.chunk = sky(loaded.chunk);
Self::Loaded(Box::new(loaded), biomes())
}
Ok(None) => {
let (blocks, random) = generator.generate(pos);
let mut chunk = Box::new(sky(Chunk::new(pos, blocks)));
chunk.generate_sky_light();
Self::Generated(chunk, random, biomes())
}
Err(error) => Self::Failed(error),
}
}
}
/// An item dropped into the world, waiting for the entity system to spawn it.
#[derive(Clone, Copy, Debug)]
pub struct DroppedItem {
pub x: f64,
pub y: f64,
pub z: f64,
pub stack: ItemStack,
pub motion: Option<glam::DVec3>,
/// Added to the initial motion, like the sheep's push to its sheared wool.
pub impulse: glam::DVec3,
pub pickup_delay: i32,
}
pub struct Level {
storage_error: Option<SaveError>,
generator: Arc<Generator>,
/// `ChunkProviderHell.hellRNG`, shared by generation and population.
pub(crate) hell_rng: JavaRandom,
/// `WorldProvider.lightBrightnessTable`.
pub(crate) brightness: [f32; 16],
/// `TileEntitySign.isEditable`: signs placed but not yet written on.
pub(crate) editable_signs: FxHashMap<[i32; 3], i32>,
chunks: FxHashMap<ChunkPos, Box<Chunk>>,
/// Chunks read or generated ahead of time on worker threads.
prefetched: FxHashMap<ChunkPos, Prefetched>,
/// `World.rand`.
pub rand: JavaRandom,
/// Stands in for the private `Random` instances some blocks own.
pub(crate) block_rand: JavaRandom,
/// Stands in for `Math.random` in effects that do not touch the simulation.
pub(crate) effect_rand: JavaRandom,
/// The flow directions `BlockFlowing` keeps per instance, shared by nested ticks.
pub(crate) flow: blocks::FlowState,
pub time: i64,
pub difficulty: u8,
pub rules: crate::Rules,
/// `!WorldServer.levelSaving`: `save-off` keeps chunks loaded and unsaved.
pub(crate) saving: bool,
pub weather: common::world::weather::Weather,
/// `World.lastLightningBolt`.
pub(crate) last_lightning: i32,
/// `World.weatherEffects`.
pub(crate) lightning: Vec<crate::entity::Lightning>,
/// `World.ambientTickCountdown`: the wait before the next cave sound.
ambient_countdown: i32,
/// Biomes of loaded chunks, for weather (`WorldChunkManager.getBiomeGenAt`).
biomes: FxHashMap<ChunkPos, Box<[common::biome::Biome]>>,
ticks: TickList,
/// `World.scheduledUpdatesAreImmediate`.
pub(crate) immediate_ticks: bool,
/// `World.editingBlocks`: suppresses neighbor notifications.
pub(crate) editing_blocks: bool,
/// `BlockSand.fallInstantly`.
pub(crate) instant_falling: bool,
/// `BlockRedstoneWire.wiresProvidePower`.
pub(crate) wires_provide_power: bool,
pub(crate) wire_updates: blocks::UpdateSet,
pub(crate) torch_updates: blocks::TorchUpdates,
/// `BlockPistonBase.ignoreUpdates` of the plain and the sticky piston.
pub(crate) piston_ignore: [bool; 2],
light: LightQueue,
/// Block changes to send to clients, since [`Self::take_updates`].
updates: FxHashMap<ChunkPos, BlockUpdates>,
storage: Option<Arc<ChunkStorage>>,
/// Chunks that differ from their saved state (`Chunk.isModified`).
unsaved: FxHashSet<ChunkPos>,
pub entities: Entities,
pub(crate) maps: Arc<std::sync::Mutex<crate::save::maps::Maps>>,
pub(crate) statistics: FxHashMap<i32, Vec<(i32, i32)>>,
/// Entity NBT of loaded chunks, kept until the entities are spawned.
stored_entities: FxHashMap<ChunkPos, Vec<Compound>>,
pub dropped_items: Vec<DroppedItem>,
pub(crate) entity_tick_buffer: Vec<i32>,
/// Riders whose mount landed this update, with the fall distance.
pub(crate) rider_falls: Vec<(i32, f32)>,
/// Mobs hurt without an attacker during their tick, and whether the damage was accepted;
/// their own `attackEntityFrom` reacts on their tick.
pub(crate) unattributed_damage: Vec<(i32, bool)>,
pub events: Vec<Packet>,
/// Events for everyone near but the player who caused them.
pub(crate) events_except: Vec<(i32, Packet)>,
ticking_tiles: FxHashSet<[i32; 3]>,
/// Identifies each tile entity created since loading, so open containers notice replacements.
tile_generations: FxHashMap<[i32; 3], u64>,
next_tile_generation: u64,
tile_tick_buffer: Vec<[i32; 3]>,
random_chunks: Vec<ChunkPos>,
update_lcg: i32,
pub(crate) spawn_chunks: Vec<ChunkPos>,
pub(crate) spawn_biomes: FxHashMap<ChunkPos, common::biome::Biome>,
packer: chunk::Packer,
}
fn in_bounds(x: i32, z: i32) -> bool {
(-LIMIT..LIMIT).contains(&x) && (-LIMIT..=LIMIT).contains(&z)
}
fn in_world(x: i32, y: i32, z: i32) -> bool {
in_bounds(x, z) && (0..HEIGHT as i32).contains(&y)
}
fn local(x: i32, y: i32, z: i32) -> (usize, usize, usize) {
((x & 15) as usize, y as usize, (z & 15) as usize)
}
/// The block position of a tile entity stored at `index` of the chunk at `pos`.
pub(crate) fn tile_position(pos: ChunkPos, index: u16) -> [i32; 3] {
let index = usize::from(index);
[
pos.min_block_x() + (index >> 11) as i32,
(index & 127) as i32,
pos.min_block_z() + (index >> 7 & 15) as i32,
]
}
/// `World.findTopSolidBlock` in a chunk column: one above the highest solid or liquid block, or -1.
fn top_solid_or_liquid(chunk: &Chunk, x: usize, z: usize) -> i32 {
(1..HEIGHT)
.rev()
.find(|&y| {
let material = chunk.block(x, y, z).material();
material.is_solid() || material.is_liquid()
})
.map_or(-1, |y| y as i32 + 1)
}
impl Level {
/// A level that loads and saves chunks through `storage`, if given. Entity ids come from
/// `ids`, so they stay unique across dimensions.
pub fn with_ids(
generator: Arc<Generator>,
storage: Option<Arc<ChunkStorage>>,
ids: Arc<std::sync::atomic::AtomicI32>,
) -> Self {
let mut rand = JavaRandom::new(generator.seed());
let ambient_countdown = rand.next_int(12_000);
let entities = Entities::new(
generator.seed() ^ 0x2545_F491 ^ i64::from(generator.dimension()),
ids,
);
Self {
storage_error: None,
block_rand: JavaRandom::new(generator.seed() ^ 0x5DEE_CE66),
effect_rand: JavaRandom::new(generator.seed() ^ 0x3C6E_F372),
flow: blocks::FlowState::default(),
hell_rng: match &*generator {
Generator::Nether(nether) => nether.initial_rng.clone(),
_ => JavaRandom::new(0),
},
brightness: common::world::light::brightness_table(generator.dimension() == 0),
editable_signs: FxHashMap::default(),
generator,
chunks: FxHashMap::default(),
prefetched: FxHashMap::default(),
rand,
time: 0,
difficulty: 2,
rules: Default::default(),
saving: true,
weather: Default::default(),
last_lightning: 0,
lightning: Vec::new(),
ambient_countdown,
biomes: FxHashMap::default(),
ticks: TickList::default(),
immediate_ticks: false,
editing_blocks: false,
instant_falling: false,
wires_provide_power: true,
wire_updates: Default::default(),
torch_updates: Default::default(),
piston_ignore: [false; 2],
light: LightQueue::default(),
updates: FxHashMap::default(),
storage,
unsaved: FxHashSet::default(),
entities,
maps: Default::default(),
statistics: Default::default(),
stored_entities: FxHashMap::default(),
dropped_items: Vec::new(),
entity_tick_buffer: Vec::new(),
rider_falls: Vec::new(),
unattributed_damage: Vec::new(),
events: Vec::new(),
events_except: Vec::new(),
ticking_tiles: FxHashSet::default(),
tile_generations: FxHashMap::default(),
next_tile_generation: 0,
tile_tick_buffer: Vec::new(),
random_chunks: Vec::new(),
update_lcg: 0,
spawn_chunks: Vec::new(),
spawn_biomes: FxHashMap::default(),
packer: chunk::Packer::default(),
}
}
pub fn seed(&self) -> i64 {
self.generator.seed()
}
pub fn dimension(&self) -> i8 {
self.generator.dimension()
}
/// `!WorldProvider.hasNoSky`.
pub fn has_sky(&self) -> bool {
self.dimension() == 0
}
pub fn generator(&self) -> &Arc<Generator> {
&self.generator
}
// ---- Chunks -------------------------------------------------
pub fn chunk(&self, pos: ChunkPos) -> Option<&Chunk> {
self.chunks.get(&pos).map(Box::as_ref)
}
pub fn is_loaded(&self, pos: ChunkPos) -> bool {
self.chunks.contains_key(&pos)
}
pub fn loaded_chunks(&self) -> impl Iterator<Item = ChunkPos> + '_ {
self.chunks.keys().copied()
}
pub fn loaded_count(&self) -> usize {
self.chunks.len()
}
pub(crate) fn mark_entity_chunk(&mut self, pos: ChunkPos) {
if self.is_loaded(pos) {
self.unsaved.insert(pos);
}
}
/// Records a changed block for saving and for clients.
fn mark(&mut self, x: i32, y: i32, z: i32) {
let pos = ChunkPos::containing(x, z);
self.unsaved.insert(pos);
self.updates
.entry(pos)
.or_default()
.mark((x & 15) as u8, y as u8, (z & 15) as u8);
}
pub fn storage(&self) -> Option<&Arc<ChunkStorage>> {
self.storage.as_ref()
}
pub(crate) fn fail_storage(&mut self, error: SaveError) {
self.storage_error.get_or_insert(error);
}
pub fn check_storage(&self) -> Result<(), SaveError> {
if let Some(storage) = &self.storage {
storage.check()?;
}
match &self.storage_error {
Some(error) => Err(SaveError::Invalid(error.to_string())),
None => Ok(()),
}
}
pub fn offer_prefetched(&mut self, pos: ChunkPos, prefetched: Prefetched) {
if !self.chunks.contains_key(&pos) {
self.prefetched.insert(pos, prefetched);
}
}
pub fn is_prefetched(&self, pos: ChunkPos) -> bool {
self.prefetched.contains_key(&pos)
}
pub(crate) fn retain_prefetched(&mut self, keep: impl Fn(ChunkPos) -> bool) {
self.prefetched.retain(|&pos, _| keep(pos));
}
/// Saves the chunk if needed and drops it.
pub fn unload_chunk(&mut self, pos: ChunkPos) {
self.save_chunk(pos);
if self.storage_error.is_some() {
return;
}
let elsewhere = |&[x, _, z]: &[i32; 3]| ChunkPos::containing(x, z) != pos;
self.editable_signs
.retain(|position, _| elsewhere(position));
self.chunks.remove(&pos);
self.prefetched.remove(&pos);
self.spawn_biomes.remove(&pos);
self.biomes.remove(&pos);
self.stored_entities.remove(&pos);
let ids: Vec<_> = self
.entities
.in_chunk(pos)
.filter(|e| !matches!(e.kind, crate::entity::Kind::Player { .. }))
.map(|e| e.entity.id)
.collect();
for id in ids {
self.entities.remove(id);
}
self.ticking_tiles.retain(elsewhere);
self.tile_generations
.retain(|position, _| elsewhere(position));
}
fn save_chunk(&mut self, pos: ChunkPos) {
if self.storage_error.is_some() {
return;
}
let dirty = self.unsaved.contains(&pos);
if let (true, Some(storage), Some(chunk)) = (dirty, &self.storage, self.chunks.get(&pos)) {
let mut entities = self.stored_entities.get(&pos).cloned().unwrap_or_default();
entities.extend(
self.entities
.in_chunk(pos)
.filter_map(crate::entity::ServerEntity::write),
);
match storage.save(chunk, &entities, self.time) {
Ok(()) => {
self.unsaved.remove(&pos);
}
Err(error) => self.storage_error = Some(error),
}
}
}
/// `ChunkProvider.saveChunks`: all modified chunks, or at most 24 for periodic saves.
pub fn save_chunks(&mut self, all: bool) {
self.spawn_dropped_items();
let limit = if all { usize::MAX } else { 24 };
let chunks = &self.chunks;
self.unsaved.retain(|pos| chunks.contains_key(pos));
let dirty: Vec<ChunkPos> = self.unsaved.iter().copied().take(limit).collect();
for pos in dirty {
self.save_chunk(pos);
}
}
/// `ChunkProvider.provideChunk`: the loaded chunk, generating and populating it on demand.
fn chunk_mut(&mut self, pos: ChunkPos) -> &mut Chunk {
if !self.chunks.contains_key(&pos) {
self.load_chunk(pos);
}
self.chunks.get_mut(&pos).unwrap_or_else(|| unreachable!())
}
/// `ChunkProvider.prepareChunk`: loads a chunk, then populates every chunk whose
/// 2x2 population area it completes, in the original order.
pub fn load_chunk(&mut self, pos: ChunkPos) {
if self.chunks.contains_key(&pos) {
return;
}
let prefetched = self
.prefetched
.remove(&pos)
.unwrap_or_else(|| Prefetched::fetch(&self.generator, self.storage.as_deref(), pos));
let (chunk, biomes, generated) = match prefetched {
Prefetched::Failed(error) => {
self.fail_storage(error);
// Keep infallible block callbacks usable until the server aborts this tick
self.chunks
.insert(pos, Box::new(Chunk::new(pos, chunk::empty_blocks())));
return;
}
Prefetched::Loaded(loaded, biomes) => {
let LoadedChunk { chunk, entities } = *loaded;
if !entities.is_empty() {
self.stored_entities.insert(pos, entities);
}
(Box::new(chunk), biomes, false)
}
Prefetched::Generated(chunk, random, biomes) => {
if let Some(random) = random {
self.hell_rng = random;
}
(chunk, biomes, true)
}
};
if generated {
self.light.push_chunk(&chunk);
self.unsaved.insert(pos);
} else {
self.light.push_borders(&chunk);
}
self.ticking_tiles.extend(
chunk
.tile_entities
.iter()
.filter(|(_, tile)| tile.ticks())
.map(|(&index, _)| tile_position(pos, index)),
);
self.chunks.insert(pos, chunk);
self.biomes.insert(pos, biomes);
if let Some(stored) = self.stored_entities.remove(&pos) {
let mut unknown = Vec::new();
for nbt in stored {
// An entity saved outside its chunk stays with it while its own chunk is
// absent; the original would not update it there either
match crate::entity::ServerEntity::read(&nbt, &mut self.entities) {
Some(entity)
if entity.chunk() == pos || self.chunks.contains_key(&entity.chunk()) =>
{
if entity.chunk() != pos {
self.unsaved.insert(entity.chunk());
self.unsaved.insert(pos);
}
self.entities.insert(entity)
}
_ => unknown.push(nbt),
}
}
if !unknown.is_empty() {
self.stored_entities.insert(pos, unknown);
}
}
let (x, z) = (pos.x, pos.z);
let loaded = |level: &Self, dx, dz| level.is_loaded(ChunkPos::new(x + dx, z + dz));
let unpopulated = |level: &Self, dx, dz| {
level
.chunk(ChunkPos::new(x + dx, z + dz))
.is_some_and(|chunk| !chunk.populated)
};
if unpopulated(self, 0, 0) && loaded(self, 1, 1) && loaded(self, 0, 1) && loaded(self, 1, 0)
{
self.populate(pos);
}
if unpopulated(self, -1, 0) && loaded(self, -1, 1) && loaded(self, 0, 1) {
self.populate(pos.offset(-1, 0));
}
if unpopulated(self, 0, -1) && loaded(self, 1, -1) && loaded(self, 1, 0) {
self.populate(pos.offset(0, -1));
}
if unpopulated(self, -1, -1) && loaded(self, 0, -1) && loaded(self, -1, 0) {
self.populate(pos.offset(-1, -1));
}
}
/// `ChunkProvider.populate`.
fn populate(&mut self, pos: ChunkPos) {
let chunk = self.chunk_mut(pos);
if chunk.populated {
return;
}
chunk.populated = true;
self.unsaved.insert(pos);
let generator = self.generator.clone();
populate::populate(self, &generator, pos);
}
/// `World.checkChunksExist`.
pub fn chunks_exist(&self, min: (i32, i32, i32), max: (i32, i32, i32)) -> bool {
if max.1 < 0 || min.1 >= HEIGHT as i32 {
return false;
}
(min.0 >> 4..=max.0 >> 4)
.all(|cx| (min.2 >> 4..=max.2 >> 4).all(|cz| self.is_loaded(ChunkPos::new(cx, cz))))
}
/// `World.checkChunksExist` for a cube of the given radius.
pub fn chunks_exist_around(&self, x: i32, y: i32, z: i32, radius: i32) -> bool {
self.chunks_exist(
(x - radius, y - radius, z - radius),
(x + radius, y + radius, z + radius),
)
}
/// Block changes since the last call, per chunk.
pub fn take_updates(&mut self) -> FxHashMap<ChunkPos, BlockUpdates> {
std::mem::take(&mut self.updates)
}
// ---- Block access -------------------------------------------
/// `World.getBlockId`.
pub fn block(&mut self, x: i32, y: i32, z: i32) -> Block {
if !in_world(x, y, z) {
return Block::AIR;
}
let (lx, ly, lz) = local(x, y, z);
self.chunk_mut(ChunkPos::containing(x, z)).block(lx, ly, lz)
}
pub fn is_air(&mut self, x: i32, y: i32, z: i32) -> bool {
self.block(x, y, z).is_air()
}
pub fn material(&mut self, x: i32, y: i32, z: i32) -> Material {
self.block(x, y, z).material()
}
/// `World.getBlockMetadata`.
pub fn metadata(&mut self, x: i32, y: i32, z: i32) -> u8 {
if !in_world(x, y, z) {
return 0;
}
let (lx, ly, lz) = local(x, y, z);
self.chunk_mut(ChunkPos::containing(x, z))
.metadata
.get(chunk::index(lx, ly, lz))
}
/// `World.isBlockNormalCube`.
pub fn is_normal_cube(&mut self, x: i32, y: i32, z: i32) -> bool {
self.block(x, y, z).is_normal_cube()
}
/// `World.getHeightValue`, which does not load chunks.
pub fn height(&self, x: i32, z: i32) -> i32 {
if !in_bounds(x, z) {
return 0;
}
let (lx, _, lz) = local(x, 0, z);
self.chunk(ChunkPos::containing(x, z))
.map_or(0, |chunk| chunk.height(lx, lz) as i32)
}
/// `World.canBlockSeeTheSky`.
pub fn sees_sky(&mut self, x: i32, y: i32, z: i32) -> bool {
let (lx, _, lz) = local(x, 0, z);
y >= self.chunk_mut(ChunkPos::containing(x, z)).height(lx, lz) as i32
}
/// `World.getSavedLightValue`.
pub fn saved_light(&self, kind: LightKind, x: i32, y: i32, z: i32) -> u8 {
if !in_bounds(x, z) {
return match kind {
LightKind::Sky => 15,
LightKind::Block => 0,
};
}
light::saved_light(&self.chunks, kind, x, y, z)
}
/// `World.getFullBlockLightValue`: the brighter of sky and block light, ignoring daylight.
pub fn full_light(&mut self, x: i32, y: i32, z: i32) -> u8 {
if y < 0 {
return 0;
}
let (lx, ly, lz) = local(x, y.min(HEIGHT as i32 - 1), z);
let chunk = self.chunk_mut(ChunkPos::containing(x, z));
let index = chunk::index(lx, ly, lz);
chunk.sky_light.get(index).max(chunk.block_light.get(index))
}
/// `WorldChunkManager.getBiomeGenAt`.
pub fn biome(&self, x: i32, z: i32) -> common::biome::Biome {
match self.biomes.get(&ChunkPos::containing(x, z)) {
Some(biomes) => biomes[((x & 15) * 16 + (z & 15)) as usize],
None => self.generator.biomes(x, z, 1, 1)[0],
}
}
/// `World.canLightningStrikeAt`, which also is where rain falls; unloaded chunks stay dry.
pub fn rained_on(&self, x: i32, y: i32, z: i32) -> bool {
if !self.weather.is_raining() || y < 0 {
return false;
}
let Some(chunk) = self.chunk(ChunkPos::containing(x, z)) else {
return false;
};
let (lx, _, lz) = local(x, 0, z);
y >= chunk.height(lx, lz) as i32
&& top_solid_or_liquid(chunk, lx, lz) <= y
&& self.biome(x, z).rains()
}
/// `World.updateWeather`: returns whether rain or thunder started or stopped.
pub fn update_weather(&mut self) -> bool {
if self.last_lightning > 0 {
self.last_lightning -= 1;
}
let flags = (self.weather.raining, self.weather.thundering);
self.weather.advance(&mut self.rand);
flags != (self.weather.raining, self.weather.thundering)
}
/// `Packet70Bed` for remote clients when rain started or stopped since `was_raining`.
pub fn rain_packet(&self, was_raining: bool) -> Option<Packet> {
(self.weather.raining != was_raining).then_some(Packet::GameState {
state: if self.weather.raining { 1 } else { 2 },
})
}
pub fn weather_packet(&self) -> Packet {
let weather = &self.weather;
Packet::WeatherState {
raining: weather.raining,
thundering: weather.thundering,
rain: weather.rain,
thunder: weather.thunder,
}
}
/// `World.findTopSolidBlock`: one above the highest solid or liquid block, or -1.
pub fn top_solid_or_liquid(&mut self, x: i32, z: i32) -> i32 {
let (lx, _, lz) = local(x, 0, z);
top_solid_or_liquid(self.chunk_mut(ChunkPos::containing(x, z)), lx, lz)
}
// ---- Block changes ------------------------------------------
/// `World.setBlock`.
pub fn set_block(&mut self, x: i32, y: i32, z: i32, block: Block) -> bool {
self.change_block(x, y, z, block, None)
}
/// `World.setBlockAndMetadata`.
pub fn set_block_with_metadata(
&mut self,
x: i32,
y: i32,
z: i32,
block: Block,
metadata: u8,
) -> bool {
self.change_block(x, y, z, block, Some(metadata))
}
/// `World.setBlockWithNotify`.
pub fn set_block_notify(&mut self, x: i32, y: i32, z: i32, block: Block) -> bool {
let changed = self.set_block(x, y, z, block);
if changed {
self.notify_neighbors(x, y, z, block);
}
changed
}
/// `World.setBlockAndMetadataWithNotify`.
pub fn set_block_with_metadata_notify(
&mut self,
x: i32,
y: i32,
z: i32,
block: Block,
metadata: u8,
) -> bool {
let changed = self.set_block_with_metadata(x, y, z, block, metadata);
if changed {
self.notify_neighbors(x, y, z, block);
}
changed
}
/// `Chunk.setBlockID` (`metadata: None`) and `Chunk.setBlockIDWithMetadata`, in their
/// original order: write the id, notify the old block, store metadata and relight, notify the new block.
pub(crate) fn change_block(
&mut self,
x: i32,
y: i32,
z: i32,
block: Block,
metadata: Option<u8>,
) -> bool {
if !in_world(x, y, z) {
return false;
}
let pos = ChunkPos::containing(x, z);
let (lx, ly, lz) = local(x, y, z);
let chunk = self.chunk_mut(pos);
let old = chunk.block(lx, ly, lz);
let unchanged = old == block
&& metadata.is_none_or(|m| chunk.metadata.get(chunk::index(lx, ly, lz)) == m);
if unchanged {
return false;
}
chunk.swap_block(lx, ly, lz, block);
if !old.is_air() {
blocks::on_removed(self, old, x, y, z);
}
let chunk = self.chunk_mut(pos);
if let Some(rewritten) = chunk.finish_block_change(lx, ly, lz, metadata) {
self.light.push_column(x, z, rewritten);
}
self.light.push_block(x, y, z);
self.mark(x, y, z);
if !block.is_air() {
blocks::on_added(self, block, x, y, z);
}
true
}
/// `World.setBlockMetadata`.
pub fn set_metadata(&mut self, x: i32, y: i32, z: i32, metadata: u8) -> bool {
if !in_world(x, y, z) {
return false;
}
let pos = ChunkPos::containing(x, z);
let (lx, ly, lz) = local(x, y, z);
self.chunk_mut(pos)
.metadata
.set(chunk::index(lx, ly, lz), metadata);
self.mark(x, y, z);
true
}
/// `World.setBlockMetadataWithNotify`.
pub fn set_metadata_notify(&mut self, x: i32, y: i32, z: i32, metadata: u8) {
if self.set_metadata(x, y, z, metadata) {
let block = self.block(x, y, z);
self.notify_neighbors(x, y, z, block);
}
}
/// `World.notifyBlocksOfNeighborChange`.
pub fn notify_neighbors(&mut self, x: i32, y: i32, z: i32, block: Block) {
for (dx, dy, dz) in NEIGHBORS {
if self.editing_blocks {
continue;
}
let (nx, ny, nz) = (x + dx, y + dy, z + dz);
let neighbor = self.block(nx, ny, nz);
if !neighbor.is_air() {
blocks::on_neighbor_changed(self, neighbor, nx, ny, nz, block);
}
}
}
// ---- Tile entities and drops --------------------------------
/// `World.getBlockTileEntity`, creating a missing tile entity for container blocks like the original.
pub fn tile_entity_mut(&mut self, x: i32, y: i32, z: i32) -> Option<&mut TileEntity> {
if !(0..HEIGHT as i32).contains(&y) {
return None;
}
let (lx, ly, lz) = local(x, y, z);
let block = self.block(x, y, z);
let chunk = self.chunk_mut(ChunkPos::containing(x, z));
if chunk.tile_entity(lx, ly, lz).is_none() && block.is_container() {
blocks::on_added(self, block, x, y, z);
}
self.mark(x, y, z);
self.chunk_mut(ChunkPos::containing(x, z))
.tile_entity_mut(lx, ly, lz)
}
/// The loaded tile entity, without creating a missing one or marking it for clients.
pub(crate) fn chunk_tile_entity_mut(
&mut self,
x: i32,
y: i32,
z: i32,
) -> Option<&mut TileEntity> {
if !(0..HEIGHT as i32).contains(&y) {
return None;
}
let (lx, ly, lz) = local(x, y, z);
self.chunks
.get_mut(&ChunkPos::containing(x, z))?
.tile_entity_mut(lx, ly, lz)
}
pub fn tile_entity(&self, x: i32, y: i32, z: i32) -> Option<&TileEntity> {
if !(0..HEIGHT as i32).contains(&y) {
return None;
}
let (lx, ly, lz) = local(x, y, z);
self.chunk(ChunkPos::containing(x, z))?
.tile_entity(lx, ly, lz)
}
/// `World.setBlockTileEntity`.
pub fn set_tile_entity(&mut self, x: i32, y: i32, z: i32, tile_entity: TileEntity) {
if !(0..128).contains(&y) {
return;
}
let pos = ChunkPos::containing(x, z);
let (lx, ly, lz) = local(x, y, z);
let ticking = tile_entity.ticks();
let chunk = self.chunk_mut(pos);
if chunk.block(lx, ly, lz).is_container() {
chunk
.tile_entities
.insert(chunk::index(lx, ly, lz) as u16, tile_entity);
self.next_tile_generation += 1;
self.tile_generations
.insert([x, y, z], self.next_tile_generation);
self.mark(x, y, z);
if ticking {
self.ticking_tiles.insert([x, y, z]);
} else {
self.editable_signs.remove(&[x, y, z]);
self.ticking_tiles.remove(&[x, y, z]);
}
} else {
tracing::warn!("attempted to place a tile entity where there was no entity tile");
}
}
/// `World.removeBlockTileEntity`.
pub(crate) fn tile_generation(&self, position: [i32; 3]) -> Option<u64> {
self.tile_generations.get(&position).copied()
}
pub fn remove_tile_entity(&mut self, x: i32, y: i32, z: i32) -> Option<TileEntity> {
if !(0..128).contains(&y) {
return None;
}
self.editable_signs.remove(&[x, y, z]);
self.ticking_tiles.remove(&[x, y, z]);
self.tile_generations.remove(&[x, y, z]);
let (lx, ly, lz) = local(x, y, z);
self.chunk_mut(ChunkPos::containing(x, z))
.tile_entities
.remove(&(chunk::index(lx, ly, lz) as u16))
}
/// `Block.dropBlockAsItem_do`.
pub fn drop_item(&mut self, x: i32, y: i32, z: i32, stack: ItemStack) {
const SPREAD: f32 = 0.7;
let mut offset =
|| f64::from(self.rand.next_float() * SPREAD) + f64::from(1.0 - SPREAD) * 0.5;
let (dx, dy, dz) = (offset(), offset(), offset());
self.dropped_items.push(DroppedItem {
x: f64::from(x) + dx,
y: f64::from(y) + dy,
z: f64::from(z) + dz,
stack,
motion: None,
impulse: glam::DVec3::ZERO,
pickup_delay: 10,
});
}
// ---- Ticks --------------------------------------------------
/// `World.scheduleBlockUpdate`.
pub fn schedule_tick(&mut self, x: i32, y: i32, z: i32, block: Block, delay: i64) {
if !self.chunks_exist_around(x, y, z, 8) {
return;
}
if self.immediate_ticks {
self.tick_block(x, y, z, block);
} else {
let time = if block.is_air() { 0 } else { self.time + delay };
self.ticks.schedule(TickTarget { x, y, z, block }, time);
}
}
/// `World.TickUpdates`: runs up to 1000 due ticks.
pub fn run_scheduled_ticks(&mut self) {
for _ in 0..self.ticks.len().min(1000) {
let Some(TickTarget { x, y, z, block }) = self.ticks.pop_due(self.time) else {
break;
};
if self.chunks_exist_around(x, y, z, 8) {
self.tick_block(x, y, z, block);
}
}
}
/// Runs a scheduled tick of `block` if it is still there.
fn tick_block(&mut self, x: i32, y: i32, z: i32, block: Block) {
if self.block(x, y, z) == block && !block.is_air() {
blocks::update_tick(self, block, x, y, z);
}
}
/// `World.advanceTime`: scheduled ticks keep their distance to the time.
pub fn advance_time(&mut self, time: i64) {
self.ticks.shift(time - self.time);
self.time = time;
}
/// Brings light up to date with all block changes so far, then packs or unpacks block ids
/// by how often they are accessed.
pub fn update_light(&mut self) {
if !self.light.is_empty() {
let changed = self.light.process(&mut self.chunks);
let chunks = &self.chunks;
self.unsaved.extend(
changed
.into_iter()
.map(|[x, _, z]| ChunkPos::new(x, z))
.filter(|pos| chunks.contains_key(pos)),
);
}
let chunks = &self.chunks;
let populated = |pos: ChunkPos| chunks.get(&pos).is_some_and(|chunk| chunk.populated);
let repacked = self.packer.tick(|| {
(chunks.iter())
.map(|(&pos, chunk)| {
// Population writes into the chunk and its +x, +z and +xz neighbors
let settled = [(0, 0), (-1, 0), (0, -1), (-1, -1)]
.into_iter()
.all(|(dx, dz)| populated(pos.offset(dx, dz)));
(pos, &**chunk, settled)
})
.collect()
});
for (pos, from, blocks) in repacked {
if let Some(chunk) = self.chunks.get_mut(&pos) {
chunk.adopt_blocks(&from, blocks);
}
}
}
}