From e72f19a81462d5db7e4759331b7ace86254bbd40 Mon Sep 17 00:00:00 2001 From: Garen Tyler Date: Wed, 4 Dec 2024 19:52:00 -0700 Subject: [PATCH] Move server to subcommand --- Dockerfile | 3 +- src/config.rs | 92 ++++++++++++++---- src/lib.rs | 17 ++-- src/main.rs | 22 +---- src/protocol/mod.rs | 4 +- src/protocol/packets/clientbound/login.rs | 54 +++++++---- src/protocol/packets/clientbound/play.rs | 96 +++++++++++-------- src/protocol/packets/mod.rs | 2 +- src/protocol/packets/serverbound/handshake.rs | 17 ++-- src/protocol/packets/serverbound/login.rs | 48 ++++++---- src/protocol/packets/serverbound/play.rs | 22 +++-- src/protocol/parsing.rs | 13 ++- src/protocol/types.rs | 11 ++- src/{ => server}/error.rs | 0 src/server/mod.rs | 52 +++++++--- src/{ => server}/net.rs | 4 +- src/world/mod.rs | 2 +- 17 files changed, 293 insertions(+), 166 deletions(-) rename src/{ => server}/error.rs (100%) rename src/{ => server}/net.rs (99%) diff --git a/Dockerfile b/Dockerfile index 2330deb..e4e68ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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" ] diff --git a/src/config.rs b/src/config.rs index 536fb92..f0af1f0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,6 +13,7 @@ pub static CONFIG: OnceCell = OnceCell::new(); /// On program startup, Args::load() should be called to initialize it. pub static ARGS: OnceCell = OnceCell::new(); static DEFAULT_ARGS: Lazy = Lazy::new(Args::default); +static DEFAULT_SERVER_ARGS: Lazy = Lazy::new(ServerArgs::default); /// Helper function to read a file from a `Path` /// and return its bytes as a `Vec`. @@ -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, pub log_dir: PathBuf, + pub subcommand: Subcommand, + server: Option, } 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::("config-file") .map_or(args.config_file, PathBuf::from); - args.server_icon = m - .get_one::("server-icon") - .map_or(args.server_icon, PathBuf::from); args.log_dir = m .get_one::("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::("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() + } +} diff --git a/src/lib.rs b/src/lib.rs index 6e28ff9..c0e57b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 = 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!(), + } } diff --git a/src/main.rs b/src/main.rs index 05b0def..4dcb78c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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; }); } diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 3564415..d2ef210 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -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}; diff --git a/src/protocol/packets/clientbound/login.rs b/src/protocol/packets/clientbound/login.rs index 2b6953a..2086db3 100644 --- a/src/protocol/packets/clientbound/login.rs +++ b/src/protocol/packets/clientbound/login.rs @@ -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 { 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 { @@ -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 { 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 { let mut output = vec![]; diff --git a/src/protocol/packets/clientbound/play.rs b/src/protocol/packets/clientbound/play.rs index 999a516..4a340c0 100644 --- a/src/protocol/packets/clientbound/play.rs +++ b/src/protocol/packets/clientbound/play.rs @@ -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 { 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 { 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 { 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 { 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 { 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 { let mut output = vec![]; diff --git a/src/protocol/packets/mod.rs b/src/protocol/packets/mod.rs index b03ea82..39737e4 100644 --- a/src/protocol/packets/mod.rs +++ b/src/protocol/packets/mod.rs @@ -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; diff --git a/src/protocol/packets/serverbound/handshake.rs b/src/protocol/packets/serverbound/handshake.rs index 56ecb8f..1042744 100644 --- a/src/protocol/packets/serverbound/handshake.rs +++ b/src/protocol/packets/serverbound/handshake.rs @@ -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 { let mut output = vec![]; diff --git a/src/protocol/packets/serverbound/login.rs b/src/protocol/packets/serverbound/login.rs index 05406a5..cb2f1fb 100644 --- a/src/protocol/packets/serverbound/login.rs +++ b/src/protocol/packets/serverbound/login.rs @@ -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 { 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 { diff --git a/src/protocol/packets/serverbound/play.rs b/src/protocol/packets/serverbound/play.rs index 71a9143..44e6fc3 100644 --- a/src/protocol/packets/serverbound/play.rs +++ b/src/protocol/packets/serverbound/play.rs @@ -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 { 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 { let mut output = vec![]; diff --git a/src/protocol/parsing.rs b/src/protocol/parsing.rs index 7d8968c..a9d52a1 100644 --- a/src/protocol/parsing.rs +++ b/src/protocol/parsing.rs @@ -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 { - if *self { vec![0x01] } else { vec![0x00] } + if *self { + vec![0x01] + } else { + vec![0x00] + } } } @@ -380,4 +388,3 @@ mod tests { } } } - diff --git a/src/protocol/types.rs b/src/protocol/types.rs index 0f3d53c..978d5d8 100644 --- a/src/protocol/types.rs +++ b/src/protocol/types.rs @@ -83,10 +83,13 @@ mod tests { fn get_positions() -> Vec<(Position, Vec)> { 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] diff --git a/src/error.rs b/src/server/error.rs similarity index 100% rename from src/error.rs rename to src/server/error.rs diff --git a/src/server/mod.rs b/src/server/mod.rs index c0d2a0f..85bbd47 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -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( 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] diff --git a/src/net.rs b/src/server/net.rs similarity index 99% rename from src/net.rs rename to src/server/net.rs index 400013f..ff35999 100644 --- a/src/net.rs +++ b/src/server/net.rs @@ -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; diff --git a/src/world/mod.rs b/src/world/mod.rs index 25d0fa5..13ca47e 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -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};