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 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(())
}
}