Move server to subcommand

This commit is contained in:
Garen Tyler 2024-12-04 19:52:00 -07:00
parent c85b9a4bc2
commit e72f19a814
Signed by: garentyler
SSH Key Fingerprint: SHA256:G4ke7blZMdpWPbkescyZ7IQYE4JAtwpI85YoJdq+S7U
17 changed files with 293 additions and 166 deletions

View File

@ -9,7 +9,7 @@ RUN cargo install cargo-watch --locked --version 8.5.3
VOLUME /app
VOLUME /app/.git
EXPOSE 25565
CMD ["cargo", "watch", "-x", "run"]
CMD ["cargo", "watch", "-x", "run -- server"]
FROM base AS planner
COPY Cargo.toml .
@ -36,3 +36,4 @@ COPY --from=builder /app/target/release/composition /app
EXPOSE 25565
USER composition
ENTRYPOINT ["tini", "--", "/app/composition"]
CMD [ "server" ]

View File

@ -13,6 +13,7 @@ pub static CONFIG: OnceCell<Config> = OnceCell::new();
/// On program startup, Args::load() should be called to initialize it.
pub static ARGS: OnceCell<Args> = OnceCell::new();
static DEFAULT_ARGS: Lazy<Args> = Lazy::new(Args::default);
static DEFAULT_SERVER_ARGS: Lazy<ServerArgs> = Lazy::new(ServerArgs::default);
/// Helper function to read a file from a `Path`
/// and return its bytes as a `Vec<u8>`.
@ -99,7 +100,11 @@ impl Config {
}
// Load the server icon
config.server_icon = args.server_icon.clone();
config.server_icon = args
.server
.as_ref()
.map(|s| s.server_icon.clone())
.unwrap_or(DEFAULT_SERVER_ARGS.server_icon.clone());
let server_icon_path = Path::new(&config.server_icon);
if server_icon_path.exists() {
@ -156,24 +161,31 @@ impl Config {
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Subcommand {
None,
Server,
}
/// All of the valid command line arguments for the composition binary.
///
/// Arguments will always override the config options specified in `composition.toml` or `Config::default()`.
#[derive(Debug)]
pub struct Args {
config_file: PathBuf,
server_icon: PathBuf,
pub log_level: Option<tracing::Level>,
pub log_dir: PathBuf,
pub subcommand: Subcommand,
server: Option<ServerArgs>,
}
impl Default for Args {
fn default() -> Self {
let config = Config::default();
Args {
config_file: PathBuf::from("composition.toml"),
server_icon: config.server_icon,
log_level: None,
log_dir: PathBuf::from("logs"),
subcommand: Subcommand::None,
server: None,
}
}
}
@ -188,10 +200,9 @@ impl Args {
ARGS.set(Self::parse()).expect("could not set ARGS");
Self::instance()
}
fn parse() -> Self {
fn command() -> clap::Command {
use std::ffi::OsStr;
let m = clap::Command::new("composition")
clap::Command::new("composition")
.about(env!("CARGO_PKG_DESCRIPTION"))
.disable_version_flag(true)
.arg(
@ -215,21 +226,16 @@ impl Args {
.short('c')
.long("config-file")
.help("Configuration file path")
.global(true)
.value_hint(clap::ValueHint::FilePath)
.default_value(OsStr::new(&DEFAULT_ARGS.config_file)),
)
.arg(
Arg::new("server-icon")
.long("server-icon")
.help("Server icon file path")
.value_hint(clap::ValueHint::FilePath)
.default_value(OsStr::new(&DEFAULT_ARGS.server_icon)),
)
.arg(
Arg::new("log-level")
.short('l')
.long("log-level")
.help("Set the log level")
.global(true)
.conflicts_with("verbose")
.value_name("level")
.value_parser(["trace", "debug", "info", "warn", "error"]),
@ -238,19 +244,30 @@ impl Args {
Arg::new("log-dir")
.long("log-dir")
.help("Set the log output directory")
.global(true)
.value_name("dir")
.value_hint(clap::ValueHint::DirPath)
.default_value(OsStr::new(&DEFAULT_ARGS.log_dir)),
)
.get_matches();
.subcommand(
clap::Command::new("server")
.about("Run composition in server mode")
.arg(
Arg::new("server-icon")
.long("server-icon")
.help("Server icon file path")
.value_hint(clap::ValueHint::FilePath)
.default_value(OsStr::new(&DEFAULT_SERVER_ARGS.server_icon)),
),
)
}
fn parse() -> Self {
let mut args = Self::default();
let m = Self::command().get_matches();
args.config_file = m
.get_one::<String>("config-file")
.map_or(args.config_file, PathBuf::from);
args.server_icon = m
.get_one::<String>("server-icon")
.map_or(args.server_icon, PathBuf::from);
args.log_dir = m
.get_one::<String>("log-dir")
.map_or(args.log_dir, PathBuf::from);
@ -276,6 +293,43 @@ impl Args {
std::process::exit(0);
}
match m.subcommand() {
Some(("server", m)) => {
args.subcommand = Subcommand::Server;
let mut server_args = ServerArgs::default();
server_args.server_icon = m
.get_one::<String>("server-icon")
.map_or(server_args.server_icon, PathBuf::from);
args.server = Some(server_args);
}
None => {
let _ = Self::command().print_help();
std::process::exit(0);
}
_ => unreachable!(),
}
args
}
}
#[derive(Debug)]
pub struct ServerArgs {
server_icon: PathBuf,
}
impl Default for ServerArgs {
fn default() -> Self {
let config = Config::default();
ServerArgs {
server_icon: config.server_icon,
}
}
}
impl ServerArgs {
pub fn instance() -> Option<&'static Self> {
Args::instance().server.as_ref()
}
pub fn load() -> Option<&'static Self> {
Args::load().server.as_ref()
}
}

View File

@ -1,9 +1,5 @@
/// Server configuration and cli options.
pub mod config;
/// When managing the server encounters errors.
pub(crate) mod error;
/// Network operations.
pub(crate) mod net;
/// The Minecraft protocol implemented in a network-agnostic way.
pub mod protocol;
/// The core server implementation.
@ -11,17 +7,18 @@ pub(crate) mod server;
/// A Minecraft world generator implementation that allows for custom worlds.
pub mod world;
use crate::config::Config;
use config::Subcommand;
use once_cell::sync::OnceCell;
use std::time::Instant;
/// A globally accessible instant of the server's start time.
/// A globally accessible instant of the composition's start time.
///
/// This should be set immediately on startup.
pub static START_TIME: OnceCell<Instant> = OnceCell::new();
/// Start the server.
#[tracing::instrument]
pub async fn start_server() -> (server::Server, tokio_util::sync::CancellationToken) {
server::Server::new(format!("0.0.0.0:{}", Config::instance().port)).await
pub async fn run(command: Subcommand) {
match command {
Subcommand::Server => server::Server::run().await,
Subcommand::None => unreachable!(),
}
}

View File

@ -36,7 +36,7 @@ pub fn main() {
.init();
// Load the config.
let config = composition::config::Config::load();
let config = composition::config::Config::instance();
match config.server_threads {
Some(1) => {
@ -58,23 +58,7 @@ pub fn main() {
}
.unwrap()
.block_on(async move {
info!("Starting {} on port {}", config.server_version, config.port);
let (mut server, running) = composition::start_server().await;
info!(
"Done! Start took {:?}",
composition::START_TIME.get().unwrap().elapsed()
);
// The main server loop.
loop {
tokio::select! {
_ = running.cancelled() => {
break;
}
_ = server.update() => {}
}
}
let _ = tokio::time::timeout(std::time::Duration::from_secs(10), server.shutdown()).await;
let args = composition::config::Args::instance();
composition::run(args.subcommand).await;
});
}

View File

@ -6,8 +6,6 @@ pub mod entities;
pub mod error;
/// Implementation of Minecraft's items and inventories.
pub mod inventory;
/// Useful types for representing the Minecraft protocol.
pub mod types;
/// Network packets.
///
/// The packet naming convention used is "DSIDName" where
@ -19,6 +17,8 @@ pub mod types;
pub mod packets;
/// Useful shared parsing functions.
pub mod parsing;
/// Useful types for representing the Minecraft protocol.
pub mod types;
pub use error::{Error, Result};

View File

@ -1,5 +1,5 @@
use crate::protocol::types::{Chat, Json, Uuid, VarInt};
use crate::protocol::parsing::Parsable;
use crate::protocol::types::{Chat, Json, Uuid, VarInt};
#[derive(Clone, Debug, PartialEq)]
pub struct CL00Disconnect {
@ -33,11 +33,14 @@ crate::protocol::packets::packet!(
let (data, public_key) = u8::parse_vec(data)?;
let (data, verify_token) = u8::parse_vec(data)?;
Ok((data, CL01EncryptionRequest {
server_id,
public_key,
verify_token,
}))
Ok((
data,
CL01EncryptionRequest {
server_id,
public_key,
verify_token,
},
))
},
|packet: &CL01EncryptionRequest| -> Vec<u8> {
let mut output = vec![];
@ -66,11 +69,14 @@ impl Parsable for CL02LoginSuccessProperty {
let (data, name) = String::parse(data)?;
let (data, value) = String::parse(data)?;
let (data, signature) = String::parse_optional(data)?;
Ok((data, CL02LoginSuccessProperty {
name,
value,
signature,
}))
Ok((
data,
CL02LoginSuccessProperty {
name,
value,
signature,
},
))
}
#[tracing::instrument]
fn serialize(&self) -> Vec<u8> {
@ -91,11 +97,14 @@ crate::protocol::packets::packet!(
let (data, username) = String::parse(data)?;
let (data, properties) = CL02LoginSuccessProperty::parse_vec(data)?;
Ok((data, CL02LoginSuccess {
uuid,
username,
properties,
}))
Ok((
data,
CL02LoginSuccess {
uuid,
username,
properties,
},
))
},
|packet: &CL02LoginSuccess| -> Vec<u8> {
let mut output = vec![];
@ -136,11 +145,14 @@ crate::protocol::packets::packet!(
|data: &'data [u8]| -> crate::protocol::parsing::IResult<&'data [u8], CL04LoginPluginRequest> {
let (data, message_id) = VarInt::parse(data)?;
let (data, channel) = String::parse(data)?;
Ok((data, CL04LoginPluginRequest {
message_id,
channel,
data: data.to_vec(),
}))
Ok((
data,
CL04LoginPluginRequest {
message_id,
channel,
data: data.to_vec(),
},
))
},
|packet: &CL04LoginPluginRequest| -> Vec<u8> {
let mut output = vec![];

View File

@ -29,16 +29,19 @@ crate::protocol::packets::packet!(
let (data, d) = VarInt::parse(data)?;
let (data, velocity) = EntityVelocity::parse(data)?;
Ok((data, CP00SpawnEntity {
id,
uuid,
kind,
position,
rotation,
head_yaw,
data: d,
velocity,
}))
Ok((
data,
CP00SpawnEntity {
id,
uuid,
kind,
position,
rotation,
head_yaw,
data: d,
velocity,
},
))
},
|packet: &CP00SpawnEntity| -> Vec<u8> {
let mut output = vec![];
@ -67,10 +70,13 @@ crate::protocol::packets::packet!(
|data: &'data [u8]| -> crate::protocol::parsing::IResult<&'data [u8], CP0BChangeDifficulty> {
let (data, difficulty) = Difficulty::parse(data)?;
let (data, is_locked) = bool::parse(data)?;
Ok((data, CP0BChangeDifficulty {
difficulty,
is_locked,
}))
Ok((
data,
CP0BChangeDifficulty {
difficulty,
is_locked,
},
))
},
|packet: &CP0BChangeDifficulty| -> Vec<u8> {
let mut output = vec![];
@ -129,12 +135,15 @@ crate::protocol::packets::packet!(
let (data, location) = Position::parse(data)?;
let (data, d) = i32::parse(data)?;
let (data, disable_relative_volume) = bool::parse(data)?;
Ok((data, CP21WorldEvent {
event,
location,
data: d,
disable_relative_volume,
}))
Ok((
data,
CP21WorldEvent {
event,
location,
data: d,
disable_relative_volume,
},
))
},
|packet: &CP21WorldEvent| -> Vec<u8> {
let mut output = vec![];
@ -159,10 +168,13 @@ crate::protocol::packets::packet!(
|data: &'data [u8]| -> crate::protocol::parsing::IResult<&'data [u8], CP50SetEntityVelocity> {
let (data, entity_id) = VarInt::parse(data)?;
let (data, entity_velocity) = EntityVelocity::parse(data)?;
Ok((data, CP50SetEntityVelocity {
entity_id,
entity_velocity,
}))
Ok((
data,
CP50SetEntityVelocity {
entity_id,
entity_velocity,
},
))
},
|packet: &CP50SetEntityVelocity| -> Vec<u8> {
let mut output = vec![];
@ -187,11 +199,14 @@ crate::protocol::packets::packet!(
let (data, experience_bar) = f32::parse(data)?;
let (data, total_experience) = VarInt::parse(data)?;
let (data, level) = VarInt::parse(data)?;
Ok((data, CP52SetExperience {
experience_bar,
total_experience,
level,
}))
Ok((
data,
CP52SetExperience {
experience_bar,
total_experience,
level,
},
))
},
|packet: &CP52SetExperience| -> Vec<u8> {
let mut output = vec![];
@ -231,16 +246,19 @@ crate::protocol::packets::packet!(
let (data, has_factor_data) = bool::parse(data)?;
// TODO: factor_codec
Ok((data, CP68EntityEffect {
entity_id,
effect_id,
amplifier,
duration,
is_ambient,
show_particles,
show_icon,
has_factor_data,
}))
Ok((
data,
CP68EntityEffect {
entity_id,
effect_id,
amplifier,
duration,
is_ambient,
show_particles,
show_icon,
has_factor_data,
},
))
},
|packet: &CP68EntityEffect| -> Vec<u8> {
let mut output = vec![];

View File

@ -3,7 +3,7 @@ pub mod clientbound;
/// Packets that are heading to the server.
pub mod serverbound;
use crate::protocol::parsing::{VarInt, Parsable};
use crate::protocol::parsing::{Parsable, VarInt};
/// Alias for a `VarInt`.
pub type PacketId = VarInt;

View File

@ -1,4 +1,4 @@
use crate::protocol::{ClientState, types::VarInt};
use crate::protocol::{types::VarInt, ClientState};
use nom::combinator::map_res;
#[derive(Clone, Debug, PartialEq)]
@ -24,12 +24,15 @@ crate::protocol::packets::packet!(
_ => Err(()),
})(data)?;
Ok((data, SH00Handshake {
protocol_version,
server_address,
server_port,
next_state,
}))
Ok((
data,
SH00Handshake {
protocol_version,
server_address,
server_port,
next_state,
},
))
},
|packet: &SH00Handshake| -> Vec<u8> {
let mut output = vec![];

View File

@ -16,10 +16,13 @@ crate::protocol::packets::packet!(
let (data, has_uuid) = bool::parse(data)?;
if has_uuid {
let (data, uuid) = Uuid::parse(data)?;
Ok((data, SL00LoginStart {
name,
uuid: Some(uuid),
}))
Ok((
data,
SL00LoginStart {
name,
uuid: Some(uuid),
},
))
} else {
Ok((data, SL00LoginStart { name, uuid: None }))
}
@ -51,10 +54,13 @@ crate::protocol::packets::packet!(
let (data, verify_token_len) = VarInt::parse(data)?;
let (data, verify_token) = take(*verify_token_len as usize)(data)?;
Ok((data, SL01EncryptionResponse {
shared_secret: shared_secret.to_vec(),
verify_token: verify_token.to_vec(),
}))
Ok((
data,
SL01EncryptionResponse {
shared_secret: shared_secret.to_vec(),
verify_token: verify_token.to_vec(),
},
))
},
|packet: &SL01EncryptionResponse| -> Vec<u8> {
let mut output = vec![];
@ -81,17 +87,23 @@ crate::protocol::packets::packet!(
let (data, message_id) = VarInt::parse(data)?;
let (data, successful) = bool::parse(data)?;
if successful {
Ok((&[], SL02LoginPluginResponse {
message_id,
successful,
data: data.to_vec(),
}))
Ok((
&[],
SL02LoginPluginResponse {
message_id,
successful,
data: data.to_vec(),
},
))
} else {
Ok((data, SL02LoginPluginResponse {
message_id,
successful,
data: vec![],
}))
Ok((
data,
SL02LoginPluginResponse {
message_id,
successful,
data: vec![],
},
))
}
},
|packet: &SL02LoginPluginResponse| -> Vec<u8> {

View File

@ -58,10 +58,13 @@ crate::protocol::packets::packet!(
|data: &'data [u8]| -> crate::protocol::parsing::IResult<&'data [u8], SP13SetPlayerPosition> {
let (data, position) = EntityPosition::parse(data)?;
let (data, on_ground) = bool::parse(data)?;
Ok((data, SP13SetPlayerPosition {
position,
on_ground,
}))
Ok((
data,
SP13SetPlayerPosition {
position,
on_ground,
},
))
},
|packet: &SP13SetPlayerPosition| -> Vec<u8> {
let mut output = vec![];
@ -114,10 +117,13 @@ crate::protocol::packets::packet!(
|data: &'data [u8]| -> crate::protocol::parsing::IResult<&'data [u8], SP15SetPlayerRotation> {
let (data, rotation) = EntityRotation::parse(data)?;
let (data, on_ground) = bool::parse(data)?;
Ok((data, SP15SetPlayerRotation {
rotation,
on_ground,
}))
Ok((
data,
SP15SetPlayerRotation {
rotation,
on_ground,
},
))
},
|packet: &SP15SetPlayerRotation| -> Vec<u8> {
let mut output = vec![];

View File

@ -1,5 +1,9 @@
pub use nom::IResult;
use nom::{bytes::streaming::{take, take_while_m_n}, number::streaming as nom_nums, combinator::map_res};
use nom::{
bytes::streaming::{take, take_while_m_n},
combinator::map_res,
number::streaming as nom_nums,
};
/// Implementation of the protocol's VarInt type.
///
@ -317,7 +321,11 @@ impl Parsable for bool {
}
#[tracing::instrument]
fn serialize(&self) -> Vec<u8> {
if *self { vec![0x01] } else { vec![0x00] }
if *self {
vec![0x01]
} else {
vec![0x00]
}
}
}
@ -380,4 +388,3 @@ mod tests {
}
}
}

View File

@ -83,10 +83,13 @@ mod tests {
fn get_positions() -> Vec<(Position, Vec<u8>)> {
vec![
// x: 01000110000001110110001100 z: 10110000010101101101001000 y: 001100111111
(Position::new(18357644, 831, -20882616), vec![
0b01000110, 0b00000111, 0b01100011, 0b00101100, 0b00010101, 0b10110100, 0b10000011,
0b00111111,
]),
(
Position::new(18357644, 831, -20882616),
vec![
0b01000110, 0b00000111, 0b01100011, 0b00101100, 0b00010101, 0b10110100,
0b10000011, 0b00111111,
],
),
]
}
#[test]

View File

@ -1,7 +1,12 @@
/// When managing the server encounters errors.
pub mod error;
/// Network operations.
pub mod net;
use crate::config::Config;
use crate::error::Result;
use crate::net::{NetworkClient, NetworkClientState};
use crate::protocol::ClientState;
use error::Result;
use net::{NetworkClient, NetworkClientState};
use std::sync::Arc;
use tokio::net::{TcpListener, ToSocketAddrs};
use tokio::{sync::RwLock, task::JoinHandle};
@ -15,6 +20,40 @@ pub struct Server {
net_tasks_handle: JoinHandle<()>,
}
impl Server {
/// Start the server.
#[tracing::instrument]
pub async fn run() {
let config = crate::config::Config::instance();
info!("Starting {} on port {}", config.server_version, config.port);
let (mut server, running) = Self::new(format!("0.0.0.0:{}", Config::instance().port)).await;
info!(
"Done! Start took {:?}",
crate::START_TIME.get().unwrap().elapsed()
);
// Spawn the ctrl-c task.
let r = running.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.unwrap();
info!("Ctrl-C received, shutting down");
r.cancel();
});
// The main server loop.
loop {
tokio::select! {
_ = running.cancelled() => {
break;
}
_ = server.update() => {}
}
}
match tokio::time::timeout(std::time::Duration::from_secs(10), server.shutdown()).await {
Ok(_) => std::process::exit(0),
Err(_) => std::process::exit(1),
}
}
#[tracing::instrument]
pub async fn new<A: 'static + ToSocketAddrs + Send + std::fmt::Debug>(
bind_address: A,
@ -34,15 +73,6 @@ impl Server {
net_tasks_handle,
};
// let (shutdown_tx, shutdown_rx) = oneshot::channel();
let r = running.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.unwrap();
info!("Ctrl-C received, shutting down");
r.cancel();
// shutdown_tx.send(()).unwrap();
});
(server, running)
}
#[tracing::instrument]

View File

@ -1,7 +1,7 @@
use crate::protocol::{
ClientState,
packets::{GenericPacket, serverbound::SL00LoginStart},
packets::{serverbound::SL00LoginStart, GenericPacket},
parsing::Parsable,
ClientState,
};
use std::{collections::VecDeque, sync::Arc, time::Instant};
use tokio::io::AsyncWriteExt;

View File

@ -6,7 +6,7 @@ pub mod error;
pub mod generators;
/// Useful re-exports.
pub mod prelude {
pub use super::{World, chunks::Chunk};
pub use super::{chunks::Chunk, World};
}
pub use crate::protocol::{blocks, entities};