Start experimenting with lobby, implement loading into lobby with text

This commit is contained in:
timvisee 2021-11-12 19:53:46 +01:00
parent db99289ea7
commit e01fd212f7
No known key found for this signature in database
GPG Key ID: B8DB720BC383E172
8 changed files with 2641 additions and 11 deletions

48
Cargo.lock generated
View File

@ -213,6 +213,12 @@ version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -626,7 +632,9 @@ dependencies = [
"libc",
"log",
"minecraft-protocol",
"named-binary-tag",
"pretty_env_logger",
"quartz_nbt",
"rand 0.8.4",
"rcon",
"serde",
@ -634,6 +642,7 @@ dependencies = [
"thiserror",
"tokio",
"toml",
"uuid",
"version-compare",
"winapi",
]
@ -660,6 +669,12 @@ dependencies = [
"value-bag",
]
[[package]]
name = "md5"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e6bcd6433cff03a4bfc3d9834d504467db1f1cf6d0ea765d37d330249ed629d"
[[package]]
name = "memchr"
version = "2.4.1"
@ -669,7 +684,7 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "minecraft-protocol"
version = "0.1.0"
source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=31041b8#31041b8fe2bc7e512d12476b958c1fe9e9077394"
source = "git+https://github.com/timvisee/rust-minecraft-protocol?branch=lazymc-v1_17_1#d26a525c7b29b61d2db64805181fb5471ea4317a"
dependencies = [
"byteorder",
"minecraft-protocol-derive",
@ -682,7 +697,7 @@ dependencies = [
[[package]]
name = "minecraft-protocol-derive"
version = "0.0.0"
source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=31041b8#31041b8fe2bc7e512d12476b958c1fe9e9077394"
source = "git+https://github.com/timvisee/rust-minecraft-protocol?branch=lazymc-v1_17_1#d26a525c7b29b61d2db64805181fb5471ea4317a"
dependencies = [
"proc-macro2",
"quote",
@ -723,9 +738,9 @@ dependencies = [
[[package]]
name = "named-binary-tag"
version = "0.2.3"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d654702943d37d67f1491769ed46484c306f9b9d0d258348904bd63ffb101e8"
checksum = "523298fac63bd954f9a2e03b962b8a4a0e95110ad1b2fa3e0d7048660ffecec3"
dependencies = [
"byteorder",
"flate2",
@ -846,6 +861,30 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "quartz_nbt"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24532990479062a9c515987986225879bd115ccb97672b1fb56d788a5adb7d39"
dependencies = [
"anyhow",
"byteorder",
"cesu8",
"flate2",
"quartz_nbt_macros",
]
[[package]]
name = "quartz_nbt_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "289baa0c8a4d1f840d2de528a7f8c29e0e9af48b3018172b3edad4f716e8daed"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "quick-error"
version = "1.2.3"
@ -1251,6 +1290,7 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a"
dependencies = [
"md5",
"rand 0.6.5",
"serde",
]

View File

@ -31,7 +31,7 @@ derive_builder = "0.10"
dotenv = "0.15"
futures = { version = "0.3", default-features = false }
log = "0.4"
minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", rev = "31041b8" }
minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", branch = "lazymc-v1_17_1" }
pretty_env_logger = "0.4"
rand = "0.8"
serde = "1.0"
@ -44,6 +44,11 @@ version-compare = "0.1"
# Feature: rcon
rust_rcon = { package = "rcon", version = "0.5", optional = true }
# Feature: lobby
named-binary-tag = "0.6"
quartz_nbt = "0.2"
uuid = { version = "0.7", features = ["v3"] }
[target.'cfg(unix)'.dependencies]
libc = "0.2"

18
res/dimension.snbt Normal file
View File

@ -0,0 +1,18 @@
{
piglin_safe: 1b,
natural: 0b,
ambient_light: 0.0f,
fixed_time: 0,
infiniburn: "minecraft:infiniburn_overworld",
respawn_anchor_works: 0b,
has_skylight: 1b,
bed_works: 0b,
effects: "minecraft:the_end",
has_raids: 0b,
min_y: 0,
height: 256,
logical_height: 256,
coordinate_scale: 1.0d,
ultrawarm: 0b,
has_ceiling: 0b
}

2093
res/dimension_codec.snbt Normal file

File diff suppressed because it is too large Load Diff

426
src/lobby.rs Normal file
View File

@ -0,0 +1,426 @@
// 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()
}

View File

@ -10,6 +10,7 @@ extern crate log;
pub(crate) mod action;
pub(crate) mod cli;
pub(crate) mod config;
pub(crate) mod lobby;
pub(crate) mod mc;
pub(crate) mod monitor;
pub(crate) mod os;

View File

@ -30,6 +30,31 @@ pub const STATUS_PACKET_ID_PING: i32 = 1;
/// Login state, login start packet ID.
pub const LOGIN_PACKET_ID_LOGIN_START: i32 = 0;
/// Login state, disconnect packet ID.
pub const LOGIN_PACKET_ID_DISCONNECT: i32 = 0;
/// Login state, login success packet ID.
pub const LOGIN_PACKET_ID_LOGIN_SUCCESS: i32 = 2;
pub mod packets {
pub mod play {
pub const CLIENT_JOIN_GAME: i32 = 0x26;
pub const SERVER_CLIENT_SETTINGS: i32 = 0x05;
pub const SERVER_PLUGIN_MESSAGE: i32 = 0x0A;
pub const SERVER_PLAYER_POS_ROT: i32 = 0x12;
pub const SERVER_PLAYER_POS: i32 = 0x11;
pub const CLIENT_KEEP_ALIVE: i32 = 0x21;
pub const CLIENT_CHUNK_DATA: i32 = 0x22;
pub const CLIENT_PLAYER_POS_LOOK: i32 = 0x38;
pub const CLIENT_SET_TITLE_TEXT: i32 = 0x59;
pub const CLIENT_SET_TITLE_SUBTITLE: i32 = 0x57;
pub const CLIENT_SET_TITLE_TIMES: i32 = 0x5A;
pub const CLIENT_TIME_UPDATE: i32 = 0x58;
pub const CLIENT_CHAT_MSG: i32 = 0x0F;
pub const CLIENT_SPAWN_POS: i32 = 0x4B;
}
}
/// Client state.
///
/// Note: this does not keep track of compression/encryption states because packets are never
@ -66,6 +91,9 @@ pub enum ClientState {
/// State to login to server.
Login,
/// State to play on the server.
Play,
}
impl ClientState {
@ -85,6 +113,7 @@ impl ClientState {
Self::Handshake => 0,
Self::Status => 1,
Self::Login => 2,
Self::Play => -1,
}
}
}

View File

@ -2,7 +2,6 @@ use std::ops::Deref;
use std::sync::Arc;
use std::time::Duration;
use crate::server::State;
use bytes::BytesMut;
use minecraft_protocol::data::chat::{Message, Payload};
use minecraft_protocol::data::server_status::*;
@ -17,8 +16,9 @@ use tokio::net::TcpStream;
use tokio::time;
use crate::config::*;
use crate::lobby;
use crate::proto::{self, Client, ClientState, RawPacket};
use crate::server::{self, Server};
use crate::server::{self, Server, State};
use crate::service;
/// Proxy the given inbound stream to a target address.
@ -31,7 +31,7 @@ pub async fn serve(
) -> Result<(), ()> {
let (mut reader, mut writer) = inbound.split();
// Incoming buffer
// Incoming buffer and packet holding queue
let mut buf = BytesMut::new();
// Remember inbound packets, used for client holding and forwarding
@ -120,8 +120,26 @@ pub async fn serve(
break;
}
if !lobby::DONT_START_SERVER {
// Start server if not starting yet
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, inbound, config, server, queue).await?;
return Ok(());
}
// Use join occupy methods
for method in &config.join.methods {
@ -283,7 +301,7 @@ async fn kick(msg: &str, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(0, data).encode()?;
let response = RawPacket::new(proto::LOGIN_PACKET_ID_DISCONNECT, data).encode()?;
writer.write_all(&response).await.map_err(|_| ())
}