diff --git a/.gitignore b/.gitignore index ea8c4bf..9026c77 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.vscode diff --git a/Cargo.lock b/Cargo.lock index bd6058c..755d921 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 9227ef1..0b16d5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md new file mode 100644 index 0000000..e725ade --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/plugins/example.lua b/plugins/example.lua index 17d45e8..a956af7 100644 --- a/plugins/example.lua +++ b/plugins/example.lua @@ -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, } diff --git a/plugins/mcchat.lua b/plugins/mcchat.lua new file mode 100644 index 0000000..0162608 --- /dev/null +++ b/plugins/mcchat.lua @@ -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, +} diff --git a/src/client/event.rs b/src/client/event.rs index e79c31b..e7d11e4 100644 --- a/src/client/event.rs +++ b/src/client/event.rs @@ -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), KeepAlive(i64), + SystemMessage(JsonValue, bool), + ChatMessage(JsonValue, JsonValue), } diff --git a/src/client/mod.rs b/src/client/mod.rs index 1ad1eab..256d07f 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -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 => (), diff --git a/src/main.rs b/src/main.rs index d9fc201..4b5e42c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,8 @@ mod ser; mod varint; mod client; +pub static QC_VERSION: &str = env!("CARGO_PKG_VERSION"); + pub type ArcMutex = Arc>; 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(); +} + diff --git a/src/plugin/event.rs b/src/plugin/event.rs index ea71deb..26efea6 100644 --- a/src/plugin/event.rs +++ b/src/plugin/event.rs @@ -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), + 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), } diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index b2e8795..b45537c 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -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 { + match value { + Value::UserData(ud) => Ok(*ud.borrow::()?), + 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 { - 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::().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(""); - 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, rx: Receiver) { +pub async fn run_plugins(tx: Sender, mut rx: Receiver) -> 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, rx: Receiver) { ) }; - 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(()) } diff --git a/src/plugin/plugin.rs b/src/plugin/plugin.rs index 18d45ec..7a2a721 100644 --- a/src/plugin/plugin.rs +++ b/src/plugin/plugin.rs @@ -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 { - 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 { - 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 { + 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(""); + 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) } diff --git a/src/plugin/server.rs b/src/plugin/server.rs index 834b76b..0cb0fa8 100644 --- a/src/plugin/server.rs +++ b/src/plugin/server.rs @@ -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> { +impl<'lua> IntoLua<'lua> for ClientInfo { + fn into_lua(self, lua: &'lua mlua::Lua) -> mlua::Result> { 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 { + 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, - pub names: HashMap, - pub info: HashMap, + pub names: RwLock>, + pub info: RwLock>, } #[derive(Clone)] -pub struct CellServer(pub Rc>); +pub struct WrappedServer(pub Arc); -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)| 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)| 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::
(1) + }) } } diff --git a/src/protocol/common.rs b/src/protocol/common.rs index 0fddc62..dc09283 100644 --- a/src/protocol/common.rs +++ b/src/protocol/common.rs @@ -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) } } diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 798f8a5..3404235 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -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 }, 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 { 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, } } diff --git a/src/protocol/v1_16_5.rs b/src/protocol/v1_16_5.rs new file mode 100644 index 0000000..7c8a600 --- /dev/null +++ b/src/protocol/v1_16_5.rs @@ -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> { + 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) + } +} diff --git a/src/protocol/v1_17_1.rs b/src/protocol/v1_17_1.rs new file mode 100644 index 0000000..be00d9b --- /dev/null +++ b/src/protocol/v1_17_1.rs @@ -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> { + 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) + } +} diff --git a/src/protocol/v1_18_2.rs b/src/protocol/v1_18_2.rs new file mode 100644 index 0000000..2207619 --- /dev/null +++ b/src/protocol/v1_18_2.rs @@ -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> { + 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) + } +} diff --git a/src/protocol/v1_19_4/mod.rs b/src/protocol/v1_19_4.rs similarity index 74% rename from src/protocol/v1_19_4/mod.rs rename to src/protocol/v1_19_4.rs index 0152014..7dae692 100644 --- a/src/protocol/v1_19_4/mod.rs +++ b/src/protocol/v1_19_4.rs @@ -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 = 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> { + 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(); diff --git a/src/protocol/v1_20_1/mod.rs b/src/protocol/v1_20_1.rs similarity index 74% rename from src/protocol/v1_20_1/mod.rs rename to src/protocol/v1_20_1.rs index 7d6df62..7aa6268 100644 --- a/src/protocol/v1_20_1/mod.rs +++ b/src/protocol/v1_20_1.rs @@ -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 = 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> { + 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) } diff --git a/src/resources/dimcodec_1.16.5.nbt b/src/resources/dimcodec_1.16.5.nbt new file mode 100644 index 0000000..dbecc03 Binary files /dev/null and b/src/resources/dimcodec_1.16.5.nbt differ diff --git a/src/resources/dimcodec_1.17.1.nbt b/src/resources/dimcodec_1.17.1.nbt new file mode 100644 index 0000000..eda60e1 Binary files /dev/null and b/src/resources/dimcodec_1.17.1.nbt differ diff --git a/src/resources/dimcodec_1.18.2.nbt b/src/resources/dimcodec_1.18.2.nbt new file mode 100644 index 0000000..e9c7357 Binary files /dev/null and b/src/resources/dimcodec_1.18.2.nbt differ diff --git a/src/resources/dimension_1.16.5.nbt b/src/resources/dimension_1.16.5.nbt new file mode 100644 index 0000000..745dbc4 Binary files /dev/null and b/src/resources/dimension_1.16.5.nbt differ diff --git a/src/resources/dimension_1.17.1.nbt b/src/resources/dimension_1.17.1.nbt new file mode 100644 index 0000000..82392df Binary files /dev/null and b/src/resources/dimension_1.17.1.nbt differ diff --git a/src/resources/dimension_1.18.2.nbt b/src/resources/dimension_1.18.2.nbt new file mode 100644 index 0000000..08444f9 Binary files /dev/null and b/src/resources/dimension_1.18.2.nbt differ diff --git a/src/resources/heightmap.nbt b/src/resources/heightmap.nbt new file mode 100644 index 0000000..f2449fc Binary files /dev/null and b/src/resources/heightmap.nbt differ diff --git a/src/protocol/v1_19_4/registry.nbt b/src/resources/registry_1.19.4.nbt similarity index 99% rename from src/protocol/v1_19_4/registry.nbt rename to src/resources/registry_1.19.4.nbt index f4990ff..63d85f6 100644 Binary files a/src/protocol/v1_19_4/registry.nbt and b/src/resources/registry_1.19.4.nbt differ diff --git a/src/protocol/v1_20_1/registry.nbt b/src/resources/registry_1.20.1.nbt similarity index 99% rename from src/protocol/v1_20_1/registry.nbt rename to src/resources/registry_1.20.1.nbt index 63e4a3a..ffad41d 100644 Binary files a/src/protocol/v1_20_1/registry.nbt and b/src/resources/registry_1.20.1.nbt differ diff --git a/src/server.rs b/src/server.rs index 33debcc..03ccafb 100644 --- a/src/server.rs +++ b/src/server.rs @@ -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, } -pub async fn run_server() -> Result<(), Box> { +struct Server { + gc_tx: Sender<(i32, ClientEvent)>, + ps_tx: Sender, + next_id: i32, + clients: HashMap, + ids: HashMap, +} + +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(()) +}