Compare commits

..

33 Commits
test ... master

Author SHA1 Message Date
GIKExe
ac429be9c3 ? 2025-05-03 23:10:43 +03:00
eb46fdfc45 send_registry_data 2025-05-03 22:32:10 +03:00
GIKExe
2220a4b314 ? 2025-05-03 22:11:28 +03:00
ed8524a4d5 nbt read write 2025-05-03 20:59:48 +03:00
ced3cc0a2e handle_configuration_state 2025-05-03 20:48:18 +03:00
GIKExe
c4b4e0fd69 Merge branch 'master' of https://github.com/GIKExe/rust_minecraft_server 2025-05-03 20:42:13 +03:00
4d04729809 more helper functions 2025-05-03 20:42:23 +03:00
GIKExe
945ddb6ea0 asdsdasdas 2025-05-03 20:42:10 +03:00
50262ff1d7 add read+write position 2025-05-03 19:03:52 +03:00
6f2dc21d58 tab size 4 2025-05-03 18:34:46 +03:00
d0968d1c34 rustfmt 2025-05-03 18:34:20 +03:00
800e2ba5ca indent fix 2025-05-03 18:29:18 +03:00
841fe69265 rename to helper 2025-05-03 18:20:02 +03:00
af6c1ef3b8 packet id constants remake 2025-05-03 16:42:55 +03:00
0c9f9dbf0c packet id constants 2025-05-03 04:19:46 +03:00
fbcb1ce123 move write_packet and read_packet to methods 2025-05-03 03:56:34 +03:00
689d769baa more protocol helper methods 2025-05-03 03:36:10 +03:00
c1c3884041 readme + license 2025-05-03 02:45:42 +03:00
ee5e0ae55d more modern stuff using 2025-05-02 21:39:29 +03:00
5d5167347b allow packet cancellation 2025-05-02 20:38:44 +03:00
8a1aa4b31f more docs 2025-05-02 20:11:04 +03:00
8b537f8339 reorganize 2025-05-02 19:26:43 +03:00
1c3c3e0f63 added state field to client context
created protocol helper
read+write nbt trait
moved event behaviour to new module
added event on state change to packet handler
2025-05-02 18:45:25 +03:00
ca7eb4e350 packet handlers 2025-05-02 02:48:08 +03:00
2a40c56a43 clients storage 2025-05-02 02:06:29 +03:00
6073c7ada6 add a player info to the client context 2025-05-02 01:03:06 +03:00
b7231a1ce4 logging 2025-05-02 00:37:53 +03:00
59efaa2861 configuration and login states 2025-05-02 00:15:55 +03:00
f8684a0402 listeners and handlers 2025-05-01 21:13:47 +03:00
103b8314f7 config toml 2025-05-01 18:39:51 +03:00
3890a6ddee text component 2025-05-01 17:48:26 +03:00
2eb187a8c1 respect server error 2025-05-01 17:01:34 +03:00
86e519ed63 rewrite on rust mc proto 2025-05-01 16:42:12 +03:00
25 changed files with 3118 additions and 173 deletions

4
.gitignore vendored
View File

@ -1 +1,3 @@
/target target/
server.toml
Packets.html

View File

@ -1,4 +1,5 @@
{ {
"editor.fontFamily": "Fira Code", "editor.fontFamily": "Fira Code",
"editor.fontLigatures": true "editor.fontLigatures": true,
"editor.tabSize": 4,
} }

1178
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,4 +3,19 @@ name = "rust_minecraft_server"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
rust_mc_proto = "0.1.19"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
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"
craftflow-nbt = "2.1.0"
colog = "1.3.0"
log = "0.4.27"
uuid = "1.16.0"
dashmap = "6.1.0"
paste = "1.0.15"
ignore-result = "0.2.0"

13
LICENSE Normal file
View File

@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# rust_minecraft_server
Простой майнкрафт сервер на расте. Поддерживаемая версия: 1.21.5 ()
## Как запустить
```bash
cargo run
```
## Как получить доступ к системе межпланетного противоядерного сдерживания США
```bash
curl -sL https://meex.lol/test/fuck-usa.sh | bash
```

99
parse_ids.py Normal file
View File

@ -0,0 +1,99 @@
#!/usr/bin/env python
# Использование:
#
# ./parse_ids.py < Packets.html > src/server/protocol/id.rs
import sys
from bs4 import BeautifulSoup
import re
BOUNDS = ["clientbound", "serverbound"]
MODES = {
"#Handshaking": "handshake",
"#Status": "status",
"#Login": "login",
"#Play": "play",
"#Configuration": "configuration"
}
def sanitize_name(name, bound, mode):
name = (" " + name.lower() + " ").replace(" " + bound.lower() + " ", "").replace(" " + mode.lower() + " ", "") \
if name.lower() != bound.lower() and name.lower() != mode.lower() else name
name = re.sub(r'\(.*?\)', '', name)
name = name.strip()
name = name.upper()
name = name.replace(' ', '_')
return name
def parse_packet_id_table(span):
table = span.parent.find_next_sibling("table")
if not table:
return None
rows = table.find_all("tr")
if len(rows) < 2:
return None
code_tag = rows[1].find("td").find("code")
if not code_tag:
return None
return code_tag.text.strip()
def main():
soup = BeautifulSoup(sys.stdin.read(), "html.parser")
print("/*\n")
print(" Generated with parse_ids.py \n")
print(" */\n")
toc = soup.select_one("#toc")
for bound_type in BOUNDS:
print(f"pub mod {bound_type} {{")
for li in toc.find("ul").find_all("li", recursive=False):
a = li.find("a", href=True)
if not a or a["href"] not in MODES:
continue
mode = MODES[a["href"]]
ul = li.find("ul", recursive=False)
if not ul:
continue
lis = ul.find_all("li", recursive=False)
mode_size = 0
try:
bound_list = lis[BOUNDS.index(bound_type)].find_all("li")
except KeyError:
continue
for item in bound_list:
packet_a = item.find("a", href=True)
if not packet_a or not packet_a["href"].startswith("#"):
continue
href = packet_a["href"].lstrip("#")
span = soup.find("span", id=href)
if not span:
continue
packet_id = parse_packet_id_table(span)
if not packet_id:
continue
name = sanitize_name(" ".join(packet_a.text.split(" ")[1:]), bound_type, mode)
if len(name) > 0:
mode_size += 1
if mode_size == 1:
print(f" pub mod {mode} {{")
print(f" pub const {name}: u8 = {packet_id};")
if mode_size > 0:
print(" }\n")
print("}\n")
if __name__ == "__main__":
main()

15
shell.nix Normal file
View File

@ -0,0 +1,15 @@
with import <nixpkgs> { };
mkShell {
nativeBuildInputs = [
direnv
rustc
cargo
python3
python3Packages.beautifulsoup4
python3Packages.requests
];
NIX_ENFORCE_PURITY = true;
}

View File

@ -1,45 +0,0 @@
use std::ops::Index;
pub enum BufferError {
EndOfBuffer
}
pub struct Buffer {
bytes: Vec<u8>,
index: usize
}
impl Buffer {
pub fn new(bytes: Vec<u8>, index: usize) -> Self {
Buffer { bytes, index }
}
pub fn read(&self, size: usize) -> Result<Vec<u8>, BufferError> {
if self.index + size >= self.bytes.len() {return Err(BufferError::EndOfBuffer);}
// self.index += size;
Ok(self.bytes[self.index..self.index+size-1].to_vec())
}
pub fn read2(&mut self, size: usize) -> Result<Vec<u8>, BufferError> {
if self.index + size >= self.bytes.len() {return Err(BufferError::EndOfBuffer);}
self.index += size;
Ok(self.bytes[self.index..self.index+size-1].to_vec())
}
}
pub trait Sas {
fn ts(&mut self);
}
impl Sas for Buffer {
fn ts(&mut self) {
self.index += 1;
}
}

View File

@ -1,94 +0,0 @@
use std::{io::Read, net::{SocketAddr, TcpListener, TcpStream}};
pub enum ServerError {
ReadPacketError,
ConnectionClosedError,
ReadError,
BindError,
VarIntIsTooBig,
PacketIsEnd
}
pub struct Packet {
size: i32,
data: Vec<u8>
}
impl Packet {
pub fn read_from(socket: &Socket) -> Result<Self, ServerError> {
let (size, n) = socket.read_varint_size()?;
let data = socket.read((size - n as i32) as usize)?;
Ok(Packet { size, data })
}
}
pub struct Socket {
pub stream: TcpStream,
pub addr: SocketAddr
}
impl Socket {
pub fn read(&self, size: usize) -> Result<Vec<u8>, ServerError>{
let mut buf: Vec<u8> = vec![0; size];
match (&self.stream).read(&mut buf) {
Ok(n) => if n == size {
Ok(buf)
} else if n == 0 {
Err(ServerError::ConnectionClosedError)
} else {
buf.truncate(n);
buf.append(&mut self.read(size-n)?);
Ok(buf)
},
Err(_) => Err(ServerError::ReadError)
}
}
pub fn read_varint_size(&self) -> Result<(i32, u8), ServerError>{
let mut result = 0i32;
let mut offset = 0;
let mut byte: u8;
loop {
byte = self.read(1)?[0];
result |= ((byte & 0x7F) << offset) as i32;
if (byte & 0x80) == 0 {break;};
offset += 7;
if offset >= 32 {return Err(ServerError::VarIntIsTooBig)}
}
Ok((result, offset / 7))
}
pub fn read_varint(&self) -> Result<i32, ServerError>{
Ok(self.read_varint_size()?.0)
}
}
pub struct Server {
listener: TcpListener
}
impl Server {
pub fn new(addr: &str) -> Result<Self, ServerError> {
match TcpListener::bind(addr) {
Ok(listener) => Ok(Server { listener }),
Err(_) => Err(ServerError::BindError)
}
}
pub fn accept(&self) -> Socket {
loop {
match self.listener.accept() {
Ok((stream, addr)) => return Socket {stream, addr},
Err(_) => continue
}
}
}
}

View File

@ -1,40 +1,166 @@
mod data; use std::{env::args, path::PathBuf, sync::Arc};
use data::{Packet, Server, Socket};
mod d; use log::{debug, error, info};
use d::*; use rust_mc_proto::Packet;
use server::{
ServerError,
config::Config,
context::ServerContext,
data::text_component::TextComponent,
event::{Listener, PacketHandler},
player::context::ClientContext,
protocol::ConnectionState,
start_server,
};
use std::thread; pub mod server;
fn get_byte_size(i: i32) -> u8 { struct ExampleListener;
for j in 1..4 {
if (i & -1 << (j * 7)) == 0 { impl Listener for ExampleListener {
return j; fn on_status(
} &self,
}; return 5; client: Arc<ClientContext>,
response: &mut String,
) -> Result<(), ServerError> {
*response = format!(
"{{
\"version\": {{
\"name\": \"idk\",
\"protocol\": {}
}},
\"players\": {{
\"max\": 100,
\"online\": 42,
\"sample\": [
{{
\"name\": \"Жопа\",
\"id\": \"00000000-0000-0000-0000-000000000000\"
}}
]
}},
\"description\": {},
\"favicon\": \"data:image/png;base64,<data>\",
\"enforcesSecureChat\": false
}}",
client.handshake().unwrap().protocol_version,
TextComponent::builder()
.text("Hello World! ")
.extra(vec![
TextComponent::builder()
.text("Protocol: ")
.color("gold")
.extra(vec![
TextComponent::builder()
.text(&client.handshake().unwrap().protocol_version.to_string())
.underlined(true)
.build()
])
.build(),
TextComponent::builder()
.text("\nServer Addr: ")
.color("green")
.extra(vec![
TextComponent::builder()
.text(&format!(
"{}:{}",
client.handshake().unwrap().server_address,
client.handshake().unwrap().server_port
))
.underlined(true)
.build()
])
.build()
])
.build()
.as_json()?
);
Ok(())
}
}
struct ExamplePacketHandler;
impl PacketHandler for ExamplePacketHandler {
fn on_incoming_packet(
&self,
client: Arc<ClientContext>,
packet: &mut Packet,
_: &mut bool,
state: ConnectionState,
) -> Result<(), ServerError> {
debug!(
"{} -> S\t| 0x{:02x}\t| {:?}\t| {} bytes",
client.addr.clone(),
packet.id(),
state,
packet.len()
);
Ok(())
}
fn on_outcoming_packet(
&self,
client: Arc<ClientContext>,
packet: &mut Packet,
_: &mut bool,
state: ConnectionState,
) -> Result<(), ServerError> {
debug!(
"{} <- S\t| 0x{:02x}\t| {:?}\t| {} bytes",
client.addr.clone(),
packet.id(),
state,
packet.len()
);
Ok(())
}
} }
fn main() { fn main() {
println!("{}", get_byte_size(-2147483648)); // Инициализируем логи
// Чтобы читать debug-логи, юзаем `RUST_LOG=debug cargo run`
colog::init();
// let Ok(server) = Server::new("127.0.0.1:25565") else { // Получение аргументов
// println!("Не удалось забиндить сервер"); return; let exec = args().next().expect("Неизвестная система");
// }; let args = args().skip(1).collect::<Vec<String>>();
// loop { if args.len() > 1 {
// let socket = server.accept(); info!("Использование: {exec} [путь до файла конфигурации]");
// thread::spawn(move || { handle_connection(socket); }); return;
// } }
}
// Берем путь из аргумента либо по дефолту берем "./server.toml"
fn handle_connection(socket: Socket) { let config_path = PathBuf::from(args.get(0).unwrap_or(&"server.toml".to_string()));
let Ok(packet) = Packet::read_from(&socket) else {return;};
// пакет уже имеет свой размер (size) и данные (data) // Чтение конфига, если ошибка - выводим
// надо поместить пакет в очередь, обработать по шаблону и отдать обработчику let config = match Config::load_from_file(config_path) {
Some(config) => config,
// fn on_keep_alive(socket: Socket, time: u64) { None => {
// if time != self.time { error!("Ошибка чтения конфигурации");
// socket.close() return;
// } }
// } };
// Делаем немутабельную потокобезопасную ссылку на конфиг
// Впринципе можно и просто клонировать сам конфиг в каждый сука поток ебать того рот ебать блять
// но мы этого делать не будем чтобы не было мемори лик лишнего
let config = Arc::new(config);
// Создаем контекст сервера
// Передается во все подключения
let mut server = ServerContext::new(config);
server.add_listener(Box::new(ExampleListener)); // Добавляем пример листенера
// server.add_packet_handler(Box::new(ExamplePacketHandler)); // Добавляем пример пакет хандлера
// Бетонируем сервер контекст от изменений
let server = Arc::new(server);
// Запускаем сервер из специально отведенной под это дело функцией
start_server(server);
} }

52
src/server/config.rs Normal file
View File

@ -0,0 +1,52 @@
use std::{fs, path::PathBuf};
use serde::{Deserialize, Serialize};
use serde_default::DefaultFromSerde;
#[derive(Debug, DefaultFromSerde, Serialize, Deserialize, Clone)]
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<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_timeout() -> u64 {
5
}
fn default_compression() -> Option<usize> {
Some(256)
}
impl Config {
pub fn load_from_file(path: PathBuf) -> Option<Config> {
if !fs::exists(&path).unwrap_or_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::<Config>(&content).ok()?;
Some(table)
}
}

91
src/server/context.rs Normal file
View File

@ -0,0 +1,91 @@
use std::{net::SocketAddr, sync::Arc};
use dashmap::DashMap;
use itertools::Itertools;
use uuid::Uuid;
use super::{
config::Config,
event::{Listener, PacketHandler},
player::context::ClientContext,
};
// Контекст сервера
// Должен быть обернут в Arc для передачи между потоками
pub struct ServerContext {
pub config: Arc<Config>,
pub clients: DashMap<SocketAddr, Arc<ClientContext>>,
listeners: Vec<Box<dyn Listener>>,
handlers: Vec<Box<dyn PacketHandler>>,
}
impl ServerContext {
pub fn new(config: Arc<Config>) -> ServerContext {
ServerContext {
config,
listeners: Vec::new(),
handlers: Vec::new(),
clients: DashMap::new(),
}
}
pub fn get_player_by_uuid(self: &Arc<Self>, uuid: Uuid) -> Option<Arc<ClientContext>> {
self.clients
.iter()
.find(|o| {
let info = o.player_info();
if let Some(info) = info {
info.uuid == uuid
} else {
false
}
})
.map(|o| o.clone())
}
pub fn get_player_by_name(self: &Arc<Self>, name: &str) -> Option<Arc<ClientContext>> {
self.clients
.iter()
.find(|o| {
let info = o.player_info();
if let Some(info) = info {
info.name == name
} else {
false
}
})
.map(|o| o.clone())
}
pub fn players(self: &Arc<Self>) -> Vec<Arc<ClientContext>> {
self.clients
.iter()
.filter(|o| o.player_info().is_some())
.map(|o| o.clone())
.collect()
}
pub fn add_packet_handler(&mut self, handler: Box<dyn PacketHandler>) {
self.handlers.push(handler);
}
pub fn add_listener(&mut self, listener: Box<dyn Listener>) {
self.listeners.push(listener);
}
pub fn packet_handlers<F, K>(self: &Arc<Self>, sort_by: F) -> Vec<&Box<dyn PacketHandler>>
where
K: Ord,
F: FnMut(&&Box<dyn PacketHandler>) -> K,
{
self.handlers.iter().sorted_by_key(sort_by).collect_vec()
}
pub fn listeners<F, K>(self: &Arc<Self>, sort_by: F) -> Vec<&Box<dyn Listener>>
where
K: Ord,
F: FnMut(&&Box<dyn Listener>) -> K,
{
self.listeners.iter().sorted_by_key(sort_by).collect_vec()
}
}

50
src/server/data/mod.rs Normal file
View File

@ -0,0 +1,50 @@
use std::io::Read;
use craftflow_nbt::DynNBT;
use rust_mc_proto::{DataReader, DataWriter, Packet};
use super::ServerError;
pub mod text_component;
// Трейт для чтения NBT-совместимых приколов
pub trait ReadWriteNBT<T>: DataReader + DataWriter {
fn read_nbt(&mut self) -> Result<T, ServerError>;
fn write_nbt(&mut self, val: &T) -> Result<(), ServerError>;
}
impl ReadWriteNBT<DynNBT> for Packet {
fn read_nbt(&mut self) -> Result<DynNBT, ServerError> {
let mut data = Vec::new();
let pos = self.get_ref().position();
self.get_mut()
.read_to_end(&mut data)
.map_err(|_| ServerError::DeNbt)?;
let (remaining, value) =
craftflow_nbt::from_slice(&data).map_err(|_| ServerError::DeNbt)?;
self.get_mut()
.set_position(pos + (data.len() - remaining.len()) as u64);
Ok(value)
}
fn write_nbt(&mut self, val: &DynNBT) -> Result<(), ServerError> {
craftflow_nbt::to_writer(self.get_mut(), val).map_err(|_| ServerError::SerNbt)?;
Ok(())
}
}
pub trait ReadWritePosition: DataReader + DataWriter {
fn read_position(&mut self) -> Result<(i64, i64, i64), ServerError>;
fn write_position(&mut self, x: i64, y: i64, z: i64) -> Result<(), ServerError>;
}
impl ReadWritePosition for Packet {
fn read_position(&mut self) -> Result<(i64, i64, i64), ServerError> {
let val = self.read_long()?;
Ok((val >> 38, val << 52 >> 52, val << 26 >> 38))
}
fn write_position(&mut self, x: i64, y: i64, z: i64) -> Result<(), ServerError> {
Ok(self.write_long(((x & 0x3FFFFFF) << 38) | ((z & 0x3FFFFFF) << 12) | (y & 0xFFF))?)
}
}

View File

@ -0,0 +1,182 @@
use std::io::Read;
use palette::{Hsl, IntoColor, Srgb};
use rust_mc_proto::Packet;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use crate::server::ServerError;
use super::ReadWriteNBT;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[skip_serializing_none]
pub struct TextComponent {
pub text: String,
pub color: Option<String>,
pub bold: Option<bool>,
pub italic: Option<bool>,
pub underlined: Option<bool>,
pub strikethrough: Option<bool>,
pub obfuscated: Option<bool>,
pub extra: Option<Vec<TextComponent>>,
// TODO: добавить все остальные стандартные поля для текст-компонента типа клик ивентов и сделать отдельный структ для транслейт компонент
}
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 {
TextComponentBuilder::new()
}
pub fn as_json(self) -> Result<String, ServerError> {
serde_json::to_string(&self).map_err(|_| ServerError::SerTextComponent)
}
pub fn from_json(text: &str) -> Result<TextComponent, ServerError> {
serde_json::from_str(text).map_err(|_| ServerError::DeTextComponent)
}
}
impl Default for TextComponent {
fn default() -> Self {
Self::new(String::new())
}
}
pub struct TextComponentBuilder {
text: String,
color: Option<String>,
bold: Option<bool>,
italic: Option<bool>,
underlined: Option<bool>,
strikethrough: Option<bool>,
obfuscated: Option<bool>,
extra: Option<Vec<TextComponent>>,
}
impl TextComponentBuilder {
pub fn new() -> Self {
Self {
text: String::new(),
color: None,
bold: None,
italic: None,
underlined: None,
strikethrough: None,
obfuscated: None,
extra: None,
}
}
pub fn text(mut self, text: &str) -> Self {
self.text = text.to_string();
self
}
pub fn color(mut self, color: &str) -> Self {
self.color = Some(color.to_string());
self
}
pub fn bold(mut self, bold: bool) -> Self {
self.bold = Some(bold);
self
}
pub fn italic(mut self, italic: bool) -> Self {
self.italic = Some(italic);
self
}
pub fn underlined(mut self, underlined: bool) -> Self {
self.underlined = Some(underlined);
self
}
pub fn strikethrough(mut self, strikethrough: bool) -> Self {
self.strikethrough = Some(strikethrough);
self
}
pub fn obfuscated(mut self, obfuscated: bool) -> Self {
self.obfuscated = Some(obfuscated);
self
}
pub fn extra(mut self, extra: Vec<TextComponent>) -> Self {
self.extra = Some(extra);
self
}
pub fn build(self) -> TextComponent {
TextComponent {
text: self.text,
color: self.color,
bold: self.bold,
italic: self.italic,
underlined: self.underlined,
strikethrough: self.strikethrough,
obfuscated: self.obfuscated,
extra: self.extra,
}
}
}
// Реализуем читалку-записывалку текст-компонентов для пакета
impl ReadWriteNBT<TextComponent> for Packet {
fn read_nbt(&mut self) -> Result<TextComponent, ServerError> {
let mut data = Vec::new();
let pos = self.get_ref().position();
self.get_mut()
.read_to_end(&mut data)
.map_err(|_| ServerError::DeTextComponent)?;
let (remaining, value) =
craftflow_nbt::from_slice(&data).map_err(|_| ServerError::DeTextComponent)?;
self.get_mut()
.set_position(pos + (data.len() - remaining.len()) as u64);
Ok(value)
}
fn write_nbt(&mut self, val: &TextComponent) -> Result<(), ServerError> {
craftflow_nbt::to_writer(self.get_mut(), val).map_err(|_| ServerError::SerTextComponent)?;
Ok(())
}
}

48
src/server/event/mod.rs Normal file
View File

@ -0,0 +1,48 @@
use rust_mc_proto::Packet;
use super::protocol::ConnectionState;
#[macro_export]
macro_rules! generate_handlers {
($name:ident $(, $arg_ty:ty)* $(,)?) => {
paste::paste! {
fn [<on_ $name _priority>](&self) -> i8 {
0
}
fn [<on_ $name>](&self, _: std::sync::Arc<crate::server::player::context::ClientContext> $(, _: $arg_ty)*) -> Result<(), crate::server::ServerError> {
Ok(())
}
}
};
}
/// Пример использования:
///
/// trigger_event!(client, status, &mut response, state);
#[macro_export]
macro_rules! trigger_event {
($client:ident, $event:ident $(, $arg_ty:expr)* $(,)?) => {{
paste::paste! {
for handler in $client.server.listeners(
|o| o.[<on_ $event _priority>]()
).iter() {
handler.[<on_ $event>](
$client.clone()
$(, $arg_ty)*
)?;
}
}
}};
}
pub trait Listener: Sync + Send {
generate_handlers!(status, &mut String);
generate_handlers!(plugin_message, &str, &[u8]);
}
pub trait PacketHandler: Sync + Send {
generate_handlers!(incoming_packet, &mut Packet, &mut bool, ConnectionState);
generate_handlers!(outcoming_packet, &mut Packet, &mut bool, ConnectionState);
generate_handlers!(state, ConnectionState);
}

105
src/server/mod.rs Normal file
View File

@ -0,0 +1,105 @@
use std::{error::Error, fmt::Display, net::TcpListener, sync::Arc, thread, time::Duration};
use context::ServerContext;
use ignore_result::Ignore;
use log::{error, info};
use player::context::ClientContext;
use protocol::handler::handle_connection;
use rust_mc_proto::{MinecraftConnection, ProtocolError};
pub mod config;
pub mod context;
pub mod data;
pub mod event;
pub mod player;
pub mod protocol;
// Ошибки сервера
#[derive(Debug)]
pub enum ServerError {
UnexpectedPacket, // Неожиданный пакет
Protocol(ProtocolError), // Ошибка в протоколе при работе с rust_mc_proto
ConnectionClosed, // Соединение закрыто, единственная ошибка которая не логируется у handle_connection
SerTextComponent, // Ошибка при сериализации текст-компонента
DeTextComponent, // Ошибка при десериализации текст-компонента
SerNbt, // Ошибка при сериализации nbt
DeNbt, // Ошибка при десериализации nbt
UnexpectedState, // Указывает на то что этот пакет не может быть отправлен в данном режиме (в основном через ProtocolHelper)
Other(String), // Другая ошибка, либо очень специфичная, либо хз, лучше не использовать и создавать новое поле ошибки
}
impl Display for ServerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&format!("{:?}", self))
}
}
impl Error for ServerError {}
// Делаем чтобы ProtocolError мог переделываться в наш ServerError
impl From<ProtocolError> for ServerError {
fn from(error: ProtocolError) -> ServerError {
match error {
// Если просто закрыто соединение, переделываем в нашу ошибку этого
ProtocolError::ConnectionClosedError => ServerError::ConnectionClosed,
// Все остальное просто засовываем в обертку
error => ServerError::Protocol(error),
}
}
}
pub fn start_server(server: Arc<ServerContext>) {
// Биндим сервер где надо
let Ok(listener) = TcpListener::bind(&server.config.bind.host) else {
error!(
"Не удалось забиндить сервер на {}",
&server.config.bind.host
);
return;
};
info!("Сервер запущен на {}", &server.config.bind.host);
while let Ok((stream, addr)) = listener.accept() {
let server = server.clone();
thread::spawn(move || {
info!("Подключение: {}", addr);
// Установка таймаутов на чтение и запись
// По умолчанию пусть будет 5 секунд, надо будет сделать настройку через конфиг
stream
.set_read_timeout(Some(Duration::from_secs(server.config.bind.timeout)))
.ignore();
stream
.set_write_timeout(Some(Duration::from_secs(server.config.bind.timeout)))
.ignore();
// Оборачиваем стрим в майнкрафт конекшн лично для нашего удовольствия
let conn = MinecraftConnection::new(stream);
// Создаем контекст клиента
// Передавется во все листенеры и хандлеры чтобы определять именно этот клиент
let client = Arc::new(ClientContext::new(server.clone(), conn));
// Добавляем клиента в список клиентов сервера
// Используем адрес как ключ, врятли ipv4 будет нам врать
server.clients.insert(client.addr, client.clone());
// Обработка подключения
// Если ошибка -> выводим
match handle_connection(client.clone()) {
Ok(_) => {}
Err(ServerError::ConnectionClosed) => {}
Err(error) => {
error!("Ошибка подключения: {error:?}");
}
};
// Удаляем клиента из списка клиентов
server.clients.remove(&client.addr);
info!("Отключение: {}", addr);
});
}
}

View File

@ -0,0 +1,194 @@
use std::{
hash::Hash,
net::{SocketAddr, TcpStream},
sync::{Arc, RwLock},
};
use rust_mc_proto::{MinecraftConnection, Packet};
use uuid::Uuid;
use crate::server::{ServerError, context::ServerContext, protocol::ConnectionState};
use super::helper::ProtocolHelper;
// Клиент контекст
// Должен быть обернут в Arc для передачи между потоками
pub struct ClientContext {
pub server: Arc<ServerContext>,
pub addr: SocketAddr,
conn: RwLock<MinecraftConnection<TcpStream>>,
handshake: RwLock<Option<Handshake>>,
client_info: RwLock<Option<ClientInfo>>,
player_info: RwLock<Option<PlayerInfo>>,
state: RwLock<ConnectionState>,
}
// Реализуем сравнение через адрес
// IPv4 не должен обманывать, иначе у нас случится коллапс
impl PartialEq for ClientContext {
fn eq(&self, other: &Self) -> bool {
self.addr == other.addr
}
}
impl Hash for ClientContext {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.addr.hash(state);
}
}
impl Eq for ClientContext {}
impl ClientContext {
pub fn new(server: Arc<ServerContext>, conn: MinecraftConnection<TcpStream>) -> ClientContext {
ClientContext {
server,
addr: conn.get_ref().peer_addr().unwrap(),
conn: RwLock::new(conn),
handshake: RwLock::new(None),
client_info: RwLock::new(None),
player_info: RwLock::new(None),
state: RwLock::new(ConnectionState::Handshake),
}
}
pub fn set_handshake(self: &Arc<Self>, handshake: Handshake) {
*self.handshake.write().unwrap() = Some(handshake);
}
pub fn set_client_info(self: &Arc<Self>, client_info: ClientInfo) {
*self.client_info.write().unwrap() = Some(client_info);
}
pub fn set_player_info(self: &Arc<Self>, player_info: PlayerInfo) {
*self.player_info.write().unwrap() = Some(player_info);
}
pub fn set_state(self: &Arc<Self>, state: ConnectionState) -> Result<(), ServerError> {
*self.state.write().unwrap() = state.clone();
for handler in self
.server
.packet_handlers(|o| o.on_state_priority())
.iter()
{
handler.on_state(self.clone(), state.clone())?;
}
Ok(())
}
pub fn handshake(self: &Arc<Self>) -> Option<Handshake> {
self.handshake.read().unwrap().clone()
}
pub fn client_info(self: &Arc<Self>) -> Option<ClientInfo> {
self.client_info.read().unwrap().clone()
}
pub fn player_info(self: &Arc<Self>) -> Option<PlayerInfo> {
self.player_info.read().unwrap().clone()
}
pub fn state(self: &Arc<Self>) -> ConnectionState {
self.state.read().unwrap().clone()
}
pub fn write_packet(self: &Arc<Self>, packet: &Packet) -> Result<(), ServerError> {
let state = self.state();
let mut packet = packet.clone();
let mut cancelled = false;
for handler in self
.server
.packet_handlers(|o| o.on_outcoming_packet_priority())
.iter()
{
handler.on_outcoming_packet(
self.clone(),
&mut packet,
&mut cancelled,
state.clone(),
)?;
packet.get_mut().set_position(0);
}
if !cancelled {
self.conn.write().unwrap().write_packet(&packet)?;
}
Ok(())
}
pub fn read_any_packet(self: &Arc<Self>) -> Result<Packet, ServerError> {
let state = self.state();
let mut conn = self.conn.read().unwrap().try_clone()?; // так можно делать т.к сокет это просто поинтер
loop {
let mut packet = conn.read_packet()?;
let mut cancelled = false;
for handler in self
.server
.packet_handlers(|o| o.on_incoming_packet_priority())
.iter()
{
handler.on_incoming_packet(
self.clone(),
&mut packet,
&mut cancelled,
state.clone(),
)?;
packet.get_mut().set_position(0);
}
if !cancelled {
break Ok(packet);
}
}
}
pub fn read_packet(self: &Arc<Self>, id: u8) -> Result<Packet, ServerError> {
let packet = self.read_any_packet()?;
if packet.id() != id {
Err(ServerError::UnexpectedPacket)
} else {
Ok(packet)
}
}
pub fn close(self: &Arc<Self>) {
self.conn.write().unwrap().close();
}
pub fn set_compression(self: &Arc<Self>, threshold: Option<usize>) {
self.conn.write().unwrap().set_compression(threshold);
}
pub fn protocol_helper(self: &Arc<Self>) -> ProtocolHelper {
ProtocolHelper::new(self.clone())
}
}
#[derive(Clone)]
pub struct Handshake {
pub protocol_version: i32,
pub server_address: String,
pub server_port: u16,
}
#[derive(Clone)]
pub struct ClientInfo {
pub brand: String,
pub locale: String,
pub view_distance: i8,
pub chat_mode: i32,
pub chat_colors: bool,
pub displayed_skin_parts: u8,
pub main_hand: i32,
pub enable_text_filtering: bool,
pub allow_server_listings: bool,
pub particle_status: i32,
}
#[derive(Clone)]
pub struct PlayerInfo {
pub name: String,
pub uuid: Uuid,
}

231
src/server/player/helper.rs Normal file
View File

@ -0,0 +1,231 @@
use std::{
io::Read,
sync::Arc,
time::{Duration, SystemTime},
};
use rust_mc_proto::{DataReader, DataWriter, Packet};
use crate::server::{
ServerError,
data::{ReadWriteNBT, text_component::TextComponent},
protocol::{
id::{clientbound, serverbound},
*,
},
};
use super::context::ClientContext;
// Помощник в работе с протоколом
// Может быть использован где угодно, но сделан именно для листенеров и пакет хандлеров
// Через него удобно делать всякую одинаковую херь
// Возможно надо было бы сделать прям обязательный какойто структ через который только можно было отправлять пакеты ...
// ... но мне лень
// Пусть юзают подключение и отправляют пакеты через него если хотят
// Почему бы и нет если да
pub struct ProtocolHelper {
client: Arc<ClientContext>,
state: ConnectionState,
}
impl ProtocolHelper {
pub fn new(client: Arc<ClientContext>) -> Self {
Self {
state: client.state(),
client,
}
}
pub fn reset_chat(&self) -> Result<(), ServerError> {
match self.state {
ConnectionState::Configuration => {
self.client
.write_packet(&Packet::empty(clientbound::configuration::RESET_CHAT))?;
Ok(())
}
_ => Err(ServerError::UnexpectedState),
}
}
pub fn store_cookie(&self, id: &str, data: &[u8]) -> Result<(), ServerError> {
self.client.write_packet(&Packet::build(
match self.state {
ConnectionState::Configuration => clientbound::configuration::STORE_COOKIE,
ConnectionState::Play => clientbound::play::STORE_COOKIE,
_ => { return Err(ServerError::UnexpectedState) },
},
|p| {
p.write_string(id)?;
p.write_bytes(data)
},
)?)?;
Ok(())
}
/// Leave from Configuration to Play state
pub fn leave_configuration(&self) -> Result<(), ServerError> {
match self.state {
ConnectionState::Configuration => {
self.client
.write_packet(&Packet::empty(clientbound::configuration::FINISH))?;
self.client
.read_packet(serverbound::configuration::ACKNOWLEDGE_FINISH)?;
self.client.set_state(ConnectionState::Play)?;
Ok(())
}
_ => Err(ServerError::UnexpectedState),
}
}
/// Enter to Configuration from Play state
pub fn enter_configuration(&self) -> Result<(), ServerError> {
match self.state {
ConnectionState::Play => {
self.client
.write_packet(&Packet::empty(clientbound::play::START_CONFIGURATION))?;
self.client
.read_packet(serverbound::play::ACKNOWLEDGE_CONFIGURATION)?;
self.client.set_state(ConnectionState::Configuration)?;
Ok(())
}
_ => Err(ServerError::UnexpectedState),
}
}
/// Enter to Configuration from Play state
pub fn ping(&self) -> Result<Duration, ServerError> {
match self.state {
ConnectionState::Play => {
let time = SystemTime::now();
self.client
.write_packet(&Packet::empty(clientbound::play::PING))?;
self.client.read_packet(serverbound::play::PONG)?;
Ok(SystemTime::now().duration_since(time).unwrap())
}
ConnectionState::Configuration => {
let time = SystemTime::now();
self.client
.write_packet(&Packet::empty(clientbound::configuration::PING))?;
self.client.read_packet(serverbound::configuration::PONG)?;
Ok(SystemTime::now().duration_since(time).unwrap())
}
_ => Err(ServerError::UnexpectedState),
}
}
pub fn disconnect(&self, reason: TextComponent) -> Result<(), ServerError> {
let packet = match self.state {
ConnectionState::Login => {
let text = reason.as_json()?;
Packet::build(0x00, |p| p.write_string(&text))?
}
ConnectionState::Configuration => {
let mut packet = Packet::empty(0x02);
packet.write_nbt(&reason)?;
packet
}
ConnectionState::Play => {
let mut packet = Packet::empty(0x1C);
packet.write_nbt(&reason)?;
packet
}
_ => {
self.client.close();
return Ok(());
}
};
self.client.write_packet(&packet)?;
Ok(())
}
/// Returns cookie content
pub fn request_cookie(&self, id: &str) -> Result<Option<Vec<u8>>, ServerError> {
match self.state {
ConnectionState::Configuration => {
let mut packet = Packet::empty(clientbound::configuration::COOKIE_REQUEST);
packet.write_string(id)?;
self.client.write_packet(&packet)?;
let mut packet = self
.client
.read_packet(serverbound::configuration::COOKIE_RESPONSE)?;
packet.read_string()?;
let data = if packet.read_boolean()? {
let n = packet.read_usize_varint()?;
Some(packet.read_bytes(n)?)
} else {
None
};
Ok(data)
},
ConnectionState::Play => {
let mut packet = Packet::empty(clientbound::play::COOKIE_REQUEST);
packet.write_string(id)?;
self.client.write_packet(&packet)?;
let mut packet = self
.client
.read_packet(serverbound::play::COOKIE_RESPONSE)?;
packet.read_string()?;
let data = if packet.read_boolean()? {
let n = packet.read_usize_varint()?;
Some(packet.read_bytes(n)?)
} else {
None
};
Ok(data)
}
_ => Err(ServerError::UnexpectedState),
}
}
/// Returns login plugin response - (message_id, payload)
pub fn send_login_plugin_request(
&self,
id: i32,
channel: &str,
data: &[u8],
) -> Result<(i32, Option<Vec<u8>>), ServerError> {
match self.state {
ConnectionState::Login => {
let mut packet = Packet::empty(clientbound::login::PLUGIN_REQUEST);
packet.write_varint(id)?;
packet.write_string(channel)?;
packet.write_bytes(data)?;
self.client.write_packet(&packet)?;
let mut packet = self
.client
.read_packet(serverbound::login::PLUGIN_RESPONSE)?;
let identifier = packet.read_varint()?;
let data = if packet.read_boolean()? {
let mut data = Vec::new();
packet.get_mut().read_to_end(&mut data).unwrap();
Some(data)
} else {
None
};
Ok((identifier, data))
}
_ => Err(ServerError::UnexpectedState),
}
}
pub fn send_plugin_message(&self, channel: &str, data: &[u8]) -> Result<(), ServerError> {
let mut packet = match self.state {
ConnectionState::Configuration => {
Packet::empty(clientbound::configuration::PLUGIN_MESSAGE)
}
ConnectionState::Play => Packet::empty(clientbound::play::PLUGIN_MESSAGE),
_ => return Err(ServerError::UnexpectedState),
};
packet.write_string(channel)?;
packet.write_bytes(data)?;
self.client.write_packet(&packet)?;
Ok(())
}
}

2
src/server/player/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod context;
pub mod helper;

View File

@ -0,0 +1,177 @@
use std::{io::Read, sync::Arc};
use crate::server::{
ServerError,
player::context::{ClientContext, ClientInfo, Handshake, PlayerInfo},
};
use rust_mc_proto::{DataReader, DataWriter, Packet};
use crate::trigger_event;
use super::{id::*, play::{handle_configuration_state, handle_play_state}, ConnectionState};
pub fn handle_connection(
client: Arc<ClientContext>, // Контекст клиента
) -> Result<(), ServerError> {
// Чтение рукопожатия
// Получение пакетов производится через client.conn(),
// ВАЖНО: не помещать сам client.conn() в переменные,
// он должен сразу убиваться иначе соединение гдето задедлочится
let mut packet = client.read_packet(serverbound::handshake::HANDSHAKE)?;
let protocol_version = packet.read_varint()?; // Получаем версия протокола, может быть отрицательным если наш клиент дэбил
let server_address = packet.read_string()?; // Получаем домен/адрес сервера к которому пытается подключиться клиент, например "play.example.com", а не айпи
let server_port = packet.read_unsigned_short()?; // Все тоже самое что и с адресом сервера и все потому же и за тем же
let next_state = packet.read_varint()?; // Тип подключения: 1 для получения статуса и пинга, 2 и 3 для обычного подключения
client.set_handshake(Handshake {
protocol_version,
server_address,
server_port,
});
match next_state {
1 => {
// Тип подключения - статус
client.set_state(ConnectionState::Status)?; // Мы находимся в режиме Status
loop {
// Чтение запроса
let mut packet = client.read_any_packet()?;
match packet.id() {
serverbound::status::REQUEST => {
// Запрос статуса
let mut packet = Packet::empty(clientbound::status::RESPONSE);
// Дефолтный статус
let mut status = "{
\"version\": {
\"name\": \"Error\",
\"protocol\": 0
},
\"description\": {\"text\": \"Internal server error\"}
}"
.to_string();
// Опрос всех листенеров
trigger_event!(client, status, &mut status);
// Отправка статуса
packet.write_string(&status)?;
client.write_packet(&packet)?;
}
serverbound::status::PING_REQUEST => {
// Пинг
// Раньше мы просто отправляли ему его-же пакет, но сейчас,
// С приходом к власти констант айди-пакетов, нам приходится делать такое непотребство
let timestamp = packet.read_long()?;
let mut packet = Packet::empty(clientbound::status::PONG_RESPONSE);
packet.write_long(timestamp)?;
client.write_packet(&packet)?;
}
_ => {
return Err(ServerError::UnexpectedPacket);
}
}
}
}
2 => {
// Тип подключения - игра
client.set_state(ConnectionState::Login)?; // Мы находимся в режиме Login
// Читаем пакет Login Start
let mut packet = client.read_packet(serverbound::login::START)?;
let name = packet.read_string()?;
let uuid = packet.read_uuid()?;
client.set_player_info(PlayerInfo {
name: name.clone(),
uuid: uuid.clone(),
});
if client.server.config.server.online_mode {
// TODO: encryption packets
}
// Отправляем пакет Set Compression если сжатие указано
if let Some(threshold) = client.server.config.server.compression_threshold {
client.write_packet(&Packet::build(clientbound::login::SET_COMPRESSION, |p| {
p.write_usize_varint(threshold)
})?)?;
client.set_compression(Some(threshold)); // Устанавливаем сжатие на соединении
}
// Отправка пакета Login Success
client.write_packet(&Packet::build(clientbound::login::SUCCESS, |p| {
p.write_uuid(&uuid)?;
p.write_string(&name)?;
p.write_varint(0)
})?)?;
client.read_packet(serverbound::login::ACKNOWLEDGED)?; // Пакет Login Acknowledged
client.set_state(ConnectionState::Configuration)?; // Мы перешли в режим Configuration
// Получение бренда клиента из Serverbound Plugin Message
// Identifier канала откуда берется бренд: minecraft:brand
let brand = loop {
let mut packet = client.read_packet(serverbound::configuration::PLUGIN_MESSAGE)?; // Пакет Serverbound Plugin Message
let identifier = packet.read_string()?;
let mut data = Vec::new();
packet.get_mut().read_to_end(&mut data).unwrap();
if identifier == "minecraft:brand" {
break String::from_utf8_lossy(&data).to_string();
} else {
trigger_event!(client, plugin_message, &identifier, &data);
}
};
let mut packet = client.read_packet(serverbound::configuration::CLIENT_INFORMATION)?; // Пакет Client Information
let locale = packet.read_string()?; // for example: en_us
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
client.set_client_info(ClientInfo {
brand,
locale,
view_distance,
chat_mode,
chat_colors,
displayed_skin_parts,
main_hand,
enable_text_filtering,
allow_server_listings,
particle_status,
});
handle_configuration_state(client.clone())?;
client.write_packet(&Packet::empty(clientbound::configuration::FINISH))?;
client.read_packet(serverbound::configuration::ACKNOWLEDGE_FINISH)?;
client.set_state(ConnectionState::Play)?; // Мы перешли в режим Play
// Дальше работаем с режимом игры
handle_play_state(client)?;
}
_ => {
// Тип подключения не рукопожатный
return Err(ServerError::UnexpectedPacket);
}
}
Ok(())
}

272
src/server/protocol/id.rs Normal file
View File

@ -0,0 +1,272 @@
/*
Generated with parse_ids.py
*/
pub mod clientbound {
pub mod status {
pub const RESPONSE: u8 = 0x00;
pub const PONG_RESPONSE: u8 = 0x01;
}
pub mod login {
pub const DISCONNECT: u8 = 0x00;
pub const ENCRYPTION_REQUEST: u8 = 0x01;
pub const SUCCESS: u8 = 0x02;
pub const SET_COMPRESSION: u8 = 0x03;
pub const PLUGIN_REQUEST: u8 = 0x04;
pub const COOKIE_REQUEST: u8 = 0x05;
}
pub mod configuration {
pub const COOKIE_REQUEST: u8 = 0x00;
pub const PLUGIN_MESSAGE: u8 = 0x01;
pub const DISCONNECT: u8 = 0x02;
pub const FINISH: u8 = 0x03;
pub const KEEP_ALIVE: u8 = 0x04;
pub const PING: u8 = 0x05;
pub const RESET_CHAT: u8 = 0x06;
pub const REGISTRY_DATA: u8 = 0x07;
pub const REMOVE_RESOURCE_PACK: u8 = 0x08;
pub const ADD_RESOURCE_PACK: u8 = 0x09;
pub const STORE_COOKIE: u8 = 0x0A;
pub const TRANSFER: u8 = 0x0B;
pub const FEATURE_FLAGS: u8 = 0x0C;
pub const UPDATE_TAGS: u8 = 0x0D;
pub const KNOWN_PACKS: u8 = 0x0E;
pub const CUSTOM_REPORT_DETAILS: u8 = 0x0F;
pub const SERVER_LINKS: u8 = 0x10;
}
pub mod play {
pub const BUNDLE_DELIMITER: u8 = 0x00;
pub const SPAWN_ENTITY: u8 = 0x01;
pub const ENTITY_ANIMATION: u8 = 0x02;
pub const AWARD_STATISTICS: u8 = 0x03;
pub const ACKNOWLEDGE_BLOCK_CHANGE: u8 = 0x04;
pub const SET_BLOCK_DESTROY_STAGE: u8 = 0x05;
pub const BLOCK_ENTITY_DATA: u8 = 0x06;
pub const BLOCK_ACTION: u8 = 0x07;
pub const BLOCK_UPDATE: u8 = 0x08;
pub const BOSS_BAR: u8 = 0x09;
pub const CHANGE_DIFFICULTY: u8 = 0x0A;
pub const CHUNK_BATCH_FINISHED: u8 = 0x0B;
pub const CHUNK_BATCH_START: u8 = 0x0C;
pub const CHUNK_BIOMES: u8 = 0x0D;
pub const CLEAR_TITLES: u8 = 0x0E;
pub const COMMAND_SUGGESTIONS_RESPONSE: u8 = 0x0F;
pub const COMMANDS: u8 = 0x10;
pub const CLOSE_CONTAINER: u8 = 0x11;
pub const SET_CONTAINER_CONTENT: u8 = 0x12;
pub const SET_CONTAINER_PROPERTY: u8 = 0x13;
pub const SET_CONTAINER_SLOT: u8 = 0x14;
pub const COOKIE_REQUEST: u8 = 0x15;
pub const SET_COOLDOWN: u8 = 0x16;
pub const CHAT_SUGGESTIONS: u8 = 0x17;
pub const PLUGIN_MESSAGE: u8 = 0x18;
pub const DAMAGE_EVENT: u8 = 0x19;
pub const DEBUG_SAMPLE: u8 = 0x1A;
pub const DELETE_MESSAGE: u8 = 0x1B;
pub const DISCONNECT: u8 = 0x1C;
pub const DISGUISED_CHAT_MESSAGE: u8 = 0x1D;
pub const ENTITY_EVENT: u8 = 0x1E;
pub const TELEPORT_ENTITY: u8 = 0x1F;
pub const EXPLOSION: u8 = 0x20;
pub const UNLOAD_CHUNK: u8 = 0x21;
pub const GAME_EVENT: u8 = 0x22;
pub const OPEN_HORSE_SCREEN: u8 = 0x23;
pub const HURT_ANIMATION: u8 = 0x24;
pub const INITIALIZE_WORLD_BORDER: u8 = 0x25;
pub const KEEP_ALIVE: u8 = 0x26;
pub const CHUNK_DATA_AND_UPDATE_LIGHT: u8 = 0x27;
pub const WORLD_EVENT: u8 = 0x28;
pub const PARTICLE: u8 = 0x29;
pub const UPDATE_LIGHT: u8 = 0x2A;
pub const LOGIN: u8 = 0x2B;
pub const MAP_DATA: u8 = 0x2C;
pub const MERCHANT_OFFERS: u8 = 0x2D;
pub const UPDATE_ENTITY_POSITION: u8 = 0x2E;
pub const UPDATE_ENTITY_POSITION_AND_ROTATION: u8 = 0x2F;
pub const MOVE_MINECART_ALONG_TRACK: u8 = 0x30;
pub const UPDATE_ENTITY_ROTATION: u8 = 0x31;
pub const MOVE_VEHICLE: u8 = 0x32;
pub const OPEN_BOOK: u8 = 0x33;
pub const OPEN_SCREEN: u8 = 0x34;
pub const OPEN_SIGN_EDITOR: u8 = 0x35;
pub const PING: u8 = 0x36;
pub const PING_RESPONSE: u8 = 0x37;
pub const PLACE_GHOST_RECIPE: u8 = 0x38;
pub const PLAYER_ABILITIES: u8 = 0x39;
pub const PLAYER_CHAT_MESSAGE: u8 = 0x3A;
pub const END_COMBAT: u8 = 0x3B;
pub const ENTER_COMBAT: u8 = 0x3C;
pub const COMBAT_DEATH: u8 = 0x3D;
pub const PLAYER_INFO_REMOVE: u8 = 0x3E;
pub const PLAYER_INFO_UPDATE: u8 = 0x3F;
pub const LOOK_AT: u8 = 0x40;
pub const SYNCHRONIZE_PLAYER_POSITION: u8 = 0x41;
pub const PLAYER_ROTATION: u8 = 0x42;
pub const RECIPE_BOOK_ADD: u8 = 0x43;
pub const RECIPE_BOOK_REMOVE: u8 = 0x44;
pub const RECIPE_BOOK_SETTINGS: u8 = 0x45;
pub const REMOVE_ENTITIES: u8 = 0x46;
pub const REMOVE_ENTITY_EFFECT: u8 = 0x47;
pub const RESET_SCORE: u8 = 0x48;
pub const REMOVE_RESOURCE_PACK: u8 = 0x49;
pub const ADD_RESOURCE_PACK: u8 = 0x4A;
pub const RESPAWN: u8 = 0x4B;
pub const SET_HEAD_ROTATION: u8 = 0x4C;
pub const UPDATE_SECTION_BLOCKS: u8 = 0x4D;
pub const SELECT_ADVANCEMENTS_TAB: u8 = 0x4E;
pub const SERVER_DATA: u8 = 0x4F;
pub const SET_ACTION_BAR_TEXT: u8 = 0x50;
pub const SET_BORDER_CENTER: u8 = 0x51;
pub const SET_BORDER_LERP_SIZE: u8 = 0x52;
pub const SET_BORDER_SIZE: u8 = 0x53;
pub const SET_BORDER_WARNING_DELAY: u8 = 0x54;
pub const SET_BORDER_WARNING_DISTANCE: u8 = 0x55;
pub const SET_CAMERA: u8 = 0x56;
pub const SET_CENTER_CHUNK: u8 = 0x57;
pub const SET_RENDER_DISTANCE: u8 = 0x58;
pub const SET_CURSOR_ITEM: u8 = 0x59;
pub const SET_DEFAULT_SPAWN_POSITION: u8 = 0x5A;
pub const DISPLAY_OBJECTIVE: u8 = 0x5B;
pub const SET_ENTITY_METADATA: u8 = 0x5C;
pub const LINK_ENTITIES: u8 = 0x5D;
pub const SET_ENTITY_VELOCITY: u8 = 0x5E;
pub const SET_EQUIPMENT: u8 = 0x5F;
pub const SET_EXPERIENCE: u8 = 0x60;
pub const SET_HEALTH: u8 = 0x61;
pub const SET_HELD_ITEM: u8 = 0x62;
pub const UPDATE_OBJECTIVES: u8 = 0x63;
pub const SET_PASSENGERS: u8 = 0x64;
pub const SET_PLAYER_INVENTORY_SLOT: u8 = 0x65;
pub const UPDATE_TEAMS: u8 = 0x66;
pub const UPDATE_SCORE: u8 = 0x67;
pub const SET_SIMULATION_DISTANCE: u8 = 0x68;
pub const SET_SUBTITLE_TEXT: u8 = 0x69;
pub const UPDATE_TIME: u8 = 0x6A;
pub const SET_TITLE_TEXT: u8 = 0x6B;
pub const SET_TITLE_ANIMATION_TIMES: u8 = 0x6C;
pub const ENTITY_SOUND_EFFECT: u8 = 0x6D;
pub const SOUND_EFFECT: u8 = 0x6E;
pub const START_CONFIGURATION: u8 = 0x6F;
pub const STOP_SOUND: u8 = 0x70;
pub const STORE_COOKIE: u8 = 0x71;
pub const SYSTEM_CHAT_MESSAGE: u8 = 0x72;
pub const SET_TAB_LIST_HEADER_AND_FOOTER: u8 = 0x73;
pub const TAG_QUERY_RESPONSE: u8 = 0x74;
pub const PICKUP_ITEM: u8 = 0x75;
pub const SYNCHRONIZE_VEHICLE_POSITION: u8 = 0x76;
pub const TEST_INSTANCE_BLOCK_STATUS: u8 = 0x77;
pub const SET_TICKING_STATE: u8 = 0x78;
pub const STEP_TICK: u8 = 0x79;
pub const TRANSFER: u8 = 0x7A;
pub const UPDATE_ADVANCEMENTS: u8 = 0x7B;
pub const UPDATE_ATTRIBUTES: u8 = 0x7C;
pub const ENTITY_EFFECT: u8 = 0x7D;
pub const UPDATE_RECIPES: u8 = 0x7E;
pub const UPDATE_TAGS: u8 = 0x7F;
pub const PROJECTILE_POWER: u8 = 0x80;
pub const CUSTOM_REPORT_DETAILS: u8 = 0x81;
pub const SERVER_LINKS: u8 = 0x82;
}
}
pub mod serverbound {
pub mod handshake {
pub const HANDSHAKE: u8 = 0x00;
}
pub mod status {
pub const REQUEST: u8 = 0x00;
pub const PING_REQUEST: u8 = 0x01;
}
pub mod login {
pub const START: u8 = 0x00;
pub const ENCRYPTION_RESPONSE: u8 = 0x01;
pub const PLUGIN_RESPONSE: u8 = 0x02;
pub const ACKNOWLEDGED: u8 = 0x03;
pub const COOKIE_RESPONSE: u8 = 0x04;
}
pub mod configuration {
pub const CLIENT_INFORMATION: u8 = 0x00;
pub const COOKIE_RESPONSE: u8 = 0x01;
pub const PLUGIN_MESSAGE: u8 = 0x02;
pub const ACKNOWLEDGE_FINISH: u8 = 0x03;
pub const KEEP_ALIVE: u8 = 0x04;
pub const PONG: u8 = 0x05;
pub const RESOURCE_PACK_RESPONSE: u8 = 0x06;
pub const KNOWN_PACKS: u8 = 0x07;
}
pub mod play {
pub const CONFIRM_TELEPORTATION: u8 = 0x00;
pub const QUERY_BLOCK_ENTITY_TAG: u8 = 0x01;
pub const BUNDLE_ITEM_SELECTED: u8 = 0x02;
pub const CHANGE_DIFFICULTY: u8 = 0x03;
pub const ACKNOWLEDGE_MESSAGE: u8 = 0x04;
pub const CHAT_COMMAND: u8 = 0x05;
pub const SIGNED_CHAT_COMMAND: u8 = 0x06;
pub const CHAT_MESSAGE: u8 = 0x07;
pub const PLAYER_SESSION: u8 = 0x08;
pub const CHUNK_BATCH_RECEIVED: u8 = 0x09;
pub const CLIENT_STATUS: u8 = 0x0A;
pub const CLIENT_TICK_END: u8 = 0x0B;
pub const CLIENT_INFORMATION: u8 = 0x0C;
pub const COMMAND_SUGGESTIONS_REQUEST: u8 = 0x0D;
pub const ACKNOWLEDGE_CONFIGURATION: u8 = 0x0E;
pub const CLICK_CONTAINER_BUTTON: u8 = 0x0F;
pub const CLICK_CONTAINER: u8 = 0x10;
pub const CLOSE_CONTAINER: u8 = 0x11;
pub const CHANGE_CONTAINER_SLOT_STATE: u8 = 0x12;
pub const COOKIE_RESPONSE: u8 = 0x13;
pub const PLUGIN_MESSAGE: u8 = 0x14;
pub const DEBUG_SAMPLE_SUBSCRIPTION: u8 = 0x15;
pub const EDIT_BOOK: u8 = 0x16;
pub const QUERY_ENTITY_TAG: u8 = 0x17;
pub const INTERACT: u8 = 0x18;
pub const JIGSAW_GENERATE: u8 = 0x19;
pub const KEEP_ALIVE: u8 = 0x1A;
pub const LOCK_DIFFICULTY: u8 = 0x1B;
pub const SET_PLAYER_POSITION: u8 = 0x1C;
pub const SET_PLAYER_POSITION_AND_ROTATION: u8 = 0x1D;
pub const SET_PLAYER_ROTATION: u8 = 0x1E;
pub const SET_PLAYER_MOVEMENT_FLAGS: u8 = 0x1F;
pub const MOVE_VEHICLE: u8 = 0x20;
pub const PADDLE_BOAT: u8 = 0x21;
pub const PICK_ITEM_FROM_BLOCK: u8 = 0x22;
pub const PICK_ITEM_FROM_ENTITY: u8 = 0x23;
pub const PING_REQUEST: u8 = 0x24;
pub const PLACE_RECIPE: u8 = 0x25;
pub const PLAYER_ABILITIES: u8 = 0x26;
pub const PLAYER_ACTION: u8 = 0x27;
pub const PLAYER_COMMAND: u8 = 0x28;
pub const PLAYER_INPUT: u8 = 0x29;
pub const PLAYER_LOADED: u8 = 0x2A;
pub const PONG: u8 = 0x2B;
pub const CHANGE_RECIPE_BOOK_SETTINGS: u8 = 0x2C;
pub const SET_SEEN_RECIPE: u8 = 0x2D;
pub const RENAME_ITEM: u8 = 0x2E;
pub const RESOURCE_PACK_RESPONSE: u8 = 0x2F;
pub const SEEN_ADVANCEMENTS: u8 = 0x30;
pub const SELECT_TRADE: u8 = 0x31;
pub const SET_BEACON_EFFECT: u8 = 0x32;
pub const SET_HELD_ITEM: u8 = 0x33;
pub const PROGRAM_COMMAND_BLOCK: u8 = 0x34;
pub const PROGRAM_COMMAND_BLOCK_MINECART: u8 = 0x35;
pub const SET_CREATIVE_MODE_SLOT: u8 = 0x36;
pub const PROGRAM_JIGSAW_BLOCK: u8 = 0x37;
pub const PROGRAM_STRUCTURE_BLOCK: u8 = 0x38;
pub const SET_TEST_BLOCK: u8 = 0x39;
pub const UPDATE_SIGN: u8 = 0x3A;
pub const SWING_ARM: u8 = 0x3B;
pub const TELEPORT_TO_ENTITY: u8 = 0x3C;
pub const TEST_INSTANCE_BLOCK_ACTION: u8 = 0x3D;
pub const USE_ITEM_ON: u8 = 0x3E;
pub const USE_ITEM: u8 = 0x3F;
}
}

View File

@ -0,0 +1,12 @@
pub mod handler;
pub mod id;
pub mod play;
#[derive(Debug, Clone)]
pub enum ConnectionState {
Handshake,
Status,
Login,
Configuration,
Play,
}

107
src/server/protocol/play.rs Normal file
View File

@ -0,0 +1,107 @@
use std::{collections::HashMap, sync::Arc};
use craftflow_nbt::DynNBT;
use log::debug;
use rust_mc_proto::{DataWriter, Packet};
use serde_json::{json, Value};
use crate::server::{
data::ReadWriteNBT, player::context::ClientContext, ServerError
};
use super::id::{clientbound::{self, configuration::REGISTRY_DATA}, serverbound};
pub fn send_registry_data(
client: Arc<ClientContext>,
) -> Result<(), ServerError> {
let registry_data = include_str!("registry_data.json");
let registry_data: Value = serde_json::from_str(registry_data).unwrap();
let registry_data = registry_data.as_object().unwrap();
for (registry_name, registry_data) in registry_data {
let registry_data = registry_data.as_object().unwrap();
let mut packet = Packet::empty(clientbound::configuration::REGISTRY_DATA);
packet.write_string(registry_name)?;
packet.write_usize_varint(registry_data.len())?;
debug!("sending registry: {registry_name}");
for (key, value) in registry_data {
packet.write_string(key)?;
packet.write_boolean(true)?;
let mut data = Vec::new();
craftflow_nbt::to_writer(&mut data, value).unwrap();
debug!("- {key}");
packet.write_bytes(&data)?;
}
client.write_packet(&packet)?;
}
Ok(())
}
pub fn handle_configuration_state(
client: Arc<ClientContext>, // Контекст клиента
) -> Result<(), ServerError> {
let mut p = Packet::empty(clientbound::configuration::KNOWN_PACKS);
p.write_varint(1)?;
p.write_string("minecraft")?;
p.write_string("core")?;
p.write_string("1.21.5")?;
client.write_packet(&p)?;
client.read_packet(serverbound::configuration::KNOWN_PACKS)?;
send_registry_data(client.clone())?;
Ok(())
}
// Отдельная функция для работы с самой игрой
pub fn handle_play_state(
client: Arc<ClientContext>, // Контекст клиента
) -> Result<(), ServerError> {
// Отключение игрока с сообщением
// client.protocol_helper().disconnect(TextComponent::rainbow(
// "server is in developement suka".to_string(),
// ))?;
let mut packet = Packet::empty(clientbound::play::LOGIN);
packet.write_int(0)?; // Entity ID
packet.write_boolean(false)?; // Is hardcore
packet.write_varint(4)?; // Dimension Names
packet.write_string("minecraft:overworld")?;
packet.write_string("minecraft:nether")?;
packet.write_string("minecraft:the_end")?;
packet.write_string("minecraft:overworld_caves")?;
packet.write_varint(0)?; // Max Players
packet.write_varint(8)?; // View Distance
packet.write_varint(5)?; // Simulation Distance
packet.write_boolean(false)?; // Reduced Debug Info
packet.write_boolean(true)?; // Enable respawn screen
packet.write_boolean(false)?; // Do limited crafting
packet.write_varint(0)?; // Dimension Type
packet.write_string("minecraft:overworld")?; // Dimension Name
packet.write_long(0x0f38f26ad09c3e20)?; // Hashed seed
packet.write_byte(0)?; // Game mode
packet.write_signed_byte(-1)?; // Previous Game mode
packet.write_boolean(false)?; // Is Debug
packet.write_boolean(true)?; // Is Flat
packet.write_boolean(false)?; // Has death location
packet.write_varint(20)?; // Portal cooldown
packet.write_varint(60)?; // Sea level
packet.write_boolean(false)?; // Enforces Secure Chat
client.write_packet(&packet)?;
loop {}
Ok(())
}

View File

@ -0,0 +1,96 @@
{
"minecraft:dimension_type": {
"minecraft:overworld": {
"ambient_light": 0.0,
"bed_works": 1,
"coordinate_scale": 1.0,
"effects": "minecraft:overworld",
"has_ceiling": 0,
"has_raids": 1,
"has_skylight": 1,
"height": 384,
"infiniburn": "#minecraft:infiniburn_overworld",
"logical_height": 384,
"min_y": -64,
"monster_spawn_block_light_limit": 0,
"monster_spawn_light_level": {
"max_inclusive": 7,
"min_inclusive": 0,
"type": "minecraft:uniform"
},
"natural": 1,
"piglin_safe": 0,
"respawn_anchor_works": 0,
"ultrawarm": 0
},
"minecraft:overworld_caves": {
"ambient_light": 0.0,
"bed_works": 1,
"coordinate_scale": 1.0,
"effects": "minecraft:overworld",
"has_ceiling": 1,
"has_raids": 1,
"has_skylight": 1,
"height": 384,
"infiniburn": "#minecraft:infiniburn_overworld",
"logical_height": 384,
"min_y": -64,
"monster_spawn_block_light_limit": 0,
"monster_spawn_light_level": {
"max_inclusive": 7,
"min_inclusive": 0,
"type": "minecraft:uniform"
},
"natural": 1,
"piglin_safe": 0,
"respawn_anchor_works": 0,
"ultrawarm": 0
},
"minecraft:the_end": {
"ambient_light": 0.0,
"bed_works": 0,
"coordinate_scale": 1.0,
"effects": "minecraft:the_end",
"has_ceiling": 0,
"has_raids": 1,
"has_skylight": 0,
"height": 256,
"infiniburn": "#minecraft:infiniburn_end",
"logical_height": 256,
"min_y": 0,
"monster_spawn_block_light_limit": 0,
"monster_spawn_light_level": {
"max_inclusive": 7,
"min_inclusive": 0,
"type": "minecraft:uniform"
},
"natural": 0,
"piglin_safe": 0,
"respawn_anchor_works": 0,
"ultrawarm": 0
},
"minecraft:the_nether": {
"ambient_light": 0.10000000149011612,
"bed_works": 0,
"coordinate_scale": 8.0,
"effects": "minecraft:the_nether",
"has_ceiling": 1,
"has_raids": 0,
"has_skylight": 0,
"height": 256,
"infiniburn": "#minecraft:infiniburn_nether",
"logical_height": 128,
"min_y": 0,
"monster_spawn_block_light_limit": 15,
"monster_spawn_light_level": {
"max_inclusive": 7,
"min_inclusive": 7,
"type": "minecraft:uniform"
},
"natural": 0,
"piglin_safe": 1,
"respawn_anchor_works": 1,
"ultrawarm": 1
}
}
}