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 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(¢ers);
level.spawn_mobs(¢ers, 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
}