use std::io;
use deepslate_protocol::codec;
use deepslate_protocol::packet::Packet;
use deepslate_protocol::packet::handshake::{HandshakeIntent, HandshakePacket};
use deepslate_protocol::packet::login::{
LoginAcknowledgedPacket, LoginPluginRequestPacket, LoginPluginResponsePacket, LoginStartPacket,
SetCompressionPacket,
};
use deepslate_protocol::types::GameProfile;
use tokio::net::TcpStream;
use tracing::{debug, warn};
use crate::config::Config;
use crate::connection::MinecraftConnection;
use crate::connection::forwarding::{self, VELOCITY_FORWARDING_CHANNEL};
use crate::event::PlayerInfo;
#[allow(clippy::too_many_lines, clippy::missing_panics_doc)]
#[expect(
clippy::large_futures,
reason = "MinecraftConnection carries large framing buffers through login handshakes"
)]
pub async fn connect_and_login(
addr: &str,
profile: &GameProfile,
player_info: &PlayerInfo,
protocol_version: i32,
config: &Config,
) -> io::Result<MinecraftConnection> {
let backend_stream = TcpStream::connect(addr).await?;
let mut conn = MinecraftConnection::new(
backend_stream,
libdeflater::CompressionLvl::new(config.compression_level)
.expect("validated in Config::validate"),
);
conn.write_packet(&HandshakePacket {
protocol_version,
server_address: addr.to_string(),
server_port: 0,
next_state: HandshakeIntent::Login,
})
.await?;
conn.write_packet(&LoginStartPacket {
username: profile.name.clone(),
uuid: profile.id,
})
.await?;
let mut forwarding_done = false;
loop {
let Some(frame) = conn.read_frame().await? else {
return Err(io::Error::new(
io::ErrorKind::ConnectionAborted,
"backend closed during login",
));
};
let mut cursor = &frame[..];
let packet_id = codec::read_packet_id(&mut cursor).map_err(protocol_err)?;
match packet_id {
LoginPluginRequestPacket::PACKET_ID => {
let plugin_msg =
LoginPluginRequestPacket::decode(&mut cursor).map_err(protocol_err)?;
if plugin_msg.channel == VELOCITY_FORWARDING_CHANNEL {
let requested_version = if plugin_msg.data.len() == 1 {
i32::from(plugin_msg.data[0])
} else {
forwarding::version::MODERN_DEFAULT
};
let forwarding_data = forwarding::create_forwarding_data(
&config.forwarding_secret,
&player_info.remote_addr,
profile,
requested_version,
);
conn.write_packet(&LoginPluginResponsePacket {
message_id: plugin_msg.message_id,
successful: true,
data: forwarding_data.into(),
})
.await?;
forwarding_done = true;
debug!("sent Velocity forwarding data to backend");
} else {
conn.write_packet(&LoginPluginResponsePacket {
message_id: plugin_msg.message_id,
successful: false,
data: bytes::Bytes::new(),
})
.await?;
}
}
SetCompressionPacket::PACKET_ID => {
let set_comp = SetCompressionPacket::decode(&mut cursor).map_err(protocol_err)?;
debug!(threshold = set_comp.threshold, "backend set compression");
conn.enable_compression(set_comp.threshold);
}
0x02 => {
if !forwarding_done {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"backend sent LoginSuccess without Velocity forwarding — \
is the backend configured for velocity modern forwarding?",
));
}
debug!("backend login success, sending LoginAcknowledged");
conn.write_packet(&LoginAcknowledgedPacket).await?;
return Ok(conn);
}
0x00 => {
let reason = deepslate_protocol::types::read_string(&mut cursor)
.unwrap_or_else(|_| "unknown".to_string());
warn!(reason, "backend disconnected during login");
return Err(io::Error::new(
io::ErrorKind::ConnectionRefused,
format!("backend rejected login: {reason}"),
));
}
0x01 => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"backend is in online-mode — it must be in offline-mode for proxy forwarding",
));
}
other => {
warn!(packet_id = other, "unexpected packet during backend login");
}
}
}
}
fn protocol_err(e: deepslate_protocol::types::ProtocolError) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, e)
}