Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
Download file

root / crates / server / src / 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);
            }
        }
    }
}