lazymc/src/lobby.rs

427 lines
13 KiB
Rust

// TODO: remove this before feature release!
#![allow(unused)]
use std::sync::Arc;
use std::time::{Duration, Instant};
use bytes::BytesMut;
use minecraft_protocol::data::chat::{Message, Payload};
use minecraft_protocol::data::server_status::*;
use minecraft_protocol::decoder::Decoder;
use minecraft_protocol::encoder::Encoder;
use minecraft_protocol::version::v1_14_4::game::{GameMode, MessagePosition};
use minecraft_protocol::version::v1_14_4::handshake::Handshake;
use minecraft_protocol::version::v1_14_4::login::{LoginDisconnect, LoginStart, LoginSuccess};
use minecraft_protocol::version::v1_14_4::status::StatusResponse;
use minecraft_protocol::version::v1_17_1::game::{
ChunkData, ClientBoundChatMessage, ClientBoundKeepAlive, JoinGame, PlayerPositionAndLook,
SetTitleSubtitle, SetTitleText, SetTitleTimes, SpawnPosition, TimeUpdate,
};
use nbt::CompoundTag;
use tokio::io::{self, AsyncWriteExt};
use tokio::net::tcp::WriteHalf;
use tokio::net::TcpStream;
use tokio::time;
use uuid::Uuid;
use crate::config::*;
use crate::proto::{self, Client, ClientState, RawPacket};
use crate::server::{self, Server, State};
use crate::service;
// TODO: remove this before releasing feature
pub const USE_LOBBY: bool = true;
pub const DONT_START_SERVER: bool = true;
const STARTING_BANNER: &str = "§2 Server is starting...";
const STARTING_BANNER_SUB: &str = "§7⌛ Please wait...";
// TODO: do not drop error here, return Box<dyn Error>
pub async fn serve(
client: Client,
mut inbound: TcpStream,
config: Arc<Config>,
server: Arc<Server>,
queue: BytesMut,
) -> Result<(), ()> {
let (mut reader, mut writer) = inbound.split();
// TODO: note this assumes the first receiving packet (over queue) is login start
// TODO: assert client is in login mode!
// Incoming buffer and packet holding queue
let mut buf = queue;
let mut hold_queue = BytesMut::new();
loop {
// Read packet from stream
let (packet, raw) = match proto::read_packet(&mut buf, &mut reader).await {
Ok(Some(packet)) => packet,
Ok(None) => break,
Err(_) => {
error!(target: "lazymc", "Closing connection, error occurred");
break;
}
};
// Grab client state
let client_state = client.state();
// Hijack login start
if client_state == ClientState::Login && packet.id == proto::LOGIN_PACKET_ID_LOGIN_START {
// Try to get login username
let login_start = LoginStart::decode(&mut packet.data.as_slice()).map_err(|_| ())?;
// TODO: remove debug message
debug!(target: "LOBBY", "Login {:?}", login_start.name);
// Respond with login success
let packet = LoginSuccess {
// TODO: use correct username here
uuid: Uuid::new_v3(
&Uuid::new_v3(&Uuid::NAMESPACE_OID, b"OfflinePlayer"),
login_start.name.as_bytes(),
),
username: login_start.name,
// uuid: Uuid::parse_str("35ee313b-d89a-41b8-b25e-d32e8aff0389").unwrap(),
// username: "Username".into(),
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(proto::LOGIN_PACKET_ID_LOGIN_SUCCESS, data).encode()?;
writer.write_all(&response).await.map_err(|_| ())?;
// Update client state to play
client.set_state(ClientState::Play);
// TODO: remove debug message
debug!(target: "LOBBY", "Sent login success, moving to play state");
// TODO: handle errors here
play_packets(&mut writer).await;
debug!(target: "LOBBY", "Done with playing packets, client disconnect?");
break;
}
if client_state == ClientState::Play
&& packet.id == proto::packets::play::SERVER_CLIENT_SETTINGS
{
debug!(target: "LOBBY", "Ignoring client settings packet");
continue;
}
if client_state == ClientState::Play
&& packet.id == proto::packets::play::SERVER_PLUGIN_MESSAGE
{
debug!(target: "LOBBY", "Ignoring plugin message packet");
continue;
}
if client_state == ClientState::Play
&& packet.id == proto::packets::play::SERVER_PLAYER_POS_ROT
{
debug!(target: "LOBBY", "Ignoring player pos rot packet");
continue;
}
if client_state == ClientState::Play && packet.id == proto::packets::play::SERVER_PLAYER_POS
{
debug!(target: "LOBBY", "Ignoring player pos packet");
continue;
}
// Show unhandled packet warning
debug!(target: "lazymc", "Received unhandled packet:");
debug!(target: "lazymc", "- State: {:?}", client_state);
debug!(target: "lazymc", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
}
// Gracefully close connection
match writer.shutdown().await {
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::NotConnected => {}
Err(_) => return Err(()),
}
Ok(())
}
/// Kick client with a message.
///
/// Should close connection afterwards.
async fn kick(msg: &str, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
let packet = LoginDisconnect {
reason: Message::new(Payload::text(msg)),
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(proto::LOGIN_PACKET_ID_DISCONNECT, data).encode()?;
writer.write_all(&response).await.map_err(|_| ())
}
async fn play_packets(writer: &mut WriteHalf<'_>) -> Result<(), ()> {
debug!(target: "LOBBY", "Send play packets");
// See: https://wiki.vg/Protocol_FAQ#What.27s_the_normal_login_sequence_for_a_client.3F
// Send game join
send_join_game(writer).await?;
// TODO: send brand plugin message
// After this, we receive:
// - PLAY_PAKCET_ID_CLIENT_SETTINGS
// - PLAY_PAKCET_ID_PLUGIN_MESSAGE
// - PLAY_PAKCET_ID_PLAYER_POS_ROT
// - PLAY_PAKCET_ID_PLAYER_POS ...
// TODO: send Update View Position ?
// TODO: send Update View Distance ?
// Send chunk data
// TODO: send_chunk_data(writer).await?;
// TODO: probably not required
send_spawn_pos(writer).await?;
// Send player location, disables download terrain screen
send_player_pos(writer).await?;
// TODO: send Update View Position
// TODO: send Spawn Position
// TODO: send Position and Look (one more time)
// Send time update
send_time_update(writer).await?;
// // Keep sending keep alive packets
send_keep_alive_loop(writer).await?;
Ok(())
}
async fn send_join_game(writer: &mut WriteHalf<'_>) -> Result<(), ()> {
// // TODO: use proper values here!
// let packet = JoinGame {
// // entity_id: 0,
// // game_mode: GameMode::Spectator,
// entity_id: 27,
// game_mode: GameMode::Hardcore,
// dimension: 23,
// max_players: 100,
// level_type: String::from("default"),
// view_distance: 10,
// reduced_debug_info: true,
// };
// TODO: use proper values here!
let packet = JoinGame {
entity_id: 0x6d,
hardcore: false,
game_mode: 3,
previous_game_mode: -1i8 as u8, // use -1i8 as u8?
world_names: vec![
"minecraft:overworld".into(),
"minecraft:the_nether".into(),
"minecraft:the_end".into(),
],
dimension_codec: snbt_to_compound_tag(include_str!("../res/dimension_codec.snbt")),
dimension: snbt_to_compound_tag(include_str!("../res/dimension.snbt")),
world_name: "minecraft:overworld".into(),
hashed_seed: 0,
max_players: 20,
view_distance: 10,
reduced_debug_info: true,
enable_respawn_screen: false,
is_debug: false,
is_flat: false,
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(proto::packets::play::CLIENT_JOIN_GAME, data).encode()?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
}
// // TODO: this is possibly broken?
// async fn send_chunk_data(writer: &mut WriteHalf<'_>) -> Result<(), ()> {
// // Send player location, disables download terrain screen
// let packet = ChunkData {
// x: 0,
// z: 0,
// primary_mask: Vec::new(),
// heightmaps: CompoundTag::named("HeightMaps"),
// biomes: Vec::new(),
// data_size: 0,
// data: Vec::new(),
// block_entities_size: 0,
// block_entities: Vec::new(),
// // primary_mask: 65535,
// // heights: CompoundTag::named("HeightMaps"),
// // data: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
// // tiles: vec![CompoundTag::named("TileEntity")],
// };
// let mut data = Vec::new();
// packet.encode(&mut data).map_err(|_| ())?;
// let response = RawPacket::new(proto::CLIENT_CHUNK_DATA, data).encode()?;
// writer.write_all(&response).await.map_err(|_| ())?;
// Ok(())
// }
async fn send_spawn_pos(writer: &mut WriteHalf<'_>) -> Result<(), ()> {
let packet = SpawnPosition {
position: 0,
angle: 0.0,
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(proto::packets::play::CLIENT_SPAWN_POS, data).encode()?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
}
async fn send_player_pos(writer: &mut WriteHalf<'_>) -> Result<(), ()> {
// Send player location, disables download terrain screen
let packet = PlayerPositionAndLook {
x: 0.0,
y: 0.0,
z: 0.0,
yaw: 0.0,
pitch: 90.0,
flags: 0b00000000,
teleport_id: 0,
dismount_vehicle: true,
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(proto::packets::play::CLIENT_PLAYER_POS_LOOK, data).encode()?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
}
async fn send_time_update(writer: &mut WriteHalf<'_>) -> Result<(), ()> {
const MC_TIME_NOON: i64 = 6000;
// Send player location, disables download terrain screen
let packet = TimeUpdate {
world_age: MC_TIME_NOON,
time_of_day: MC_TIME_NOON,
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(proto::packets::play::CLIENT_TIME_UPDATE, data).encode()?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
}
async fn send_keep_alive(writer: &mut WriteHalf<'_>) -> Result<(), ()> {
// Send player location, disables download terrain screen
// TODO: keep picking random ID!
let packet = ClientBoundKeepAlive { id: 0 };
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(proto::packets::play::CLIENT_KEEP_ALIVE, data).encode()?;
writer.write_all(&response).await.map_err(|_| ())?;
// TODO: require to receive correct keepalive!
Ok(())
}
async fn send_keep_alive_loop(writer: &mut WriteHalf<'_>) -> Result<(), ()> {
// TODO: use interval of 10 sec?
let mut poll_interval = time::interval(Duration::from_secs(10));
loop {
// TODO: wait for start signal over channel instead of polling
poll_interval.tick().await;
debug!(target: "LOBBY", "Sending keep-alive to client");
send_keep_alive(writer).await?;
send_title(writer).await?;
}
Ok(())
}
async fn send_title(writer: &mut WriteHalf<'_>) -> Result<(), ()> {
// Set title
let packet = SetTitleText {
text: Message::new(Payload::text(STARTING_BANNER)),
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(proto::packets::play::CLIENT_SET_TITLE_TEXT, data).encode()?;
writer.write_all(&response).await.map_err(|_| ())?;
// Set subtitle
let packet = SetTitleSubtitle {
text: Message::new(Payload::text(STARTING_BANNER_SUB)),
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response =
RawPacket::new(proto::packets::play::CLIENT_SET_TITLE_SUBTITLE, data).encode()?;
writer.write_all(&response).await.map_err(|_| ())?;
// Set times
let packet = SetTitleTimes {
fade_in: 0,
stay: i32::MAX,
fade_out: 0,
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(proto::packets::play::CLIENT_SET_TITLE_TIMES, data).encode()?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
}
/// Read NBT CompoundTag from SNBT.
fn snbt_to_compound_tag(data: &str) -> CompoundTag {
use nbt::decode::read_compound_tag;
use quartz_nbt::io::{self, Flavor};
use quartz_nbt::snbt;
use std::io::Cursor;
// Parse SNBT data
let compound = snbt::parse(data).expect("failed to parse SNBT");
// Encode to binary
let mut binary = Vec::new();
io::write_nbt(&mut binary, None, &compound, Flavor::Uncompressed);
// Parse binary with usable NBT create
read_compound_tag(&mut &*binary).unwrap()
}