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