diff --git a/res/lazymc.toml b/res/lazymc.toml index 64191c9..cfcd260 100644 --- a/res/lazymc.toml +++ b/res/lazymc.toml @@ -97,6 +97,33 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui" # Forwarded-to server will receive original client handshake and login request as received by lazymc. #address = "127.0.0.1:25565" +[join.lobby] +# Lobby occupation method. +# The client joins a fake lobby server with an empty world, floating in space. +# A message is overlayed on screen to notify the server is starting. +# The client will be teleported to the real server once it is ready. +# This may keep the client occupied forever if no timeout is set. +# Consumes client, not allowing other join methods afterwards. + +# !!! WARNING !!! +# This is highly experimental and unstable. +# This may break the game and crash clients. +# Don't enable this unless you know what you're doing. +# +# - Only works with offline mode +# - Only works with server packet compression disabled +# - Only works with vanilla Minecraft clients, does not work with modded +# - Only tested with Minecraft 1.17.1 + +# Maxiumum time in seconds in the lobby while the server starts. +#timeout = 600 + +# Message banner in lobby shown to client. +#message = "§2Server is starting\n§7⌛ Please wait..." + +# Sound effect to play when server is ready. +#ready_sound = "block.note_block.chime" + [lockout] # Enable to prevent everybody from connecting through lazymc. Instantly kicks player. #enabled = false @@ -123,5 +150,5 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui" [config] # lazymc version this configuration is for. -# Do not change unless you know what you're doing. +# Don't change unless you know what you're doing. version = "0.2.0" diff --git a/src/config.rs b/src/config.rs index dbd7dbe..8bc7618 100644 --- a/src/config.rs +++ b/src/config.rs @@ -238,6 +238,9 @@ pub enum Method { /// Forward connection to another host. Forward, + + /// Keep client in temporary fake lobby until server is ready. + Lobby, } /// Join configuration. @@ -258,6 +261,10 @@ pub struct Join { /// Join forward configuration. #[serde(default)] pub forward: JoinForward, + + /// Join lobby configuration. + #[serde(default)] + pub lobby: JoinLobby, } impl Default for Join { @@ -267,6 +274,7 @@ impl Default for Join { kick: Default::default(), hold: Default::default(), forward: Default::default(), + lobby: Default::default(), } } } @@ -320,6 +328,29 @@ impl Default for JoinForward { } } } +/// Join lobby configuration. +#[derive(Debug, Deserialize)] +#[serde(default)] +pub struct JoinLobby { + /// Hold client in lobby for number of seconds on connect while server starts. + pub timeout: u32, + + /// Message banner in lobby shown to client. + pub message: String, + + /// Sound effect to play when server is ready. + pub ready_sound: Option, +} + +impl Default for JoinLobby { + fn default() -> Self { + Self { + timeout: 10 * 60, + message: "§2Server is starting\n§7⌛ Please wait...".into(), + ready_sound: Some("block.note_block.chime".into()), + } + } +} /// Lockout configuration. #[derive(Debug, Deserialize)] diff --git a/src/lobby.rs b/src/lobby.rs index e5c5979..1fa0dbf 100644 --- a/src/lobby.rs +++ b/src/lobby.rs @@ -30,11 +30,7 @@ use crate::proxy; use crate::server::{Server, State}; // TODO: remove this before releasing feature -pub const USE_LOBBY: bool = true; pub const DONT_START_SERVER: bool = false; -const STARTING_BANNER: &str = "§2Server is starting\n§7⌛ Please wait..."; -const JOIN_SOUND: bool = true; -const JOIN_SOUND_NAME: &str = "block.note_block.chime"; /// Interval to send keep-alive packets at. const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(10); @@ -42,9 +38,6 @@ const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(10); /// Auto incrementing ID source for keep alive packets. const KEEP_ALIVE_ID: AtomicU64 = AtomicU64::new(0); -/// Lobby clients may wait a maximum of 10 minutes for the server to come online. -const SERVER_WAIT_TIMEOUT: Duration = Duration::from_secs(10 * 60); - /// Timeout for creating new server connection for lobby client. const SERVER_CONNECT_TIMEOUT: Duration = Duration::from_secs(2 * 60); @@ -129,18 +122,17 @@ pub async fn serve( send_lobby_play_packets(&mut writer, &server).await?; // Wait for server to come online, then set up new connection to it - stage_wait(server.clone(), &mut writer).await?; + stage_wait(&server, &config, &mut writer).await?; let (mut outbound, mut server_buf) = connect_to_server(client_info, &config).await?; // Grab join game packet from server let join_game = wait_for_server_join_game(&mut outbound, &mut server_buf).await?; - // Reset lobby title, player position and play sound effect + // Reset lobby title send_lobby_title(&mut writer, "").await?; - if JOIN_SOUND { - send_lobby_player_pos(&mut writer).await?; - send_lobby_sound_effect(&mut writer).await?; - } + + // Play ready sound if configured + play_lobby_ready_sound(&mut writer, &config).await?; // Wait a second because Notchian servers are slow // See: https://wiki.vg/Protocol#Login_Success @@ -205,6 +197,23 @@ async fn respond_login_success( Ok(()) } +/// Play lobby ready sound effect if configured. +async fn play_lobby_ready_sound(writer: &mut WriteHalf<'_>, config: &Config) -> Result<(), ()> { + if let Some(sound_name) = config.join.lobby.ready_sound.as_ref() { + // Must not be empty string + if sound_name.trim().is_empty() { + warn!(target: "lazymc::lobby", "Lobby ready sound effect is an empty string, you should remove the configuration item instead"); + return Ok(()); + } + + // Play sound effect + send_lobby_player_pos(writer).await?; + send_lobby_sound_effect(writer, sound_name).await?; + } + + Ok(()) +} + /// Send packets to client to get workable play state for lobby world. async fn send_lobby_play_packets(writer: &mut WriteHalf<'_>, server: &Server) -> Result<(), ()> { // See: https://wiki.vg/Protocol_FAQ#What.27s_the_normal_login_sequence_for_a_client.3F @@ -405,9 +414,9 @@ async fn send_lobby_title(writer: &mut WriteHalf<'_>, text: &str) -> Result<(), } /// Send lobby ready sound effect to client. -async fn send_lobby_sound_effect(writer: &mut WriteHalf<'_>) -> Result<(), ()> { +async fn send_lobby_sound_effect(writer: &mut WriteHalf<'_>, sound_name: &str) -> Result<(), ()> { let packet = NamedSoundEffect { - sound_name: JOIN_SOUND_NAME.into(), + sound_name: sound_name.into(), sound_category: 0, effect_pos_x: 0, effect_pos_y: 0, @@ -453,7 +462,7 @@ async fn send_respawn_from_join(writer: &mut WriteHalf<'_>, join_game: JoinGame) /// An infinite keep-alive loop. /// /// This will keep sending keep-alive and title packets to the client until it is dropped. -async fn keep_alive_loop(writer: &mut WriteHalf<'_>) -> Result<(), ()> { +async fn keep_alive_loop(writer: &mut WriteHalf<'_>, config: &Config) -> Result<(), ()> { let mut interval = time::interval(KEEP_ALIVE_INTERVAL); loop { @@ -463,7 +472,7 @@ async fn keep_alive_loop(writer: &mut WriteHalf<'_>) -> Result<(), ()> { // Send keep alive and title packets send_keep_alive(writer).await?; - send_lobby_title(writer, STARTING_BANNER).await?; + send_lobby_title(writer, &config.join.lobby.message).await?; } } @@ -472,17 +481,21 @@ async fn keep_alive_loop(writer: &mut WriteHalf<'_>) -> Result<(), ()> { /// In this stage we wait for the server to come online. /// /// During this stage we keep sending keep-alive and title packets to the client to keep it active. -async fn stage_wait<'a>(server: Arc, writer: &mut WriteHalf<'a>) -> Result<(), ()> { +async fn stage_wait<'a>( + server: &Server, + config: &Config, + writer: &mut WriteHalf<'a>, +) -> Result<(), ()> { select! { - a = keep_alive_loop(writer) => a, - b = wait_for_server(server) => b, + a = keep_alive_loop(writer, config) => a, + b = wait_for_server(server, config) => b, } } /// Wait for the server to come online. /// /// Returns `Ok(())` once the server is online, returns `Err(())` if waiting failed. -async fn wait_for_server<'a>(server: Arc) -> Result<(), ()> { +async fn wait_for_server<'a>(server: &Server, config: &Config) -> Result<(), ()> { debug!(target: "lazymc::lobby", "Waiting on server to come online..."); // A task to wait for suitable server state @@ -514,7 +527,8 @@ async fn wait_for_server<'a>(server: Arc) -> Result<(), ()> { }; // Wait for server state with timeout - match time::timeout(SERVER_WAIT_TIMEOUT, task_wait).await { + let timeout = Duration::from_secs(config.join.lobby.timeout as u64); + match time::timeout(timeout, task_wait).await { // Relay client to proxy Ok(true) => { debug!(target: "lazymc::lobby", "Server ready for lobby client"); @@ -526,7 +540,7 @@ async fn wait_for_server<'a>(server: Arc) -> Result<(), ()> { // Timeout reached, disconnect Err(_) => { - warn!(target: "lazymc::lobby", "Lobby client waiting for server to come online reached timeout of {}s", SERVER_WAIT_TIMEOUT.as_secs()); + warn!(target: "lazymc::lobby", "Lobby client waiting for server to come online reached timeout of {}s", timeout.as_secs()); } } diff --git a/src/status.rs b/src/status.rs index fa98168..fde52d0 100644 --- a/src/status.rs +++ b/src/status.rs @@ -140,22 +140,6 @@ pub async fn serve( Server::start(config.clone(), server.clone(), username).await; } - // Lobby mode - if lobby::USE_LOBBY { - // // Hold login packet and remaining read bytes - // hold_queue.extend(raw); - // hold_queue.extend(buf.split_off(0)); - - // Build queue with login packet and any additionally received - let mut queue = BytesMut::with_capacity(raw.len() + buf.len()); - queue.extend(raw); - queue.extend(buf.split_off(0)); - - // Start lobby - lobby::serve(client, client_info, inbound, config, server, queue).await?; - return Ok(()); - } - // Use join occupy methods for method in &config.join.methods { match method { @@ -214,6 +198,22 @@ pub async fn serve( // TODO: do not consume client here, allow other join method on fail } + + // Lobby method, keep client in lobby while server starts + Method::Lobby => { + trace!(target: "lazymc", "Using lobby method to occupy joining client"); + + // Build queue with login packet and any additionally received + let mut queue = BytesMut::with_capacity(raw.len() + buf.len()); + queue.extend(raw); + queue.extend(buf.split_off(0)); + + // Start lobby + lobby::serve(client, client_info, inbound, config, server, queue).await?; + return Ok(()); + + // TODO: do not consume client here, allow other join method on fail + } } }