120 lines
4.2 KiB
Rust
120 lines
4.2 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.
|
|
|
|
/// Entity components describing players and other world-streaming anchors.
|
|
pub mod player;
|
|
/// Authoritative chunk storage and generation logic for the server.
|
|
pub mod world_server;
|
|
|
|
use std::collections::HashSet;
|
|
use std::fs;
|
|
|
|
use anyhow::Context;
|
|
use bevy_ecs::prelude::{Query, ResMut, Schedule, With, World};
|
|
use glam::Vec3;
|
|
use shared::generator::{VoxelGenerator, WorldGenConfig};
|
|
use shared::world::{ChunkPos, EntityPos};
|
|
use tracing::{debug, info};
|
|
|
|
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);
|
|
|
|
let mut world = World::new();
|
|
world.insert_resource(ServerWorld::new(generator));
|
|
|
|
// 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");
|
|
|
|
// Drive several ticks, marching the dummy one chunk along +X between each. The manual movement stands in for network-driven player input and exists only to exercise load/unload as the anchor moves.
|
|
for step in 0..5 {
|
|
info!(step, "tick");
|
|
schedule.run(&mut world);
|
|
|
|
let mut movers = world.query_filtered::<&mut Position, With<Player>>();
|
|
for mut position in movers.iter_mut(&mut world) {
|
|
position.0.chunk.x += 1;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|