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

use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, Write};
use std::net::{SocketAddr, ToSocketAddrs};
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::Mutex;
use std::sync::atomic::Ordering;
use std::sync::mpsc::RecvTimeoutError;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use server::Rules;
use server::dedicated::{DedicatedServer, DedicatedSettings, Properties};
use tracing::level_filters::LevelFilter;
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::EnvFilter;
use tracing_subscriber::fmt::format::Writer;
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
use tracing_subscriber::registry::LookupSpan;

fn main() -> ExitCode {
    init_logging();
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            tracing::error!("{error}");
            ExitCode::FAILURE
        }
    }
}

/// `ConsoleLogManager`: the console and an appended `server.log`, in the original format.
fn init_logging() {
    struct Tee(Option<File>);
    impl Write for Tee {
        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
            io::stderr().write_all(buf)?;
            if let Some(file) = &mut self.0 {
                file.write_all(buf)?;
            }
            Ok(buf.len())
        }
        fn flush(&mut self) -> io::Result<()> {
            io::stderr().flush()
        }
    }
    let file = OpenOptions::new()
        .create(true)
        .append(true)
        .open("server.log");
    let file_error = file.as_ref().err().map(ToString::to_string);
    tracing_subscriber::fmt()
        .with_env_filter(
            EnvFilter::builder()
                .with_default_directive(LevelFilter::INFO.into())
                .from_env_lossy(),
        )
        .event_format(ConsoleFormat)
        .with_writer(Mutex::new(Tee(file.ok())))
        .init();
    if let Some(error) = file_error {
        tracing::warn!("Failed to log to server.log: {error}");
    }
}

struct ConsoleFormat;

impl<S, N> FormatEvent<S, N> for ConsoleFormat
where
    S: Subscriber + for<'a> LookupSpan<'a>,
    N: for<'a> FormatFields<'a> + 'static,
{
    fn format_event(
        &self,
        ctx: &FmtContext<'_, S, N>,
        mut writer: Writer<'_>,
        event: &Event<'_>,
    ) -> std::fmt::Result {
        let level = match *event.metadata().level() {
            Level::ERROR => "SEVERE",
            Level::WARN => "WARNING",
            Level::INFO => "INFO",
            Level::DEBUG => "FINE",
            Level::TRACE => "FINER",
        };
        write!(
            writer,
            "{} [{level}] ",
            chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
        )?;
        ctx.field_format().format_fields(writer.by_ref(), event)?;
        writeln!(writer)
    }
}

fn run() -> Result<(), Box<dyn std::error::Error>> {
    tracing::info!("Starting minecraft server version Beta 1.7.3");
    tracing::info!("Loading properties");
    let mut properties = Properties::load(PathBuf::from("server.properties"));
    let mut ip = properties.string("server-ip", "");
    if properties.bool("online-mode", false) {
        tracing::warn!("online-mode is not supported; usernames are not authenticated");
    }
    let spawn_animals = properties.bool("spawn-animals", true);
    let pvp = properties.bool("pvp", true);
    let mut allow_flight = properties.bool("allow-flight", false);
    let mut port = properties.int("server-port", 25565);
    let view_distance = properties.int("view-distance", 10).clamp(3, 15);
    let max_players = properties.int("max-players", 20).max(0) as usize;
    let white_list = properties.bool("white-list", false);
    let mut world = PathBuf::from(properties.string("level-name", "world"));
    let level_seed = properties.string("level-seed", "");
    let mut seed = (!level_seed.is_empty()).then(|| {
        level_seed.parse().unwrap_or_else(|_| {
            i64::from(level_seed.encode_utf16().fold(0_i32, |hash, ch| {
                hash.wrapping_mul(31).wrapping_add(i32::from(ch))
            }))
        })
    });
    let spawn_monsters = properties.bool("spawn-monsters", true);
    let allow_nether = properties.bool("allow-nether", true);
    let mut args = std::env::args().skip(1);
    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--help" | "-h" => {
                println!(
                    "minecraft-server [nogui] [--world PATH] [--port PORT] [--bind IP] [--seed SEED] [--allow-flight]\nLoads server.properties; offline login only. Use stop or Ctrl-C to save and exit."
                );
                return Ok(());
            }
            "nogui" => {}
            "--world" => world = args.next().ok_or("Missing world path")?.into(),
            "--port" => port = args.next().ok_or("Missing port")?.parse()?,
            "--bind" => ip = args.next().ok_or("Missing bind IP")?,
            "--seed" => seed = Some(args.next().ok_or("Missing seed")?.parse()?),
            "--allow-flight" => allow_flight = true,
            _ => return Err(format!("Unknown argument: {arg}").into()),
        }
    }
    let seed = match seed {
        Some(seed) => seed,
        None => SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos() as i64,
    };
    let port = u16::try_from(port).map_err(|_| format!("Invalid server-port {port}"))?;
    tracing::info!(
        "Starting Minecraft server on {}:{port}",
        if ip.is_empty() { "*" } else { &ip }
    );
    let address = if ip.is_empty() {
        SocketAddr::from(([0, 0, 0, 0], port))
    } else {
        (ip.as_str(), port)
            .to_socket_addrs()?
            .next()
            .ok_or_else(|| format!("Cannot resolve server-ip {ip}"))?
    };
    tracing::warn!("**** SERVER IS RUNNING IN OFFLINE/INSECURE MODE!");
    tracing::warn!("The server will make no attempt to authenticate usernames. Beware.");
    tracing::info!("Preparing level \"{}\"", world.display());
    let difficulty = properties
        .optional_int("difficulty")
        .map_or(u8::from(spawn_monsters), |d| d.clamp(0, 3) as u8);
    let mut server = DedicatedServer::start(DedicatedSettings {
        world: server::Settings {
            name: world
                .file_name()
                .unwrap_or_default()
                .to_string_lossy()
                .into_owned(),
            world,
            seed,
            // The client meshes a column once its neighbors arrived, so it gets one more ring
            view: server::ViewArea {
                radius: view_distance + 1,
                shape: server::ViewShape::Square,
            },
        },
        address,
        allow_flight,
        difficulty,
        max_players,
        white_list,
        rules: Rules {
            pvp,
            spawn_monsters,
            spawn_animals,
            allow_nether,
            dedicated_spawn: None,
        },
        dir: PathBuf::from("."),
        properties,
    })?;
    let running = server.running.clone();
    ctrlc::set_handler(move || running.store(false, Ordering::Relaxed))?;
    let (sender, commands) = std::sync::mpsc::channel();
    std::thread::Builder::new()
        .name("server-console".into())
        .spawn(move || {
            for command in io::stdin().lock().lines().map_while(Result::ok) {
                if sender.send(command).is_err() {
                    break;
                }
            }
        })?;
    while server.running.load(Ordering::Relaxed) {
        match commands.recv_timeout(Duration::from_millis(50)) {
            Ok(command) => server.command(command),
            Err(RecvTimeoutError::Timeout) => {}
            // Without a console, the server runs until stopped otherwise
            Err(RecvTimeoutError::Disconnected) => break,
        }
    }
    Ok(server.wait()?)
}