mirror of
https://github.com/GIKExe/rust_mc_serv.git
synced 2025-06-24 10:22:57 +03:00
commit
24ad08f640
4
.gitignore
vendored
4
.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
/target
|
target/
|
||||||
|
server.toml
|
||||||
|
Packets.html
|
17
.vscode/settings.json
vendored
17
.vscode/settings.json
vendored
@ -1,4 +1,19 @@
|
|||||||
{
|
{
|
||||||
"editor.fontFamily": "Fira Code",
|
"editor.fontFamily": "Fira Code",
|
||||||
"editor.fontLigatures": true
|
"editor.fontLigatures": true,
|
||||||
|
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.insertSpaces": false,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.formatOnPaste": true,
|
||||||
|
|
||||||
|
"[rust]": {
|
||||||
|
"editor.defaultFormatter": "rust-lang.rust-analyzer",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"rust-analyzer.rustfmt.extraArgs": [
|
||||||
|
"--config",
|
||||||
|
"tab_spaces=2"
|
||||||
|
]
|
||||||
}
|
}
|
1180
Cargo.lock
generated
1180
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
@ -1,6 +1,21 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rust_minecraft_server"
|
name = "rust_mc_serv"
|
||||||
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
13
LICENSE
Normal 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.
|
65
README.md
Normal file
65
README.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# rust_mc_serv
|
||||||
|
|
||||||
|
Простой майнкрафт сервер на расте. Поддерживаемая версия: 1.21.5 (PVN 770)
|
||||||
|
|
||||||
|
## Как запустить
|
||||||
|
|
||||||
|
Перед тем как запускать, вам нужно получить бинарник, это можно сделать следующими способами:
|
||||||
|
|
||||||
|
### Скачать из релиза
|
||||||
|
|
||||||
|
На данный момент проект находится в разработке, так что релизов нет
|
||||||
|
|
||||||
|
Если хотите собрать последнюю версию сервера вручную, обратитесь к следующему способу.
|
||||||
|
|
||||||
|
### Собрать самим
|
||||||
|
|
||||||
|
Для того чтобы собрать проект самим, вам нужно:
|
||||||
|
|
||||||
|
1. Скачать и установить [Rust](https://www.rust-lang.org/)
|
||||||
|
2. Скачать исходный код проекта (через zip или `git clone`)
|
||||||
|
3. Открыть терминал в папке проекта и выполнить следующие команды:
|
||||||
|
|
||||||
|
Для запуска:
|
||||||
|
```bash
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
Для сборки (готовый бинарник будет в `target/release`):
|
||||||
|
```bash
|
||||||
|
cargo build -r
|
||||||
|
```
|
||||||
|
|
||||||
|
### Использовать как библиотеку
|
||||||
|
|
||||||
|
Вы можете использовать проект как библиотеку для своих серверов
|
||||||
|
|
||||||
|
Пример добавления в `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
rust_mc_serv = { git = "https://github.com/GIKExe/rust_minecraft_server.git" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Пример запуска сервера:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let config = Arc::new(Config::default());
|
||||||
|
let mut server = ServerContext::new(config);
|
||||||
|
|
||||||
|
server.add_listener(Box::new(ExampleListener)); // Добавляем пример листенера
|
||||||
|
server.add_packet_handler(Box::new(ExamplePacketHandler)); // Добавляем пример пакет хандлера
|
||||||
|
|
||||||
|
start_server(Arc::new(server));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
По умолчанию, конфиг будет создан в файле `config.toml` в рабочей директории. Чтобы изменить этот путь, укажите его в первом аргументе к серверу, пример: `./rust_mc_serv /path/to/config.toml`
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Этот проект полностью лицензирован под лицензией WTFPL. Он абсолютно бесплатен и не имеет ограничений в использовании.
|
||||||
|
|
||||||
|
## Содействие
|
||||||
|
|
||||||
|
Если вы хотите помочь проекту, не стесняйтесь отправлять пулл реквесты!
|
99
parse_ids.py
Normal file
99
parse_ids.py
Normal 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()
|
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "stable"
|
3
rustfmt.toml
Normal file
3
rustfmt.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Пример настроек
|
||||||
|
tab_spaces = 2
|
||||||
|
hard_tabs = true
|
16
shell.nix
Normal file
16
shell.nix
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
with import <nixpkgs> { };
|
||||||
|
|
||||||
|
mkShell {
|
||||||
|
nativeBuildInputs = [
|
||||||
|
direnv
|
||||||
|
rustc
|
||||||
|
cargo
|
||||||
|
rustfmt
|
||||||
|
python3
|
||||||
|
python3Packages.beautifulsoup4
|
||||||
|
python3Packages.requests
|
||||||
|
];
|
||||||
|
|
||||||
|
NIX_ENFORCE_PURITY = true;
|
||||||
|
}
|
1
sniff_packets/.gitignore
vendored
Normal file
1
sniff_packets/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
target/
|
192
sniff_packets/Cargo.lock
generated
Normal file
192
sniff_packets/Cargo.lock
generated
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||||
|
|
||||||
|
[[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"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "craftflow-nbt"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03a2d5312462b00f8420ace884a696f243be136ada9f50bf5f3d9858ff0c8e8e"
|
||||||
|
dependencies = [
|
||||||
|
"cesu8",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.95"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust_mc_proto"
|
||||||
|
version = "0.1.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "734168f5b9aef1991db4b11c0bcd71c0336b0a5ba98269f0df39b41b8463ac8c"
|
||||||
|
dependencies = [
|
||||||
|
"flate2",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.140"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sniff-packets"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"craftflow-nbt",
|
||||||
|
"rust_mc_proto",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
11
sniff_packets/Cargo.toml
Normal file
11
sniff_packets/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "sniff_packets"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rust_mc_proto = "0.1.19"
|
||||||
|
serde = "1.0.219"
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
craftflow-nbt = "2.1.0"
|
||||||
|
uuid = "1.16.0"
|
198
sniff_packets/src/main.rs
Normal file
198
sniff_packets/src/main.rs
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
use std::{fs, io::Read};
|
||||||
|
|
||||||
|
use craftflow_nbt::DynNBT;
|
||||||
|
use rust_mc_proto::{prelude::*, write_packet, MCConnTcp, Packet, ProtocolError};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
||||||
|
pub trait ReadWriteNBT<T>: DataReader + DataWriter {
|
||||||
|
fn read_nbt(&mut self) -> Result<T, ProtocolError>;
|
||||||
|
fn write_nbt(&mut self, val: &T) -> Result<(), ProtocolError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadWriteNBT<DynNBT> for Packet {
|
||||||
|
fn read_nbt(&mut self) -> Result<DynNBT, ProtocolError> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
let pos = self.get_ref().position();
|
||||||
|
self.get_mut()
|
||||||
|
.read_to_end(&mut data)
|
||||||
|
.map_err(|_| ProtocolError::StringParseError)?;
|
||||||
|
let (remaining, value) =
|
||||||
|
craftflow_nbt::from_slice(&data).map_err(|_| ProtocolError::StringParseError)?;
|
||||||
|
self.get_mut()
|
||||||
|
.set_position(pos + (data.len() - remaining.len()) as u64);
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_nbt(&mut self, val: &DynNBT) -> Result<(), ProtocolError> {
|
||||||
|
craftflow_nbt::to_writer(self.get_mut(), val).map_err(|_| ProtocolError::StringParseError)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn main() -> Result<(), ProtocolError> {
|
||||||
|
let mut conn = MCConnTcp::connect("localhost:25565").unwrap();
|
||||||
|
|
||||||
|
conn.write_packet(&Packet::build(0x00, |packet| {
|
||||||
|
packet.write_varint(770)?;
|
||||||
|
packet.write_string("localhost")?;
|
||||||
|
packet.write_unsigned_short(25565)?;
|
||||||
|
packet.write_varint(2)
|
||||||
|
})?)?;
|
||||||
|
|
||||||
|
conn.write_packet(&Packet::build(0x00, |packet| {
|
||||||
|
packet.write_string("TheMixRay")?;
|
||||||
|
packet.write_uuid(&Uuid::default())
|
||||||
|
})?)?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut packet = conn.read_packet()?;
|
||||||
|
|
||||||
|
if packet.id() == 0x03 {
|
||||||
|
let threshold = packet.read_varint()?;
|
||||||
|
|
||||||
|
if threshold >= 0 {
|
||||||
|
conn.set_compression(Some(threshold as usize));
|
||||||
|
}
|
||||||
|
} else if packet.id() == 0x02 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.write_packet(&Packet::empty(0x03))?;
|
||||||
|
|
||||||
|
conn.write_packet(&Packet::build(0x02, |packet| {
|
||||||
|
packet.write_string("minecraft:brand")?;
|
||||||
|
packet.write_string("vanilla")
|
||||||
|
})?)?;
|
||||||
|
|
||||||
|
conn.write_packet(&Packet::build(0x00, |packet| {
|
||||||
|
packet.write_string("en_us")?;
|
||||||
|
packet.write_signed_byte(12)?;
|
||||||
|
packet.write_varint(0)?;
|
||||||
|
packet.write_boolean(true)?;
|
||||||
|
packet.write_byte(127)?;
|
||||||
|
packet.write_varint(1)?;
|
||||||
|
packet.write_boolean(true)?;
|
||||||
|
packet.write_boolean(true)?;
|
||||||
|
packet.write_varint(0)
|
||||||
|
})?)?;
|
||||||
|
|
||||||
|
let mut packet = conn.read_packet()?; // server brand
|
||||||
|
|
||||||
|
let id = packet.read_string()?;
|
||||||
|
println!("message id: {}", id);
|
||||||
|
println!("message data: {}", String::from_utf8_lossy(&packet.get_bytes()[id.len()+1..]));
|
||||||
|
|
||||||
|
let mut packet = conn.read_packet()?; // feature flags
|
||||||
|
|
||||||
|
let flags_len = packet.read_varint()?;
|
||||||
|
|
||||||
|
println!("got {} feature flags:", flags_len);
|
||||||
|
|
||||||
|
for _ in 0..flags_len {
|
||||||
|
let flag = packet.read_string()?;
|
||||||
|
|
||||||
|
println!("flag: {}", flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut packet = conn.read_packet()?; // wait for known packs packet
|
||||||
|
|
||||||
|
if packet.id() != 0x0E {
|
||||||
|
println!("got unexpected packet while looking for 0x0E: 0x{:02X}", packet.id());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let packs_len = packet.read_varint()?;
|
||||||
|
|
||||||
|
println!("got {} known packs:", packs_len);
|
||||||
|
|
||||||
|
for _ in 0..packs_len {
|
||||||
|
println!("{}:{} v{}", packet.read_string()?, packet.read_string()?, packet.read_string()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.set_id(0x07); // make it serverbound
|
||||||
|
|
||||||
|
conn.write_packet(&packet)?;
|
||||||
|
|
||||||
|
let mut data = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut packet = conn.read_packet()?;
|
||||||
|
|
||||||
|
if packet.id() != 0x07 { // update tags
|
||||||
|
let registries_len = packet.read_varint()?;
|
||||||
|
|
||||||
|
println!("got update tags: {}", registries_len);
|
||||||
|
|
||||||
|
for _ in 0..registries_len {
|
||||||
|
let registry = packet.read_string()?;
|
||||||
|
|
||||||
|
println!("registry: {}", registry);
|
||||||
|
|
||||||
|
let tags_len = packet.read_varint()?;
|
||||||
|
|
||||||
|
for _ in 0..tags_len {
|
||||||
|
let tag_name = packet.read_string()?;
|
||||||
|
|
||||||
|
println!("tag: {}", tag_name);
|
||||||
|
|
||||||
|
let entries_len = packet.read_varint()?;
|
||||||
|
|
||||||
|
for _ in 0..entries_len {
|
||||||
|
let entry = packet.read_varint()?;
|
||||||
|
|
||||||
|
println!("entry: {}", entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::write("update-tags.bin", packet.get_bytes()).unwrap();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("got registry: {}", packet.read_string()?);
|
||||||
|
|
||||||
|
let entries_len = packet.read_varint()?;
|
||||||
|
|
||||||
|
for _ in 0..entries_len {
|
||||||
|
let entry_id = packet.read_string()?;
|
||||||
|
let has_data = packet.read_boolean()?;
|
||||||
|
|
||||||
|
if has_data {
|
||||||
|
let entry_data = packet.read_nbt()?;
|
||||||
|
|
||||||
|
println!("entry: {}, data: {:?}", entry_id, entry_data);
|
||||||
|
} else {
|
||||||
|
println!("entry: {}, no data", entry_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_packet(&mut data, None, 0, &packet)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::write("registry-data.bin", &data).unwrap();
|
||||||
|
|
||||||
|
let packet = conn.read_packet()?;
|
||||||
|
conn.write_packet(&packet)?; // finish conf
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut packet = conn.read_packet()?;
|
||||||
|
|
||||||
|
if packet.id() == 0x41 {
|
||||||
|
let id = packet.read_varint()?;
|
||||||
|
|
||||||
|
conn.write_packet(&Packet::build(0x00, |packet| packet.write_varint(id))?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if packet.id() == 0x27 {
|
||||||
|
// here you can read "Chunk Data and Update Light" packet
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
52
src/config.rs
Normal file
52
src/config.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
94
src/context.rs
Normal file
94
src/context.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
45
src/d.rs
45
src/d.rs
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
94
src/data.rs
94
src/data.rs
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
51
src/data/mod.rs
Normal file
51
src/data/mod.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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))?)
|
||||||
|
}
|
||||||
|
}
|
184
src/data/text_component.rs
Normal file
184
src/data/text_component.rs
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
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::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(())
|
||||||
|
}
|
||||||
|
}
|
49
src/event/mod.rs
Normal file
49
src/event/mod.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use rust_mc_proto::Packet;
|
||||||
|
|
||||||
|
use super::{ServerError, player::context::ClientContext, protocol::ConnectionState};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! generate_handlers {
|
||||||
|
($name:ident $(, $arg_ty:ty)* $(,)?) => {
|
||||||
|
paste::paste! {
|
||||||
|
fn [<on_ $name _priority>](&self) -> i8 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn [<on_ $name>](&self, _: Arc<ClientContext> $(, _: $arg_ty)*) -> Result<(), 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);
|
||||||
|
}
|
136
src/lib.rs
Normal file
136
src/lib.rs
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
use std::{error::Error, fmt::Display, net::TcpListener, sync::Arc, thread, time::Duration};
|
||||||
|
|
||||||
|
use config::Config;
|
||||||
|
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(u8), // Неожиданный пакет
|
||||||
|
WrongPacket, // Пакет поломан, неверные данные
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// server start helper
|
||||||
|
// pub struct Server {
|
||||||
|
// context: Arc<ServerContext>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl Server {
|
||||||
|
// pub fn new(context: ServerContext) -> Self {
|
||||||
|
// Self {
|
||||||
|
// context: Arc::new(context),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn context(&self) -> &ServerContext {
|
||||||
|
// &self.context
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn start(&self) {
|
||||||
|
// start_server(self.context.clone());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl Default for Server {
|
||||||
|
// fn default() -> Self {
|
||||||
|
// Self {
|
||||||
|
// context: Arc::new(ServerContext::new(Arc::new(Config::default()))),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
184
src/main.rs
184
src/main.rs
@ -1,40 +1,164 @@
|
|||||||
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 rust_mc_serv::{
|
||||||
|
ServerError,
|
||||||
|
config::Config,
|
||||||
|
context::ServerContext,
|
||||||
|
data::text_component::TextComponent,
|
||||||
|
event::{Listener, PacketHandler},
|
||||||
|
player::context::ClientContext,
|
||||||
|
protocol::ConnectionState,
|
||||||
|
start_server,
|
||||||
|
};
|
||||||
|
|
||||||
use std::thread;
|
struct ExampleListener;
|
||||||
|
|
||||||
fn get_byte_size(i: i32) -> u8 {
|
impl Listener for ExampleListener {
|
||||||
for j in 1..4 {
|
fn on_status(
|
||||||
if (i & -1 << (j * 7)) == 0 {
|
&self,
|
||||||
return j;
|
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(())
|
||||||
}
|
}
|
||||||
}; return 5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
306
src/player/context.rs
Normal file
306
src/player/context.rs
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
hash::Hash,
|
||||||
|
net::{SocketAddr, TcpStream},
|
||||||
|
sync::{
|
||||||
|
Arc, Mutex, RwLock,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
},
|
||||||
|
thread,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use rust_mc_proto::{MinecraftConnection, Packet};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::helper::ProtocolHelper;
|
||||||
|
use crate::{ServerError, context::ServerContext, protocol::ConnectionState};
|
||||||
|
|
||||||
|
// Клиент контекст
|
||||||
|
// Должен быть обернут в 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>,
|
||||||
|
packet_buffer: Mutex<VecDeque<Packet>>,
|
||||||
|
read_loop: AtomicBool,
|
||||||
|
is_alive: AtomicBool,
|
||||||
|
position: RwLock<(f64, f64, f64)>,
|
||||||
|
velocity: RwLock<(f64, f64, f64)>,
|
||||||
|
rotation: RwLock<(f32, f32)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Реализуем сравнение через адрес
|
||||||
|
// 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),
|
||||||
|
packet_buffer: Mutex::new(VecDeque::new()),
|
||||||
|
read_loop: AtomicBool::new(false),
|
||||||
|
is_alive: AtomicBool::new(true),
|
||||||
|
position: RwLock::new((0.0, 0.0, 0.0)),
|
||||||
|
velocity: RwLock::new((0.0, 0.0, 0.0)),
|
||||||
|
rotation: RwLock::new((0.0, 0.0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 set_position(self: &Arc<Self>, position: (f64, f64, f64)) {
|
||||||
|
*self.position.write().unwrap() = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_velocity(self: &Arc<Self>, velocity: (f64, f64, f64)) {
|
||||||
|
*self.velocity.write().unwrap() = velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_rotation(self: &Arc<Self>, rotation: (f32, f32)) {
|
||||||
|
*self.rotation.write().unwrap() = rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn position(self: &Arc<Self>) -> (f64, f64, f64) {
|
||||||
|
self.position.read().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn velocity(self: &Arc<Self>) -> (f64, f64, f64) {
|
||||||
|
self.velocity.read().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rotation(self: &Arc<Self>) -> (f32, f32) {
|
||||||
|
self.rotation.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 {
|
||||||
|
match self.conn.write().unwrap().write_packet(&packet) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
self.is_alive.store(false, Ordering::SeqCst);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_read_loop(self: &Arc<Self>) -> Result<(), ServerError> {
|
||||||
|
self.read_loop.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
|
let mut conn = self.conn.read().unwrap().try_clone()?; // так можно делать т.к сокет это просто поинтер
|
||||||
|
|
||||||
|
while self.is_alive() {
|
||||||
|
let mut packet = match conn.read_packet() {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
self.is_alive.store(false, Ordering::SeqCst);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut cancelled = false;
|
||||||
|
let state = self.state();
|
||||||
|
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 {
|
||||||
|
self.packet_buffer.lock().unwrap().push_back(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Please avoid using of this bullshit
|
||||||
|
pub fn read_any_packet(self: &Arc<Self>) -> Result<Packet, ServerError> {
|
||||||
|
if self.read_loop.load(Ordering::SeqCst) {
|
||||||
|
loop {
|
||||||
|
if let Some(packet) = self.packet_buffer.lock().unwrap().pop_front() {
|
||||||
|
return Ok(packet);
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(4));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let state = self.state();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut packet = match self.conn.write().unwrap().read_packet() {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
self.is_alive.store(false, Ordering::SeqCst);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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>, ids: &[u8]) -> Result<Packet, ServerError> {
|
||||||
|
if self.read_loop.load(Ordering::SeqCst) {
|
||||||
|
loop {
|
||||||
|
{
|
||||||
|
let mut locked = self.packet_buffer.lock().unwrap();
|
||||||
|
for (i, packet) in locked.clone().iter().enumerate() {
|
||||||
|
if ids.contains(&packet.id()) {
|
||||||
|
locked.remove(i);
|
||||||
|
return Ok(packet.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(4));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let packet = match self.read_any_packet() {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
self.is_alive.store(false, Ordering::SeqCst);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ids.contains(&packet.id()) {
|
||||||
|
Ok(packet)
|
||||||
|
} else {
|
||||||
|
Err(ServerError::UnexpectedPacket(packet.id()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_packet_back(self: &Arc<Self>, packet: Packet) {
|
||||||
|
self.packet_buffer.lock().unwrap().push_back(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 is_alive(self: &Arc<Self>) -> bool {
|
||||||
|
self.is_alive.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
238
src/player/helper.rs
Normal file
238
src/player/helper.rs
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
use std::{
|
||||||
|
io::Read,
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
use rust_mc_proto::{DataReader, DataWriter, Packet};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
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/player/mod.rs
Normal file
2
src/player/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod context;
|
||||||
|
pub mod helper;
|
191
src/protocol/handler.rs
Normal file
191
src/protocol/handler.rs
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
use std::{io::Read, sync::Arc};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ServerError,
|
||||||
|
player::context::{ClientContext, ClientInfo, Handshake, PlayerInfo},
|
||||||
|
};
|
||||||
|
use rust_mc_proto::{DataReader, DataWriter, Packet};
|
||||||
|
|
||||||
|
use crate::trigger_event;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
ConnectionState,
|
||||||
|
id::*,
|
||||||
|
play::{handle_configuration_state, handle_play_state},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const BRAND: &str = "rust_mc_serv";
|
||||||
|
|
||||||
|
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)?;
|
||||||
|
}
|
||||||
|
id => {
|
||||||
|
return Err(ServerError::UnexpectedPacket(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.write_packet(&Packet::build(
|
||||||
|
clientbound::configuration::PLUGIN_MESSAGE,
|
||||||
|
|p| {
|
||||||
|
p.write_string("minecraft:brand")?;
|
||||||
|
p.write_string(BRAND)
|
||||||
|
},
|
||||||
|
)?)?;
|
||||||
|
|
||||||
|
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::UnexpectedState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
272
src/protocol/id.rs
Normal file
272
src/protocol/id.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
12
src/protocol/mod.rs
Normal file
12
src/protocol/mod.rs
Normal 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,
|
||||||
|
}
|
378
src/protocol/play.rs
Normal file
378
src/protocol/play.rs
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
use std::{
|
||||||
|
io::Cursor,
|
||||||
|
sync::Arc,
|
||||||
|
thread,
|
||||||
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
use rust_mc_proto::{DataReader, DataWriter, Packet, read_packet};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ServerError,
|
||||||
|
data::{ReadWriteNBT, text_component::TextComponent},
|
||||||
|
player::context::ClientContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::id::*;
|
||||||
|
|
||||||
|
pub fn send_update_tags(client: Arc<ClientContext>) -> Result<(), ServerError> {
|
||||||
|
// TODO: rewrite this hardcode bullshit
|
||||||
|
|
||||||
|
client.write_packet(&Packet::from_bytes(
|
||||||
|
clientbound::configuration::UPDATE_TAGS,
|
||||||
|
include_bytes!("update-tags.bin"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_registry_data(client: Arc<ClientContext>) -> Result<(), ServerError> {
|
||||||
|
// TODO: rewrite this hardcode bullshit
|
||||||
|
|
||||||
|
let mut registry_data = Cursor::new(include_bytes!("registry-data.bin"));
|
||||||
|
|
||||||
|
while let Ok(mut packet) = read_packet(&mut registry_data, None) {
|
||||||
|
packet.set_id(clientbound::configuration::REGISTRY_DATA);
|
||||||
|
client.write_packet(&packet)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавки в Configuration стейт чтобы все работало
|
||||||
|
pub fn handle_configuration_state(
|
||||||
|
client: Arc<ClientContext>, // Контекст клиента
|
||||||
|
) -> Result<(), ServerError> {
|
||||||
|
let mut packet = Packet::empty(clientbound::configuration::FEATURE_FLAGS);
|
||||||
|
packet.write_varint(1)?;
|
||||||
|
packet.write_string("minecraft:vanilla")?;
|
||||||
|
client.write_packet(&packet)?;
|
||||||
|
|
||||||
|
let mut packet = Packet::empty(clientbound::configuration::KNOWN_PACKS);
|
||||||
|
packet.write_varint(1)?;
|
||||||
|
packet.write_string("minecraft")?;
|
||||||
|
packet.write_string("core")?;
|
||||||
|
packet.write_string("1.21.5")?;
|
||||||
|
client.write_packet(&packet)?;
|
||||||
|
|
||||||
|
client.read_packet(&[serverbound::configuration::KNOWN_PACKS])?;
|
||||||
|
|
||||||
|
send_registry_data(client.clone())?;
|
||||||
|
send_update_tags(client.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_login(client: Arc<ClientContext>) -> Result<(), ServerError> {
|
||||||
|
// Отправка пакета Login
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_game_event(
|
||||||
|
client: Arc<ClientContext>,
|
||||||
|
event: u8,
|
||||||
|
value: f32,
|
||||||
|
) -> Result<(), ServerError> {
|
||||||
|
let mut packet = Packet::empty(clientbound::play::GAME_EVENT);
|
||||||
|
|
||||||
|
packet.write_byte(event)?;
|
||||||
|
packet.write_float(value)?;
|
||||||
|
|
||||||
|
client.write_packet(&packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sync_player_pos(
|
||||||
|
client: Arc<ClientContext>,
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
z: f64,
|
||||||
|
vel_x: f64,
|
||||||
|
vel_y: f64,
|
||||||
|
vel_z: f64,
|
||||||
|
yaw: f32,
|
||||||
|
pitch: f32,
|
||||||
|
flags: i32,
|
||||||
|
) -> Result<(), ServerError> {
|
||||||
|
let timestamp = (SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis()
|
||||||
|
& 0xFFFFFFFF) as i32;
|
||||||
|
|
||||||
|
let mut packet = Packet::empty(clientbound::play::SYNCHRONIZE_PLAYER_POSITION);
|
||||||
|
|
||||||
|
packet.write_varint(timestamp)?;
|
||||||
|
packet.write_double(x)?;
|
||||||
|
packet.write_double(y)?;
|
||||||
|
packet.write_double(z)?;
|
||||||
|
packet.write_double(vel_x)?;
|
||||||
|
packet.write_double(vel_y)?;
|
||||||
|
packet.write_double(vel_z)?;
|
||||||
|
packet.write_float(yaw)?;
|
||||||
|
packet.write_float(pitch)?;
|
||||||
|
packet.write_int(flags)?;
|
||||||
|
|
||||||
|
client.write_packet(&packet)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_center_chunk(client: Arc<ClientContext>, x: i32, z: i32) -> Result<(), ServerError> {
|
||||||
|
let mut packet = Packet::empty(clientbound::play::SET_CENTER_CHUNK);
|
||||||
|
|
||||||
|
packet.write_varint(x)?;
|
||||||
|
packet.write_varint(z)?;
|
||||||
|
|
||||||
|
client.write_packet(&packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_example_chunk(client: Arc<ClientContext>, x: i32, z: i32) -> Result<(), ServerError> {
|
||||||
|
let mut packet = Packet::empty(clientbound::play::CHUNK_DATA_AND_UPDATE_LIGHT);
|
||||||
|
|
||||||
|
packet.write_int(x)?;
|
||||||
|
packet.write_int(z)?;
|
||||||
|
|
||||||
|
// heightmap
|
||||||
|
|
||||||
|
packet.write_varint(1)?; // heightmaps count
|
||||||
|
packet.write_varint(0)?; // MOTION_BLOCKING
|
||||||
|
packet.write_varint(256)?; // Length of the following long array (16 * 16 = 256)
|
||||||
|
for _ in 0..256 {
|
||||||
|
packet.write_long(0)?; // height - 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// sending chunk data
|
||||||
|
|
||||||
|
let mut chunk_data = Vec::new();
|
||||||
|
|
||||||
|
// we want to fill the area from -64 to 0, so it will be 4 chunk sections
|
||||||
|
|
||||||
|
for _ in 0..4 {
|
||||||
|
chunk_data.write_short(4096)?; // non-air blocks count, 16 * 16 * 16 = 4096 stone blocks
|
||||||
|
|
||||||
|
// blocks paletted container
|
||||||
|
chunk_data.write_byte(0)?; // Bits Per Entry, use Single valued palette format
|
||||||
|
chunk_data.write_varint(10)?; // block state id in the registry (1 for stone)
|
||||||
|
|
||||||
|
// biomes palleted container
|
||||||
|
chunk_data.write_byte(0)?; // Bits Per Entry, use Single valued palette format
|
||||||
|
chunk_data.write_varint(27)?; // biome id in the registry
|
||||||
|
}
|
||||||
|
|
||||||
|
// air chunk sections
|
||||||
|
|
||||||
|
for _ in 0..20 {
|
||||||
|
chunk_data.write_short(0)?; // non-air blocks count, 0
|
||||||
|
|
||||||
|
// blocks paletted container
|
||||||
|
chunk_data.write_byte(0)?; // Bits Per Entry, use Single valued palette format
|
||||||
|
chunk_data.write_varint(0)?; // block state id in the registry (0 for air)
|
||||||
|
|
||||||
|
// biomes palleted container
|
||||||
|
chunk_data.write_byte(0)?; // Bits Per Entry, use Single valued palette format
|
||||||
|
chunk_data.write_varint(27)?; // biome id in the registry
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.write_usize_varint(chunk_data.len())?;
|
||||||
|
packet.write_bytes(&chunk_data)?;
|
||||||
|
|
||||||
|
packet.write_byte(0)?;
|
||||||
|
|
||||||
|
// light data
|
||||||
|
|
||||||
|
packet.write_byte(0)?;
|
||||||
|
packet.write_byte(0)?;
|
||||||
|
packet.write_byte(0)?;
|
||||||
|
packet.write_byte(0)?;
|
||||||
|
packet.write_byte(0)?;
|
||||||
|
packet.write_byte(0)?;
|
||||||
|
|
||||||
|
client.write_packet(&packet)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_keep_alive(client: Arc<ClientContext>) -> Result<(), ServerError> {
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs() as i64;
|
||||||
|
|
||||||
|
let mut packet = Packet::empty(clientbound::play::KEEP_ALIVE);
|
||||||
|
packet.write_long(timestamp)?;
|
||||||
|
client.write_packet(&packet)?;
|
||||||
|
|
||||||
|
let mut packet = client.read_packet(&[serverbound::play::KEEP_ALIVE])?;
|
||||||
|
let timestamp2 = packet.read_long()?;
|
||||||
|
if timestamp2 != timestamp {
|
||||||
|
// Послать клиента нахуй
|
||||||
|
Err(ServerError::WrongPacket)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_system_message(
|
||||||
|
client: Arc<ClientContext>,
|
||||||
|
message: TextComponent,
|
||||||
|
is_action_bar: bool,
|
||||||
|
) -> Result<(), ServerError> {
|
||||||
|
let mut packet = Packet::empty(clientbound::play::SYSTEM_CHAT_MESSAGE);
|
||||||
|
packet.write_nbt(&message)?;
|
||||||
|
packet.write_boolean(is_action_bar)?;
|
||||||
|
client.write_packet(&packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_example_chunks_in_distance(
|
||||||
|
client: Arc<ClientContext>,
|
||||||
|
chunks: &mut Vec<(i32, i32)>,
|
||||||
|
distance: i32,
|
||||||
|
center: (i32, i32),
|
||||||
|
) -> Result<(), ServerError> {
|
||||||
|
for x in -distance + center.0..=distance + center.0 {
|
||||||
|
for z in -distance + center.1..=distance + center.1 {
|
||||||
|
if !chunks.contains(&(x, z)) {
|
||||||
|
send_example_chunk(client.clone(), x as i32, z as i32)?;
|
||||||
|
chunks.push((x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отдельная функция для работы с самой игрой
|
||||||
|
pub fn handle_play_state(
|
||||||
|
client: Arc<ClientContext>, // Контекст клиента
|
||||||
|
) -> Result<(), ServerError> {
|
||||||
|
thread::spawn({
|
||||||
|
let client = client.clone();
|
||||||
|
|
||||||
|
move || {
|
||||||
|
let _ = client.run_read_loop();
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
send_login(client.clone())?;
|
||||||
|
sync_player_pos(client.clone(), 8.0, 0.0, 8.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)?;
|
||||||
|
send_game_event(client.clone(), 13, 0.0)?; // 13 - Start waiting for level chunks
|
||||||
|
set_center_chunk(client.clone(), 0, 0)?;
|
||||||
|
|
||||||
|
let mut chunks = Vec::new();
|
||||||
|
|
||||||
|
let view_distance = client.client_info().unwrap().view_distance as i32;
|
||||||
|
|
||||||
|
send_example_chunks_in_distance(client.clone(), &mut chunks, view_distance, (0, 0))?;
|
||||||
|
|
||||||
|
thread::spawn({
|
||||||
|
let client = client.clone();
|
||||||
|
|
||||||
|
move || -> Result<(), ServerError> {
|
||||||
|
while client.is_alive() {
|
||||||
|
let mut packet = client.read_any_packet()?;
|
||||||
|
|
||||||
|
match packet.id() {
|
||||||
|
serverbound::play::SET_PLAYER_POSITION => {
|
||||||
|
let x = packet.read_double()?;
|
||||||
|
let y = packet.read_double()?;
|
||||||
|
let z = packet.read_double()?;
|
||||||
|
let _ = packet.read_byte()?; // flags
|
||||||
|
|
||||||
|
client.set_position((x, y, z));
|
||||||
|
}
|
||||||
|
serverbound::play::SET_PLAYER_POSITION_AND_ROTATION => {
|
||||||
|
let x = packet.read_double()?;
|
||||||
|
let y = packet.read_double()?;
|
||||||
|
let z = packet.read_double()?;
|
||||||
|
let yaw = packet.read_float()?;
|
||||||
|
let pitch = packet.read_float()?;
|
||||||
|
let _ = packet.read_byte()?; // flags
|
||||||
|
|
||||||
|
client.set_position((x, y, z));
|
||||||
|
client.set_rotation((yaw, pitch));
|
||||||
|
}
|
||||||
|
serverbound::play::SET_PLAYER_ROTATION => {
|
||||||
|
let yaw = packet.read_float()?;
|
||||||
|
let pitch = packet.read_float()?;
|
||||||
|
let _ = packet.read_byte()?; // flags
|
||||||
|
|
||||||
|
client.set_rotation((yaw, pitch));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
client.push_packet_back(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut ticks_alive = 0u64;
|
||||||
|
|
||||||
|
while client.is_alive() {
|
||||||
|
if ticks_alive % 200 == 0 {
|
||||||
|
// 10 secs timer
|
||||||
|
send_keep_alive(client.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ticks_alive % 20 == 0 {
|
||||||
|
// 1 sec timer
|
||||||
|
let (x, y, z) = client.position();
|
||||||
|
|
||||||
|
let (chunk_x, chunk_z) = ((x / 16.0) as i64, (z / 16.0) as i64);
|
||||||
|
let (chunk_x, chunk_z) = (chunk_x as i32, chunk_z as i32);
|
||||||
|
|
||||||
|
set_center_chunk(client.clone(), chunk_x, chunk_z)?;
|
||||||
|
send_example_chunks_in_distance(
|
||||||
|
client.clone(),
|
||||||
|
&mut chunks,
|
||||||
|
view_distance,
|
||||||
|
(chunk_x, chunk_z),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
send_system_message(
|
||||||
|
client.clone(),
|
||||||
|
TextComponent::rainbow(format!("Pos: {} {} {}", x as i64, y as i64, z as i64)),
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
send_system_message(
|
||||||
|
client.clone(),
|
||||||
|
TextComponent::rainbow(format!("Ticks alive: {}", ticks_alive)),
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(50)); // 1 tick
|
||||||
|
ticks_alive += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
BIN
src/protocol/registry-data.bin
Normal file
BIN
src/protocol/registry-data.bin
Normal file
Binary file not shown.
BIN
src/protocol/update-tags.bin
Normal file
BIN
src/protocol/update-tags.bin
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user