synvael/crates/server/src/main.rs

189 lines
7.8 KiB
Rust

// SPDX-License-Identifier: AGPL-3.0-only
//! Dedicated server for Synvael.
//!
//! The server handles the authoritative game simulation, including world management, physics, and combat.
/// A bounded LRU cache of regenerated chunk baselines, shared across the worker pool.
pub mod chunk_cache;
/// Per-connection chunk-streaming state: desired-set tracking and delivery.
pub mod client_stream;
/// Entity components describing players and other world-streaming anchors.
pub mod player;
/// On-disk persistence: region files and the atomic durability layer.
pub mod save;
/// Authoritative chunk storage and generation logic for the server.
pub mod world_server;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration;
use anyhow::Context;
use bevy_ecs::prelude::{Query, ResMut, Schedule, With, World};
use glam::Vec3;
use shared::generator::{VoxelGenerator, WorldGenConfig};
use shared::world::{Chunk, ChunkPos, EntityPos};
use tracing::{debug, info, warn};
use client_stream::ClientStream;
use net::{NetworkServer, ServerEvent};
use player::{Player, Position, ViewDistance};
use world_server::{ServerWorld, cylinder_chunks};
/// Streaming system: loads and unloads chunks so that the resident set matches the union of the cylinders around every player anchor.
fn stream_chunks(
anchors: Query<(&Position, &ViewDistance), With<Player>>,
mut world: ResMut<ServerWorld>,
) {
// Desired set is the union of every anchor's cylinder; a chunk survives as long as it lies within any one player's view.
let mut desired = HashSet::new();
for (position, view) in &anchors {
cylinder_chunks(position.0.chunk, view.0, &mut desired);
}
let stats = world.reconcile(&desired);
info!(
loaded = stats.loaded,
unloaded = stats.unloaded,
resident = stats.resident,
in_flight = stats.in_flight,
"streaming reconcile"
);
}
fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("debug")),
)
.init();
info!("Starting Synvael server");
let config_str = fs::read_to_string("assets/data/worldgen/default.json")
.context("reading worldgen config assets/data/worldgen/default.json")?;
let worldgen_config: WorldGenConfig =
serde_json::from_str(&config_str).context("parsing worldgen config as JSON")?;
info!("Successfully loaded world configuration");
debug!("Base height: {}", worldgen_config.base_height);
debug!("Noise scale: {}", worldgen_config.noise_scale);
debug!("Surface block: {}", worldgen_config.surface_block.0);
debug!("Subsurface block: {}", worldgen_config.subsurface_block.0);
debug!("Stone block: {}", worldgen_config.stone_block.0);
let seed = 4_813_530;
let generator = VoxelGenerator::new(worldgen_config, seed);
// The region directory holds the `.region` save files for this world
// TODO: resolve it per named world under a shared save root.
let region_dir = std::path::PathBuf::from("saves/default/region");
// Number of chunk baselines the worker pool retains before evicting the least-recently-used entry
// TODO: make this configurable through server configs
let cache_capacity =
std::num::NonZeroUsize::new(4_096).context("chunk cache capacity is non-zero")?;
let mut world = World::new();
world.insert_resource(ServerWorld::new(generator, region_dir, cache_capacity));
// Spawn a single dummy player anchor at the world origin.
world.spawn((
Player,
Position(EntityPos::new(ChunkPos::new(0, 0, 0), Vec3::ZERO)),
ViewDistance(4),
));
// A schedule is one tick's worth of systems; running it advances the world.
let mut schedule = Schedule::default();
schedule.add_systems(stream_chunks);
// Loading phase: dispatch the initial region and wait for the worker pool to finish before granting control.
info!("Streaming initial region");
loop {
schedule.run(&mut world);
let server_world = world.resource::<ServerWorld>();
let resident = server_world.loaded_count();
let in_flight = server_world.in_flight_count();
// Loading progress is simply the resident fraction of all known chunks.
let total = resident + in_flight;
debug!(resident, in_flight, total, "loading progress");
// The region is ready once at least one chunk has been generated and none remain in flight.
if server_world.streaming_idle() && resident > 0 {
break;
}
}
info!("Initial region ready; granting player control");
// Spawn the networking thread and bind the QUIC endpoint. The synchronous simulation loop below communicates with it only by draining events.
let bind = SocketAddr::from((Ipv4Addr::LOCALHOST, net::DEFAULT_PORT));
let (network, local_addr) = NetworkServer::spawn(
bind,
env!("CARGO_PKG_VERSION").to_owned(),
// Placeholder advisory tick rate.
20,
)
.context("spawning network server")?;
info!(%local_addr, "network endpoint listening");
// Chunk diffs are computed against an all-air baseline so each delivered payload is self-contained: the client renders only server-owned content and has no generator to reconstruct a worldgen baseline. Allocated once and shared across every delivery.
let empty_baseline = Chunk::default();
// Per-connection streaming state, keyed by the stable session id the network thread assigns.
let mut clients: HashMap<u64, ClientStream> = HashMap::new();
// Authoritative simulation loop.
loop {
// Fold network events into per-client subscription state.
for event in network.poll_events() {
match event {
ServerEvent::ClientConnected { id, hello, chunks } => {
info!(id, name = %hello.player_identity.display_name, "client connected");
clients.insert(id, ClientStream::new(chunks));
}
ServerEvent::ClientDisconnected { id, reason } => {
info!(id, %reason, "client disconnected");
clients.remove(&id);
}
ServerEvent::ChunkSubscribe { id, request } => {
if let Some(client) = clients.get_mut(&id) {
let (added, drops) = client.resubscribe(request.center, request.radius);
info!(
id,
added, drops, "client {id}: +{added} chunks, -{drops} drops"
);
} else {
warn!(id, "chunk subscribe from unknown session");
}
}
}
}
// Reconcile the resident world to the union of every client's desired set. A chunk survives as long as any connected client wants it; when no client is connected the union is empty and the world drains.
let mut desired = HashSet::new();
for client in clients.values() {
desired.extend(client.desired().iter().copied());
}
world.resource_mut::<ServerWorld>().reconcile(&desired);
// Deliver newly-resident chunks to each client. Loads dispatched above may not be resident this tick; `flush` retries on later ticks until the worker pool returns them.
let server_world = world.resource::<ServerWorld>();
for (id, client) in &mut clients {
let delivered = client.flush(server_world, &empty_baseline);
if delivered > 0 {
debug!(id, delivered, "delivered resident chunks");
}
}
// Advisory ~20 Hz cadence until the real tick scheduler lands.
std::thread::sleep(Duration::from_millis(50));
}
}