diff --git a/Cargo.lock b/Cargo.lock index f7dc68a..8799c2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,15 @@ dependencies = [ "libc", ] +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -41,6 +50,18 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cc" version = "1.2.20" @@ -50,6 +71,12 @@ dependencies = [ "shlex", ] +[[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" @@ -141,6 +168,24 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + +[[package]] +name = "fastnbt" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4a73a95dc65551ccd98e1ecd1adb5d1ba5361146963b31f481ca42fc0520a3" +dependencies = [ + "byteorder", + "cesu8", + "serde", + "serde_bytes", +] + [[package]] name = "flate2" version = "1.1.1" @@ -300,6 +345,72 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "palette_derive", + "phf", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -324,6 +435,21 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "rust_mc_proto" version = "0.1.19" @@ -338,7 +464,9 @@ dependencies = [ name = "rust_minecraft_server" version = "0.1.0" dependencies = [ + "fastnbt", "itertools", + "palette", "rust_mc_proto", "serde", "serde_default", @@ -368,6 +496,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +dependencies = [ + "serde", +] + [[package]] name = "serde_default" version = "0.2.0" @@ -448,6 +585,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "strsim" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index cf60f6d..33cc562 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,5 @@ serde_with = { version = "3.12.0", features = ["macros"] } serde_default = "0.2.0" toml = "0.8.22" itertools = "0.14.0" +palette = "0.7.6" +fastnbt = "2.5.0" diff --git a/src/config.rs b/src/config.rs index c8ededa..90c5226 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,27 +5,37 @@ use serde_default::DefaultFromSerde; #[derive(Debug, DefaultFromSerde, Serialize, Deserialize, Clone)] -pub struct ServerConfig { - /// Хост где забиндить сервер +pub struct BindConfig { #[serde(default = "default_host")] pub host: String, - - /// Таймаут подключения в секундах #[serde(default = "default_timeout")] pub timeout: u64, } +#[derive(Debug, DefaultFromSerde, Serialize, Deserialize, Clone)] +pub struct ServerConfig { + #[serde(default)] pub online_mode: bool, + #[serde(default = "default_compression")] pub compression_threshold: Option, +} + +#[derive(Debug, DefaultFromSerde, Serialize, Deserialize, Clone)] +pub struct Config { + #[serde(default)] pub bind: BindConfig, + #[serde(default)] pub server: ServerConfig, +} + fn default_host() -> String { "127.0.0.1:25565".to_string() } fn default_timeout() -> u64 { 5 } +fn default_compression() -> Option { Some(256) } -impl ServerConfig { - pub fn load_from_file(path: PathBuf) -> Option { +impl Config { + pub fn load_from_file(path: PathBuf) -> Option { if !fs::exists(&path).unwrap_or_default() { - let table = ServerConfig::default(); + let table = Config::default(); fs::create_dir_all(&path.parent()?).ok()?; fs::write(&path, toml::to_string_pretty(&table).ok()?).ok()?; return Some(table); } let content = fs::read_to_string(&path).ok()?; - let table = toml::from_str::(&content).ok()?; + let table = toml::from_str::(&content).ok()?; Some(table) } } \ No newline at end of file diff --git a/src/context.rs b/src/context.rs index 4269a53..7e5843a 100644 --- a/src/context.rs +++ b/src/context.rs @@ -3,16 +3,16 @@ use std::{net::{SocketAddr, TcpStream}, sync::{atomic::{AtomicI32, AtomicU16, Or use itertools::Itertools; use rust_mc_proto::{MinecraftConnection, Packet}; -use crate::{config::ServerConfig, data::ServerError}; +use crate::{config::Config, data::ServerError}; pub struct ServerContext { - pub config: Arc, + pub config: Arc, listeners: Vec>, handlers: Vec> } impl ServerContext { - pub fn new(config: Arc) -> ServerContext { + pub fn new(config: Arc) -> ServerContext { ServerContext { config, listeners: Vec::new(), diff --git a/src/data.rs b/src/data.rs index 19d148b..347aa2f 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,7 +1,8 @@ use std::{error::Error, fmt::Display}; +use palette::{Hsl, IntoColor, Srgb}; use serde::{Deserialize, Serialize}; -use rust_mc_proto::{DataReader, DataWriter, ProtocolError}; +use rust_mc_proto::ProtocolError; use serde_with::skip_serializing_none; // Ошибки сервера @@ -52,62 +53,64 @@ pub struct TextComponent { } impl TextComponent { + pub fn new(text: String) -> Self { + Self { + text, + color: None, + bold: None, + italic: None, + underlined: None, + strikethrough: None, + obfuscated: None, + extra: None + } + } + + pub fn rainbow(text: String) -> TextComponent { + if text.is_empty() { + return TextComponent::new(text); + } + + let children = text.char_indices() + .map(|(i, c)| { + let hue = (i as f32) / (text.chars().count() as f32) * 360.0; + let hsl = Hsl::new(hue, 1.0, 0.5); + let rgb: Srgb = hsl.into_color(); + let r = (rgb.red * 255.0).round() as u8; + let g = (rgb.green * 255.0).round() as u8; + let b = (rgb.blue * 255.0).round() as u8; + let mut component = TextComponent::new(c.to_string()); + component.color = Some(format!("#{:02X}{:02X}{:02X}", r, g, b)); + component + }) + .collect::>(); + + let mut parent = children[0].clone(); + parent.extra = Some(children[1..].to_vec()); + parent + } + pub fn builder() -> TextComponentBuilder { TextComponentBuilder::new() } - pub fn to_string(self) -> Result { - self.try_into() + pub fn as_nbt(self) -> Result, ServerError> { + fastnbt::to_bytes(&self) + .map_err(|_| ServerError::SerTextComponent) } - pub fn from_string(text: String) -> Result { - Self::try_from(text) + pub fn from_nbt(bytes: &[u8]) -> Result { + fastnbt::from_bytes(bytes) + .map_err(|_| ServerError::DeTextComponent) } -} -pub trait WriteTextComponent { - fn write_text_component(&mut self, component: &TextComponent) -> Result<(), ServerError>; -} - -impl WriteTextComponent for T { - fn write_text_component(&mut self, component: &TextComponent) -> Result<(), ServerError> { - Ok(self.write_string(TryInto::::try_into(component.clone())?.as_str())?) - } -} - -pub trait ReadTextComponent { - fn read_text_component(&mut self) -> Result; -} - -impl ReadTextComponent for T { - fn read_text_component(&mut self) -> Result { - TextComponent::try_from(self.read_string()?) - } -} - -impl TryInto for TextComponent { - type Error = ServerError; - - fn try_into(self) -> Result { + pub fn as_json(self) -> Result { serde_json::to_string(&self) .map_err(|_| ServerError::SerTextComponent) } -} -impl TryFrom for TextComponent { - type Error = ServerError; - - fn try_from(value: String) -> Result { - serde_json::from_str(&value) - .map_err(|_| ServerError::DeTextComponent) - } -} - -impl TryFrom<&str> for TextComponent { - type Error = ServerError; - - fn try_from(value: &str) -> Result { - serde_json::from_str(&value) + pub fn from_json(text: &str) -> Result { + serde_json::from_str(text) .map_err(|_| ServerError::DeTextComponent) } } diff --git a/src/main.rs b/src/main.rs index 269c4c7..0c36012 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ -use std::{env::args, io::{Read, Write}, net::TcpListener, path::PathBuf, sync::Arc, thread, time::Duration}; +use std::{env::args, io::Read, net::TcpListener, path::PathBuf, sync::Arc, thread, time::Duration}; -use config::ServerConfig; +use config::Config; use context::{ClientContext, Listener, PacketHandler, ServerContext}; use rust_mc_proto::{DataReader, DataWriter, MinecraftConnection, Packet}; @@ -63,7 +63,7 @@ impl Listener for ExampleListener { .build() ]) .build() - .to_string()? + .as_json()? ); Ok(()) @@ -89,7 +89,7 @@ fn main() { let config_path = PathBuf::from(args.get(0).unwrap_or(&"server.toml".to_string())); // Чтение конфига, если ошибка - выводим - let config = match ServerConfig::load_from_file(config_path) { + let config = match Config::load_from_file(config_path) { Some(config) => config, None => { println!("Ошибка чтения конфигурации"); @@ -113,12 +113,12 @@ fn main() { let server = Arc::new(server); // Биндим сервер где надо - let Ok(listener) = TcpListener::bind(&server.config.host) else { - println!("Не удалось забиндить сервер на {}", &server.config.host); + let Ok(listener) = TcpListener::bind(&server.config.bind.host) else { + println!("Не удалось забиндить сервер на {}", &server.config.bind.host); return; }; - println!("Сервер запущен на {}", &server.config.host); + println!("Сервер запущен на {}", &server.config.bind.host); while let Ok((stream, addr)) = listener.accept() { let server = server.clone(); @@ -128,8 +128,8 @@ fn main() { // Установка таймаутов на чтение и запись // По умолчанию пусть будет 5 секунд, надо будет сделать настройку через конфиг - stream.set_read_timeout(Some(Duration::from_secs(server.config.timeout))).pohuy(); - stream.set_write_timeout(Some(Duration::from_secs(server.config.timeout))).pohuy(); + stream.set_read_timeout(Some(Duration::from_secs(server.config.bind.timeout))).pohuy(); + stream.set_write_timeout(Some(Duration::from_secs(server.config.bind.timeout))).pohuy(); // Оборачиваем стрим в майнкрафт конекшн лично для нашего удовольствия let conn = MinecraftConnection::new(stream); @@ -214,22 +214,101 @@ fn handle_connection( } } }, - 2 | 3 => { // Тип подключения - игра + 2 => { // Тип подключения - игра + // Мы находимся в режиме Login + + // Читаем пакет Login Start + let mut packet = client.conn().read_packet()?; + + let player_name = packet.read_string()?; + let player_uuid = packet.read_uuid()?; + + if client.server.config.server.online_mode { + // TODO: encryption packets + } + + // Отправляем пакет Set Compression если сжатие указано + if let Some(threshold) = client.server.config.server.compression_threshold { + client.conn().write_packet(&Packet::build(0x03, |p| p.write_usize_varint(threshold))?)?; + client.conn().set_compression(Some(threshold)); // Устанавливаем сжатие на соединении + } + + // Отправка пакета Login Success + client.conn().write_packet(&Packet::build(0x02, |p| { + p.write_uuid(&player_uuid)?; + p.write_string(&player_name)?; + p.write_varint(0) + })?)?; + + let packet = client.conn().read_packet()?; + + if packet.id() != 0x03 { + return Err(ServerError::UnknownPacket(format!("Неизвестный пакет при ожидании Login Acknowledged"))); + } + + // Мы перешли в режим Configuration + + let mut packet = client.conn().read_packet()?; + + if packet.id() == 0x02 { // Пакет Serverbound Plugin Message + let identifier = packet.read_string()?; + let mut data = Vec::new(); + packet.get_mut().read_to_end(&mut data).unwrap(); + + // TODO: Сделать запись всех этих полезных данных в клиент контекст + + println!("got plugin message: {}", identifier); + } + + let mut packet = client.conn().read_packet()?; + + if packet.id() == 0x00 { // Пакет Serverbound Plugin Message + let locale = packet.read_string()?; // for example: ru_RU + let view_distance = packet.read_signed_byte()?; // client-side render distance in chunks + let chat_mode = packet.read_varint()?; // 0: enabled, 1: commands only, 2: hidden. See Chat#Client chat mode for more information. + let chat_colors = packet.read_boolean()?; // this settings does nothing on client but can be used on serverside + let displayed_skin_parts = packet.read_byte()?; // bit mask https://minecraft.wiki/w/Java_Edition_protocol#Client_Information_(configuration) + let main_hand = packet.read_varint()?; // 0 for left and 1 for right + let enable_text_filtering = packet.read_boolean()?; // filtering text for profanity, always false for offline mode + let allow_server_listings = packet.read_boolean()?; // allows showing player in server listings in status + let particle_status = packet.read_varint()?; // 0 for all, 1 for decreased, 2 for minimal + + // TODO: Сделать запись всех этих полезных данных в клиент контекст + + println!("got client information:"); + println!("locale: {locale}"); + println!("view_distance: {view_distance}"); + println!("chat_mode: {chat_mode}"); + println!("chat_colors: {chat_colors}"); + println!("displayed_skin_parts: {displayed_skin_parts}"); + println!("main_hand: {main_hand}"); + println!("enable_text_filtering: {enable_text_filtering}"); + println!("allow_server_listings: {allow_server_listings}"); + println!("particle_status: {particle_status}"); + } + + // TODO: Заюзать Listener'ы чтобы они подмешивали сюда чото + + client.conn().write_packet(&Packet::empty(0x03))?; + + let packet = client.conn().read_packet()?; + + if packet.id() != 0x03 { + return Err(ServerError::UnknownPacket(format!("Неизвестный пакет при ожидании Acknowledge Finish Configuration"))); + } + + // Мы перешли в режим Play + // Отключение игрока с сообщением - // Заглушка так сказать - let mut packet = Packet::empty(0x00); + // Отправляет в формате NBT TAG_String (https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/NBT#Specification:string_tag) + client.conn().write_packet(&Packet::build(0x1C, |p| { + let message = "server is in developmenet lol".to_string(); + p.write_byte(0x08)?; // NBT Type Name (TAG_String) + p.write_unsigned_short(message.len() as u16)?; // String length in unsigned short + p.write_bytes(message.as_bytes()) + })?)?; - packet.write_string(&TextComponent::builder() - .text("This server is in developement!!") - .color("gold") - .bold(true) - .build() - .to_string()?)?; - - client.conn().write_packet(&packet)?; - - // TODO: Чтение Configuration (возможно с примешиванием Listener'ов) - // TODO: Обработчик пакетов Play (тоже трейт), который уже будет дергать Listener'ы + // TODO: Сделать отправку пакетов Play }, _ => { return Err(ServerError::UnknownPacket(format!("Неизвестный NextState при рукопожатии")));