diff --git a/Cargo.lock b/Cargo.lock index 10895f8..bd6058c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,17 +32,6 @@ version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" -[[package]] -name = "async-trait" -version = "0.1.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -64,12 +53,27 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + [[package]] name = "byteorder" version = "1.4.3" @@ -153,6 +157,43 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "futures-core" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "gimli" version = "0.27.3" @@ -212,6 +253,16 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.19" @@ -245,10 +296,27 @@ dependencies = [ ] [[package]] -name = "num-traits" -version = "0.2.15" +name = "mlua" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "07366ed2cd22a3b000aed076e2b68896fb46f06f1f5786c5962da73c0af01577" +dependencies = [ + "bstr", + "cc", + "futures-core", + "futures-task", + "futures-util", + "num-traits", + "once_cell", + "pkg-config", + "rustc-hash", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", ] @@ -272,12 +340,53 @@ dependencies = [ "memchr", ] +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "pin-project-lite" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + [[package]] name = "proc-macro2" version = "1.0.66" @@ -292,11 +401,10 @@ name = "quectocraft" version = "0.2.0" dependencies = [ "anyhow", - "async-trait", "env_logger", "hematite-nbt", "log", - "num-traits", + "mlua", "serde_json", "tokio", "uuid", @@ -311,6 +419,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regex" version = "1.9.1" @@ -346,13 +463,19 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" dependencies = [ - "bitflags", + "bitflags 2.3.3", "errno", "libc", "linux-raw-sys", @@ -365,6 +488,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "serde" version = "1.0.171" @@ -402,6 +531,30 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + [[package]] name = "socket2" version = "0.4.9" @@ -444,7 +597,9 @@ dependencies = [ "libc", "mio", "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys", diff --git a/Cargo.toml b/Cargo.toml index c23d9ea..9227ef1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,11 @@ version = "0.2.0" edition = "2021" [dependencies] -tokio = { version = "1.29", features = ["macros", "rt-multi-thread", "io-util", "net", "sync", "time"] } -async-trait = "0.1" -num-traits = "0.2" +tokio = { version = "1.29", features = ["full"] } serde_json = "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"] } diff --git a/plugins/example.lua b/plugins/example.lua new file mode 100644 index 0000000..17d45e8 --- /dev/null +++ b/plugins/example.lua @@ -0,0 +1,20 @@ +return { + id = "example", + name = "Example Plugin", + version = "0.1.0", + authors = {"John Doe", "Jane Doe"}, + description = "Example plugin", + license = "MIT", + + 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") + end, +} diff --git a/src/event.rs b/src/client/event.rs similarity index 100% rename from src/event.rs rename to src/client/event.rs diff --git a/src/client.rs b/src/client/mod.rs similarity index 94% rename from src/client.rs rename to src/client/mod.rs index ee81d11..1ad1eab 100644 --- a/src/client.rs +++ b/src/client/mod.rs @@ -6,9 +6,12 @@ use serde_json::json; use tokio::{net::{TcpStream, tcp::{OwnedReadHalf, OwnedWriteHalf}}, select, io::{AsyncReadExt, AsyncWriteExt}}; use uuid::Uuid; -use crate::{varint::VarInt, ser::{Deserializable, Position}, protocol::{self, Protocol, ProtocolState, ClientPacket, ServerPacket}, event::{ClientEvent, ServerEvent}, Player, ClientInfo}; +use crate::{varint::VarInt, ser::{Deserializable, Position}, protocol::{self, Protocol, ProtocolState, ClientPacket, ServerPacket}, Player, ClientInfo}; +use event::{ClientEvent, ServerEvent}; -type Sender = tokio::sync::mpsc::Sender<(u64, ClientEvent)>; +pub mod event; + +type Sender = tokio::sync::mpsc::Sender<(i32, ClientEvent)>; type Receiver = tokio::sync::mpsc::Receiver; const OFFLINE_NAMESPACE: Uuid = Uuid::from_bytes([0xc3, 0xe3, 0xe7, 0xa5, 0x58, 0xe5, 0x4c, 0xf4, 0x84, 0xef, 0x27, 0x4b, 0x9e, 0xf1, 0x5c, 0xc3]); @@ -30,7 +33,7 @@ async fn read_varint_async(r: &mut OwnedReadHalf) -> std::io::Result return Ok(Some(result as i32)) } } - return Ok(None) + Ok(None) } async fn write_varint_async(w: &mut OwnedWriteHalf, i: i32) -> std::io::Result<()> { @@ -49,7 +52,7 @@ async fn write_varint_async(w: &mut OwnedWriteHalf, i: i32) -> std::io::Result<( } struct ClientState { - id: u64, + id: i32, proto: Protocol, state: ProtocolState, r: OwnedReadHalf, @@ -63,7 +66,7 @@ enum ReadPacketOutcome { Eof, } -pub async fn run_client(id: u64, stream: TcpStream, tx: Sender, rx: Receiver) -> anyhow::Result<()> { +pub async fn run_client(id: i32, stream: TcpStream, tx: Sender, rx: Receiver) -> anyhow::Result<()> { debug!("running client #{id}"); let (r, w) = stream.into_split(); let client = ClientState { @@ -92,7 +95,7 @@ impl ClientState { None => ReadPacketOutcome::None, }) } - None => return Ok(ReadPacketOutcome::Eof), + None => Ok(ReadPacketOutcome::Eof), } } @@ -138,12 +141,11 @@ impl ClientState { async fn slp(mut self, _tx: Sender, mut rx: Receiver) -> anyhow::Result<()> { debug!("#{} entering slp", self.id); loop { select! { - ev = rx.recv() => match ev { - Some(ServerEvent::Disconnect(msg)) => { + ev = rx.recv() => { + if let Some(ServerEvent::Disconnect(msg)) = ev { debug!("#{} disconnecting: {}", self.id, msg.unwrap_or_default()); return Ok(()) - }, - _ => (), + } }, pk = self.read_packet() => match pk? { ReadPacketOutcome::Packet(ClientPacket::PingRequest(data)) => { @@ -196,8 +198,8 @@ impl ClientState { return Ok(()) } loop { select! { - ev = rx.recv() => match ev { - Some(ServerEvent::Disconnect(msg)) => { + ev = rx.recv() => { + if let Some(ServerEvent::Disconnect(msg)) = ev { self.write_packet(ServerPacket::LoginDisconnect(json!( { "text": msg @@ -205,8 +207,7 @@ impl ClientState { ))).await?; info!("#{} disconnecting: {}", self.id, msg.unwrap_or_default()); return Ok(()) - }, - _ => (), + } }, pk = self.read_packet() => match pk? { ReadPacketOutcome::Packet(ClientPacket::LoginStart { name, uuid }) => { @@ -229,7 +230,7 @@ impl ClientState { async fn play(mut self, tx: Sender, mut rx: Receiver) -> anyhow::Result<()> { debug!("#{} entering play", self.id); self.write_packet(ServerPacket::JoinGame { - eid: self.id as i32, + eid: self.id, gamemode: 3, hardcode: false, }).await?; diff --git a/src/main.rs b/src/main.rs index 743a66e..d9fc201 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,24 @@ -use std::{collections::HashMap, time::Duration, net::ToSocketAddrs}; +use std::sync::Arc; -use event::ServerEvent; -use log::{info, error}; -use tokio::{net::TcpListener, sync::mpsc::{Sender, self}, select}; +use tokio::sync::Mutex; use uuid::Uuid; -use crate::{client::run_client, event::ClientEvent}; - -mod event; mod protocol; +mod server; +mod plugin; mod ser; mod varint; mod client; +pub type ArcMutex = Arc>; pub type JsonValue = serde_json::Value; +#[derive(Clone, Debug)] +pub struct Player { + name: String, + uuid: Uuid, +} + #[derive(Clone, Debug)] pub struct ClientInfo { addr: String, @@ -23,82 +27,8 @@ pub struct ClientInfo { proto_name: &'static str, } -#[derive(Debug)] -pub struct Player { - name: String, - uuid: Uuid, -} - -struct Client { - tx: Sender, - keepalive: Option, - info: Option, - player: Option, -} #[tokio::main] -async fn main() -> Result<(), Box> { - env_logger::init(); - let socket_addr = "0.0.0.0:25565".to_socket_addrs()?.next().expect("invalid server address"); - let listener = TcpListener::bind(socket_addr).await?; - let mut clients = HashMap::new(); - let mut next_id = 0; - let (c_tx, mut c_rx) = mpsc::channel(16); - let mut keepalive = tokio::time::interval(Duration::from_secs(15)); - info!("listening on {socket_addr}"); - loop { select! { - conn = listener.accept() => { - let c_tx = c_tx.clone(); - let (stream, addr) = conn?; - let id = next_id; - next_id += 1; - let (s_tx, s_rx) = mpsc::channel(16); - clients.insert(id, Client { - tx: s_tx, - keepalive: None, - info: None, - player: None, - }); - info!("#{id} connected from {addr}"); - tokio::spawn(async move { - let c_tx2 = c_tx.clone(); - match run_client(id, stream, c_tx, s_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 &mut clients { - 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?; - } - } - }, - ev = c_rx.recv() => { - let Some(ev) = ev else { - return Err("reciever closed".into()); - }; - let id = ev.0; - let client = clients.get_mut(&id).unwrap(); - match ev.1 { - ClientEvent::Handshake(info) => client.info = Some(info), - ClientEvent::Join(player) => clients.get_mut(&id).unwrap().player = Some(player), - ClientEvent::Disconnect => { clients.remove(&id); }, - ClientEvent::KeepAlive(data) => { - let client = clients.get_mut(&id).unwrap(); - if client.keepalive == Some(data) { - client.keepalive = None; - } else { - client.keepalive = Some(0); - } - } - } - } - } } +async fn main() { + server::run_server().await.unwrap() } diff --git a/src/plugin/event.rs b/src/plugin/event.rs new file mode 100644 index 0000000..ea71deb --- /dev/null +++ b/src/plugin/event.rs @@ -0,0 +1,26 @@ +use mlua::{Value, FromLua, Table}; +use uuid::Uuid; + +use crate::{Player, ClientInfo}; + +use super::UuidUD; + +pub enum PluginEvent { + Kick(Uuid, Option), +} + +impl<'lua> FromLua<'lua> for PluginEvent { + fn from_lua(lua_value: Value<'lua>, lua: &'lua mlua::Lua) -> mlua::Result { + let table = Table::from_lua(lua_value, lua)?; + let ty: Box = table.get("type")?; + match ty.as_ref() { + "kick" => Ok(Self::Kick(table.get::<_, UuidUD>("uuid")?.0, table.get("msg")?)), + _ => Err(mlua::Error::RuntimeError(format!("unknown event type {ty}"))), + } + } +} + +pub enum ServerEvent { + Join(Player, ClientInfo), + Leave(Player), +} diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs new file mode 100644 index 0000000..b2e8795 --- /dev/null +++ b/src/plugin/mod.rs @@ -0,0 +1,136 @@ +use std::{io::ErrorKind, fs, cell::RefCell, rc::Rc}; + +use log::{warn, debug, info}; +use mlua::{Lua, ToLua, Function}; +use tokio::sync::mpsc::Sender; +use std::sync::mpsc::Receiver; +use uuid::Uuid; + +use self::{plugin::Plugin, event::{PluginEvent, ServerEvent}, server::{CellServer, Server}}; + +pub mod event; +mod plugin; +mod server; + +#[derive(Clone, Copy, Eq, PartialEq)] +struct UuidUD(Uuid); + +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) + ); + methods.add_meta_method("__eq", + |lua, this, UuidUD(uuid)| (this.0 == uuid).to_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}"); + } + 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 +} + +pub async fn run_plugins(tx: Sender, rx: Receiver) { + let lua = unsafe { + mlua::Lua::unsafe_new_with( + mlua::StdLib::ALL_SAFE | mlua::StdLib::DEBUG, + mlua::LuaOptions::default() + ) + }; + + let plugins = load_plugins(&lua); + + let server = Rc::new(RefCell::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); + } + } + } + + while let Ok(ev) = rx.recv() { + 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); + } + } + } + }, + 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); + } + } + } + } +} diff --git a/src/plugin/plugin.rs b/src/plugin/plugin.rs new file mode 100644 index 0000000..18d45ec --- /dev/null +++ b/src/plugin/plugin.rs @@ -0,0 +1,76 @@ +use std::fs::DirEntry; + +use log::{debug, Record}; +use mlua::{Function, FromLua, Lua, Table, Value}; + +pub struct Plugin<'lua> { + pub table: Table<'lua>, + pub id: String, +} + +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 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: &'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"); + } + 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) + } +} diff --git a/src/plugin/server.rs b/src/plugin/server.rs new file mode 100644 index 0000000..834b76b --- /dev/null +++ b/src/plugin/server.rs @@ -0,0 +1,61 @@ +use std::{collections::HashMap, cell::RefCell, rc::Rc}; + +use mlua::{Value, ToLua}; +use tokio::sync::mpsc::Sender; +use uuid::Uuid; + +use crate::ClientInfo; + +use super::{event::PluginEvent, UuidUD}; + +impl<'lua> ToLua<'lua> for ClientInfo { + fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result> { + let table = lua.create_table()?; + table.set("addr", self.addr)?; + table.set("port", self.port)?; + table.set("proto_name", self.proto_name)?; + table.set("proto_version", self.proto_version)?; + Ok(Value::Table(table)) + } +} + +pub struct Server { + pub tx: Sender, + pub names: HashMap, + pub info: HashMap, +} + +#[derive(Clone)] +pub struct CellServer(pub Rc>); + +impl mlua::UserData for CellServer { + fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method("players", |lua, this, ()| { + let table = lua.create_table()?; + for (k, v) in &this.0.borrow().names { + 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), + 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), + None => Ok(Value::Nil) + } + }); + methods.add_async_method("kick", |_, CellServer(this), (UuidUD(uuid), msg)| async move { + let tx = this.borrow().tx.clone(); + tx + .send(PluginEvent::Kick(uuid, msg)) + .await + .map_err(|e| mlua::Error::RuntimeError(e.to_string())) + }); + } +} + diff --git a/src/protocol/v1_19_4/mod.rs b/src/protocol/v1_19_4/mod.rs index cd0f956..0152014 100644 --- a/src/protocol/v1_19_4/mod.rs +++ b/src/protocol/v1_19_4/mod.rs @@ -53,10 +53,10 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> VarInt(0x1A).serialize(&mut w)?; msg.serialize(&mut w)?; }, - ServerPacket::PluginMessage { channel, mut data } => { + ServerPacket::PluginMessage { channel, data } => { VarInt(0x17).serialize(&mut w)?; channel.serialize(&mut w)?; - w.write_all(&mut data)?; + w.write_all(&data)?; }, ServerPacket::ChunkData { x, z } => { VarInt(0x24).serialize(&mut w)?; diff --git a/src/protocol/v1_20_1/mod.rs b/src/protocol/v1_20_1/mod.rs index 7ea1186..7d6df62 100644 --- a/src/protocol/v1_20_1/mod.rs +++ b/src/protocol/v1_20_1/mod.rs @@ -54,10 +54,10 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> VarInt(0x1A).serialize(&mut w)?; msg.serialize(&mut w)?; }, - ServerPacket::PluginMessage { channel, mut data } => { + ServerPacket::PluginMessage { channel, data } => { VarInt(0x17).serialize(&mut w)?; channel.serialize(&mut w)?; - w.write_all(&mut data)?; + w.write_all(&data)?; }, ServerPacket::ChunkData { x, z } => { VarInt(0x24).serialize(&mut w)?; diff --git a/src/ser.rs b/src/ser.rs index dcda2a7..233709f 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -103,7 +103,7 @@ impl Deserializable for VarInt { return Ok(VarInt(result as i32)) } } - return Err(anyhow!("VarInt too long")) + Err(anyhow!("VarInt too long")) } } @@ -134,7 +134,7 @@ impl Deserializable for VarLong { return Ok(VarLong(result as i64)) } } - return Err(anyhow!("VarLong too long")) + Err(anyhow!("VarLong too long")) } } @@ -191,8 +191,8 @@ where T: Deserializable { let mut arr: [MaybeUninit; N] = unsafe { MaybeUninit::uninit().assume_init() }; - for i in 0..N { - arr[i] = MaybeUninit::new(T::deserialize(r)?); + for item in arr.iter_mut().take(N) { + *item = MaybeUninit::new(T::deserialize(r)?); } // Safety: since MaybeUninit and T have the same memory // layout, so will [MaybeUninit; N] and [T; N]. diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..33debcc --- /dev/null +++ b/src/server.rs @@ -0,0 +1,117 @@ +use std::{collections::HashMap, time::Duration, net::ToSocketAddrs}; + +use log::{info, error}; +use tokio::{net::TcpListener, sync::mpsc::{Sender, self}, select}; + +use crate::{client::run_client, Player, ClientInfo, client::event::{ServerEvent, ClientEvent}, plugin::{run_plugins, event::PluginEvent}}; + +type PServerEvent = crate::plugin::event::ServerEvent; + +pub struct Client { + tx: Sender, + keepalive: Option, + info: Option, + player: Option, +} + +pub async fn run_server() -> Result<(), Box> { + env_logger::init(); + + let (ps_tx, ps_rx) = std::sync::mpsc::channel(); + let (pc_tx, mut pc_rx) = mpsc::channel(256); + + tokio::spawn(run_plugins(pc_tx, ps_rx)); + + let socket_addr = "0.0.0.0:25565".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 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?; + } + } + }, + ev = pc_rx.recv() => { + let Some(ev) = ev else { + return Err("plugin channel closed".into()); + }; + 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; + } + } + } + } + } + }, + ev = gc_rx.recv() => { + let Some(ev) = ev else { + return Err("reciever closed".into()); + }; + 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); + } + } + } + } + } } +}