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 / dedicated / mod.rs

mod admin;
mod commands;
mod properties;
mod transport;

use std::net::{SocketAddr, TcpListener};
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread::JoinHandle;
use std::time::Instant;

use common::protocol::Packet;
use common::world::ChunkPos;
use glam::DVec3;

use self::admin::Admin;
use self::commands::Commands;
pub use self::properties::Properties;
use crate::save::{SaveError, WorldSave, read_gzip};
use crate::{Rules, SERVER_STACK, Server, Settings, StartError, disconnect, join, wait_tick};

pub struct DedicatedSettings {
    pub world: Settings,
    pub address: SocketAddr,
    pub allow_flight: bool,
    pub difficulty: u8,
    pub max_players: usize,
    pub white_list: bool,
    pub rules: Rules,
    /// The directory of `ops.txt` and the other admin lists.
    pub dir: PathBuf,
    pub properties: Properties,
}

pub struct DedicatedServer {
    pub address: SocketAddr,
    pub running: Arc<AtomicBool>,
    commands: Sender<String>,
    thread: Option<JoinHandle<Result<(), SaveError>>>,
}

impl DedicatedServer {
    pub fn start(settings: DedicatedSettings) -> Result<Self, StartError> {
        let listener = TcpListener::bind(settings.address)?;
        listener.set_nonblocking(true)?;
        let address = listener.local_addr()?;
        let save = WorldSave::open(&settings.world.world)?;
        let existing = save.load_info()?;
        let running = Arc::new(AtomicBool::new(true));
        let (commands, receiver) = mpsc::channel();
        let thread = std::thread::Builder::new()
            .name("dedicated-server".into())
            .stack_size(SERVER_STACK)
            .spawn({
                let running = running.clone();
                move || {
                    let result = (|| {
                        let mut server = Server::new(settings.world, save, existing, None)?;
                        server.dedicated = true;
                        let [x, _, z] = server.info.spawn;
                        for world in &mut server.worlds {
                            world.level.difficulty = settings.difficulty.min(3);
                            world.level.rules = Rules {
                                dedicated_spawn: Some([x, z]),
                                ..settings.rules
                            };
                        }
                        prepare_spawn(&mut server, &running)?;
                        let mut dedicated = Dedicated {
                            admin: Admin::load(&settings.dir, settings.white_list),
                            properties: settings.properties,
                            allow_flight: settings.allow_flight,
                            max_players: settings.max_players,
                        };
                        let result = dedicated.run(&mut server, listener, &running, receiver);
                        tracing::info!("Stopping server");
                        for world in &mut server.worlds {
                            for player in &mut world.players {
                                player.close_container(&mut world.level);
                                player.send(disconnect("Server closed"));
                            }
                        }
                        tracing::info!("Saving chunks");
                        let saved = server.save(true);
                        result.and(saved)
                    })();
                    running.store(false, Ordering::Relaxed);
                    result
                }
            })?;
        Ok(Self {
            address,
            running,
            commands,
            thread: Some(thread),
        })
    }

    pub fn command(&self, command: String) {
        let _ = self.commands.send(command);
    }

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

    pub fn wait(&mut self) -> Result<(), SaveError> {
        join(self.thread.take())
    }
}

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

/// `MinecraftServer.initWorld`: loads the chunks around the spawn of each dimension.
fn prepare_spawn(server: &mut Server, running: &AtomicBool) -> Result<(), SaveError> {
    const RADIUS: i32 = 196;
    let [x, _, z] = server.info.spawn;
    let count = 1 + usize::from(server.worlds[0].level.rules.allow_nether);
    for (index, dimension) in [0, -1].into_iter().enumerate().take(count) {
        tracing::info!("Preparing start region for level {index}");
        let level = &mut server.world(dimension)?.level;
        let mut reported = Instant::now();
        let side = (RADIUS * 2 + 1).pow(2);
        for dx in (-RADIUS..=RADIUS).step_by(16) {
            for dz in (-RADIUS..=RADIUS).step_by(16) {
                if !running.load(Ordering::Relaxed) {
                    return Ok(());
                }
                if reported.elapsed().as_secs() >= 1 {
                    let done = (dx + RADIUS) * (RADIUS * 2 + 1) + dz + 1;
                    tracing::info!("Preparing spawn area: {}%", done * 100 / side);
                    reported = Instant::now();
                }
                level.load_chunk(ChunkPos::containing(x + dx, z + dz));
                level.update_light();
            }
        }
        level.check_storage()?;
    }
    Ok(())
}

/// The dedicated server's state beyond the game: admission, settings and the console.
struct Dedicated {
    admin: Admin,
    properties: Properties,
    allow_flight: bool,
    max_players: usize,
}

impl Dedicated {
    fn run(
        &mut self,
        server: &mut Server,
        listener: TcpListener,
        running: &AtomicBool,
        console: Receiver<String>,
    ) -> Result<(), SaveError> {
        let (incoming, logins) = mpsc::sync_channel::<transport::Login>(32);
        let mut peers = Vec::new();
        let mut next_tick = Instant::now();
        tracing::info!(
            "Done! For help, type \"help\" or \"?\" (listening on {})",
            listener.local_addr().unwrap()
        );
        while running.load(Ordering::Relaxed) {
            peers.retain(|peer: &transport::Peer| !peer.finished());
            while peers.len() < self.max_players.saturating_add(16) {
                match listener.accept() {
                    Ok((stream, _)) => match transport::Peer::start(stream, incoming.clone()) {
                        Ok(peer) => peers.push(peer),
                        Err(error) => tracing::warn!("cannot start connection: {error}"),
                    },
                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break,
                    Err(error) => {
                        return Err(SaveError::Invalid(format!("Accept failed: {error}")));
                    }
                }
            }
            for login in logins.try_iter() {
                self.login(server, login)?;
            }
            server.tick()?;
            let typed = console.try_iter().map(|command| (None, command));
            let chat = server
                .commands
                .drain(..)
                .map(|(name, command)| (Some(name), command));
            let commands: Vec<_> = typed.chain(chat).collect();
            for (source, command) in commands {
                Commands {
                    server: &mut *server,
                    admin: &mut self.admin,
                    properties: &mut self.properties,
                    running,
                }
                .handle(source.as_deref(), command.trim())?;
            }
            wait_tick(&mut next_tick);
        }
        Ok(())
    }

    /// `ServerConfigurationManager.login` and `NetLoginHandler.doLogin`.
    fn login(&mut self, server: &mut Server, login: transport::Login) -> Result<(), SaveError> {
        let transport::Login {
            name,
            address,
            connection,
        } = login;
        let online = server.players().filter(|p| !p.disconnected);
        let others = online
            .filter(|p| !p.name.eq_ignore_ascii_case(&name))
            .count();
        let full = (others >= self.max_players).then_some("The server is full!");
        let refusal = self.admin.refusal(&name, address.ip()).or(full);
        if let Some(reason) = refusal {
            tracing::info!("Disconnecting {name} [/{address}]: {reason}");
            connection.send(disconnect(reason));
            return Ok(());
        }
        // The previous session is saved before its state is read back
        for player in server.players_mut() {
            if player.name.eq_ignore_ascii_case(&name) {
                player.kick("You logged in from another location");
            }
        }
        server.remove_disconnected()?;
        let path = server.save.dir().join(format!("players/{name}.dat"));
        let stored = match read_gzip(&path) {
            Ok(stored) => Some(stored),
            Err(SaveError::Io { source, .. }) if source.kind() == std::io::ErrorKind::NotFound => {
                None
            }
            Err(error) => {
                connection.send(disconnect(error.to_string()));
                return Ok(());
            }
        };
        let dimension = stored.as_ref().map_or(0, |nbt| nbt.int("Dimension"));
        if !matches!(dimension, -1 | 0) {
            connection.send(disconnect("Unsupported saved dimension"));
            return Ok(());
        }
        let spawn = server.info.spawn;
        let world = server.world(dimension as i8)?;
        let mut player = crate::player::ServerPlayer::join(
            connection,
            &name,
            &mut world.level,
            stored.as_ref(),
            spawn,
            0.0,
        );
        player.allow_flight = self.allow_flight;
        player.op = self.admin.ops.contains(&name);
        player.send_weather(&world.level, false);
        let (id, DVec3 { x, y, z }) = (player.entity_id, player.position(&world.level));
        tracing::info!("{name} [/{address}] logged in with entity id {id} at ({x}, {y}, {z})");
        world.players.push(player);
        server.broadcast(Packet::chat(format!("§e{name} joined the game.")));
        Ok(())
    }
}