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 client src world.rs
//! The client's copy of the world, filled by the server: port of `WorldClient`.
use std::collections::hash_map::Entry;
use std::sync::mpsc::{self, Receiver, TryRecvError};
use std::sync::{Arc, OnceLock};
use common::climate::Climate;
use common::entity::mob::MobKind;
use common::entity::{Entity, EntityWorld};
use common::math::floor_f64;
use common::physics::Aabb;
use common::world::chunk::{self, HEIGHT, Nibbles, WIDTH, index};
use common::world::light::{self, ChunkMap, LightQueue};
use common::world::shape::{self, BlockView};
use common::world::{Block, Chunk, ChunkPos, TileEntity};
use glam::DVec3;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::render::mesher::MeshInput;
/// Ticks the blocks of a column stay unpacked after they arrive, for quick first light and
/// meshes.
const PACK_DELAY: u64 = 100;
/// Block reads of meshing a section, see [`chunk::Heat`].
const MESH_READS: u32 = crate::render::mesher::PADDED.pow(3) as u32;
/// Biome tints: `ColorizerGrass` and `ColorizerFoliage` sampled with the climate of `WorldChunkManager`.
pub struct BiomeColors {
climate: Climate,
/// `misc/grasscolor.png` as 0xRRGGBB.
grass: Arc<[u32]>,
/// `misc/foliagecolor.png` as 0xRRGGBB.
foliage: Arc<[u32]>,
}
impl BiomeColors {
fn lookup(map: &[u32], temperature: f64, rainfall: f64) -> u32 {
let rainfall = rainfall * temperature;
let column = ((1.0 - temperature) * 255.0) as usize;
let row = ((1.0 - rainfall) * 255.0) as usize;
map[row << 8 | column]
}
/// `BlockGrass.colorMultiplier` at an arbitrary column, as 0xRRGGBB.
pub fn grass_at(&self, x: i32, z: i32) -> u32 {
self.at(&self.grass, x, z)
}
fn at(&self, map: &[u32], x: i32, z: i32) -> u32 {
let climate = self.climate.sample(x, z, 1, 1);
Self::lookup(map, climate.temperature[0], climate.rainfall[0])
}
/// `BlockTallGrass.colorMultiplier`: the grass color of a hashed nearby column.
pub fn tall_grass_at(&self, x: i32, y: i32, z: i32) -> u32 {
let mut hash = i64::from(
x.wrapping_mul(3_129_871)
.wrapping_add(z.wrapping_mul(6_129_781))
.wrapping_add(y),
);
hash = hash
.wrapping_mul(hash)
.wrapping_mul(42_317_861)
.wrapping_add(hash.wrapping_mul(11));
self.grass_at(
(i64::from(x) + (hash >> 14 & 31)) as i32,
(i64::from(z) + (hash >> 24 & 31)) as i32,
)
}
pub(crate) fn column(&self, pos: ChunkPos) -> ColumnColors {
let climate = self
.climate
.sample(pos.min_block_x(), pos.min_block_z(), 16, 16);
let tint = |map: &[u32]| {
std::array::from_fn(|i| {
let (x, z) = (i % 16, i / 16);
Self::lookup(
map,
climate.temperature[x * 16 + z],
climate.rainfall[x * 16 + z],
)
})
};
ColumnColors {
grass: tint(&self.grass),
foliage: tint(&self.foliage),
}
}
}
/// Per-column tints of one chunk as 0xRRGGBB, indexed `z * 16 + x`.
pub struct ColumnColors {
pub grass: [u32; 256],
pub foliage: [u32; 256],
}
pub struct Column {
pub chunk: Arc<Chunk>,
/// Filled by the first mesh job of the column.
pub colors: Arc<OnceLock<ColumnColors>>,
/// Whether the full column data has arrived since `PreChunk`.
pub filled: bool,
/// Renewed whenever the server replaces the column's light, so older relights skip it.
epoch: u64,
/// The tick the column's blocks last arrived in full.
arrived: u64,
}
/// Light that a worker relaxed over a snapshot of the columns.
struct Relit {
/// The new sky and block light of the chunks it changed, with the epochs it started from.
chunks: Vec<(ChunkPos, u64, Option<Nibbles>, Option<Nibbles>)>,
changed: FxHashSet<[i32; 3]>,
}
impl Relit {
fn run(mut queue: LightQueue, snapshot: FxHashMap<ChunkPos, (u64, Arc<Chunk>)>) -> Self {
let mut chunks: FxHashMap<ChunkPos, Arc<Chunk>> = snapshot
.iter()
.map(|(&pos, (_, chunk))| (pos, chunk.clone()))
.collect();
let changed = queue.process(&mut chunks);
let fresh = |new: &Nibbles, old: &Nibbles| (!new.ptr_eq(old)).then(|| new.clone());
let chunks = chunks
.into_iter()
.filter_map(|(pos, chunk)| {
let (epoch, old) = &snapshot[&pos];
let sky = fresh(&chunk.sky_light, &old.sky_light);
let block = fresh(&chunk.block_light, &old.block_light);
(sky.is_some() || block.is_some()).then_some((pos, *epoch, sky, block))
})
.collect();
Self { chunks, changed }
}
}
struct Columns(FxHashMap<ChunkPos, Column>);
impl ChunkMap for Columns {
fn chunk(&self, pos: ChunkPos) -> Option<&Chunk> {
self.0.get(&pos).map(|c| c.chunk.as_ref())
}
fn chunk_mut(&mut self, pos: ChunkPos) -> Option<&mut Chunk> {
self.0.get_mut(&pos).map(|c| Arc::make_mut(&mut c.chunk))
}
}
pub struct ClientWorld {
/// `TileEntityMobSpawner.yaw` and `yaw2` of the spawners near the player.
pub spawner_yaws: FxHashMap<[i32; 3], (f64, f64)>,
pub vehicles: Vec<(i32, Aabb)>,
pub maps: FxHashMap<i16, common::map::Map>,
biome_colors: Arc<BiomeColors>,
columns: Columns,
light: LightQueue,
/// The relight running on a worker, if any.
relighting: Option<Receiver<Relit>>,
packer: chunk::Packer,
/// Calls of [`Self::repack_blocks`], once per tick.
ticks: u64,
epochs: u64,
/// Sections to remesh, as section coordinates.
dirty: FxHashSet<[i32; 3]>,
pub time: i64,
pub spawn: [i32; 3],
/// Sounds raised by the local simulation, to be played by the sound system.
pub sounds: Vec<(String, DVec3, f32, f32)>,
/// Unpositioned sound effects (`playSoundFX`): name, volume and pitch.
pub sound_effects: Vec<(&'static str, f32, f32)>,
pub particles: crate::particles::Particles,
/// `World.rand`, for client-side effects.
pub rand: common::JavaRandom,
pub weather: common::world::weather::Weather,
/// `World.lightningFlash`: ticks the sky stays lit by lightning.
pub lightning_flash: i32,
/// `World.weatherEffects`.
pub bolts: Vec<crate::weather::Bolt>,
seed: i64,
dimension: i8,
/// `WorldProvider.lightBrightnessTable`.
pub brightness: [f32; 16],
/// Where pistons are moving blocks (`TileEntityPiston`).
pistons: FxHashSet<[i32; 3]>,
}
impl ClientWorld {
pub fn new(seed: i64, grass_colormap: Box<[u32]>, foliage_colormap: Box<[u32]>) -> Self {
Self::with_colors(seed, 0, grass_colormap.into(), foliage_colormap.into())
}
fn with_colors(seed: i64, dimension: i8, grass: Arc<[u32]>, foliage: Arc<[u32]>) -> Self {
let has_sky = dimension == 0;
Self {
spawner_yaws: FxHashMap::default(),
vehicles: Vec::new(),
maps: Default::default(),
biome_colors: Arc::new(BiomeColors {
climate: if has_sky {
Climate::new(seed)
} else {
Climate::Hell
},
grass,
foliage,
}),
columns: Columns(FxHashMap::default()),
light: LightQueue::default(),
relighting: None,
packer: chunk::Packer::default(),
ticks: 0,
epochs: 0,
dirty: FxHashSet::default(),
time: 0,
spawn: [0; 3],
sounds: Vec::new(),
sound_effects: Vec::new(),
particles: crate::particles::Particles::default(),
rand: common::JavaRandom::new(seed),
weather: Default::default(),
lightning_flash: 0,
bolts: Vec::new(),
seed,
dimension,
brightness: common::world::light::brightness_table(has_sky),
pistons: FxHashSet::default(),
}
}
pub fn column_count(&self) -> usize {
self.columns.0.len()
}
pub fn seed(&self) -> i64 {
self.seed
}
pub fn dimension(&self) -> i8 {
self.dimension
}
/// `WorldProvider.hasNoSky`, inverted: only the Overworld has a sky.
pub fn has_sky(&self) -> bool {
self.dimension == 0
}
/// `World.getCelestialAngle`; it is always midnight in the Nether.
pub fn celestial_angle(&self, partial: f32) -> f32 {
if self.has_sky() {
common::world::sky::celestial_angle(self.time, partial)
} else {
0.5
}
}
/// `World.findTopSolidBlock`: one above the highest solid or liquid block, or -1.
pub fn top_solid_or_liquid(&self, x: i32, z: i32) -> i32 {
(1..HEIGHT as i32)
.rev()
.find(|&y| {
let material = BlockView::block(self, x, y, z).material();
material.is_solid() || material.is_liquid()
})
.map_or(-1, |y| y + 1)
}
/// `WorldChunkManager.getBiomeGenAt`.
pub fn biome(&self, x: i32, z: i32) -> common::biome::Biome {
self.climate().sample(x, z, 1, 1).biomes[0]
}
/// `World.canLightningStrikeAt`: where rain falls.
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.columns.chunk(ChunkPos::containing(x, z)) else {
return false;
};
y >= chunk.height((x & 15) as usize, (z & 15) as usize) as i32
&& self.top_solid_or_liquid(x, z) <= y
&& self.biome(x, z).rains()
}
/// `WorldClient` of the joined world and dimension: empty, keeping only the time and the
/// spawn until the server updates them.
pub fn reset(&mut self, seed: i64, dimension: i8) {
let BiomeColors { grass, foliage, .. } = &*self.biome_colors;
let mut world = Self::with_colors(seed, dimension, grass.clone(), foliage.clone());
world.time = self.time;
world.spawn = self.spawn;
world.maps = std::mem::take(&mut self.maps);
*self = world;
}
pub fn climate(&self) -> &Climate {
&self.biome_colors.climate
}
/// The tile entities in the columns around `center`, with their position and whether their
/// column is filled.
fn tiles_near(
&self,
center: ChunkPos,
radius: i32,
) -> impl Iterator<Item = (bool, [i32; 3], &TileEntity)> {
(-radius..=radius)
.flat_map(move |dx| (-radius..=radius).map(move |dz| center.offset(dx, dz)))
.filter_map(|pos| Some((pos, self.columns.0.get(&pos)?)))
.flat_map(|(pos, column)| {
column
.chunk
.tile_entities
.iter()
.map(move |(&index, tile)| {
let index = usize::from(index);
let position = [
pos.min_block_x() + (index >> 11) as i32,
(index & 127) as i32,
pos.min_block_z() + (index >> 7 & 15) as i32,
];
(column.filled, position, tile)
})
})
}
/// The signs in the columns around `center`, with their text.
pub fn signs_near(&self, center: ChunkPos, radius: i32) -> Vec<([i32; 3], &[String; 4])> {
self.tiles_near(center, radius)
.filter_map(|(filled, position, tile)| match tile {
TileEntity::Sign(lines) if filled => Some((position, lines)),
_ => None,
})
.collect()
}
/// The mob spawners in the columns around `center`, with their mob and spawn delay.
pub fn spawner_mobs_near(
&self,
center: ChunkPos,
radius: i32,
) -> Vec<([i32; 3], MobKind, i16)> {
self.tiles_near(center, radius)
.filter_map(|(_, position, tile)| match tile {
TileEntity::MobSpawner { entity_id, delay } => {
Some((position, MobKind::from_name(entity_id)?, *delay))
}
_ => None,
})
.collect()
}
/// `TileEntityMobSpawner.updateEntity` on the client: its mob spins while a player is near.
pub fn tick_spawners(&mut self, player: DVec3) {
for (yaw, previous) in self.spawner_yaws.values_mut() {
*previous = *yaw;
}
let center = ChunkPos::containing(floor_f64(player.x), floor_f64(player.z));
let spawners = self.spawner_mobs_near(center, 2);
self.spawner_yaws
.retain(|position, _| spawners.iter().any(|(p, ..)| p == position));
for ([x, y, z], _, delay) in spawners {
let middle = DVec3::new(f64::from(x), f64::from(y), f64::from(z)) + 0.5;
if middle.distance_squared(player) >= 256.0 {
continue;
}
let (yaw, previous) = self.spawner_yaws.entry([x, y, z]).or_default();
*yaw += f64::from(1000.0 / (f32::from(delay) + 200.0));
while *yaw > 360.0 {
*yaw -= 360.0;
*previous -= 360.0;
}
}
}
/// Positions of the mob spawners in the columns around `center`.
pub fn spawners_near(&self, center: ChunkPos, radius: i32) -> Vec<[i32; 3]> {
self.tiles_near(center, radius)
.filter(|(_, _, tile)| matches!(tile, TileEntity::MobSpawner { .. }))
.map(|(_, position, _)| position)
.collect()
}
/// Whether a column has its data and can be meshed.
pub fn contains(&self, pos: ChunkPos) -> bool {
self.columns.0.get(&pos).is_some_and(|c| c.filled)
}
/// `WorldClient.doPreChunk`: creates an empty column, or drops one.
pub fn pre_chunk(&mut self, pos: ChunkPos, load: bool) {
if load {
let chunk = Chunk::new(pos, chunk::empty_blocks());
let column = Column {
chunk: Arc::new(if self.has_sky() {
chunk
} else {
chunk.without_sky()
}),
colors: Arc::default(),
filled: false,
epoch: self.epochs,
arrived: self.ticks,
};
self.epochs += 1;
self.columns.0.insert(pos, column);
} else {
self.columns.0.remove(&pos);
}
}
/// `World.setChunkData`: copies a box of blocks and light into the columns it covers.
/// Returns the columns that became complete.
pub fn set_region(
&mut self,
x: i32,
y: i32,
z: i32,
size: [u8; 3],
data: &[u8],
) -> Vec<ChunkPos> {
let [sx, sy, sz] = size.map(|s| i32::from(s) + 1);
let expected = (sx * sy * sz) as usize * 5 / 2;
if data.len() < expected {
tracing::warn!("chunk data too short: {} < {expected}", data.len());
return Vec::new();
}
let ys = y.max(0) as usize..(y + sy).min(HEIGHT as i32) as usize;
let mut offset = 0;
let mut completed = Vec::new();
let mut partial = false;
for cx in x >> 4..=(x + sx - 1) >> 4 {
for cz in z >> 4..=(z + sz - 1) >> 4 {
let pos = ChunkPos::new(cx, cz);
let xs = (x - cx * 16).max(0) as usize..(x + sx - cx * 16).min(16) as usize;
let zs = (z - cz * 16).max(0) as usize..(z + sz - cz * 16).min(16) as usize;
let full = xs.len() == WIDTH && zs.len() == WIDTH && ys.len() == HEIGHT;
let Some(column) = self.columns.0.get_mut(&pos) else {
offset += xs.len() * zs.len() * ys.len() * 5 / 2;
continue;
};
let chunk = Arc::make_mut(&mut column.chunk);
let old_heights: Vec<usize> = xs
.clone()
.flat_map(|x| zs.clone().map(move |z| (x, z)))
.map(|(x, z)| chunk.height(x, z))
.collect();
offset += chunk.read_region(&data[offset..], xs.clone(), ys.clone(), zs.clone());
if full {
if !column.filled {
completed.push(pos);
}
column.filled = true;
column.arrived = self.ticks;
column.epoch = self.epochs;
self.epochs += 1;
self.light.push_borders(chunk);
} else {
partial = true;
// The server's light outside the box follows from the new blocks
let columns = xs.flat_map(|x| zs.clone().map(move |z| (x, z)));
for ((lx, lz), old) in columns.zip(old_heights) {
let (bx, bz) =
(pos.min_block_x() + lx as i32, pos.min_block_z() + lz as i32);
let new = chunk.height(lx, lz);
if new != old {
self.light.push_column(bx, bz, old.min(new)..old.max(new));
}
for y in ys.clone() {
self.light.push_block(bx, y as i32, bz);
}
}
}
}
}
// So does the light right around the box, which the new light inside may change
if partial {
let inside = |v: i32, start: i32, size: i32| (start..start + size).contains(&v);
for bx in x - 1..=x + sx {
for bz in z - 1..=z + sz {
for by in y - 1..=y + sy {
if !(inside(bx, x, sx) && inside(by, y, sy) && inside(bz, z, sz)) {
self.light.push_block(bx, by, bz);
}
}
}
}
}
// Faces and smooth lighting next to the box depend on it too
let top = (y + sy).min(HEIGHT as i32);
for sx_ in (x - 1) >> 4..=(x + sx) >> 4 {
for sy_ in (y.max(1) - 1) >> 4..=top.min(HEIGHT as i32 - 1) >> 4 {
for sz_ in (z - 1) >> 4..=(z + sz) >> 4 {
self.dirty.insert([sx_, sy_, sz_]);
}
}
}
completed
}
/// `WorldClient.setBlockAndMetadataAndInvalidate`: a block change from the server, relit locally.
pub fn set_block(&mut self, x: i32, y: i32, z: i32, block: Block, metadata: u8) {
if !(0..HEIGHT as i32).contains(&y) {
return;
}
let pos = ChunkPos::containing(x, z);
let (lx, ly, lz) = ((x & 15) as usize, y as usize, (z & 15) as usize);
let unchanged = |chunk: &Chunk| {
chunk.block(lx, ly, lz) == block && chunk.metadata.get(index(lx, ly, lz)) == metadata
};
if self.columns.chunk(pos).is_none_or(unchanged) {
return;
}
let Some(chunk) = self.columns.chunk_mut(pos) else {
return;
};
let old = chunk.swap_block(lx, ly, lz, block);
let key = key(x, y, z);
if old != block && old.is_container() {
chunk.tile_entities.remove(&key);
}
// `Chunk.getChunkBlockTileEntity` creates the blank board observers see while it is written
if matches!(block, Block::SIGN | Block::WALL_SIGN) {
chunk
.tile_entities
.entry(key)
.or_insert_with(TileEntity::sign);
}
if let Some(rewritten) = chunk.finish_block_change(lx, ly, lz, Some(metadata)) {
// `markBlocksDirtyVertical`: the rewritten column and the faces next to it
for sy in (rewritten.start.max(1) - 1) >> 4..=rewritten.end.min(HEIGHT - 1) >> 4 {
for dx in [-1, 0, 1] {
for dz in [-1, 0, 1] {
self.dirty.insert([(x + dx) >> 4, sy as i32, (z + dz) >> 4]);
}
}
}
self.light.push_column(x, z, rewritten);
}
self.light.push_block(x, y, z);
for dx in [-1, 0, 1] {
for dy in [-1, 0, 1] {
for dz in [-1, 0, 1] {
self.dirty
.insert([(x + dx) >> 4, (y + dy) >> 4, (z + dz) >> 4]);
}
}
}
}
pub fn set_tile_entity(&mut self, x: i32, y: i32, z: i32, tile_entity: TileEntity) {
if let Some(chunk) = self.columns.chunk_mut(ChunkPos::containing(x, z))
&& (0..HEIGHT as i32).contains(&y)
{
if matches!(tile_entity, TileEntity::Piston { .. }) {
self.pistons.insert([x, y, z]);
}
chunk.tile_entities.insert(key(x, y, z), tile_entity);
self.dirty.insert([x >> 4, y >> 4, z >> 4]);
}
}
/// `TileEntityPiston.updateEntity` on the client: half a block per tick, pushing the local
/// player, then the carried block is put in place.
pub fn tick_pistons(&mut self, player: &mut common::entity::Entity) {
let mut finished = Vec::new();
let mut pushes = Vec::new();
for &[x, y, z] in &self.pistons {
let Some(chunk) = self.columns.chunk_mut(ChunkPos::containing(x, z)) else {
finished.push(([x, y, z], None));
continue;
};
let Some(TileEntity::Piston {
block,
metadata,
facing,
progress,
last_progress,
extending,
..
}) = chunk.tile_entities.get_mut(&key(x, y, z))
else {
finished.push(([x, y, z], None));
continue;
};
*last_progress = *progress;
let (block, facing) = (Block(*block as u8), *facing as u8);
if *last_progress >= 1.0 {
pushes.push(([x, y, z], block, facing, *extending, 1.0, 0.25));
finished.push(([x, y, z], Some((block, *metadata as u8))));
} else {
*progress = (*progress + 0.5).min(1.0);
if *extending {
let distance = *progress - *last_progress + 1.0 / 16.0;
pushes.push(([x, y, z], block, facing, true, *progress, distance));
}
}
}
for ([x, y, z], block, facing, extending, progress, distance) in pushes {
let remaining = if extending {
1.0 - progress
} else {
progress - 1.0
};
if let Some(area) =
shape::moving_collision_box(&*self, block, x, y, z, facing, remaining)
&& area.intersects(&player.bb)
{
let [dx, dy, dz] = shape::SIDE_OFFSETS[usize::from(facing.min(5))];
let motion = DVec3::new(dx as f64, dy as f64, dz as f64) * f64::from(distance);
player.move_entity(self, motion);
}
}
for ([x, y, z], carried) in finished {
self.pistons.remove(&[x, y, z]);
let Some(chunk) = self.columns.chunk_mut(ChunkPos::containing(x, z)) else {
continue;
};
chunk.tile_entities.remove(&key(x, y, z));
if let Some((block, metadata)) = carried
&& BlockView::block(self, x, y, z) == Block::MOVING_PISTON
{
self.set_block(x, y, z, block, metadata);
}
self.dirty.insert([x >> 4, y >> 4, z >> 4]);
}
}
/// The blocks pistons are moving, with their tile entities.
pub fn moving_blocks(&self) -> impl Iterator<Item = ([i32; 3], &TileEntity)> {
self.pistons
.iter()
.filter_map(|&[x, y, z]| Some(([x, y, z], BlockView::tile_entity(self, x, y, z)?)))
}
/// Settles light after block changes, like the client's own lighting updates, on a worker so
/// rendering never waits for it. Applies the last finished relight, then starts the next one.
///
/// Relit arrays replace the current ones. Whatever changed since the snapshot is queued
/// again, and light has a unique fixed point, so the next relight settles it.
pub fn update_light(&mut self) {
if let Some(relighting) = self.relighting.take() {
match relighting.try_recv() {
Ok(relit) => self.apply_light(relit),
Err(TryRecvError::Empty) => {
self.relighting = Some(relighting);
return;
}
Err(TryRecvError::Disconnected) => {}
}
}
if self.light.is_empty() {
return;
}
let queue = std::mem::take(&mut self.light);
let touched: FxHashSet<ChunkPos> = queue.chunks().iter().copied().collect();
let mut snapshot = FxHashMap::default();
for center in touched {
for dx in -light::REACH..=light::REACH {
for dz in -light::REACH..=light::REACH {
let pos = center.offset(dx, dz);
if let Entry::Vacant(entry) = snapshot.entry(pos)
&& let Some(column) = self.columns.0.get(&pos)
{
entry.insert((column.epoch, column.chunk.clone()));
}
}
}
}
let (sender, receiver) = mpsc::sync_channel(1);
rayon::spawn(move || drop(sender.send(Relit::run(queue, snapshot))));
self.relighting = Some(receiver);
}
fn apply_light(&mut self, relit: Relit) {
for (pos, epoch, sky, block) in relit.chunks {
let Some(column) = self.columns.0.get_mut(&pos).filter(|c| c.epoch == epoch) else {
continue;
};
let chunk = Arc::make_mut(&mut column.chunk);
if let Some(sky) = sky {
chunk.sky_light = sky;
}
if let Some(block) = block {
chunk.block_light = block;
}
}
self.dirty.extend(relit.changed);
}
/// Packs or unpacks block ids by how often they are accessed, on a worker. Columns stay
/// unpacked for a while after they arrive, for quick first light and meshes.
pub fn repack_blocks(&mut self) {
self.ticks += 1;
let (columns, ticks) = (&self.columns.0, self.ticks);
let repacked = self.packer.tick(|| {
(columns.iter())
.map(|(&pos, c)| (pos, &*c.chunk, c.filled && c.arrived + PACK_DELAY <= ticks))
.collect()
});
for (pos, from, blocks) in repacked {
if let Some(column) = self.columns.0.get_mut(&pos)
&& column.chunk.blocks().ptr_eq(&from)
{
Arc::make_mut(&mut column.chunk).adopt_blocks(&from, blocks);
}
}
}
/// Sections to remesh since the last call.
pub fn take_dirty(&mut self) -> FxHashSet<[i32; 3]> {
std::mem::take(&mut self.dirty)
}
/// Everything needed to mesh one section, if the chunk and all eight neighbors are loaded.
pub fn mesh_input(
&self,
pos: ChunkPos,
section_y: i32,
fancy: bool,
smooth_lighting: bool,
) -> Option<MeshInput> {
let column = |i: i32| {
let column = self.columns.0.get(&pos.offset(i % 3 - 1, i / 3 - 1));
column.filter(|c| c.filled)
};
let center = column(4)?;
center.chunk.heat.add(MESH_READS);
let chunks: Vec<_> = (0..9)
.map(|i| Some(column(i)?.chunk.clone()))
.collect::<Option<_>>()?;
Some(MeshInput {
chunks: chunks.try_into().ok()?,
colors: center.colors.clone(),
biome_colors: self.biome_colors.clone(),
section_y,
fancy,
smooth_lighting,
})
}
/// Aux effect 2001 (`RenderGlobal.playAuxSFX`): the sound and debris of a broken block.
pub fn break_effect(&mut self, [x, y, z]: [i32; 3], block: Block, metadata: u8) {
let Some(properties) = block.properties() else {
return;
};
let corner = DVec3::new(f64::from(x), f64::from(y), f64::from(z));
let tint = self.debris_tint(block, metadata, [x, y, z]);
self.particles.break_block(block, metadata, corner, tint);
self.sounds.push((
properties.sound.dig().into(),
corner + 0.5,
(properties.sound.volume() + 1.0) / 2.0,
properties.sound.pitch() * 0.8,
));
}
/// `EntityDiggingFX.applyColourMultiplier`: `Block.colorMultiplier` as 0xRRGGBB, except for
/// grass blocks.
pub fn debris_tint(&self, block: Block, metadata: u8, [x, y, z]: [i32; 3]) -> u32 {
let colors = &self.biome_colors;
match block {
Block::LEAVES if metadata & 1 == 1 => 0x61_9961,
Block::LEAVES if metadata & 2 == 2 => 0x80_A755,
Block::LEAVES => colors.at(&colors.foliage, x, z),
Block::TALL_GRASS if metadata != 0 => colors.tall_grass_at(x, y, z),
Block::REDSTONE_WIRE => 0x80_0000,
_ => 0xFF_FFFF,
}
}
/// Port of `World.getBlockLightValue` (without the slab rule) with the given sky darkness.
pub fn light_level(&self, x: f64, y: f64, z: f64, sky_darkness: u32) -> u8 {
let [x, y, z] = [x, y, z].map(floor_f64);
let Some((chunk, [x, y, z])) = self.locate(x, y.min(HEIGHT as i32 - 1), z) else {
return 0;
};
let i = index(x, y, z);
let sky = i32::from(chunk.sky_light.get(i)) - sky_darkness as i32;
sky.max(i32::from(chunk.block_light.get(i))).max(0) as u8
}
/// The loaded chunk holding a block, and the block's position within it.
fn locate(&self, x: i32, y: i32, z: i32) -> Option<(&Chunk, [usize; 3])> {
let chunk = self.columns.chunk(ChunkPos::containing(x, z))?;
let local = [(x & 15) as usize, y as usize, (z & 15) as usize];
(0..HEIGHT as i32).contains(&y).then_some((chunk, local))
}
/// `World.blockExists`.
pub fn block_exists(&self, x: i32, y: i32, z: i32) -> bool {
(0..HEIGHT as i32).contains(&y) && self.columns.0.contains_key(&ChunkPos::containing(x, z))
}
}
impl BlockView for ClientWorld {
fn block(&self, x: i32, y: i32, z: i32) -> Block {
self.locate(x, y, z)
.map_or(Block::AIR, |(chunk, [x, y, z])| chunk.block(x, y, z))
}
fn metadata(&self, x: i32, y: i32, z: i32) -> u8 {
self.locate(x, y, z)
.map_or(0, |(chunk, [x, y, z])| chunk.metadata.get(index(x, y, z)))
}
fn tile_entity(&self, x: i32, y: i32, z: i32) -> Option<&TileEntity> {
let (chunk, [x, y, z]) = self.locate(x, y, z)?;
chunk.tile_entity(x, y, z)
}
}
/// The tile entity key of a block within its chunk.
fn key(x: i32, y: i32, z: i32) -> u16 {
index((x & 15) as usize, y as usize, (z & 15) as usize) as u16
}
impl EntityWorld for ClientWorld {
fn is_remote(&self) -> bool {
true
}
fn chunks_exist(&self, min: [i32; 3], max: [i32; 3]) -> 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.columns.0.contains_key(&ChunkPos::new(cx, cz)))
})
}
fn colliding_boxes(&self, entity: &Entity, region: &Aabb) -> Vec<Aabb> {
let mut boxes = Vec::new();
let (x0, x1) = (floor_f64(region.min.x), floor_f64(region.max.x + 1.0));
let (y0, y1) = (floor_f64(region.min.y), floor_f64(region.max.y + 1.0));
let (z0, z1) = (floor_f64(region.min.z), floor_f64(region.max.z + 1.0));
for x in x0..x1 {
for z in z0..z1 {
if !self.block_exists(x, 64, z) {
continue;
}
for y in y0 - 1..y1 {
let block = BlockView::block(self, x, y, z);
if !block.is_air() {
shape::collision_boxes(self, block, x, y, z, region, &mut boxes);
}
}
}
}
boxes.extend(
self.vehicles
.iter()
.filter(|(id, bounds)| Some(*id) != entity.riding && bounds.intersects(region))
.map(|(_, bounds)| *bounds),
);
boxes
}
fn rained_on(&self, x: i32, y: i32, z: i32) -> bool {
ClientWorld::rained_on(self, x, y, z)
}
fn on_collided(&mut self, block: Block, _pos: [i32; 3], entity: &mut Entity) {
match block {
Block::WEB => entity.in_web = true,
Block::PORTAL => entity.touch_portal(),
Block::SOUL_SAND => {
entity.motion.x *= 0.4;
entity.motion.z *= 0.4;
}
_ => {}
}
}
fn play_sound(&mut self, entity: &Entity, sound: &str, volume: f32, pitch: f32) {
self.sounds
.push((sound.to_owned(), entity.pos, volume, pitch));
}
fn spawn_particle(&mut self, name: &str, pos: DVec3, velocity: DVec3) {
self.particles.effect(name, pos, velocity);
}
}