This commit is contained in:
TriMill 2023-08-01 00:56:38 -04:00
parent 546a8a83a6
commit 1c2cf9bd3f
30 changed files with 1187 additions and 396 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target
.vscode

76
Cargo.lock generated
View file

@ -67,11 +67,12 @@ checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
[[package]]
name = "bstr"
version = "0.2.17"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
dependencies = [
"memchr",
"serde",
]
[[package]]
@ -126,6 +127,15 @@ dependencies = [
"termcolor",
]
[[package]]
name = "erased-serde"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da96524cc884f6558f1769b6c46686af2fe8e8b4cd253bd5a3cdba8181b8e070"
dependencies = [
"serde",
]
[[package]]
name = "errno"
version = "0.3.1"
@ -163,17 +173,6 @@ version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
[[package]]
name = "futures-macro"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-task"
version = "0.3.28"
@ -187,7 +186,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
dependencies = [
"futures-core",
"futures-macro",
"futures-task",
"pin-project-lite",
"pin-utils",
@ -297,19 +295,30 @@ dependencies = [
[[package]]
name = "mlua"
version = "0.8.9"
version = "0.9.0-rc.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07366ed2cd22a3b000aed076e2b68896fb46f06f1f5786c5962da73c0af01577"
checksum = "f5369212118d0f115c9adbe7f7905e36fb3ef2994266039c51fc3e96374d705d"
dependencies = [
"bstr",
"cc",
"futures-core",
"futures-task",
"erased-serde",
"futures-util",
"mlua-sys",
"num-traits",
"once_cell",
"pkg-config",
"rustc-hash",
"serde",
"serde-value",
]
[[package]]
name = "mlua-sys"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3daecc55a656cae8e54fc599701ab597b67cdde68f780cac8c1c49b9cfff2f5"
dependencies = [
"cc",
"cfg-if",
"pkg-config",
]
[[package]]
@ -346,6 +355,15 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "ordered-float"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87"
dependencies = [
"num-traits",
]
[[package]]
name = "parking_lot"
version = "0.12.1"
@ -405,6 +423,8 @@ dependencies = [
"hematite-nbt",
"log",
"mlua",
"semver",
"serde",
"serde_json",
"tokio",
"uuid",
@ -494,6 +514,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "semver"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]]
name = "serde"
version = "1.0.171"
@ -503,6 +529,16 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-value"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
dependencies = [
"ordered-float",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.171"

View file

@ -6,9 +6,11 @@ edition = "2021"
[dependencies]
tokio = { version = "1.29", features = ["full"] }
serde_json = "1.0"
serde = "1.0"
anyhow = "1.0"
uuid = { version = "1.4", features = ["v5"] }
hematite-nbt = "0.5.2"
log = "0.4"
env_logger = "0.10"
mlua = { version = "0.8", features = ["luajit52", "async"] }
mlua = { version = "0.9.0-rc.1", features = ["luajit52", "async", "send", "serialize"] }
semver = "1.0"

12
README.md Normal file
View file

@ -0,0 +1,12 @@
# Building
quectocraft requires that `luajit` is installed. it should be
available via your package manager.
## Alpine
install `luajit`, `luajit-dev`, and `pkgconfig` and build with
```sh
RUSTFLAGS="-C target-feature=-crt-static" cargo build --release
```

View file

@ -1,20 +1,11 @@
return {
id = "example",
name = "Example Plugin",
version = "0.1.0",
authors = {"John Doe", "Jane Doe"},
description = "Example plugin",
license = "MIT",
server_version = "0.2",
init = function(self, srv)
self.info("initializing " .. self.name)
end,
on_join = function(self, srv, uuid, name)
srv:kick(uuid, "people named " .. name .. " are not allowed here")
end,
on_leave = function(self, srv, uuid, name)
self.info(name .. " left")
init = function(self)
srv:info("quectocraft version " .. srv.version)
for k, _ in pairs(srv.plugins) do
srv:info("found plugin: " .. k)
end
end,
}

39
plugins/mcchat.lua Normal file
View file

@ -0,0 +1,39 @@
return {
-- id is required, server_version is recommended, all other fields are optional
id = "mcchat",
name = "MCChat",
version = "0.1.0",
server_version = "0.2",
authors = {"trimill"},
description = "Adds basic chat and join/leave messages",
license = "MIT",
on_join = function(self, _, name)
srv:info(name .. " joined the game")
srv:broadcast_msg({
translate = "multiplayer.player.joined",
with = { { text = name } },
color = "yellow",
})
end,
on_leave = function(self, _, name)
srv:info(name .. " left the game")
srv:broadcast_msg({
translate = "multiplayer.player.left",
with = { { text = name } },
color = "yellow",
})
end,
on_message = function(self, msg, _, name)
srv:info("<" .. name .. "> " .. msg)
srv:broadcast_chat({
translate = "chat.type.text",
with = {
{ text = name },
{ text = msg },
},
})
end,
}

View file

@ -1,10 +1,12 @@
use crate::{Player, ClientInfo};
use crate::{Player, ClientInfo, JsonValue};
#[derive(Debug)]
pub enum ClientEvent {
Handshake(ClientInfo),
Join(Player),
KeepAlive(i64),
Command(String),
Message(String),
Disconnect,
}
@ -12,4 +14,6 @@ pub enum ClientEvent {
pub enum ServerEvent {
Disconnect(Option<String>),
KeepAlive(i64),
SystemMessage(JsonValue, bool),
ChatMessage(JsonValue, JsonValue),
}

View file

@ -240,6 +240,7 @@ impl ClientState {
data: b"\x0bquectocraft".to_vec(),
}).await?;
// fabric clients need a 3x3 of chunks to load
for x in -1..=1 {
for z in -1..=1 {
self.write_packet(ServerPacket::ChunkData { x, z }).await?;
@ -251,6 +252,12 @@ impl ClientState {
angle: 0.0
}).await?;
self.write_packet(ServerPacket::PositionAndLook {
pos: [0.0, 64.0, 0.0],
look: [0.0, 0.0],
flags: 0,
}).await?;
loop { select! {
ev = rx.recv() => match ev {
Some(ServerEvent::Disconnect(msg)) => {
@ -262,9 +269,17 @@ impl ClientState {
info!("#{} disconnecting: {}", self.id, msg.unwrap_or_default());
return Ok(())
},
Some(ServerEvent::SystemMessage(msg, overlay)) => {
debug!("#{} sending system message: {msg}", self.id);
self.write_packet(ServerPacket::SystemMessage(msg, overlay)).await?;
},
Some(ServerEvent::ChatMessage(msg, name)) => {
debug!("#{} sending chat message: {msg}", self.id);
self.write_packet(ServerPacket::ChatMessage(msg, name)).await?;
},
Some(ServerEvent::KeepAlive(data)) => {
self.write_packet(ServerPacket::KeepAlive(data)).await?;
debug!("#{} sending keepalive: {data}", self.id);
self.write_packet(ServerPacket::KeepAlive(data)).await?;
},
None => (),
},
@ -274,6 +289,14 @@ impl ClientState {
debug!("#{} recieved keepalive: {data}", self.id);
tx.send((self.id, ClientEvent::KeepAlive(data))).await?;
},
ClientPacket::ChatMessage(msg) => {
debug!("#{} recieved message: {msg}", self.id);
tx.send((self.id, ClientEvent::Message(msg))).await?;
},
ClientPacket::Command(cmd) => {
debug!("#{} recieved command: {cmd}", self.id);
tx.send((self.id, ClientEvent::Command(cmd))).await?;
},
_ => (),
}
ReadPacketOutcome::None => (),

View file

@ -10,6 +10,8 @@ mod ser;
mod varint;
mod client;
pub static QC_VERSION: &str = env!("CARGO_PKG_VERSION");
pub type ArcMutex<T> = Arc<Mutex<T>>;
pub type JsonValue = serde_json::Value;
@ -32,3 +34,29 @@ pub struct ClientInfo {
async fn main() {
server::run_server().await.unwrap()
}
// Temporary code used to parse packets collected from vanilla Minecraft servers,
// mostly to collect dimension/registry codecs
#[cfg(no)]
fn main() {
use varint::VarInt;
use ser::Deserializable;
use std::io::{Cursor, Write};
let packet = include_bytes!("/home/trimill/code/minecraft/joingame/1.16.5_bin");
let mut r = Cursor::new(packet);
let _packet_id = VarInt::deserialize(&mut r).unwrap();
let _eid = i32::deserialize(&mut r).unwrap();
let _hc = bool::deserialize(&mut r).unwrap();
let _gm = u8::deserialize(&mut r).unwrap();
let _pgm = i8::deserialize(&mut r).unwrap();
let dc = VarInt::deserialize(&mut r).unwrap().0;
for _ in 0..dc {
let _dim = String::deserialize(&mut r).unwrap();
}
let _dim_codec: nbt::Blob = nbt::from_reader(&mut r).unwrap();
let init = r.position() as usize;
let _dim: nbt::Blob = nbt::from_reader(&mut r).unwrap();
let end = r.position() as usize;
std::io::stdout().lock().write_all(&packet[init..end]).unwrap();
}

View file

@ -1,12 +1,16 @@
use mlua::{Value, FromLua, Table};
use uuid::Uuid;
use crate::{Player, ClientInfo};
use crate::{Player, ClientInfo, JsonValue};
use super::UuidUD;
pub enum PluginEvent {
Kick(Uuid, Option<String>),
Chat(Uuid, JsonValue, JsonValue),
BroadcastChat(JsonValue, JsonValue),
System(Uuid, JsonValue, bool),
BroadcastSystem(JsonValue, bool),
}
impl<'lua> FromLua<'lua> for PluginEvent {
@ -23,4 +27,6 @@ impl<'lua> FromLua<'lua> for PluginEvent {
pub enum ServerEvent {
Join(Player, ClientInfo),
Leave(Player),
Command(Player, String),
Message(Player, String),
}

View file

@ -1,12 +1,11 @@
use std::{io::ErrorKind, fs, cell::RefCell, rc::Rc};
use std::sync::Arc;
use log::{warn, debug, info};
use mlua::{Lua, ToLua, Function};
use tokio::sync::mpsc::Sender;
use std::sync::mpsc::Receiver;
use log::warn;
use mlua::{Lua, Function, IntoLua, FromLua, Value, Table, AnyUserData};
use tokio::sync::mpsc::{Sender, Receiver};
use uuid::Uuid;
use self::{plugin::Plugin, event::{PluginEvent, ServerEvent}, server::{CellServer, Server}};
use self::{event::{PluginEvent, ServerEvent}, server::{Server, WrappedServer}, plugin::load_plugins};
pub mod event;
mod plugin;
@ -15,57 +14,42 @@ mod server;
#[derive(Clone, Copy, Eq, PartialEq)]
struct UuidUD(Uuid);
impl<'lua> FromLua<'lua> for UuidUD {
fn from_lua(value: mlua::Value<'lua>, _lua: &'lua Lua) -> mlua::Result<Self> {
match value {
Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
v => Err(mlua::Error::FromLuaConversionError { from: v.type_name(), to: "Uuid", message: Some("Expected Uuid".to_owned()) })
}
}
}
impl mlua::UserData for UuidUD {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_meta_method("__tostring",
|lua, this, ()| this.0.to_string().to_lua(lua)
|lua, this, ()| this.0.to_string().into_lua(lua)
);
methods.add_meta_method("__eq",
|lua, this, UuidUD(uuid)| (this.0 == uuid).to_lua(lua)
|lua, this, UuidUD(uuid)| (this.0 == uuid).into_lua(lua)
);
}
}
pub fn load_plugins(lua: &Lua) -> Vec<Plugin> {
debug!("loading plugins");
let entries = match fs::read_dir("./plugins") {
Ok(n) => n,
Err(e) if e.kind() == ErrorKind::NotFound => {
warn!("plugins directory does not exist, creating");
if let Err(e) = fs::create_dir("./plugins") {
warn!("failed to create plugins directory: {e}");
fn broadcast_event<'lua, F>(lua: &'lua Lua, fname: &str, callback: F) -> mlua::Result<()>
where F: Fn(&str, Table<'lua>, AnyUserData, Function) -> mlua::Result<()> {
let srv: AnyUserData = lua.globals().get("srv")?;
let plugins: Table = srv.nth_user_value(1)?;
for (id, table) in plugins.pairs::<String, Table>().filter_map(Result::ok) {
if let Ok(f) = table.get::<_, Function>(fname) {
srv.set_nth_user_value(2, id.clone())?;
if let Err(e) = callback(&id, table.clone(), srv.clone(), f) {
warn!("error in {fname} in plugin {}: {e}", id);
}
return Vec::new()
},
Err(e) => {
warn!("failed to load plugins: {e}");
return Vec::new()
}
};
let mut plugins = Vec::new();
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(e) => {
warn!("error reading entry: {e}");
continue
}
};
let path = entry.path();
let spath = path.to_str().unwrap_or("<non-unicode path>");
debug!("loading plugin at {spath}");
match Plugin::load(&entry, lua) {
Ok(p) => {
info!("loaded plugin {}", p.id);
plugins.push(p);
}
Err(e) => warn!("error loading plugin at {spath}: {e}"),
}
}
plugins
}
}
Ok(())
}
pub async fn run_plugins(tx: Sender<PluginEvent>, rx: Receiver<ServerEvent>) {
pub async fn run_plugins(tx: Sender<PluginEvent>, mut rx: Receiver<ServerEvent>) -> mlua::Result<()> {
let lua = unsafe {
mlua::Lua::unsafe_new_with(
mlua::StdLib::ALL_SAFE | mlua::StdLib::DEBUG,
@ -73,64 +57,61 @@ pub async fn run_plugins(tx: Sender<PluginEvent>, rx: Receiver<ServerEvent>) {
)
};
let plugins = load_plugins(&lua);
let server = Rc::new(RefCell::new(Server {
let server = WrappedServer(Arc::new(Server {
tx,
names: Default::default(),
info: Default::default(),
}));
for plugin in &plugins {
if let Ok(init) = plugin.table.get::<_, Function>("init") {
if let Err(e) = init.call::<_, ()>((plugin.table.clone(), )) {
warn!("error initializing plugin {}: {e}", plugin.id);
}
}
{
let server_any: AnyUserData = FromLua::from_lua(IntoLua::into_lua(server.clone(), &lua)?, &lua)?;
let plugins = load_plugins(&lua)?;
server_any.set_nth_user_value(1, plugins)?;
server_any.set_nth_user_value(2, Value::Nil)?;
lua.globals().set("srv", server_any.clone())?;
}
while let Ok(ev) = rx.recv() {
broadcast_event(&lua, "init", |_, pl, _, f| {
f.call(pl)
})?;
while let Some(ev) = rx.recv().await {
match ev {
ServerEvent::Join(player, info) => {
{
let mut guard = server.borrow_mut();
guard.names.insert(player.uuid, player.name.clone());
guard.info.insert(player.uuid, info);
}
for plugin in &plugins {
if let Ok(f) = plugin.table.get::<_, Function>("on_join") {
let res = f.call::<_, ()>((
plugin.table.clone(),
CellServer(server.clone()),
UuidUD(player.uuid),
player.name.clone()
));
if let Err(e) = res {
warn!("error responding to join event in plugin {}: {e}", plugin.id);
}
}
}
server.names.write().await.insert(player.uuid, player.name.clone());
server.info.write().await.insert(player.uuid, info);
broadcast_event(&lua, "on_join", |_, pl, _, f| {
f.call::<_, ()>((
pl,
UuidUD(player.uuid),
player.name.clone()
))
})?;
},
ServerEvent::Leave(player) => {
for plugin in &plugins {
if let Ok(f) = plugin.table.get::<_, Function>("on_leave") {
let res = f.call::<_, ()>((
plugin.table.clone(),
CellServer(server.clone()),
UuidUD(player.uuid),
player.name.clone()
));
if let Err(e) = res {
warn!("error responding to leave event in plugin {}: {e}", plugin.id);
}
}
}
{
let mut guard = server.borrow_mut();
guard.names.remove(&player.uuid);
guard.info.remove(&player.uuid);
}
}
broadcast_event(&lua, "on_leave", |_, pl, _, f| {
f.call::<_, ()>((
pl,
UuidUD(player.uuid),
player.name.clone()
))
})?;
server.names.write().await.remove(&player.uuid);
server.info.write().await.remove(&player.uuid);
},
ServerEvent::Message(player, msg) => {
broadcast_event(&lua, "on_message", |_, pl, _, f| {
f.call::<_, ()>((
pl,
msg.clone(),
UuidUD(player.uuid),
player.name.clone()
))
})?;
},
ServerEvent::Command(_, _) => () // TODO command registerment
}
}
Ok(())
}

View file

@ -1,76 +1,72 @@
use std::fs::DirEntry;
use std::{fs::{DirEntry, self}, io::ErrorKind};
use log::{debug, Record};
use mlua::{Function, FromLua, Lua, Table, Value};
use anyhow::anyhow;
use log::{debug, warn, info};
use mlua::{Lua, Table};
pub struct Plugin<'lua> {
pub table: Table<'lua>,
pub id: String,
}
use crate::QC_VERSION;
impl<'lua> FromLua<'lua> for Plugin<'lua> {
fn from_lua(lua_value: Value<'lua>, lua: &'lua Lua) -> mlua::Result<Self> {
let table = Table::from_lua(lua_value, lua)?;
Ok(Plugin {
id: table.get("id")?,
table,
})
fn load_plugin<'lua>(entry: &DirEntry, lua: &'lua Lua) -> anyhow::Result<(String, Table<'lua>)> {
let ty = entry.file_type()?;
let mut path = entry.path();
if ty.is_dir() {
path.push("main.lua");
}
}
let chunk = lua.load(path);
debug!("evaluating plugin");
let table: Table = chunk.eval()?;
fn lua_locinfo(lua: &Lua) -> Option<(u32, String)> {
let debug = lua.globals().get::<_, Table>("debug").ok()?;
let getinfo = debug.get::<_, Function>("getinfo").ok()?;
let data = getinfo.call::<_, Table>((2,)).ok()?;
let line = data.get("currentline").ok()?;
let file = data.get("short_src").ok()?;
Some((line, file))
}
let id = table.get("id")
.map_err(|e| anyhow!("could not get plugin id: {e}"))?;
fn lua_log(level: log::Level, path: &'static str, msg: String, lua: &Lua) {
let mut record = Record::builder();
record.level(level);
record.target(path);
record.module_path_static(Some(path));
let locinfo = lua_locinfo(lua);
if let Some((line, file)) = &locinfo {
record.line(Some(*line));
record.file(Some(file));
}
log::logger().log(&record.args(format_args!("{}", msg)).build());
}
const LEVELS: [(&str, log::Level); 5] = [
("trace", log::Level::Trace),
("debug", log::Level::Debug),
("info", log::Level::Info),
("warn", log::Level::Warn),
("error", log::Level::Error),
];
impl<'lua> Plugin<'lua> {
pub fn load(entry: &DirEntry, lua: &'lua Lua) -> anyhow::Result<Self> {
let ty = entry.file_type()?;
let mut path = entry.path();
if ty.is_dir() {
path.push("main.lua");
if let Ok(qc_ver) = table.get::<_, String>("server_version") {
debug!("checking plugin version");
let req = semver::VersionReq::parse(&qc_ver)?;
if !req.matches(&semver::Version::parse(QC_VERSION).unwrap()) {
return Err(anyhow!("incompatible with quectocraft version {QC_VERSION}: expected {req}"));
}
let chunk = lua.load(&path);
debug!("evaluating plugin");
let plugin = Self::from_lua(chunk.eval()?, lua)?;
// leak: plugins are only loaded at the beginning of the program
// and the path needs to live forever anyway, so this is fine
let path: &'static str = Box::leak(format!("qcplugin::{}", plugin.id).into_boxed_str());
for (name, level) in LEVELS {
plugin.table.set(name, lua.create_function(move |lua, (msg,): (String,)| {
lua_log(level, path, msg, lua);
Ok(())
})?)?;
}
Ok(plugin)
} else {
warn!("plugin '{id}' is missing a version reqirement");
}
Ok((id, table))
}
pub fn load_plugins(lua: &Lua) -> mlua::Result<Table> {
let plugins = lua.create_table()?;
debug!("loading plugins");
let entries = match fs::read_dir("./plugins") {
Ok(n) => n,
Err(e) if e.kind() == ErrorKind::NotFound => {
warn!("plugins directory does not exist, creating");
if let Err(e) = fs::create_dir("./plugins") {
warn!("failed to create plugins directory: {e}");
}
return Ok(plugins)
},
Err(e) => {
warn!("failed to load plugins: {e}");
return Ok(plugins)
}
};
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(e) => {
warn!("error reading entry: {e}");
continue
}
};
let path = entry.path();
let spath = path.to_str().unwrap_or("<non-unicode path>");
debug!("loading plugin at {spath}");
match load_plugin(&entry, lua) {
Ok((id, table)) => {
info!("loaded plugin {}", id);
plugins.set(id, table)?;
}
Err(e) => warn!("error loading plugin at {spath}: {e}"),
}
}
Ok(plugins)
}

View file

@ -1,15 +1,17 @@
use std::{collections::HashMap, cell::RefCell, rc::Rc};
use std::{collections::HashMap, sync::Arc};
use mlua::{Value, ToLua};
use tokio::sync::mpsc::Sender;
use log::{Record, warn};
use mlua::{Value, IntoLua, Lua, Table, Function, AnyUserData, LuaSerdeExt};
use serde_json::json;
use tokio::sync::{mpsc::Sender, RwLock};
use uuid::Uuid;
use crate::ClientInfo;
use crate::{ClientInfo, QC_VERSION, JsonValue};
use super::{event::PluginEvent, UuidUD};
impl<'lua> ToLua<'lua> for ClientInfo {
fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<Value<'lua>> {
impl<'lua> IntoLua<'lua> for ClientInfo {
fn into_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<Value<'lua>> {
let table = lua.create_table()?;
table.set("addr", self.addr)?;
table.set("port", self.port)?;
@ -19,43 +21,164 @@ impl<'lua> ToLua<'lua> for ClientInfo {
}
}
fn lua_locinfo(lua: &Lua) -> Option<(u32, String)> {
let debug = lua.globals().get::<_, Table>("debug").ok()?;
let getinfo = debug.get::<_, Function>("getinfo").ok()?;
let data = getinfo.call::<_, Table>((2,)).ok()?;
let line = data.get("currentline").ok()?;
let file = data.get("short_src").ok()?;
Some((line, file))
}
fn lua_log(level: log::Level, path: &str, msg: String, lua: &Lua) {
let mut record = Record::builder();
record.level(level);
record.target(path);
record.module_path(Some(path));
let locinfo = lua_locinfo(lua);
if let Some((line, file)) = &locinfo {
record.line(Some(*line));
record.file(Some(file));
}
log::logger().log(&record.args(format_args!("{}", msg)).build());
}
const LEVELS: [(&str, log::Level); 5] = [
("trace", log::Level::Trace),
("debug", log::Level::Debug),
("info", log::Level::Info),
("warn", log::Level::Warn),
("error", log::Level::Error),
];
fn value_to_chat<'lua>(lua: &'lua Lua, value: Value<'lua>) -> mlua::Result<JsonValue> {
match value {
Value::String(s) => Ok(json!{{"text": s.to_str()?}}),
Value::Nil => Ok(json!{{"text":""}}),
Value::Table(_) => Ok(lua.from_value(value)?),
_ => return Err(mlua::Error::RuntimeError("()".to_owned())),
}
}
pub struct Server {
pub tx: Sender<PluginEvent>,
pub names: HashMap<Uuid, String>,
pub info: HashMap<Uuid, ClientInfo>,
pub names: RwLock<HashMap<Uuid, String>>,
pub info: RwLock<HashMap<Uuid, ClientInfo>>,
}
#[derive(Clone)]
pub struct CellServer(pub Rc<RefCell<Server>>);
pub struct WrappedServer(pub Arc<Server>);
impl mlua::UserData for CellServer {
impl std::ops::Deref for WrappedServer {
type Target = Server;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl mlua::UserData for WrappedServer {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_method("players", |lua, this, ()| {
// commands
methods.add_async_method("add_command", |_lua, _this, ()| async {
warn!("TODO add command");
Ok(())
});
// players
methods.add_async_method("players", |lua, this, ()| async {
let table = lua.create_table()?;
for (k, v) in &this.0.borrow().names {
for (k, v) in this.names.read().await.iter() {
table.set(UuidUD(*k), v.clone())?;
}
Ok(table)
});
methods.add_method("get_name", |lua, this, UuidUD(uuid)| {
match this.0.borrow().names.get(&uuid) {
Some(s) => s.clone().to_lua(lua),
methods.add_async_method("get_name", |lua, this, UuidUD(uuid)| async move {
match this.info.read().await.get(&uuid) {
Some(s) => s.clone().into_lua(lua),
None => Ok(Value::Nil),
}
});
methods.add_method("get_info", |lua, this, UuidUD(uuid)| {
match this.0.borrow().info.get(&uuid) {
Some(i) => i.clone().to_lua(lua),
methods.add_async_method("get_info", |lua, this, UuidUD(uuid)| async move {
match this.info.read().await.get(&uuid) {
Some(i) => i.clone().into_lua(lua),
None => Ok(Value::Nil)
}
});
methods.add_async_method("kick", |_, CellServer(this), (UuidUD(uuid), msg)| async move {
let tx = this.borrow().tx.clone();
tx
// events
methods.add_async_method("kick", |_, this, (UuidUD(uuid), msg)| async move {
this.tx
.send(PluginEvent::Kick(uuid, msg))
.await
.map_err(|e| mlua::Error::RuntimeError(e.to_string()))
});
methods.add_async_method("send_msg", |lua, this, (UuidUD(uuid), msg, overlay): (_, Value, Option<bool>)| async move {
let msg = match msg {
Value::String(s) => json!{{"text": s.to_str()?}},
Value::Table(_) => lua.from_value(msg)?,
_ => return Err(mlua::Error::RuntimeError("()".to_owned())),
};
this.tx
.send(PluginEvent::System(uuid, msg, overlay == Some(true)))
.await
.map_err(|e| mlua::Error::RuntimeError(e.to_string()))
});
methods.add_async_method("broadcast_msg", |lua, this, (msg, overlay): (Value, Option<bool>)| async move {
let msg = match msg {
Value::String(s) => json!{{"text": s.to_str()?}},
Value::Table(_) => lua.from_value(msg)?,
_ => return Err(mlua::Error::RuntimeError("()".to_owned())),
};
this.tx
.send(PluginEvent::BroadcastSystem(msg, overlay == Some(true)))
.await
.map_err(|e| mlua::Error::RuntimeError(e.to_string()))
});
methods.add_async_method("send_chat", |lua, this, (UuidUD(uuid), msg, name): (_, Value, Value)| async move {
let msg = value_to_chat(lua, msg)?;
let name = value_to_chat(lua, name)?;
this.tx
.send(PluginEvent::Chat(uuid, msg, name))
.await
.map_err(|e| mlua::Error::RuntimeError(e.to_string()))
});
methods.add_async_method("broadcast_chat", |lua, this, (msg, name): (Value, Value)| async move {
let msg = value_to_chat(lua, msg)?;
let name = value_to_chat(lua, name)?;
this.tx
.send(PluginEvent::BroadcastChat(msg, name))
.await
.map_err(|e| mlua::Error::RuntimeError(e.to_string()))
});
// utility
methods.add_function("check_semver", |_, (ver, req): (String, String)| {
let ver = semver::Version::parse(&ver)
.map_err(|_| mlua::Error::runtime(format!("invalid version: {ver}")))?;
let req = semver::VersionReq::parse(&req)
.map_err(|_| mlua::Error::runtime(format!("invalid version request: {req}")))?;
Ok(req.matches(&ver))
});
// logging
for (name, level) in LEVELS {
methods.add_function(name, move |lua, (this, msg): (AnyUserData, String)| {
let mut id: String = this.nth_user_value(2)?;
id.insert_str(0, "plugin::");
lua_log(level, &id, msg, lua);
Ok(())
});
}
}
fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field("version", QC_VERSION);
fields.add_field_function_get("plugins", |_, this| {
this.nth_user_value::<Table>(1)
})
}
}

View file

@ -1,6 +1,7 @@
use std::io::{Write, Read};
use anyhow::anyhow;
use log::warn;
use super::{ServerPacket, ClientPacket};
use crate::{varint::VarInt, ser::{Serializable, Deserializable}};
@ -53,7 +54,7 @@ fn decode(mut r: Box<&mut dyn Read>, state: ProtocolState, _len: i32, id: i32) -
Ok(Some(ClientPacket::PingRequest(payload)))
},
_ => {
println!("invalid id {id:#x} in state {state:?}");
warn!("invalid id {id:#x} in state {state:?}");
Ok(None)
}
}

View file

@ -5,8 +5,11 @@ use uuid::Uuid;
use crate::{JsonValue, ser::Position};
mod common;
mod v1_19_4;
mod v1_20_1;
mod v1_19_4;
mod v1_18_2;
mod v1_17_1;
mod v1_16_5;
pub use common::COMMON as COMMON;
@ -49,6 +52,8 @@ pub enum ClientPacket {
data: Vec<u8>
},
KeepAlive(i64),
ChatMessage(String),
Command(String),
}
#[derive(Clone, Debug)]
@ -76,6 +81,13 @@ pub enum ServerPacket {
z: i32,
},
SetDefaultSpawn { pos: Position, angle: f32 },
PositionAndLook {
pos: [f64; 3],
look: [f32; 2],
flags: u8,
},
SystemMessage(JsonValue, bool),
ChatMessage(JsonValue, JsonValue),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@ -90,6 +102,9 @@ pub const fn get_protocol(version: i32) -> Option<Protocol> {
match version {
v1_20_1::VERSION => Some(v1_20_1::PROTOCOL),
v1_19_4::VERSION => Some(v1_19_4::PROTOCOL),
v1_18_2::VERSION => Some(v1_18_2::PROTOCOL),
v1_17_1::VERSION => Some(v1_17_1::PROTOCOL),
v1_16_5::VERSION => Some(v1_16_5::PROTOCOL),
_ => None,
}
}

138
src/protocol/v1_16_5.rs Normal file
View file

@ -0,0 +1,138 @@
use std::io::{Write, Read};
use log::trace;
use crate::{ser::{Serializable, Deserializable}, varint::VarInt};
use super::{ServerPacket, ClientPacket, Protocol, ProtocolState, common::COMMON};
pub const VERSION: i32 = 754;
pub const PROTOCOL: Protocol = Protocol {
name: "1.16.5",
version: VERSION,
encode,
decode,
};
fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> anyhow::Result<()> {
trace!("1.18.2 encoding {ev:?}");
match ev {
ServerPacket::LoginSuccess { name, uuid } => {
VarInt(0x02).serialize(&mut w)?;
uuid.serialize(&mut w)?;
name.serialize(&mut w)?;
},
ServerPacket::ChatMessage(msg, _) => {
VarInt(0x0E).serialize(&mut w)?;
msg.serialize(&mut w)?;
0u8.serialize(&mut w)?; // chat
0u128.serialize(&mut w)?; // null uuid
},
ServerPacket::SystemMessage(msg, overlay) => {
VarInt(0x0E).serialize(&mut w)?;
msg.serialize(&mut w)?;
if overlay { 2u8 } else { 1u8 } // system or overlay
.serialize(&mut w)?;
0u128.serialize(&mut w)?; // null uuid
},
ServerPacket::PlayDisconnect(msg) => {
VarInt(0x19).serialize(&mut w)?;
msg.serialize(&mut w)?;
},
ServerPacket::KeepAlive(data) => {
VarInt(0x1F).serialize(&mut w)?;
data.serialize(&mut w)?;
},
ServerPacket::PluginMessage { channel, data } => {
VarInt(0x17).serialize(&mut w)?;
channel.serialize(&mut w)?;
w.write_all(&data)?;
},
ServerPacket::ChunkData { x, z } => {
VarInt(0x20).serialize(&mut w)?;
// chunk x
x.serialize(&mut w)?;
// chunk z
z.serialize(&mut w)?;
// full chunk
true.serialize(&mut w)?;
// bit mask
VarInt(0).serialize(&mut w)?;
// heightmap
include_bytes!("../resources/heightmap.nbt").serialize(&mut w)?;
// biomes
VarInt(1024).serialize(&mut w)?;
[VarInt(127); 1024].serialize(&mut w)?;
// chunk data size and chunk data
VarInt(0).serialize(&mut w)?;
// block entities
VarInt(0).serialize(&mut w)?;
},
ServerPacket::JoinGame { eid, gamemode, hardcode } => {
VarInt(0x24).serialize(&mut w)?;
eid.serialize(&mut w)?;
hardcode.serialize(&mut w)?;
gamemode.serialize(&mut w)?;
(-1i8).serialize(&mut w)?; // prev gamemode undefined
VarInt(1).serialize(&mut w)?; // dimension count
"qc:world".serialize(&mut w)?; // register one dimension
include_bytes!("../resources/dimcodec_1.16.5.nbt").serialize(&mut w)?; // dimension codec
include_bytes!("../resources/dimension_1.16.5.nbt").serialize(&mut w)?; // dimension data
"qc:world".serialize(&mut w)?; // dimension name
0i64.serialize(&mut w)?; // seed
VarInt(65535).serialize(&mut w)?; // max players
VarInt(8).serialize(&mut w)?; // view dist
false.serialize(&mut w)?; // reduce debug info
false.serialize(&mut w)?; // respawn screen
false.serialize(&mut w)?; // is debug
false.serialize(&mut w)?; // is flat
},
ServerPacket::PositionAndLook { pos, look, flags } => {
VarInt(0x34).serialize(&mut w)?;
pos.serialize(&mut w)?;
look.serialize(&mut w)?;
flags.serialize(&mut w)?;
VarInt(0).serialize(&mut w)?; // teleport id
}
ServerPacket::SetDefaultSpawn { pos, angle: _ } => {
VarInt(0x42).serialize(&mut w)?;
pos.serialize(&mut w)?;
},
_ => { (COMMON.encode)(w, state, ev)?; }
}
Ok(())
}
fn decode(mut r: Box<&mut dyn Read>, state: ProtocolState, len: i32, id: i32) -> anyhow::Result<Option<ClientPacket>> {
trace!("1.18.2 decoding {state:?} {id}");
type Ps = ProtocolState;
match (state, id) {
(Ps::Login, 0x00) => {
let name = String::deserialize(&mut r)?;
Ok(Some(ClientPacket::LoginStart { name, uuid: None }))
}
(Ps::Login, 0x01 | 0x02) => Ok(None), // unsupported
(Ps::Play, 0x03) => {
let mut msg = String::deserialize(&mut r)?;
if msg.starts_with('/') {
msg.remove(0);
Ok(Some(ClientPacket::Command(msg)))
} else {
Ok(Some(ClientPacket::ChatMessage(msg)))
}
},
(Ps::Play, 0x0A) => {
let channel = String::deserialize(&mut r)?;
let mut data = Vec::new();
r.read_to_end(&mut data)?;
Ok(Some(ClientPacket::PluginMessage { channel, data }))
}
(Ps::Play, 0x10) => {
let data = i64::deserialize(&mut r)?;
Ok(Some(ClientPacket::KeepAlive(data)))
}
(Ps::Play, 0x11 | 0x12 | 0x13 | 0x14) => Ok(None), // position & rotation
_ => (COMMON.decode)(r, state, len, id)
}
}

138
src/protocol/v1_17_1.rs Normal file
View file

@ -0,0 +1,138 @@
use std::io::{Write, Read};
use log::trace;
use crate::{ser::{Serializable, Deserializable}, varint::VarInt};
use super::{ServerPacket, ClientPacket, Protocol, ProtocolState, common::COMMON};
pub const VERSION: i32 = 756;
pub const PROTOCOL: Protocol = Protocol {
name: "1.17.1",
version: VERSION,
encode,
decode,
};
fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> anyhow::Result<()> {
trace!("1.18.2 encoding {ev:?}");
match ev {
ServerPacket::LoginSuccess { name, uuid } => {
VarInt(0x02).serialize(&mut w)?;
uuid.serialize(&mut w)?;
name.serialize(&mut w)?;
},
ServerPacket::ChatMessage(msg, _) => {
VarInt(0x0F).serialize(&mut w)?;
msg.serialize(&mut w)?;
0u8.serialize(&mut w)?; // chat
0u128.serialize(&mut w)?; // null uuid
},
ServerPacket::SystemMessage(msg, overlay) => {
VarInt(0x0F).serialize(&mut w)?;
msg.serialize(&mut w)?;
if overlay { 2u8 } else { 1u8 } // system or overlay
.serialize(&mut w)?;
0u128.serialize(&mut w)?; // null uuid
},
ServerPacket::PluginMessage { channel, data } => {
VarInt(0x18).serialize(&mut w)?;
channel.serialize(&mut w)?;
w.write_all(&data)?;
},
ServerPacket::PlayDisconnect(msg) => {
VarInt(0x1A).serialize(&mut w)?;
msg.serialize(&mut w)?;
},
ServerPacket::KeepAlive(data) => {
VarInt(0x21).serialize(&mut w)?;
data.serialize(&mut w)?;
},
ServerPacket::ChunkData { x, z } => {
VarInt(0x22).serialize(&mut w)?;
// chunk x
x.serialize(&mut w)?;
// chunk z
z.serialize(&mut w)?;
// bit mask
VarInt(0).serialize(&mut w)?;
// heightmap
include_bytes!("../resources/heightmap.nbt").serialize(&mut w)?;
// biomes
VarInt(1024).serialize(&mut w)?;
[VarInt(127); 1024].serialize(&mut w)?;
// chunk data size and chunk data
VarInt(0).serialize(&mut w)?;
// block entities
VarInt(0).serialize(&mut w)?;
},
ServerPacket::JoinGame { eid, gamemode, hardcode } => {
VarInt(0x26).serialize(&mut w)?;
eid.serialize(&mut w)?;
hardcode.serialize(&mut w)?;
gamemode.serialize(&mut w)?;
(-1i8).serialize(&mut w)?; // prev gamemode undefined
VarInt(1).serialize(&mut w)?; // dimension count
"qc:world".serialize(&mut w)?; // register one dimension
include_bytes!("../resources/dimcodec_1.17.1.nbt").serialize(&mut w)?; // dimension codec
include_bytes!("../resources/dimension_1.17.1.nbt").serialize(&mut w)?; // dimension data
"qc:world".serialize(&mut w)?; // dimension name
0i64.serialize(&mut w)?; // seed
VarInt(65535).serialize(&mut w)?; // max players
VarInt(8).serialize(&mut w)?; // view dist
false.serialize(&mut w)?; // reduce debug info
false.serialize(&mut w)?; // respawn screen
false.serialize(&mut w)?; // is debug
false.serialize(&mut w)?; // is flat
},
ServerPacket::PositionAndLook { pos, look, flags } => {
VarInt(0x38).serialize(&mut w)?;
pos.serialize(&mut w)?;
look.serialize(&mut w)?;
flags.serialize(&mut w)?;
VarInt(0).serialize(&mut w)?; // teleport id
false.serialize(&mut w)?; // dismount
}
ServerPacket::SetDefaultSpawn { pos, angle } => {
VarInt(0x4B).serialize(&mut w)?;
pos.serialize(&mut w)?;
angle.serialize(&mut w)?;
},
_ => { (COMMON.encode)(w, state, ev)?; }
}
Ok(())
}
fn decode(mut r: Box<&mut dyn Read>, state: ProtocolState, len: i32, id: i32) -> anyhow::Result<Option<ClientPacket>> {
trace!("1.18.2 decoding {state:?} {id}");
type Ps = ProtocolState;
match (state, id) {
(Ps::Login, 0x00) => {
let name = String::deserialize(&mut r)?;
Ok(Some(ClientPacket::LoginStart { name, uuid: None }))
}
(Ps::Login, 0x01 | 0x02) => Ok(None), // unsupported
(Ps::Play, 0x03) => {
let mut msg = String::deserialize(&mut r)?;
if msg.starts_with('/') {
msg.remove(0);
Ok(Some(ClientPacket::Command(msg)))
} else {
Ok(Some(ClientPacket::ChatMessage(msg)))
}
},
(Ps::Play, 0x0A) => {
let channel = String::deserialize(&mut r)?;
let mut data = Vec::new();
r.read_to_end(&mut data)?;
Ok(Some(ClientPacket::PluginMessage { channel, data }))
}
(Ps::Play, 0x0F) => {
let data = i64::deserialize(&mut r)?;
Ok(Some(ClientPacket::KeepAlive(data)))
}
(Ps::Play, 0x11 | 0x12 | 0x13) => Ok(None), // position & rotation
_ => (COMMON.decode)(r, state, len, id)
}
}

146
src/protocol/v1_18_2.rs Normal file
View file

@ -0,0 +1,146 @@
use std::io::{Write, Read};
use log::trace;
use crate::{ser::{Serializable, Deserializable}, varint::VarInt};
use super::{ServerPacket, ClientPacket, Protocol, ProtocolState, common::COMMON};
pub const VERSION: i32 = 758;
pub const PROTOCOL: Protocol = Protocol {
name: "1.18.2",
version: VERSION,
encode,
decode,
};
fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> anyhow::Result<()> {
trace!("1.18.2 encoding {ev:?}");
match ev {
ServerPacket::LoginSuccess { name, uuid } => {
VarInt(0x02).serialize(&mut w)?;
uuid.serialize(&mut w)?;
name.serialize(&mut w)?;
},
ServerPacket::ChatMessage(msg, _) => {
VarInt(0x0F).serialize(&mut w)?;
msg.serialize(&mut w)?;
0u8.serialize(&mut w)?; // chat
0u128.serialize(&mut w)?; // null uuid
},
ServerPacket::SystemMessage(msg, overlay) => {
VarInt(0x0F).serialize(&mut w)?;
msg.serialize(&mut w)?;
if overlay { 2u8 } else { 1u8 } // system or overlay
.serialize(&mut w)?;
0u128.serialize(&mut w)?; // null uuid
},
ServerPacket::PluginMessage { channel, data } => {
VarInt(0x18).serialize(&mut w)?;
channel.serialize(&mut w)?;
w.write_all(&data)?;
},
ServerPacket::PlayDisconnect(msg) => {
VarInt(0x1A).serialize(&mut w)?;
msg.serialize(&mut w)?;
},
ServerPacket::KeepAlive(data) => {
VarInt(0x21).serialize(&mut w)?;
data.serialize(&mut w)?;
},
ServerPacket::ChunkData { x, z } => {
VarInt(0x22).serialize(&mut w)?;
// chunk x
x.serialize(&mut w)?;
// chunk z
z.serialize(&mut w)?;
// heightmap
include_bytes!("../resources/heightmap.nbt").serialize(&mut w)?;
// chunk data size and chunk data
VarInt(192).serialize(&mut w)?;
[0u8; 192].serialize(&mut w)?;
// block entities
VarInt(0).serialize(&mut w)?;
// trust edges
true.serialize(&mut w)?;
// light masks
VarInt(0).serialize(&mut w)?;
VarInt(0).serialize(&mut w)?;
VarInt(0).serialize(&mut w)?;
VarInt(0).serialize(&mut w)?;
// sky light array len
VarInt(0).serialize(&mut w)?;
// block light array len
VarInt(0).serialize(&mut w)?;
},
ServerPacket::JoinGame { eid, gamemode, hardcode } => {
VarInt(0x26).serialize(&mut w)?;
eid.serialize(&mut w)?;
hardcode.serialize(&mut w)?;
gamemode.serialize(&mut w)?;
(-1i8).serialize(&mut w)?; // prev gamemode undefined
VarInt(1).serialize(&mut w)?; // dimension count
"qc:world".serialize(&mut w)?; // register one dimension
include_bytes!("../resources/dimcodec_1.18.2.nbt").serialize(&mut w)?; // dimension codec
include_bytes!("../resources/dimension_1.18.2.nbt").serialize(&mut w)?; // dimension data
"qc:world".serialize(&mut w)?; // dimension name
0i64.serialize(&mut w)?; // seed
VarInt(65535).serialize(&mut w)?; // max players
VarInt(8).serialize(&mut w)?; // view dist
VarInt(0).serialize(&mut w)?; // sim dist
false.serialize(&mut w)?; // reduce debug info
false.serialize(&mut w)?; // respawn screen
false.serialize(&mut w)?; // is debug
false.serialize(&mut w)?; // is flat
},
ServerPacket::PositionAndLook { pos, look, flags } => {
VarInt(0x38).serialize(&mut w)?;
pos.serialize(&mut w)?;
look.serialize(&mut w)?;
flags.serialize(&mut w)?;
VarInt(0).serialize(&mut w)?;
false.serialize(&mut w)?;
},
ServerPacket::SetDefaultSpawn { pos, angle } => {
VarInt(0x4B).serialize(&mut w)?;
pos.serialize(&mut w)?;
angle.serialize(&mut w)?;
},
_ => { (COMMON.encode)(w, state, ev)?; }
}
Ok(())
}
fn decode(mut r: Box<&mut dyn Read>, state: ProtocolState, len: i32, id: i32) -> anyhow::Result<Option<ClientPacket>> {
trace!("1.18.2 decoding {state:?} {id}");
type Ps = ProtocolState;
match (state, id) {
(Ps::Login, 0x00) => {
let name = String::deserialize(&mut r)?;
Ok(Some(ClientPacket::LoginStart { name, uuid: None }))
}
(Ps::Login, 0x01 | 0x02) => Ok(None), // unsupported
(Ps::Play, 0x03) => {
let mut msg = String::deserialize(&mut r)?;
if msg.starts_with('/') {
msg.remove(0);
Ok(Some(ClientPacket::Command(msg)))
} else {
Ok(Some(ClientPacket::ChatMessage(msg)))
}
},
(Ps::Play, 0x0A) => {
let channel = String::deserialize(&mut r)?;
let mut data = Vec::new();
r.read_to_end(&mut data)?;
Ok(Some(ClientPacket::PluginMessage { channel, data }))
}
(Ps::Play, 0x0F) => {
let data = i64::deserialize(&mut r)?;
Ok(Some(ClientPacket::KeepAlive(data)))
}
(Ps::Play, 0x11 | 0x12 | 0x13) => Ok(None), // position & rotation
_ => (COMMON.decode)(r, state, len, id)
}
}

View file

@ -1,5 +1,6 @@
use std::io::{Write, Read};
use log::trace;
use uuid::Uuid;
use crate::{ser::{Serializable, Deserializable}, varint::VarInt};
@ -16,6 +17,7 @@ pub const PROTOCOL: Protocol = Protocol {
};
fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> anyhow::Result<()> {
trace!("1.19. encoding {ev:?}");
match ev {
ServerPacket::LoginSuccess { name, uuid } => {
VarInt(0x02).serialize(&mut w)?;
@ -24,40 +26,26 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) ->
// number of properties (0)
VarInt(0x00).serialize(&mut w)?;
},
ServerPacket::JoinGame { eid, gamemode, hardcode } => {
VarInt(0x28).serialize(&mut w)?;
eid.serialize(&mut w)?;
hardcode.serialize(&mut w)?;
gamemode.serialize(&mut w)?;
(-1i8).serialize(&mut w)?; // prev gamemode undefined
VarInt(1).serialize(&mut w)?; // dimension count
"qc:world".serialize(&mut w)?; // register one dimension
include_bytes!("registry.nbt").serialize(&mut w)?; // registry codec
"minecraft:the_end".serialize(&mut w)?; // dimension type
"qc:world".serialize(&mut w)?; // dimension name
0i64.serialize(&mut w)?; // seed
VarInt(65535).serialize(&mut w)?; // max players
VarInt(8).serialize(&mut w)?; // view dist
VarInt(0).serialize(&mut w)?; // sim dist
false.serialize(&mut w)?; // reduce debug info
false.serialize(&mut w)?; // respawn screen
false.serialize(&mut w)?; // is debug
false.serialize(&mut w)?; // is flat
false.serialize(&mut w)?; // has death location
},
ServerPacket::KeepAlive(data) => {
VarInt(0x23).serialize(&mut w)?;
data.serialize(&mut w)?;
},
ServerPacket::PlayDisconnect(msg) => {
VarInt(0x1A).serialize(&mut w)?;
msg.serialize(&mut w)?;
},
ServerPacket::ChatMessage(msg, name) => {
VarInt(0x1B).serialize(&mut w)?;
msg.serialize(&mut w)?;
VarInt(7).serialize(&mut w)?; // chat type from registry codec
name.serialize(&mut w)?; // chat type name
false.serialize(&mut w)?; // no target
},
ServerPacket::PluginMessage { channel, data } => {
VarInt(0x17).serialize(&mut w)?;
channel.serialize(&mut w)?;
w.write_all(&data)?;
},
ServerPacket::KeepAlive(data) => {
VarInt(0x23).serialize(&mut w)?;
data.serialize(&mut w)?;
},
ServerPacket::ChunkData { x, z } => {
VarInt(0x24).serialize(&mut w)?;
// chunk x
@ -65,26 +53,10 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) ->
// chunk z
z.serialize(&mut w)?;
// heightmap
let hmdata = [0i64; 37];
let mut heightmap = nbt::Blob::new();
heightmap.insert("MOTION_BLOCKING", hmdata.as_ref()).unwrap();
heightmap.serialize(&mut w)?;
// chunk data
let mut chunk_data: Vec<u8> = Vec::new();
for _ in 0..(384 / 16) {
// number of non-air blocks
(0i16).serialize(&mut chunk_data)?;
// block states
(0u8).serialize(&mut chunk_data)?;
VarInt(0).serialize(&mut chunk_data)?;
VarInt(0).serialize(&mut chunk_data)?;
// biomes
(0u8).serialize(&mut chunk_data)?;
VarInt(0).serialize(&mut chunk_data)?;
VarInt(0).serialize(&mut chunk_data)?;
}
VarInt(chunk_data.len() as i32).serialize(&mut w)?;
w.write_all(&chunk_data)?;
include_bytes!("../resources/heightmap.nbt").serialize(&mut w)?;
// chunk data size and chunk data
VarInt(192).serialize(&mut w)?;
[0u8; 192].serialize(&mut w)?;
// block entities
VarInt(0).serialize(&mut w)?;
// trust edges
@ -99,17 +71,51 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) ->
// block light array len
VarInt(0).serialize(&mut w)?;
},
ServerPacket::JoinGame { eid, gamemode, hardcode } => {
VarInt(0x28).serialize(&mut w)?;
eid.serialize(&mut w)?;
hardcode.serialize(&mut w)?;
gamemode.serialize(&mut w)?;
(-1i8).serialize(&mut w)?; // prev gamemode undefined
VarInt(1).serialize(&mut w)?; // dimension count
"qc:world".serialize(&mut w)?; // register one dimension
include_bytes!("../resources/registry_1.19.4.nbt").serialize(&mut w)?; // registry codec
"minecraft:the_end".serialize(&mut w)?; // dimension type
"qc:world".serialize(&mut w)?; // dimension name
0i64.serialize(&mut w)?; // seed
VarInt(65535).serialize(&mut w)?; // max players
VarInt(8).serialize(&mut w)?; // view dist
VarInt(0).serialize(&mut w)?; // sim dist
false.serialize(&mut w)?; // reduce debug info
false.serialize(&mut w)?; // respawn screen
false.serialize(&mut w)?; // is debug
false.serialize(&mut w)?; // is flat
false.serialize(&mut w)?; // has death location
},
ServerPacket::PositionAndLook { pos, look, flags } => {
VarInt(0x3C).serialize(&mut w)?;
pos.serialize(&mut w)?;
look.serialize(&mut w)?;
flags.serialize(&mut w)?;
VarInt(0).serialize(&mut w)?;
},
ServerPacket::SetDefaultSpawn { pos, angle } => {
VarInt(0x50).serialize(&mut w)?;
pos.serialize(&mut w)?;
angle.serialize(&mut w)?;
},
ServerPacket::SystemMessage(msg, overlay) => {
VarInt(0x64).serialize(&mut w)?;
msg.serialize(&mut w)?;
overlay.serialize(&mut w)?;
},
_ => { (COMMON.encode)(w, state, ev)?; }
}
Ok(())
}
fn decode(mut r: Box<&mut dyn Read>, state: ProtocolState, len: i32, id: i32) -> anyhow::Result<Option<ClientPacket>> {
trace!("1.19.4 decoding {state:?} {id}");
type Ps = ProtocolState;
match (state, id) {
(Ps::Login, 0x00) => {
@ -121,6 +127,14 @@ fn decode(mut r: Box<&mut dyn Read>, state: ProtocolState, len: i32, id: i32) ->
Ok(Some(ClientPacket::LoginStart { name, uuid }))
}
(Ps::Login, 0x01 | 0x02) => Ok(None), // unsupported
(Ps::Play, 0x04) => {
let cmd = String::deserialize(&mut r)?;
Ok(Some(ClientPacket::Command(cmd)))
},
(Ps::Play, 0x05) => {
let msg = String::deserialize(&mut r)?;
Ok(Some(ClientPacket::ChatMessage(msg)))
},
(Ps::Play, 0x0D) => {
let channel = String::deserialize(&mut r)?;
let mut data = Vec::new();

View file

@ -1,5 +1,6 @@
use std::io::{Write, Read};
use log::trace;
use uuid::Uuid;
use crate::{ser::{Serializable, Deserializable}, varint::VarInt};
@ -16,6 +17,7 @@ pub const PROTOCOL: Protocol = Protocol {
};
fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> anyhow::Result<()> {
trace!("1.20.1 encoding {ev:?}");
match ev {
ServerPacket::LoginSuccess { name, uuid } => {
VarInt(0x02).serialize(&mut w)?;
@ -24,6 +26,49 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) ->
// number of properties (0)
VarInt(0x00).serialize(&mut w)?;
},
ServerPacket::PluginMessage { channel, data } => {
VarInt(0x17).serialize(&mut w)?;
channel.serialize(&mut w)?;
w.write_all(&data)?;
},
ServerPacket::PlayDisconnect(msg) => {
VarInt(0x1A).serialize(&mut w)?;
msg.serialize(&mut w)?;
},
ServerPacket::ChatMessage(msg, name) => {
VarInt(0x1B).serialize(&mut w)?;
msg.serialize(&mut w)?;
VarInt(7).serialize(&mut w)?; // chat type from registry codec
name.serialize(&mut w)?;
false.serialize(&mut w)?; // no target
},
ServerPacket::KeepAlive(data) => {
VarInt(0x23).serialize(&mut w)?;
data.serialize(&mut w)?;
},
ServerPacket::ChunkData { x, z } => {
VarInt(0x24).serialize(&mut w)?;
// chunk x
x.serialize(&mut w)?;
// chunk z
z.serialize(&mut w)?;
// heightmap
include_bytes!("../resources/heightmap.nbt").serialize(&mut w)?;
// chunk data size and chunk data
VarInt(192).serialize(&mut w)?;
[0u8; 192].serialize(&mut w)?;
// block entities
VarInt(0).serialize(&mut w)?;
// light masks
VarInt(0).serialize(&mut w)?;
VarInt(0).serialize(&mut w)?;
VarInt(0).serialize(&mut w)?;
VarInt(0).serialize(&mut w)?;
// sky light array len
VarInt(0).serialize(&mut w)?;
// block light array len
VarInt(0).serialize(&mut w)?;
},
ServerPacket::JoinGame { eid, gamemode, hardcode } => {
VarInt(0x28).serialize(&mut w)?;
eid.serialize(&mut w)?;
@ -32,7 +77,7 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) ->
(-1i8).serialize(&mut w)?; // prev gamemode undefined
VarInt(1).serialize(&mut w)?; // dimension count
"qc:world".serialize(&mut w)?; // register one dimension
include_bytes!("registry.nbt").serialize(&mut w)?; // registry codec
include_bytes!("../resources/registry_1.20.1.nbt").serialize(&mut w)?; // registry codec
"minecraft:the_end".serialize(&mut w)?; // dimension type
"qc:world".serialize(&mut w)?; // dimension name
0i64.serialize(&mut w)?; // seed
@ -46,56 +91,11 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) ->
false.serialize(&mut w)?; // has death location
VarInt(1).serialize(&mut w)?; // portal cooldown
},
ServerPacket::KeepAlive(data) => {
VarInt(0x23).serialize(&mut w)?;
data.serialize(&mut w)?;
},
ServerPacket::PlayDisconnect(msg) => {
VarInt(0x1A).serialize(&mut w)?;
msg.serialize(&mut w)?;
},
ServerPacket::PluginMessage { channel, data } => {
VarInt(0x17).serialize(&mut w)?;
channel.serialize(&mut w)?;
w.write_all(&data)?;
},
ServerPacket::ChunkData { x, z } => {
VarInt(0x24).serialize(&mut w)?;
// chunk x
x.serialize(&mut w)?;
// chunk z
z.serialize(&mut w)?;
// heightmap
let hmdata = [0i64; 37];
let mut heightmap = nbt::Blob::new();
heightmap.insert("MOTION_BLOCKING", hmdata.as_ref()).unwrap();
heightmap.serialize(&mut w)?;
// chunk data
let mut chunk_data: Vec<u8> = Vec::new();
for _ in 0..(384 / 16) {
// number of non-air blocks
(0i16).serialize(&mut chunk_data)?;
// block states
(0u8).serialize(&mut chunk_data)?;
VarInt(0).serialize(&mut chunk_data)?;
VarInt(0).serialize(&mut chunk_data)?;
// biomes
(0u8).serialize(&mut chunk_data)?;
VarInt(0).serialize(&mut chunk_data)?;
VarInt(0).serialize(&mut chunk_data)?;
}
VarInt(chunk_data.len() as i32).serialize(&mut w)?;
w.write_all(&chunk_data)?;
// block entities
VarInt(0).serialize(&mut w)?;
// light masks
VarInt(0).serialize(&mut w)?;
VarInt(0).serialize(&mut w)?;
VarInt(0).serialize(&mut w)?;
VarInt(0).serialize(&mut w)?;
// sky light array len
VarInt(0).serialize(&mut w)?;
// block light array len
ServerPacket::PositionAndLook { pos, look, flags } => {
VarInt(0x3C).serialize(&mut w)?;
pos.serialize(&mut w)?;
look.serialize(&mut w)?;
flags.serialize(&mut w)?;
VarInt(0).serialize(&mut w)?;
},
ServerPacket::SetDefaultSpawn { pos, angle } => {
@ -103,12 +103,18 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) ->
pos.serialize(&mut w)?;
angle.serialize(&mut w)?;
},
ServerPacket::SystemMessage(msg, overlay) => {
VarInt(0x64).serialize(&mut w)?;
msg.serialize(&mut w)?;
overlay.serialize(&mut w)?;
},
_ => { (COMMON.encode)(w, state, ev)?; }
}
Ok(())
}
fn decode(mut r: Box<&mut dyn Read>, state: ProtocolState, len: i32, id: i32) -> anyhow::Result<Option<ClientPacket>> {
trace!("1.20.1 decoding {state:?} {id}");
type Ps = ProtocolState;
match (state, id) {
(Ps::Login, 0x00) => {
@ -120,16 +126,24 @@ fn decode(mut r: Box<&mut dyn Read>, state: ProtocolState, len: i32, id: i32) ->
Ok(Some(ClientPacket::LoginStart { name, uuid }))
}
(Ps::Login, 0x01 | 0x02) => Ok(None), // unsupported
(Ps::Play, 0x04) => {
let cmd = String::deserialize(&mut r)?;
Ok(Some(ClientPacket::Command(cmd)))
},
(Ps::Play, 0x05) => {
let msg = String::deserialize(&mut r)?;
Ok(Some(ClientPacket::ChatMessage(msg)))
},
(Ps::Play, 0x0D) => {
let channel = String::deserialize(&mut r)?;
let mut data = Vec::new();
r.read_to_end(&mut data)?;
Ok(Some(ClientPacket::PluginMessage { channel, data }))
}
},
(Ps::Play, 0x12) => {
let data = i64::deserialize(&mut r)?;
Ok(Some(ClientPacket::KeepAlive(data)))
}
},
(Ps::Play, 0x14 | 0x15 | 0x16) => Ok(None), // position & rotation
_ => (COMMON.decode)(r, state, len, id)
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/resources/heightmap.nbt Normal file

Binary file not shown.

View file

@ -1,7 +1,9 @@
use std::{collections::HashMap, time::Duration, net::ToSocketAddrs};
use std::{collections::HashMap, time::Duration, net::{ToSocketAddrs, SocketAddr}};
use log::{info, error};
use tokio::{net::TcpListener, sync::mpsc::{Sender, self}, select};
use tokio::{net::{TcpListener, TcpStream}, sync::mpsc::{Sender, self}, select};
use uuid::Uuid;
use anyhow::anyhow;
use crate::{client::run_client, Player, ClientInfo, client::event::{ServerEvent, ClientEvent}, plugin::{run_plugins, event::PluginEvent}};
@ -14,104 +16,185 @@ pub struct Client {
player: Option<Player>,
}
pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> {
struct Server {
gc_tx: Sender<(i32, ClientEvent)>,
ps_tx: Sender<PServerEvent>,
next_id: i32,
clients: HashMap<i32, Client>,
ids: HashMap<Uuid, i32>,
}
pub async fn run_server() -> anyhow::Result<()> {
env_logger::init();
let (ps_tx, ps_rx) = std::sync::mpsc::channel();
let (ps_tx, ps_rx) = mpsc::channel(256);
let (pc_tx, mut pc_rx) = mpsc::channel(256);
tokio::spawn(run_plugins(pc_tx, ps_rx));
tokio::spawn(async {
if let Err(e) = run_plugins(pc_tx, ps_rx).await {
println!("error in plugins: {e}");
}
});
let socket_addr = "0.0.0.0:25565".to_socket_addrs()?.next().expect("invalid server address");
let socket_addr = "0.0.0.0:25567".to_socket_addrs()?
.next().expect("invalid server address");
let listener = TcpListener::bind(socket_addr).await?;
let mut next_id = 0i32;
let (gc_tx, mut gc_rx) = mpsc::channel(16);
let mut server = Server {
gc_tx, ps_tx,
next_id: 0,
clients: HashMap::new(),
ids: HashMap::new(),
};
let mut keepalive = tokio::time::interval(Duration::from_secs(15));
let mut clients = HashMap::new();
info!("listening on {socket_addr}");
loop { select! {
conn = listener.accept() => {
let gc_tx = gc_tx.clone();
let (stream, addr) = conn?;
let id = next_id;
next_id = next_id.wrapping_add(1);
let (gs_tx, gs_rx) = mpsc::channel(16);
clients.insert(id, Client {
tx: gs_tx,
keepalive: None,
info: None,
player: None,
});
info!("#{id} connected from {addr}");
tokio::spawn(async move {
let c_tx2 = gc_tx.clone();
match run_client(id, stream, gc_tx, gs_rx).await {
Ok(_) => info!("client {id} disconnected"),
Err(e) => error!("client {id} error: {e}"),
}
c_tx2.send((id, ClientEvent::Disconnect)).await.unwrap();
});
},
_ = keepalive.tick() => {
for client in clients.values_mut() {
if client.keepalive.is_some() {
client.tx.send(ServerEvent::Disconnect(Some("Failed to respond to keep alives".to_owned()))).await?;
} else if client.player.is_some() {
let data = 1;
client.keepalive = Some(data);
client.tx.send(ServerEvent::KeepAlive(data)).await?;
}
}
accept_connection(&mut server, stream, addr).await?;
},
_ = keepalive.tick() => handle_keepalive(&mut server).await?,
ev = pc_rx.recv() => {
let Some(ev) = ev else {
return Err("plugin channel closed".into());
return Err(anyhow!("plugin channel closed"));
};
match ev {
PluginEvent::Kick(u, msg) => {
for client in clients.values() {
if let Some(Player { uuid, .. }) = client.player {
if uuid == u {
client.tx.send(ServerEvent::Disconnect(msg.clone())).await?;
break;
}
}
}
}
}
handle_plugin_event(&mut server, ev).await?;
},
ev = gc_rx.recv() => {
let Some(ev) = ev else {
return Err("reciever closed".into());
return Err(anyhow!("reciever closed"));
};
let id = ev.0;
let client = clients.get_mut(&id).unwrap();
match ev.1 {
ClientEvent::Handshake(info) => client.info = Some(info),
ClientEvent::Join(player) => {
client.player = Some(player.clone());
ps_tx.send(PServerEvent::Join(player, client.info.clone().unwrap()))?;
},
ClientEvent::Disconnect => {
let player = clients.get(&id).unwrap().player.clone();
clients.remove(&id);
if let Some(player) = player {
ps_tx.send(PServerEvent::Leave(player))?;
}
},
ClientEvent::KeepAlive(data) => {
if client.keepalive == Some(data) {
client.keepalive = None;
} else {
client.keepalive = Some(0);
}
}
}
handle_client_event(&mut server, ev.0, ev.1).await?;
}
} }
}
async fn accept_connection(server: &mut Server, stream: TcpStream, addr: SocketAddr) -> anyhow::Result<()> {
let gc_tx = server.gc_tx.clone();
let id = server.next_id;
server.next_id = server.next_id.wrapping_add(1);
let (gs_tx, gs_rx) = mpsc::channel(16);
server.clients.insert(id, Client {
tx: gs_tx,
keepalive: None,
info: None,
player: None,
});
info!("#{id} connected from {addr}");
tokio::spawn(async move {
let c_tx2 = gc_tx.clone();
match run_client(id, stream, gc_tx, gs_rx).await {
Ok(_) => info!("client {id} disconnected"),
Err(e) => error!("client {id} error: {e}"),
}
c_tx2.send((id, ClientEvent::Disconnect)).await.unwrap();
});
Ok(())
}
async fn handle_keepalive(server: &mut Server) -> anyhow::Result<()> {
for client in server.clients.values_mut() {
if client.keepalive.is_some() {
client.tx.send(ServerEvent::Disconnect(Some("Failed to respond to keep alives".to_owned()))).await?;
} else if client.player.is_some() {
let data = 1;
client.keepalive = Some(data);
client.tx.send(ServerEvent::KeepAlive(data)).await?;
}
}
Ok(())
}
async fn handle_plugin_event(server: &mut Server, ev: PluginEvent) -> anyhow::Result<()> {
match ev {
PluginEvent::Kick(uuid, msg) => {
if let Some(id) = server.ids.get(&uuid) {
server.clients[id].tx
.send(ServerEvent::Disconnect(msg))
.await?;
}
},
PluginEvent::Chat(uuid, msg, name) => {
if let Some(id) = server.ids.get(&uuid) {
server.clients[id].tx
.send(ServerEvent::ChatMessage(msg, name))
.await?;
}
},
PluginEvent::BroadcastChat(msg, name) => {
for client in server.clients.values() {
if client.player.is_some() {
client.tx.send(ServerEvent::ChatMessage(msg.clone(), name.clone())).await?;
}
}
},
PluginEvent::System(uuid, msg, overlay) => {
if let Some(id) = server.ids.get(&uuid) {
server.clients[id].tx
.send(ServerEvent::SystemMessage(msg, overlay))
.await?;
}
},
PluginEvent::BroadcastSystem(msg, overlay) => {
for client in server.clients.values() {
if client.player.is_some() {
client.tx.send(ServerEvent::SystemMessage(msg.clone(), overlay)).await?;
}
}
},
}
Ok(())
}
async fn handle_client_event(server: &mut Server, id: i32, ev: ClientEvent) -> anyhow::Result<()> {
match ev {
ClientEvent::Handshake(info) => {
server.clients.get_mut(&id).unwrap().info = Some(info)
},
ClientEvent::Join(player) => {
if let Some(other_id) = server.ids.remove(&player.uuid) {
let other_client = server.clients.remove(&other_id).unwrap();
server.ps_tx
.send(PServerEvent::Leave(other_client.player.unwrap()))
.await?;
other_client.tx
.send(ServerEvent::Disconnect(Some("logged in elsewhere".to_owned())))
.await?;
}
let client = server.clients.get_mut(&id).unwrap();
client.player = Some(player.clone());
server.ids.insert(player.uuid, id);
server.ps_tx.send(PServerEvent::Join(player, client.info.clone().unwrap())).await?;
},
ClientEvent::Disconnect => if let Some(client) = server.clients.remove(&id) {
if let Some(player) = client.player {
server.ids.remove(&player.uuid);
server.ps_tx.send(PServerEvent::Leave(player)).await?;
}
},
ClientEvent::KeepAlive(data) => {
let client = server.clients.get_mut(&id).unwrap();
if client.keepalive == Some(data) {
client.keepalive = None;
} else {
client.keepalive = Some(0);
}
},
ClientEvent::Command(cmd) => {
let player = server.clients.get(&id).unwrap().player.clone().unwrap();
server.ps_tx.send(PServerEvent::Command(player, cmd)).await?;
},
ClientEvent::Message(msg) => {
let player = server.clients.get(&id).unwrap().player.clone().unwrap();
server.ps_tx.send(PServerEvent::Message(player, msg)).await?;
},
}
Ok(())
}