configuration and login states

This commit is contained in:
MeexReay 2025-05-02 00:15:55 +03:00
parent f8684a0402
commit 59efaa2861
6 changed files with 316 additions and 79 deletions

143
Cargo.lock generated
View File

@ -23,6 +23,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.4.0" version = "1.4.0"
@ -41,6 +50,18 @@ version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 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]] [[package]]
name = "cc" name = "cc"
version = "1.2.20" version = "1.2.20"
@ -50,6 +71,12 @@ dependencies = [
"shlex", "shlex",
] ]
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -141,6 +168,24 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 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]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.1" version = "1.1.1"
@ -300,6 +345,72 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 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]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@ -324,6 +435,21 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "rust_mc_proto" name = "rust_mc_proto"
version = "0.1.19" version = "0.1.19"
@ -338,7 +464,9 @@ dependencies = [
name = "rust_minecraft_server" name = "rust_minecraft_server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"fastnbt",
"itertools", "itertools",
"palette",
"rust_mc_proto", "rust_mc_proto",
"serde", "serde",
"serde_default", "serde_default",
@ -368,6 +496,15 @@ dependencies = [
"serde_derive", "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]] [[package]]
name = "serde_default" name = "serde_default"
version = "0.2.0" version = "0.2.0"
@ -448,6 +585,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"

View File

@ -11,3 +11,5 @@ serde_with = { version = "3.12.0", features = ["macros"] }
serde_default = "0.2.0" serde_default = "0.2.0"
toml = "0.8.22" toml = "0.8.22"
itertools = "0.14.0" itertools = "0.14.0"
palette = "0.7.6"
fastnbt = "2.5.0"

View File

@ -5,27 +5,37 @@ use serde_default::DefaultFromSerde;
#[derive(Debug, DefaultFromSerde, Serialize, Deserialize, Clone)] #[derive(Debug, DefaultFromSerde, Serialize, Deserialize, Clone)]
pub struct ServerConfig { pub struct BindConfig {
/// Хост где забиндить сервер
#[serde(default = "default_host")] pub host: String, #[serde(default = "default_host")] pub host: String,
/// Таймаут подключения в секундах
#[serde(default = "default_timeout")] pub timeout: u64, #[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<usize>,
}
#[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_host() -> String { "127.0.0.1:25565".to_string() }
fn default_timeout() -> u64 { 5 } fn default_timeout() -> u64 { 5 }
fn default_compression() -> Option<usize> { Some(256) }
impl ServerConfig { impl Config {
pub fn load_from_file(path: PathBuf) -> Option<ServerConfig> { pub fn load_from_file(path: PathBuf) -> Option<Config> {
if !fs::exists(&path).unwrap_or_default() { if !fs::exists(&path).unwrap_or_default() {
let table = ServerConfig::default(); let table = Config::default();
fs::create_dir_all(&path.parent()?).ok()?; fs::create_dir_all(&path.parent()?).ok()?;
fs::write(&path, toml::to_string_pretty(&table).ok()?).ok()?; fs::write(&path, toml::to_string_pretty(&table).ok()?).ok()?;
return Some(table); return Some(table);
} }
let content = fs::read_to_string(&path).ok()?; let content = fs::read_to_string(&path).ok()?;
let table = toml::from_str::<ServerConfig>(&content).ok()?; let table = toml::from_str::<Config>(&content).ok()?;
Some(table) Some(table)
} }
} }

View File

@ -3,16 +3,16 @@ use std::{net::{SocketAddr, TcpStream}, sync::{atomic::{AtomicI32, AtomicU16, Or
use itertools::Itertools; use itertools::Itertools;
use rust_mc_proto::{MinecraftConnection, Packet}; use rust_mc_proto::{MinecraftConnection, Packet};
use crate::{config::ServerConfig, data::ServerError}; use crate::{config::Config, data::ServerError};
pub struct ServerContext { pub struct ServerContext {
pub config: Arc<ServerConfig>, pub config: Arc<Config>,
listeners: Vec<Box<dyn Listener>>, listeners: Vec<Box<dyn Listener>>,
handlers: Vec<Box<dyn PacketHandler>> handlers: Vec<Box<dyn PacketHandler>>
} }
impl ServerContext { impl ServerContext {
pub fn new(config: Arc<ServerConfig>) -> ServerContext { pub fn new(config: Arc<Config>) -> ServerContext {
ServerContext { ServerContext {
config, config,
listeners: Vec::new(), listeners: Vec::new(),

View File

@ -1,7 +1,8 @@
use std::{error::Error, fmt::Display}; use std::{error::Error, fmt::Display};
use palette::{Hsl, IntoColor, Srgb};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use rust_mc_proto::{DataReader, DataWriter, ProtocolError}; use rust_mc_proto::ProtocolError;
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
// Ошибки сервера // Ошибки сервера
@ -52,62 +53,64 @@ pub struct TextComponent {
} }
impl 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::<Vec<TextComponent>>();
let mut parent = children[0].clone();
parent.extra = Some(children[1..].to_vec());
parent
}
pub fn builder() -> TextComponentBuilder { pub fn builder() -> TextComponentBuilder {
TextComponentBuilder::new() TextComponentBuilder::new()
} }
pub fn to_string(self) -> Result<String, ServerError> { pub fn as_nbt(self) -> Result<Vec<u8>, ServerError> {
self.try_into() fastnbt::to_bytes(&self)
.map_err(|_| ServerError::SerTextComponent)
} }
pub fn from_string(text: String) -> Result<TextComponent, ServerError> { pub fn from_nbt(bytes: &[u8]) -> Result<TextComponent, ServerError> {
Self::try_from(text) fastnbt::from_bytes(bytes)
} .map_err(|_| ServerError::DeTextComponent)
} }
pub trait WriteTextComponent { pub fn as_json(self) -> Result<String, ServerError> {
fn write_text_component(&mut self, component: &TextComponent) -> Result<(), ServerError>;
}
impl<T: DataWriter> WriteTextComponent for T {
fn write_text_component(&mut self, component: &TextComponent) -> Result<(), ServerError> {
Ok(self.write_string(TryInto::<String>::try_into(component.clone())?.as_str())?)
}
}
pub trait ReadTextComponent {
fn read_text_component(&mut self) -> Result<TextComponent, ServerError>;
}
impl<T: DataReader> ReadTextComponent for T {
fn read_text_component(&mut self) -> Result<TextComponent, ServerError> {
TextComponent::try_from(self.read_string()?)
}
}
impl TryInto<String> for TextComponent {
type Error = ServerError;
fn try_into(self) -> Result<String, Self::Error> {
serde_json::to_string(&self) serde_json::to_string(&self)
.map_err(|_| ServerError::SerTextComponent) .map_err(|_| ServerError::SerTextComponent)
} }
}
impl TryFrom<String> for TextComponent { pub fn from_json(text: &str) -> Result<TextComponent, ServerError> {
type Error = ServerError; serde_json::from_str(text)
fn try_from(value: String) -> Result<Self, Self::Error> {
serde_json::from_str(&value)
.map_err(|_| ServerError::DeTextComponent)
}
}
impl TryFrom<&str> for TextComponent {
type Error = ServerError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
serde_json::from_str(&value)
.map_err(|_| ServerError::DeTextComponent) .map_err(|_| ServerError::DeTextComponent)
} }
} }

View File

@ -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 context::{ClientContext, Listener, PacketHandler, ServerContext};
use rust_mc_proto::{DataReader, DataWriter, MinecraftConnection, Packet}; use rust_mc_proto::{DataReader, DataWriter, MinecraftConnection, Packet};
@ -63,7 +63,7 @@ impl Listener for ExampleListener {
.build() .build()
]) ])
.build() .build()
.to_string()? .as_json()?
); );
Ok(()) Ok(())
@ -89,7 +89,7 @@ fn main() {
let config_path = PathBuf::from(args.get(0).unwrap_or(&"server.toml".to_string())); 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, Some(config) => config,
None => { None => {
println!("Ошибка чтения конфигурации"); println!("Ошибка чтения конфигурации");
@ -113,12 +113,12 @@ fn main() {
let server = Arc::new(server); let server = Arc::new(server);
// Биндим сервер где надо // Биндим сервер где надо
let Ok(listener) = TcpListener::bind(&server.config.host) else { let Ok(listener) = TcpListener::bind(&server.config.bind.host) else {
println!("Не удалось забиндить сервер на {}", &server.config.host); println!("Не удалось забиндить сервер на {}", &server.config.bind.host);
return; return;
}; };
println!("Сервер запущен на {}", &server.config.host); println!("Сервер запущен на {}", &server.config.bind.host);
while let Ok((stream, addr)) = listener.accept() { while let Ok((stream, addr)) = listener.accept() {
let server = server.clone(); let server = server.clone();
@ -128,8 +128,8 @@ fn main() {
// Установка таймаутов на чтение и запись // Установка таймаутов на чтение и запись
// По умолчанию пусть будет 5 секунд, надо будет сделать настройку через конфиг // По умолчанию пусть будет 5 секунд, надо будет сделать настройку через конфиг
stream.set_read_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.timeout))).pohuy(); stream.set_write_timeout(Some(Duration::from_secs(server.config.bind.timeout))).pohuy();
// Оборачиваем стрим в майнкрафт конекшн лично для нашего удовольствия // Оборачиваем стрим в майнкрафт конекшн лично для нашего удовольствия
let conn = MinecraftConnection::new(stream); 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
// Отключение игрока с сообщением // Отключение игрока с сообщением
// Заглушка так сказать // Отправляет в формате NBT TAG_String (https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/NBT#Specification:string_tag)
let mut packet = Packet::empty(0x00); 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() // TODO: Сделать отправку пакетов Play
.text("This server is in developement!!")
.color("gold")
.bold(true)
.build()
.to_string()?)?;
client.conn().write_packet(&packet)?;
// TODO: Чтение Configuration (возможно с примешиванием Listener'ов)
// TODO: Обработчик пакетов Play (тоже трейт), который уже будет дергать Listener'ы
}, },
_ => { _ => {
return Err(ServerError::UnknownPacket(format!("Неизвестный NextState при рукопожатии"))); return Err(ServerError::UnknownPacket(format!("Неизвестный NextState при рукопожатии")));