alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH 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
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()?)
}