Add lobby method to configuration

This commit is contained in:
timvisee 2021-11-15 17:30:59 +01:00
parent 4907780f7c
commit ae6e877f17
No known key found for this signature in database
GPG Key ID: B8DB720BC383E172
4 changed files with 112 additions and 40 deletions

View File

@ -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. # Forwarded-to server will receive original client handshake and login request as received by lazymc.
#address = "127.0.0.1:25565" #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] [lockout]
# Enable to prevent everybody from connecting through lazymc. Instantly kicks player. # Enable to prevent everybody from connecting through lazymc. Instantly kicks player.
#enabled = false #enabled = false
@ -123,5 +150,5 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
[config] [config]
# lazymc version this configuration is for. # 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" version = "0.2.0"

View File

@ -238,6 +238,9 @@ pub enum Method {
/// Forward connection to another host. /// Forward connection to another host.
Forward, Forward,
/// Keep client in temporary fake lobby until server is ready.
Lobby,
} }
/// Join configuration. /// Join configuration.
@ -258,6 +261,10 @@ pub struct Join {
/// Join forward configuration. /// Join forward configuration.
#[serde(default)] #[serde(default)]
pub forward: JoinForward, pub forward: JoinForward,
/// Join lobby configuration.
#[serde(default)]
pub lobby: JoinLobby,
} }
impl Default for Join { impl Default for Join {
@ -267,6 +274,7 @@ impl Default for Join {
kick: Default::default(), kick: Default::default(),
hold: Default::default(), hold: Default::default(),
forward: 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<String>,
}
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. /// Lockout configuration.
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]

View File

@ -30,11 +30,7 @@ use crate::proxy;
use crate::server::{Server, State}; use crate::server::{Server, State};
// TODO: remove this before releasing feature // TODO: remove this before releasing feature
pub const USE_LOBBY: bool = true;
pub const DONT_START_SERVER: bool = false; 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. /// Interval to send keep-alive packets at.
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(10); 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. /// Auto incrementing ID source for keep alive packets.
const KEEP_ALIVE_ID: AtomicU64 = AtomicU64::new(0); 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. /// Timeout for creating new server connection for lobby client.
const SERVER_CONNECT_TIMEOUT: Duration = Duration::from_secs(2 * 60); 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?; send_lobby_play_packets(&mut writer, &server).await?;
// Wait for server to come online, then set up new connection to it // 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?; let (mut outbound, mut server_buf) = connect_to_server(client_info, &config).await?;
// Grab join game packet from server // Grab join game packet from server
let join_game = wait_for_server_join_game(&mut outbound, &mut server_buf).await?; 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?; send_lobby_title(&mut writer, "").await?;
if JOIN_SOUND {
send_lobby_player_pos(&mut writer).await?; // Play ready sound if configured
send_lobby_sound_effect(&mut writer).await?; play_lobby_ready_sound(&mut writer, &config).await?;
}
// Wait a second because Notchian servers are slow // Wait a second because Notchian servers are slow
// See: https://wiki.vg/Protocol#Login_Success // See: https://wiki.vg/Protocol#Login_Success
@ -205,6 +197,23 @@ async fn respond_login_success(
Ok(()) 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. /// Send packets to client to get workable play state for lobby world.
async fn send_lobby_play_packets(writer: &mut WriteHalf<'_>, server: &Server) -> Result<(), ()> { 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 // 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. /// 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 { let packet = NamedSoundEffect {
sound_name: JOIN_SOUND_NAME.into(), sound_name: sound_name.into(),
sound_category: 0, sound_category: 0,
effect_pos_x: 0, effect_pos_x: 0,
effect_pos_y: 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. /// An infinite keep-alive loop.
/// ///
/// This will keep sending keep-alive and title packets to the client until it is dropped. /// 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); let mut interval = time::interval(KEEP_ALIVE_INTERVAL);
loop { loop {
@ -463,7 +472,7 @@ async fn keep_alive_loop(writer: &mut WriteHalf<'_>) -> Result<(), ()> {
// Send keep alive and title packets // Send keep alive and title packets
send_keep_alive(writer).await?; 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. /// 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. /// 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<Server>, writer: &mut WriteHalf<'a>) -> Result<(), ()> { async fn stage_wait<'a>(
server: &Server,
config: &Config,
writer: &mut WriteHalf<'a>,
) -> Result<(), ()> {
select! { select! {
a = keep_alive_loop(writer) => a, a = keep_alive_loop(writer, config) => a,
b = wait_for_server(server) => b, b = wait_for_server(server, config) => b,
} }
} }
/// Wait for the server to come online. /// Wait for the server to come online.
/// ///
/// Returns `Ok(())` once the server is online, returns `Err(())` if waiting failed. /// Returns `Ok(())` once the server is online, returns `Err(())` if waiting failed.
async fn wait_for_server<'a>(server: Arc<Server>) -> Result<(), ()> { async fn wait_for_server<'a>(server: &Server, config: &Config) -> Result<(), ()> {
debug!(target: "lazymc::lobby", "Waiting on server to come online..."); debug!(target: "lazymc::lobby", "Waiting on server to come online...");
// A task to wait for suitable server state // A task to wait for suitable server state
@ -514,7 +527,8 @@ async fn wait_for_server<'a>(server: Arc<Server>) -> Result<(), ()> {
}; };
// Wait for server state with timeout // 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 // Relay client to proxy
Ok(true) => { Ok(true) => {
debug!(target: "lazymc::lobby", "Server ready for lobby client"); debug!(target: "lazymc::lobby", "Server ready for lobby client");
@ -526,7 +540,7 @@ async fn wait_for_server<'a>(server: Arc<Server>) -> Result<(), ()> {
// Timeout reached, disconnect // Timeout reached, disconnect
Err(_) => { 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());
} }
} }

View File

@ -140,22 +140,6 @@ pub async fn serve(
Server::start(config.clone(), server.clone(), username).await; 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 // Use join occupy methods
for method in &config.join.methods { for method in &config.join.methods {
match method { match method {
@ -214,6 +198,22 @@ pub async fn serve(
// TODO: do not consume client here, allow other join method on fail // 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
}
} }
} }