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 / lib.rs

//! The game server. Singleplayer runs it in-process as an integrated server.

mod blocks;
mod caves;
mod chunks;
pub mod dedicated;
pub mod entity;
pub mod level;
pub mod nether;
mod player;
mod populate;
pub mod save;
mod spawn;
pub mod terrain;

use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU8, AtomicU32, Ordering};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};

use common::math::floor_f64;
use common::protocol::{Connection, Packet};
use common::world::ChunkPos;
use common::world::weather::Weather;
use glam::IVec3;

use crate::chunks::ChunkLoader;
pub use crate::chunks::{ViewArea, ViewShape};
use crate::level::Level;
use crate::player::ServerPlayer;
use crate::save::{SaveError, WorldInfo, WorldSave};
use crate::terrain::{Generator, OverworldGenerator};

const TICK: Duration = Duration::from_millis(50);
/// `World.autosavePeriod`, in ticks.
const AUTOSAVE_PERIOD: i64 = 40;
/// In chunks.
const MAX_VIEW_RADIUS: i32 = 129;
/// Instant fluid updates during population recurse deeply.
const SERVER_STACK: usize = 64 << 20;

#[derive(Clone, Debug)]
pub struct Settings {
    /// The world directory, created if missing.
    pub world: PathBuf,
    /// Name and seed for a new world; an existing world keeps its own.
    pub name: String,
    pub seed: i64,
    /// The chunks around the player that are sent to the client.
    pub view: ViewArea,
}

/// The dedicated server's gameplay policies from `server.properties`.
#[derive(Clone, Copy, Debug)]
pub struct Rules {
    pub pvp: bool,
    pub spawn_monsters: bool,
    pub spawn_animals: bool,
    pub allow_nether: bool,
    /// The dedicated world spawn x and z: non-operators may not change blocks within 16 blocks
    /// of it in the Overworld, and chunks within 128 blocks of it stay loaded.
    pub dedicated_spawn: Option<[i32; 2]>,
}

impl Default for Rules {
    /// Singleplayer: `EntityPlayer.isPVPEnabled` is false and nothing is restricted.
    fn default() -> Self {
        Self {
            pvp: false,
            spawn_monsters: true,
            spawn_animals: true,
            allow_nether: true,
            dedicated_spawn: None,
        }
    }
}

#[derive(Debug, thiserror::Error)]
pub enum StartError {
    #[error(transparent)]
    Save(#[from] SaveError),
    #[error("cannot start the server thread: {0}")]
    Thread(#[from] std::io::Error),
}

/// What the integrated server does each tick.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Activity {
    Running,
    /// A pause screen is open.
    Paused,
    /// The client waits for terrain: chunks stream, the world stands still.
    Preparing,
}

/// The state the integrated server shares with its thread.
struct Shared {
    running: AtomicBool,
    /// The percentage of a legacy save converted so far, or -1.
    conversion: AtomicI32,
    activity: AtomicU8,
    options: AtomicU32,
}

/// Runs the server on its own thread until dropped.
pub struct IntegratedServer {
    shared: Arc<Shared>,
    thread: Option<JoinHandle<Result<(), SaveError>>>,
}

impl IntegratedServer {
    pub fn start(settings: Settings) -> Result<(Self, Connection), StartError> {
        let (server_end, client_end) = common::protocol::local_connection();
        let shared = Arc::new(Shared {
            running: AtomicBool::new(true),
            conversion: AtomicI32::new(-1),
            activity: AtomicU8::new(Activity::Preparing as u8),
            options: AtomicU32::new(pack_options(settings.view, 2)),
        });
        let thread = std::thread::Builder::new()
            .name("server".into())
            .stack_size(SERVER_STACK)
            .spawn({
                let shared = shared.clone();
                move || {
                    let failure = server_end.sender();
                    let conversion = &shared.conversion;
                    let started = (|| {
                        let save = WorldSave::open_converting(&settings.world, &|percent| {
                            conversion.store(percent, Ordering::Relaxed)
                        })?;
                        conversion.store(-1, Ordering::Relaxed);
                        let existing = save.load_info()?;
                        Server::new(settings, save, existing, Some(server_end))
                    })();
                    match started {
                        Ok(server) => server.run(&shared),
                        Err(error) => {
                            conversion.store(-1, Ordering::Relaxed);
                            tracing::error!("world failed to start: {error}");
                            let _ = failure.send(disconnect(error.to_string()));
                            Err(error)
                        }
                    }
                }
            })
            .map(Some)?;
        Ok((Self { shared, thread }, client_end))
    }

    /// The percentage of a legacy save converted to McRegion, while converting.
    pub fn conversion(&self) -> Option<i32> {
        Some(self.shared.conversion.load(Ordering::Relaxed)).filter(|&percent| percent >= 0)
    }

    pub fn shutdown(&mut self) -> Result<(), SaveError> {
        self.shared.running.store(false, Ordering::Relaxed);
        join(self.thread.take())
    }

    pub fn set_options(&self, view: ViewArea, difficulty: u8) {
        self.shared
            .options
            .store(pack_options(view, difficulty), Ordering::Relaxed);
    }

    pub fn set_activity(&self, activity: Activity) {
        self.shared
            .activity
            .store(activity as u8, Ordering::Relaxed);
    }
}

impl Drop for IntegratedServer {
    fn drop(&mut self) {
        if let Err(error) = self.shutdown() {
            tracing::error!("server shutdown failed: {error}");
        }
    }
}

fn join(thread: Option<JoinHandle<Result<(), SaveError>>>) -> Result<(), SaveError> {
    let panicked = |_| Err(SaveError::Invalid("server thread panicked".into()));
    thread.map_or(Ok(()), |thread| thread.join().unwrap_or_else(panicked))
}

/// The view area in bits 8 and up, a disk in bit 2 and the difficulty in bits 0 and 1.
fn pack_options(view: ViewArea, difficulty: u8) -> u32 {
    let disk = u32::from(view.shape == ViewShape::Disk) << 2;
    (view.radius.clamp(1, MAX_VIEW_RADIUS) as u32) << 8 | disk | u32::from(difficulty.min(3))
}

fn disconnect(reason: impl Into<String>) -> Packet {
    Packet::Disconnect {
        reason: reason.into(),
    }
}

/// Sleeps until the next tick is due, or catches up when behind.
fn wait_tick(next_tick: &mut Instant) {
    *next_tick += TICK;
    let now = Instant::now();
    if *next_tick > now {
        std::thread::sleep(*next_tick - now);
    } else {
        *next_tick = now;
    }
}

/// One dimension: its level, the chunks loaded for its players, and those players.
struct World {
    level: Level,
    chunks: ChunkLoader,
    players: Vec<ServerPlayer>,
}

impl World {
    fn new(level: Level, view: ViewArea) -> Self {
        Self {
            level,
            chunks: ChunkLoader::new(view),
            players: Vec::new(),
        }
    }

    /// Whether anything still runs here: players, or chunks waiting to unload.
    fn active(&self) -> bool {
        !self.players.is_empty() || self.level.loaded_chunks().next().is_some()
    }
}

struct Server {
    save: WorldSave,
    info: WorldInfo,
    view: ViewArea,
    /// The Overworld, and the Nether once someone went there.
    worlds: Vec<World>,
    ids: Arc<AtomicI32>,
    dedicated: bool,
    /// Operator commands from chat, with the issuing player's name.
    commands: Vec<(String, String)>,
}

/// The index of a dimension in [`Server::worlds`].
fn world_index(dimension: i8) -> usize {
    usize::from(dimension == -1)
}

impl Server {
    fn new(
        settings: Settings,
        save: WorldSave,
        existing: Option<WorldInfo>,
        connection: Option<Connection>,
    ) -> Result<Self, SaveError> {
        let mut info = existing
            .clone()
            .unwrap_or_else(|| WorldInfo::new(settings.seed, &settings.name));
        let ids = Arc::new(AtomicI32::new(0));
        let generator = Arc::new(Generator::Overworld(OverworldGenerator::new(info.seed)));
        let mut level = Level::with_ids(generator, Some(save.overworld.clone()), ids.clone());
        level.maps = Arc::new(std::sync::Mutex::new(save::maps::Maps::open(save.dir())?));
        level.time = info.time;
        level.weather = Weather::new(
            info.raining,
            info.rain_time,
            info.thundering,
            info.thunder_time,
        );
        if existing.is_none() {
            let (x, z) = spawn::find(&mut level);
            info.spawn = [x, 64, z];
        }
        let [x, y, z] = info.spawn;
        tracing::info!(
            "world {:?} with seed {}, spawn at {x} {y} {z}",
            info.name,
            info.seed
        );
        let mut server = Self {
            save,
            view: settings.view,
            worlds: vec![World::new(level, settings.view)],
            ids,
            info,
            dedicated: false,
            commands: Vec::new(),
        };
        if let Some(connection) = connection {
            let dimension = server.info.dimension() as i8;
            let stored = server.info.player.clone();
            let spawn = server.info.spawn;
            let level = &mut server.world(dimension)?.level;
            let mut player = ServerPlayer::join(
                connection,
                "Player",
                level,
                stored.as_ref(),
                spawn,
                player::EYE,
            );
            player.send_weather(level, false);
            server.worlds[world_index(dimension)].players.push(player);
        }
        Ok(server)
    }

    /// The world of a dimension, creating the Nether on first use.
    fn world(&mut self, dimension: i8) -> Result<&mut World, SaveError> {
        let index = world_index(dimension);
        if index >= self.worlds.len() {
            let generator = Arc::new(Generator::Nether(nether::NetherGenerator::new(
                self.info.seed,
            )));
            let storage = self.save.nether()?;
            let overworld = &self.worlds[0].level;
            let mut level = Level::with_ids(generator, Some(storage), self.ids.clone());
            level.maps = overworld.maps.clone();
            level.time = overworld.time;
            level.difficulty = overworld.difficulty;
            level.weather = Self::nether_weather(&overworld.weather);
            level.rules = overworld.rules;
            self.worlds.push(World::new(level, self.view));
        }
        Ok(&mut self.worlds[index])
    }

    /// `World.calculateInitialWeather` of a Nether that shares the Overworld's flags.
    fn nether_weather(w: &Weather) -> Weather {
        Weather::new(w.raining, w.rain_time, w.thundering, w.thunder_time)
    }

    fn players(&self) -> impl Iterator<Item = &ServerPlayer> {
        self.worlds.iter().flat_map(|world| &world.players)
    }

    fn players_mut(&mut self) -> impl Iterator<Item = &mut ServerPlayer> {
        self.worlds.iter_mut().flat_map(|world| &mut world.players)
    }

    /// `ServerConfigurationManager.getPlayerEntity`: names match ignoring case.
    fn player_mut(&mut self, name: &str) -> Option<&mut ServerPlayer> {
        self.players_mut()
            .find(|player| player.name.eq_ignore_ascii_case(name))
    }

    /// `ServerConfigurationManager.sendPacketToAllPlayers`.
    fn broadcast(&mut self, packet: Packet) {
        for player in self.players_mut() {
            player.send(packet.clone());
        }
    }

    /// Delivers chat that players addressed beyond their own dimension.
    fn deliver_messages(&mut self) {
        let mut messages = Vec::new();
        for player in self.players_mut() {
            messages.extend(
                std::mem::take(&mut player.messages)
                    .into_iter()
                    .map(|message| (player.name.clone(), message)),
            );
        }
        for (sender, message) in messages {
            match message {
                player::Message::All(message) => self.broadcast(Packet::chat(message)),
                player::Message::Tell { to, message } => {
                    if let Some(player) = self.player_mut(&to) {
                        player.send(Packet::chat(message));
                    } else if let Some(player) = self.player_mut(&sender) {
                        player.send(Packet::chat("§cThere's no player by that name online."));
                    }
                }
                player::Message::Command(command) => self.commands.push((sender, command)),
            }
        }
    }

    /// `ServerConfigurationManager.playerLoggedOut` for every disconnected player.
    fn remove_disconnected(&mut self) -> Result<(), SaveError> {
        let mut left = Vec::new();
        for world in &mut self.worlds {
            for player in world.players.iter_mut().filter(|p| p.disconnected) {
                tracing::info!("{} lost connection", player.name);
                player.close_container(&mut world.level);
                player.wake_up(&mut world.level, true, false);
                world.level.mount(player.entity_id, None);
                if self.dedicated {
                    self.save
                        .save_player(&player.name, &player_nbt(player, &world.level))?;
                    left.push(format!("§e{} left the game.", player.name));
                } else {
                    player.write_nbt(
                        &world.level,
                        self.info.player.get_or_insert_with(Default::default),
                    );
                }
            }
            for player in world.players.extract_if(.., |player| player.disconnected) {
                let level = &mut world.level;
                level.entities.remove(player.entity_id);
                level.statistics.remove(&player.entity_id);
                level.maps.lock().unwrap().holders.remove(&player.entity_id);
            }
        }
        for message in left {
            self.broadcast(Packet::chat(message));
        }
        Ok(())
    }

    fn run(mut self, shared: &Shared) -> Result<(), SaveError> {
        let result = (|| {
            let mut next_tick = Instant::now();
            for world in &self.worlds {
                world.level.check_storage()?;
            }
            while shared.running.load(Ordering::Relaxed) && self.players().next().is_some() {
                let options = shared.options.load(Ordering::Relaxed);
                self.view = ViewArea {
                    radius: (options >> 8) as i32,
                    shape: if options & 4 != 0 {
                        ViewShape::Disk
                    } else {
                        ViewShape::Square
                    },
                };
                for world in &mut self.worlds {
                    world.chunks.set_view(self.view);
                    world.level.difficulty = (options & 3) as u8;
                }
                match shared.activity.load(Ordering::Relaxed) {
                    a if a == Activity::Running as u8 => self.tick()?,
                    a if a == Activity::Preparing as u8 => self.prepare()?,
                    _ => {}
                }
                wait_tick(&mut next_tick);
            }
            for world in &mut self.worlds {
                for player in &mut world.players {
                    player.close_container(&mut world.level);
                }
            }
            self.save(true)
        })();
        if let Err(error) = &result {
            tracing::error!("world stopped: {error}");
            for player in self.players_mut() {
                player.send(disconnect(error.to_string()));
            }
        }
        result
    }

    /// `World.saveWorld`: `level.dat` and either every modified chunk or a batch of them.
    fn save(&mut self, all: bool) -> Result<(), SaveError> {
        self.save.check_session_lock()?;
        if self.dedicated {
            for world in &self.worlds {
                for player in &world.players {
                    self.save
                        .save_player(&player.name, &player_nbt(player, &world.level))?;
                }
            }
        }
        if !self.worlds[0].level.saving {
            return Ok(());
        }
        for world in &mut self.worlds {
            let level = &mut world.level;
            level.check_storage()?;
            level.update_light();
            level.save_chunks(all);
            level.check_storage()?;
            if all && let Some(storage) = level.storage() {
                storage.flush()?;
            }
        }
        self.worlds[0].level.maps.lock().unwrap().save()?;
        let overworld = &self.worlds[0].level;
        self.info.time = overworld.time;
        let weather = &overworld.weather;
        self.info.raining = weather.raining;
        self.info.rain_time = weather.rain_time;
        self.info.thundering = weather.thundering;
        self.info.thunder_time = weather.thunder_time;
        if !self.dedicated
            && let Some(world) = self.worlds.iter().find(|world| !world.players.is_empty())
        {
            world.players[0].write_nbt(
                &world.level,
                self.info.player.get_or_insert_with(Default::default),
            );
        }
        self.save.save_info(&mut self.info)
    }

    /// The sleeping part of `World.tick`: once everyone is fully asleep, a monster may wake
    /// them up, or the night ends.
    fn skip_night(world: &mut World) {
        let World { level, players, .. } = world;
        if players.is_empty() || !players.iter().all(|p| p.fully_asleep(level)) {
            return;
        }
        let ids: Vec<i32> = players.iter().map(|p| p.entity_id).collect();
        let woken = if level.difficulty >= 1 && level.rules.spawn_monsters {
            level.sleep_spawn(&ids)
        } else {
            Vec::new()
        };
        if woken.is_empty() {
            let time = level.time + 24_000;
            level.time = time - time % 24_000;
            let was_raining = level.weather.raining;
            level.weather.clear();
            for player in players.iter_mut() {
                if player.sleeping(level) {
                    player.wake_up(level, false, true);
                }
                player.send(Packet::UpdateTime { time: level.time });
                player.send_weather(level, was_raining);
            }
        } else {
            for player in players.iter_mut().filter(|p| woken.contains(&p.entity_id)) {
                player.wake_up(level, true, false);
            }
        }
    }

    fn tick(&mut self) -> Result<(), SaveError> {
        for world in &mut self.worlds {
            for player in &mut world.players {
                for packet in player.receive() {
                    player.handle(&mut world.level, packet);
                }
            }
        }
        self.deliver_messages();
        // A singleplayer respawn may have repaired the world spawn
        let spawn = self.players().next().map(|player| player.spawn);
        if let Some(spawn) = spawn.filter(|_| !self.dedicated) {
            self.info.spawn = spawn;
        }
        // The dedicated Overworld always runs; singleplayer only runs the player's dimension
        if self.dedicated || !self.worlds[0].players.is_empty() {
            let overworld = &mut self.worlds[0];
            let was_raining = overworld.level.weather.raining;
            if overworld.level.update_weather() {
                for player in &mut overworld.players {
                    player.send_weather(&overworld.level, was_raining);
                }
                if let Some(packet) = overworld.level.rain_packet(was_raining) {
                    for player in self.worlds.iter_mut().skip(1).flat_map(|w| &mut w.players) {
                        player.send(packet.clone());
                    }
                }
            }
            Self::skip_night(&mut self.worlds[0]);
        }
        let time = self.worlds.iter().map(|w| w.level.time).max().unwrap_or(0) + 1;
        if time % AUTOSAVE_PERIOD == 0 {
            self.save(false)?;
        }
        let spawn = self.info.spawn;
        for world in &mut self.worlds {
            world.level.time = time;
            if world.active() {
                Self::tick_world(world, time, spawn)?;
            }
        }
        self.travel()?;
        self.remove_disconnected()?;
        Ok(())
    }

    /// Streams terrain to the players without advancing the world, while the client shows its
    /// loading screen (`Minecraft.preloadWorld`).
    fn prepare(&mut self) -> Result<(), SaveError> {
        for world in self.worlds.iter_mut().filter(|world| world.active()) {
            world.chunks.update(&mut world.level, &mut world.players);
            world.level.check_storage()?;
        }
        self.remove_disconnected()
    }

    /// `World.tick` and `World.updateEntities` of one dimension, then its players and chunks.
    fn tick_world(world: &mut World, time: i64, spawn: [i32; 3]) -> Result<(), SaveError> {
        let World {
            level,
            chunks,
            players,
        } = world;
        level.run_scheduled_ticks();
        let centers: Vec<_> = players
            .iter()
            .map(|player| {
                let at = player.position(level);
                ChunkPos::containing(floor_f64(at.x), floor_f64(at.z))
            })
            .collect();
        level.tick_random_blocks(&centers);
        level.spawn_mobs(&centers, spawn);
        level.tick_tiles();
        level.tick_entities();
        for player in players.iter_mut() {
            player.tick_survival(level);
        }
        for player in players.iter_mut() {
            player.tick_interaction(level);
        }
        if time % 20 == 0 {
            for player in players.iter_mut() {
                player.send(Packet::UpdateTime { time });
            }
        }
        // Sent at once like the original, before the block changes collected this tick
        let positions: Vec<_> = players.iter().map(|p| p.position(level)).collect();
        let events = level.events.drain(..).map(|packet| (None, packet));
        let except = level
            .events_except
            .drain(..)
            .map(|(id, packet)| (Some(id), packet));
        for (excluded, packet) in events.chain(except) {
            for (player, position) in players.iter_mut().zip(&positions) {
                let skip = excluded == Some(player.entity_id)
                    || match &packet {
                        Packet::Animation {
                            entity,
                            animation: 1,
                        } => *entity == player.entity_id,
                        // `WorldServer.addLightningBolt` reaches players within 512 blocks
                        Packet::Weather { x, y, z, .. } => {
                            let bolt = IVec3::new(*x, *y, *z).as_dvec3();
                            position.distance_squared(bolt / 32.0) >= 512.0 * 512.0
                        }
                        // `WorldManager.playAuxSFX` reaches players within 64 blocks
                        Packet::AuxEffect { x, y, z, .. } => {
                            let at = IVec3::new(*x, (*y).into(), *z).as_dvec3();
                            position.distance_squared(at) >= 64.0 * 64.0
                        }
                        _ => false,
                    };
                if !skip {
                    player.send(packet.clone());
                }
            }
        }
        chunks.update(level, players);
        for player in players.iter_mut() {
            player.track_entities(level, chunks.tracking_distance());
        }
        level.check_storage()
    }

    /// Moves players whose portal timer ran out to the other dimension.
    fn travel(&mut self) -> Result<(), SaveError> {
        for from in 0..self.worlds.len() {
            let dimension = self.worlds[from].level.dimension();
            let leaving: Vec<_> = self.worlds[from]
                .players
                .extract_if(.., |player| player.destination(dimension).is_some())
                .collect();
            for mut player in leaving {
                let Some(destination) = player.destination(dimension) else {
                    continue;
                };
                let to = world_index(destination);
                self.world(destination)?;
                let [from, to] = self
                    .worlds
                    .get_disjoint_mut([from, to])
                    .expect("dimensions differ");
                player.change_dimension(&mut from.level, &mut to.level);
                to.players.push(player);
            }
        }
        Ok(())
    }
}

fn player_nbt(player: &ServerPlayer, level: &Level) -> common::nbt::Compound {
    let mut nbt = level
        .entities
        .get(player.entity_id)
        .map_or_else(Default::default, |entity| entity.extra.clone());
    player.write_nbt(level, &mut nbt);
    nbt
}