diff --git a/Cargo.lock b/Cargo.lock index f5c2fcb..c7b6f8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -440,9 +440,9 @@ dependencies = [ [[package]] name = "mlua" -version = "0.9.0-rc.1" +version = "0.9.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5369212118d0f115c9adbe7f7905e36fb3ef2994266039c51fc3e96374d705d" +checksum = "01a6500a9fb74b519a85ac206cd57f9f91b270ce39d6cb12ab06a8ed29c3563d" dependencies = [ "bstr", "erased-serde", @@ -457,9 +457,9 @@ dependencies = [ [[package]] name = "mlua-sys" -version = "0.2.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3daecc55a656cae8e54fc599701ab597b67cdde68f780cac8c1c49b9cfff2f5" +checksum = "aa5b61f6c943d77dd6ab5f670865670f65b978400127c8bf31c2df7d6e76289a" dependencies = [ "cc", "cfg-if", diff --git a/Cargo.toml b/Cargo.toml index f0ca663..fdbe866 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,14 +4,14 @@ version = "0.2.0" 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"] } -log = "0.4" env_logger = "0.10" -mlua = { version = "0.9.0-rc.1", features = ["luajit52", "async", "send", "serialize"] } -semver = "1.0" -reqwest = { version = "0.11", features = ["rustls-tls", "json"], default-features = false } +log = "0.4" +mlua = { version = "0.9.0-rc.3", features = ["luajit52", "async", "send", "serialize"] } once_cell = "1.18" +reqwest = { version = "0.11", features = ["rustls-tls", "json"], default-features = false } +semver = "1.0" +serde = "1.0" +serde_json = "1.0" +tokio = { version = "1.29", features = ["full"] } +uuid = { version = "1.4", features = ["v5"] } diff --git a/PLUGINS.md b/PLUGINS.md new file mode 100644 index 0000000..b3d6978 --- /dev/null +++ b/PLUGINS.md @@ -0,0 +1,97 @@ +# quectocraft plugins + +## File structure + +Plugins are located in the `plugins` directory. Plugins may either be stored as a single file in the `plugins` directory or in a subdirectory, in which case the plugin will be loaded from a file named `main.lua`. + +## Versioning + +Server and plugin versions use [Semantic Versioning](https://semver.org/) to specify versions, specifically the variety used by Cargo, rust's package manager. Refer to [the Cargo docs](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html) for information on specifying versions and version requirements + +## Plugin table + +The plugin's main file returns a table representing the plugin. The `id` property is required or the plugin will fail to load. The `server_version` property is recommended to ensure that the plugin is running under a compatible server version. The `priority` field, which defaults to 50 and must be between 1 and 100 inclusive, controls the order that events are sent to plugins; plugins with higher priority will be initialized and recieve events before those with lower priority. + +All other fields listed are optional and are currently unused by the server, although they may be used by the server in the future or by other plugins. + +| Key | Type | Required? | Description | +|------------------|---------|-------------|-------------------------------------------------------------------| +| `id` | string | required | The plugin's id, which should be a unique `snake_case` identifier | +| `server_version` | string | recommended | The version requirement for the server | +| `priority` | integer | optional | Determines the order in which plugins recieve events | +| `name` | string | optional | The plugin's name | +| `version` | string | optional | The plugin's version | +| `description` | string | optional | The plugin's description | +| `authors` | table | optional | A list of plugin authors | +| `license` | string | optional | The plugin's license | + +The table may also contain the following functions that serve as event handlers. All functions take an initial `self` argument holding the plugin's own table. + +- `init(self)` - run for each plugin after all have been loaded +- `on_join(self, uuid, name)` - a player joined the server +- `on_leave(self, uuid, name)` - a player left the server +- `on_message(self, message, uuid, name)` - a player sent a message +- `on_command(self, command, args, full_command, uuid, name)` - a player ran a command + - `command`: the command that was run (with the plugin namespace removed if present) + - `args`: a table of the arguments to the command created by naïvely splitting the command string on whitespace + - `full_command`: the full command message containing the (possibly namespaced) name and arguments but not including the initial slash + +Returning `true` from any of these methods besides `init` will cancel the event: it will not be sent to any other plugins. This does not prevent the action that caused the event. + +Plugins may use their tables to store arbitrary extra data. + +## Data types + +- `uuid`: A UUID, probably of a player. Supports testing for equality via `==` and conversion to a string via `tostring()`. +- `srv`: The interface to the server, as described below. +- `msg`: A Minecraft JSON chat component represented as a table. A string may also be used if no formatting is needed, or `nil` may be passed to use an empty or default message. + +## `srv` + +Plugins can use the global `srv` object to perform logging and communicate to and query the server. + +### Logging + +The methods `trace`, `debug`, `info`, `warn`, and `error` can be used for logging. Each takes a single string argument and logs it to the terminal using the same logging interface as used by the server. + +### Players + +The method `players` retrieves a table listing the users connected to the server where the keys are UUIDs and the values are player names. + +The method `get_name(uuid)` will get a player's name given a UUID, and the method `get_uuid(name)` will get a player's UUID given their name. + +The method `get_info(uuid)` returns a table containing the following: +- `addr` - the address the player connected to the server with +- `port` - the port the player connected with +- `proto_name` - the name of the protocol the player is using (ex. `1.20.1`) +- `proto_version` - the numerical version of the protocol (ex. `763`) + +`kick(uuid, msg)` can be used to disconnect a player from the server. If `msg` is `nil`, the default, localized kick message will be used. + +### Chat + +- `send_msg(msg, target)` sends a system message to the player specified by the `target` UUID, or to all players if no UUID is specified +- `send_chat(msg, target)` does the same but as a chat message + +Chat messages should be used for messages with content originating from players, system messages should be used for information from the server or in response to commands. + +### Commands + +Plugins will only recieve `on_command` events for commands they have registered. + +- `register_command(name)` will register the commands `id:name`, where `id` is the plugin's id, and `name`, provided that another plugin has not already registered a command named `name`. +- `add_command(name)` is a convenience method for registering a command and creating basic Brigadier nodes to indicate to clients that the command exists. (note: full Brigadier API is WIP) +- `update_commands()` will resend the command data to connected clients. This is only necessary if adding new commands after the `init` phase completes, which should be avoided if possible. + +### Utility + +NOTE: these are functions, not methods. + +- `parse_uuid(str)` will attempt to parse a UUID from a string, returning `nil` if the argument is not a valid string representation of a UUID. +- `check_semver(ver, req)` checks if the semantic version `ver` matches the version requirement `req`. It will error if either the version or version requirement fails to parse. + +## Fields + +- `version` is the quectocraft server version (this is not a Minecraft protocol version) +- `plugins` is a table containing all loaded plugins, where the keys are plugin IDs. + diff --git a/README.md b/README.md index e725ade..d2e391a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ -# Building +# quectocraft + +## Plugin API + +see [PLUGINS.md](PLUGINS.md) + +## Building quectocraft requires that `luajit` is installed. it should be available via your package manager. -## Alpine +### Alpine install `luajit`, `luajit-dev`, and `pkgconfig` and build with diff --git a/plugins/example.lua b/plugins/example.lua index a956af7..a56e4ce 100644 --- a/plugins/example.lua +++ b/plugins/example.lua @@ -3,9 +3,47 @@ return { server_version = "0.2", init = function(self) - srv:info("quectocraft version " .. srv.version) - for k, _ in pairs(srv.plugins) do - srv:info("found plugin: " .. k) + srv:add_command("selfkick") + srv:add_command("fail") + end, + + on_join = function(self, uuid, name) + local info = srv:get_info(uuid) + for k, v in pairs(info) do + srv:warn(k .. ": " .. tostring(v)) + end + srv:send_msg({ + { + text = "welcome, ", + color = "green", + }, { + text = name, + color = "yellow", + }, { + text = ", to the server!\nthere are many things to do, such as ", + color = "green", + }, { + text = "leaving", + color = "light_purple" + }, + }, uuid) + end, + + on_command = function(self, cmd, args, _, uuid) + if cmd == "fail" then + error("failure has occured") + elseif cmd == "selfkick" then + srv:kick(uuid, { + { + text = "be", + color = "dark_red", + }, + { + text = "gone", + color = "red", + bold = true, + } + }) end end, } diff --git a/plugins/mcchat.lua b/plugins/mcchat.lua index 0162608..4f52ae2 100644 --- a/plugins/mcchat.lua +++ b/plugins/mcchat.lua @@ -3,14 +3,21 @@ return { id = "mcchat", name = "MCChat", version = "0.1.0", + priority = 80, server_version = "0.2", authors = {"trimill"}, description = "Adds basic chat and join/leave messages", license = "MIT", + init = function(self) + srv:add_command("tell") + srv:add_command("msg") + srv:add_command("me") + end, + on_join = function(self, _, name) srv:info(name .. " joined the game") - srv:broadcast_msg({ + srv:send_msg({ translate = "multiplayer.player.joined", with = { { text = name } }, color = "yellow", @@ -19,7 +26,7 @@ return { on_leave = function(self, _, name) srv:info(name .. " left the game") - srv:broadcast_msg({ + srv:send_msg({ translate = "multiplayer.player.left", with = { { text = name } }, color = "yellow", @@ -28,12 +35,74 @@ return { on_message = function(self, msg, _, name) srv:info("<" .. name .. "> " .. msg) - srv:broadcast_chat({ + srv:send_chat({ translate = "chat.type.text", with = { { text = name }, { text = msg }, }, }) + return true end, + + on_command = function(self, cmd, args, _, uuid, name) + if cmd == "me" then + + local msg = table.concat(args, " ") + srv:info("* " .. name .. " " .. msg) + srv:send_chat({ + translate = "chat.type.emote", + with = { + { text = name }, + { text = msg } + }, + }) + + elseif cmd == "tell" or cmd == "msg" then + + local target = args[1] + + if target == nil then + srv:send_msg({ + text = cmd .. ": no target specified", + color = "red", + }, uuid) + return + end + + local target_uuid = srv:get_uuid(target) + + if target_uuid == nil then + srv:send_msg({ + text = cmd .. ": player not found: " .. target, + color = "red", + }, uuid) + return + end + + local msg = table.concat(args, " ", 2) + + srv:info(name .. " whispers to " .. target .. ": " .. msg) + + srv:send_chat({ + translate = "commands.message.display.outgoing", + with = { + { text = target }, + { text = msg } + }, + color = "gray", + italic = true, + }, uuid) + srv:send_chat({ + translate = "commands.message.display.incoming", + with = { + { text = name }, + { text = msg } + }, + color = "gray", + italic = true, + }, target_uuid); + + end + end } diff --git a/src/client/event.rs b/src/client/event.rs index e7d11e4..c5ae230 100644 --- a/src/client/event.rs +++ b/src/client/event.rs @@ -1,4 +1,12 @@ -use crate::{Player, ClientInfo, JsonValue}; +use crate::{Player, JsonValue, command::Commands}; + +#[derive(Clone, Debug)] +pub struct ClientInfo { + pub addr: String, + pub port: u16, + pub proto_version: i32, + pub proto_name: &'static str, +} #[derive(Debug)] pub enum ClientEvent { @@ -12,8 +20,9 @@ pub enum ClientEvent { #[derive(Debug)] pub enum ServerEvent { - Disconnect(Option), + Disconnect(Option), KeepAlive(i64), SystemMessage(JsonValue, bool), ChatMessage(JsonValue, JsonValue), + UpdateCommands(Commands), } diff --git a/src/client/mod.rs b/src/client/mod.rs index c85b280..997081a 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -6,7 +6,7 @@ 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}, Player, ClientInfo, JsonValue, HTTP_CLIENT}; +use crate::{varint::VarInt, ser::{Deserializable, Position}, protocol::{self, Protocol, ProtocolState, ClientPacket, ServerPacket}, Player, JsonValue, HTTP_CLIENT, CONFIG, client::event::ClientInfo}; use event::{ClientEvent, ServerEvent}; pub mod event; @@ -185,6 +185,7 @@ impl ClientState { } else { "server list ping" }; + let max_players = CONFIG.get().unwrap().max_players; self.write_packet(ServerPacket::StatusResponse(json!( { "version": { @@ -192,7 +193,7 @@ impl ClientState { "protocol": self.proto.version, }, "players": { - "max": 1337, + "max": max_players, "online": 0, }, "description": { @@ -217,7 +218,10 @@ impl ClientState { if self.proto == protocol::COMMON { self.write_packet(ServerPacket::LoginDisconnect(json!( { - "text": "Unsupported client version" + "translate": "multiplayer.disconnect.outdated_client", + "with": [ + { "text": "1.13.2, 1.14.4, 1.15.2, 1.16.5, 1.17.1, 1.18.2, 1.19.4, 1.20.1" } + ] } ))).await?; debug!("#{} disconnecting due to unsupported version", self.id); @@ -226,12 +230,11 @@ impl ClientState { loop { select! { ev = rx.recv() => { if let Some(ServerEvent::Disconnect(msg)) = ev { - self.write_packet(ServerPacket::LoginDisconnect(json!( - { - "text": msg - } - ))).await?; - info!("#{} disconnecting: {}", self.id, msg.unwrap_or_default()); + let msg = msg.unwrap_or_else(|| json!({ + "translate": "multiplayer.disconnected.generic" + })); + info!("#{} disconnecting: {}", self.id, msg); + self.write_packet(ServerPacket::LoginDisconnect(msg)).await?; return Ok(()) } }, @@ -293,12 +296,11 @@ impl ClientState { loop { select! { ev = rx.recv() => match ev { Some(ServerEvent::Disconnect(msg)) => { - self.write_packet(ServerPacket::PlayDisconnect(json!( - { - "text": msg - } - ))).await?; - info!("#{} disconnecting: {}", self.id, msg.unwrap_or_default()); + let msg = msg.unwrap_or_else(|| json!({ + "translate": "multiplayer.disconnect.generic" + })); + info!("#{} disconnecting: {}", self.id, msg); + self.write_packet(ServerPacket::PlayDisconnect(msg)).await?; return Ok(()) }, Some(ServerEvent::SystemMessage(msg, overlay)) => { @@ -313,6 +315,10 @@ impl ClientState { debug!("#{} sending keepalive: {data}", self.id); self.write_packet(ServerPacket::KeepAlive(data)).await?; }, + Some(ServerEvent::UpdateCommands(data)) => { + debug!("#{} sending command data", self.id); + self.write_packet(ServerPacket::CommandData(data)).await?; + }, None => (), }, pk = self.read_packet() => match pk? { diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..d102585 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,263 @@ +use crate::{ser::Serializable, varint::VarInt}; + +#[derive(Clone, Debug)] +pub struct Commands(Vec); + +impl Commands { + pub fn new() -> Self { + let root = Node { children: Vec::new(), data: NodeData::Root }; + let basic = Node { + children: Vec::new(), + data: NodeData::Argument { + redirect: None, + name: "args".to_owned(), + parser: Parser::String(StringType::GreedyPhrase), + suggestion: None, + executable: true, + } + }; + Self(vec![root, basic]) + } + + pub fn add_node(&mut self, data: NodeData) -> Result { + if let NodeData::Root = data { + return Err("node data cannot be a root") + } + if let NodeData::Argument { redirect: Some(redir), .. } = data { + if redir < 0 || (redir as usize) >= self.0.len() { + return Err("invalid redirect") + } + } + if let NodeData::Literal { redirect: Some(redir), .. } = data { + if redir < 0 || (redir as usize) >= self.0.len() { + return Err("invalid redirect") + } + } + let node = Node { + children: Vec::new(), + data, + }; + self.0.push(node); + Ok(self.0.len() as i32 - 1) + } + + pub fn add_child(&mut self, parent: i32, child: i32) -> Result<(), &'static str> { + if parent < 0 || (parent as usize) >= self.0.len() { + return Err("invalid parent node") + } + if child < 0 || (child as usize) >= self.0.len() { + return Err("invalid child node") + } + self.0[parent as usize].children.push(child); + Ok(()) + } + + pub fn add_simple_command(&mut self, name: String) -> Result<(), &'static str> { + let new_id = self.add_node(NodeData::Literal { + redirect: None, + name, + executable: true + })?; + self.add_child(Commands::root(), new_id)?; + self.add_child(new_id, Commands::basic_arg())?; + Ok(()) + } + + pub fn len(&self) -> i32 { + self.0.len() as i32 + } + + pub fn serialize(&self, w: &mut impl std::io::Write, new: bool) -> anyhow::Result<()> { + for node in &self.0 { + node.serialize(w, new)?; + } + Ok(()) + } + + pub const fn root() -> i32 { 0 } + + pub const fn basic_arg() -> i32 { 1 } +} + +#[derive(Clone, Debug)] +struct Node { + children: Vec, + data: NodeData, +} + +impl Node { + pub fn serialize(&self, w: &mut impl std::io::Write, new: bool) -> anyhow::Result<()> { + match &self.data { + NodeData::Root => { + // flags (root node) + 0u8.serialize(w)?; + // children + VarInt(self.children.len() as i32).serialize(w)?; + for child in &self.children { + VarInt(*child).serialize(w)?; + } + }, + NodeData::Literal { redirect, name, executable } => { + // flags: + let flags: u8 = 0x01 // literal node + | (*executable as u8 * 0x04) // is executable + | (redirect.is_some() as u8 * 0x08); // has redirect + flags.serialize(w)?; + // children + VarInt(self.children.len() as i32).serialize(w)?; + for child in &self.children { + VarInt(*child).serialize(w)?; + } + // redirect + if let Some(redir) = redirect { + VarInt(*redir).serialize(w)?; + } + // name + name.serialize(w)?; + }, + NodeData::Argument { redirect, name, parser, suggestion, executable } => { + // flags: + let flags: u8 = 0x02 // argument node + | (*executable as u8 * 0x04) // is executable + | (redirect.is_some() as u8 * 0x08) // has redirect + | (suggestion.is_some() as u8 * 0x10); // has suggestion + flags.serialize(w)?; + // children + VarInt(self.children.len() as i32).serialize(w)?; + for child in &self.children { + VarInt(*child).serialize(w)?; + } + // redirect + if let Some(redir) = redirect { + VarInt(*redir).serialize(w)?; + } + // name + name.serialize(w)?; + // parser + parser.serialize(w, new)?; + // suggestion + if let Some(sug) = suggestion { + sug.serialize(w)?; + } + }, + } + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub enum NodeData { + Root, + Literal { + redirect: Option, + name: String, + executable: bool, + }, + Argument { + redirect: Option, + name: String, + parser: Parser, + suggestion: Option, + executable: bool, + }, +} + +#[derive(Clone, Copy, Debug)] +pub enum Suggestion { + AskServer +} + +impl Serializable for Suggestion { + fn serialize(&self, w: &mut impl std::io::Write) -> anyhow::Result<()> { + match self { + Suggestion::AskServer => "minecraft:ask_server".serialize(w) + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum Parser { + Bool, + Float { min: Option, max: Option }, + Double { min: Option, max: Option }, + Integer { min: Option, max: Option }, + Long { min: Option, max: Option }, + String(StringType), +} + +impl Parser { + pub fn serialize(&self, w: &mut impl std::io::Write, new: bool) -> anyhow::Result<()> { + match (self, new) { + (Parser::Bool, true) => VarInt(0).serialize(w)?, + (Parser::Bool, false) => "brigadier:bool".serialize(w)?, + (Parser::Float { .. }, true) => VarInt(1).serialize(w)?, + (Parser::Float { .. }, false) => "brigadier:float".serialize(w)?, + (Parser::Double { .. }, true) => VarInt(2).serialize(w)?, + (Parser::Double { .. }, false) => "brigadier:double".serialize(w)?, + (Parser::Integer { .. }, true) => VarInt(3).serialize(w)?, + (Parser::Integer { .. }, false) => "brigadier:integer".serialize(w)?, + (Parser::Long { .. }, true) => VarInt(4).serialize(w)?, + (Parser::Long { .. }, false) => "brigadier:long".serialize(w)?, + (Parser::String(_), true) => VarInt(5).serialize(w)?, + (Parser::String(_), false) => "brigadier:string".serialize(w)?, + }; + match self { + Parser::Bool => (), + Parser::Float { min, max } => { + let flags: u8 = (min.is_some() as u8) | (max.is_some() as u8 * 0x02); + flags.serialize(w)?; + if let Some(min) = min { + min.serialize(w)?; + } + if let Some(max) = max { + max.serialize(w)?; + } + } + Parser::Double { min, max } => { + let flags: u8 = (min.is_some() as u8) | (max.is_some() as u8 * 0x02); + flags.serialize(w)?; + if let Some(min) = min { + min.serialize(w)?; + } + if let Some(max) = max { + max.serialize(w)?; + } + } + Parser::Integer { min, max } => { + let flags: u8 = (min.is_some() as u8) | (max.is_some() as u8 * 0x02); + flags.serialize(w)?; + if let Some(min) = min { + min.serialize(w)?; + } + if let Some(max) = max { + max.serialize(w)?; + } + } + Parser::Long { min, max } => { + let flags: u8 = (min.is_some() as u8) | (max.is_some() as u8 * 0x02); + flags.serialize(w)?; + if let Some(min) = min { + min.serialize(w)?; + } + if let Some(max) = max { + max.serialize(w)?; + } + } + Parser::String(ty) => { + match ty { + StringType::SingleWord => VarInt(0), + StringType::QuotablePhrase => VarInt(1), + StringType::GreedyPhrase => VarInt(2), + }.serialize(w)?; + } + } + Ok(()) + } +} + +#[derive(Clone, Copy, Debug)] +pub enum StringType { + SingleWord, + QuotablePhrase, + GreedyPhrase, +} diff --git a/src/main.rs b/src/main.rs index 3936aa4..42d9ed3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ -use std::{sync::Arc, time::Duration}; +use std::{time::Duration, net::SocketAddr}; use once_cell::sync::OnceCell; -use tokio::sync::Mutex; use uuid::Uuid; mod protocol; @@ -10,11 +9,12 @@ mod plugin; mod ser; mod varint; mod client; +mod command; pub const QC_VERSION: &str = env!("CARGO_PKG_VERSION"); pub static HTTP_CLIENT: OnceCell = OnceCell::new(); +pub static CONFIG: OnceCell = OnceCell::new(); -pub type ArcMutex = Arc>; pub type JsonValue = serde_json::Value; #[derive(Clone, Debug)] @@ -24,45 +24,30 @@ pub struct Player { } #[derive(Clone, Debug)] -pub struct ClientInfo { - addr: String, - port: u16, - proto_version: i32, - proto_name: &'static str, +pub struct ServerConfig { + address: SocketAddr, + max_players: u32, } - #[tokio::main] async fn main() { + let address: SocketAddr = std::env::var("QC_ADDRESS") + .unwrap_or_else(|_| "0.0.0.0:25565".to_owned()) + .parse().unwrap(); + + let max_players: u32 = std::env::var("QC_MAX_PLAYERS") + .map(|s| s.parse().unwrap()) + .unwrap_or(20); + + CONFIG.set(ServerConfig { + address, + max_players + }).unwrap(); + HTTP_CLIENT.set(reqwest::ClientBuilder::new() .timeout(Duration::from_secs(5)) .build().unwrap() ).unwrap(); + 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 26efea6..995f8c2 100644 --- a/src/plugin/event.rs +++ b/src/plugin/event.rs @@ -1,31 +1,23 @@ -use mlua::{Value, FromLua, Table}; use uuid::Uuid; -use crate::{Player, ClientInfo, JsonValue}; - -use super::UuidUD; +use crate::{Player, JsonValue}; pub enum PluginEvent { - Kick(Uuid, Option), - Chat(Uuid, JsonValue, JsonValue), - BroadcastChat(JsonValue, JsonValue), - System(Uuid, JsonValue, bool), - BroadcastSystem(JsonValue, bool), + Kick(Uuid, Option), + Chat(JsonValue, Option), + System(JsonValue, Option), + UpdateCommands, } -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}"))), - } - } +#[derive(Clone)] +pub struct ConnectionInfo { + pub server_addr: (String, u16), + pub remote_addr: (String, u16), + pub protocol: (i32, String), } pub enum ServerEvent { - Join(Player, ClientInfo), + Join(Player, ConnectionInfo), Leave(Player), Command(Player, String), Message(Player, String), diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index b45537c..36a872a 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -2,9 +2,11 @@ use std::sync::Arc; use log::warn; use mlua::{Lua, Function, IntoLua, FromLua, Value, Table, AnyUserData}; -use tokio::sync::mpsc::{Sender, Receiver}; +use tokio::sync::{mpsc::{Sender, Receiver}, RwLock}; use uuid::Uuid; +use crate::command::Commands; + use self::{event::{PluginEvent, ServerEvent}, server::{Server, WrappedServer}, plugin::load_plugins}; pub mod event; @@ -15,12 +17,12 @@ mod server; struct UuidUD(Uuid); impl<'lua> FromLua<'lua> for UuidUD { - fn from_lua(value: mlua::Value<'lua>, _lua: &'lua Lua) -> mlua::Result { + 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 { @@ -34,22 +36,41 @@ impl mlua::UserData for UuidUD { } } -fn broadcast_event<'lua, F>(lua: &'lua Lua, fname: &str, callback: F) -> mlua::Result<()> -where F: Fn(&str, Table<'lua>, AnyUserData, Function) -> mlua::Result<()> { +fn broadcast_event<'lua, F>(lua: &'lua Lua, prio: &[String], 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) { + for id in prio { + let table: Table = plugins.get(id.clone())?; + 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); + let result = callback(id, table.clone(), srv.clone(), f); + match result { + Ok(Some(true)) => return Ok(()), + Ok(_) => (), + Err(e) => warn!("error in {fname} in plugin {}: {e}", id), } - } - } + } + } Ok(()) } -pub async fn run_plugins(tx: Sender, mut rx: Receiver) -> mlua::Result<()> { +fn run_event<'lua, F>(lua: &'lua Lua, fname: &str, id: &str, callback: F) -> mlua::Result> +where F: FnOnce(Table<'lua>, AnyUserData, Function) -> mlua::Result<()> { + let srv: AnyUserData = lua.globals().get("srv")?; + let plugins: Table = srv.nth_user_value(1)?; + let table: Table = plugins.get(id)?; + if let Ok(f) = table.get::<_, Function>(fname) { + srv.set_nth_user_value(2, id.clone())?; + if let Err(e) = callback(table.clone(), srv.clone(), f) { + warn!("error in {fname} in plugin {}: {e}", id); + return Ok(Some(e)); + } + } + Ok(None) +} + +pub async fn run_plugins(tx: Sender, mut rx: Receiver, commands: Arc>) -> mlua::Result<()> { let lua = unsafe { mlua::Lua::unsafe_new_with( mlua::StdLib::ALL_SAFE | mlua::StdLib::DEBUG, @@ -59,58 +80,108 @@ pub async fn run_plugins(tx: Sender, mut rx: Receiver) let server = WrappedServer(Arc::new(Server { tx, + commands, + command_ids: Default::default(), names: Default::default(), info: Default::default(), })); + let prio; { let server_any: AnyUserData = FromLua::from_lua(IntoLua::into_lua(server.clone(), &lua)?, &lua)?; - let plugins = load_plugins(&lua)?; + let (plugins, priorities) = load_plugins(&lua)?; + prio = priorities; server_any.set_nth_user_value(1, plugins)?; server_any.set_nth_user_value(2, Value::Nil)?; lua.globals().set("srv", server_any.clone())?; } - broadcast_event(&lua, "init", |_, pl, _, f| { + broadcast_event(&lua, &prio, "init", |_, pl, _, f| { f.call(pl) })?; + server.tx.send(PluginEvent::UpdateCommands).await.unwrap(); + while let Some(ev) = rx.recv().await { match ev { - ServerEvent::Join(player, info) => { + ServerEvent::Join(player, conn_info) => { 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() - )) - })?; + server.info.write().await.insert(player.uuid, conn_info); + broadcast_event(&lua, &prio, "on_join", |_, pl, _, f| { + f.call(( + pl, + UuidUD(player.uuid), + player.name.clone() + )) + })?; }, ServerEvent::Leave(player) => { - broadcast_event(&lua, "on_leave", |_, pl, _, f| { - f.call::<_, ()>(( - pl, - UuidUD(player.uuid), - player.name.clone() - )) - })?; + broadcast_event(&lua, &prio, "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, + broadcast_event(&lua, &prio, "on_message", |_, pl, _, f| { + f.call(( + pl, msg.clone(), - UuidUD(player.uuid), - player.name.clone() - )) - })?; + UuidUD(player.uuid), + player.name.clone() + )) + })?; }, - ServerEvent::Command(_, _) => () // TODO command registerment + ServerEvent::Command(sender, msg) => { + let msg2 = msg.clone(); + let mut parts = msg2.split_whitespace(); + let cmd = parts.next().unwrap_or_default(); + + if let Some(id) = server.command_ids.read().await.get(cmd) { + let cmd = cmd.strip_prefix(&(id.clone() + ":")).unwrap_or(cmd); + + let args = parts.map(|s| s.to_owned()).collect::>(); + let result = run_event(&lua, "on_command", id, |pl, _, f| { + f.call::<_, ()>(( + pl, + cmd.to_owned(), + args, + msg, + UuidUD(sender.uuid), + sender.name, + )) + })?; + if result.is_some() { + let msg = serde_json::json!{{ + "text": "An internal error occured while trying to execute that command", + "color": "red", + }}; + server.tx.send(PluginEvent::System(msg, Some(sender.uuid))) + .await.unwrap(); + } + + } else { + // TODO couldn't do vanilla localization keys, so + // make this easier for plugins to modify/localize + let msg = serde_json::json!{[ + { + "text": "Unknown command: ", + "color": "red", + }, + { + "text": cmd, + "color": "gold", + } + ]}; + server.tx.send(PluginEvent::System(msg, Some(sender.uuid))) + .await.unwrap(); + } + } } } Ok(()) diff --git a/src/plugin/plugin.rs b/src/plugin/plugin.rs index 7a2a721..1f96c8e 100644 --- a/src/plugin/plugin.rs +++ b/src/plugin/plugin.rs @@ -6,7 +6,13 @@ use mlua::{Lua, Table}; use crate::QC_VERSION; -fn load_plugin<'lua>(entry: &DirEntry, lua: &'lua Lua) -> anyhow::Result<(String, Table<'lua>)> { +struct PluginData<'lua> { + id: String, + priority: i32, + table: Table<'lua>, +} + +fn load_plugin<'lua>(entry: &DirEntry, lua: &'lua Lua) -> anyhow::Result> { let ty = entry.file_type()?; let mut path = entry.path(); if ty.is_dir() { @@ -19,6 +25,14 @@ fn load_plugin<'lua>(entry: &DirEntry, lua: &'lua Lua) -> anyhow::Result<(String let id = table.get("id") .map_err(|e| anyhow!("could not get plugin id: {e}"))?; + let priority = table.get::<_, Option>("priority") + .map_err(|_| anyhow!("invalid priority value"))? + .unwrap_or(50); + + if !(0..=100).contains(&priority) { + return Err(anyhow!("priority {priority} out of range 0..=100")) + } + if let Ok(qc_ver) = table.get::<_, String>("server_version") { debug!("checking plugin version"); let req = semver::VersionReq::parse(&qc_ver)?; @@ -29,10 +43,10 @@ fn load_plugin<'lua>(entry: &DirEntry, lua: &'lua Lua) -> anyhow::Result<(String warn!("plugin '{id}' is missing a version reqirement"); } - Ok((id, table)) + Ok(PluginData { id, priority, table }) } -pub fn load_plugins(lua: &Lua) -> mlua::Result { +pub fn load_plugins(lua: &Lua) -> mlua::Result<(Table, Vec)> { let plugins = lua.create_table()?; debug!("loading plugins"); let entries = match fs::read_dir("./plugins") { @@ -42,13 +56,14 @@ pub fn load_plugins(lua: &Lua) -> mlua::Result
{ if let Err(e) = fs::create_dir("./plugins") { warn!("failed to create plugins directory: {e}"); } - return Ok(plugins) + return Ok((plugins, vec![])) }, Err(e) => { warn!("failed to load plugins: {e}"); - return Ok(plugins) + return Ok((plugins, vec![])) } }; + let mut priorities = Vec::new(); for entry in entries { let entry = match entry { Ok(e) => e, @@ -61,12 +76,21 @@ pub fn load_plugins(lua: &Lua) -> mlua::Result
{ 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)?; + Ok(plugin) => { + if let Ok(true) = plugins.contains_key(plugin.id.clone()) { + warn!("a plugin with id {} is already registered, skipping", plugin.id) + } else { + info!("loaded plugin {}", plugin.id); + plugins.set(plugin.id.clone(), plugin.table)?; + priorities.push((plugin.priority, plugin.id)); + } } Err(e) => warn!("error loading plugin at {spath}: {e}"), } } - Ok(plugins) + priorities.sort_by_cached_key(|(i, _)| -i); + debug!("plugin priorities: {priorities:?}"); + let priorities = priorities.into_iter() + .map(|(_, id)| id).collect(); + Ok((plugins, priorities)) } diff --git a/src/plugin/server.rs b/src/plugin/server.rs index 0cb0fa8..44da4ee 100644 --- a/src/plugin/server.rs +++ b/src/plugin/server.rs @@ -1,22 +1,24 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{collections::{HashMap, hash_map::Entry}, sync::Arc}; -use log::{Record, warn}; +use log::{Record, debug}; 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, QC_VERSION, JsonValue}; +use crate::{QC_VERSION, JsonValue, command::Commands}; -use super::{event::PluginEvent, UuidUD}; +use super::{event::{PluginEvent, ConnectionInfo}, UuidUD}; -impl<'lua> IntoLua<'lua> for ClientInfo { +impl<'lua> IntoLua<'lua> for ConnectionInfo { 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)?; - table.set("proto_name", self.proto_name)?; - table.set("proto_version", self.proto_version)?; + table.set("server_addr", self.server_addr.0)?; + table.set("server_port", self.server_addr.1)?; + table.set("remote_addr", self.remote_addr.0)?; + table.set("remote_port", self.remote_addr.1)?; + table.set("proto_version", self.protocol.0)?; + table.set("proto_name", self.protocol.1)?; Ok(Value::Table(table)) } } @@ -41,7 +43,6 @@ fn lua_log(level: log::Level, path: &str, msg: String, lua: &Lua) { record.file(Some(file)); } log::logger().log(&record.args(format_args!("{}", msg)).build()); - } const LEVELS: [(&str, log::Level); 5] = [ @@ -54,18 +55,29 @@ const LEVELS: [(&str, log::Level); 5] = [ fn value_to_chat<'lua>(lua: &'lua Lua, value: Value<'lua>) -> mlua::Result { match value { + Value::Table(_) => Ok(lua.from_value(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())), + _ => Err(mlua::Error::RuntimeError("()".to_owned())), + } +} + +fn opt_value_to_chat<'lua>(lua: &'lua Lua, value: Value<'lua>) -> mlua::Result> { + match value { + Value::Table(_) => Ok(Some(lua.from_value(value)?)), + Value::String(s) => Ok(Some(json!{{"text": s.to_str()?}})), + Value::Nil => Ok(None), + _ => Err(mlua::Error::RuntimeError("()".to_owned())), } } pub struct Server { pub tx: Sender, + pub commands: Arc>, + pub command_ids: RwLock>, pub names: RwLock>, - pub info: RwLock>, + pub info: RwLock>, } #[derive(Clone)] @@ -82,8 +94,59 @@ impl std::ops::Deref for WrappedServer { impl mlua::UserData for WrappedServer { fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { // commands - methods.add_async_method("add_command", |_lua, _this, ()| async { - warn!("TODO add command"); + methods.add_async_function("add_command", |_lua, (this, name): (AnyUserData, String)| async move { + let id: String = this.nth_user_value(2)?; + + debug!("registering simple command '{name}' for plugin '{id}'"); + + let this_srv: std::cell::Ref = this.borrow()?; + + let mut cmds_guard = this_srv.commands.write().await; + let mut cmd_ids_guard = this_srv.command_ids.write().await; + + let qualified_name = id.clone() + ":" + &name; + + // unqualified name + if let Some(other_id) = cmd_ids_guard.get(&name) { + debug!("cannot register unqualified command '{name}' for plugin '{id}', as it is already in use by plugin '{other_id}'") + } else { + cmd_ids_guard.insert(name.clone(), id.clone()); + + cmds_guard.add_simple_command(name) + .map_err(mlua::Error::runtime)?; + } + + // qualified name + cmd_ids_guard.insert(qualified_name.clone(), id); + + cmds_guard.add_simple_command(qualified_name) + .map_err(mlua::Error::runtime)?; + + drop(cmd_ids_guard); + drop(cmds_guard); + + Ok(()) + }); + + methods.add_async_function("register_command", |_lua, (this, name): (AnyUserData, String)| async move { + let id: String = this.nth_user_value(2)?; + let this_srv: std::cell::Ref = this.borrow()?; + + let mut cmd_ids_guard = this_srv.command_ids.write().await; + + let qualified_name = id.clone() + ":" + &name; + cmd_ids_guard.insert(qualified_name, id.clone()); + + if let Entry::Vacant(entry) = cmd_ids_guard.entry(name) { + entry.insert(id.clone()); + Ok(true) + } else { + Ok(false) + } + }); + + methods.add_async_method("update_commands", |_lua, this, ()| async { + this.tx.send(PluginEvent::UpdateCommands).await.unwrap(); Ok(()) }); @@ -96,11 +159,19 @@ impl mlua::UserData for WrappedServer { Ok(table) }); methods.add_async_method("get_name", |lua, this, UuidUD(uuid)| async move { - match this.info.read().await.get(&uuid) { + match this.names.read().await.get(&uuid) { Some(s) => s.clone().into_lua(lua), None => Ok(Value::Nil), } }); + methods.add_async_method("get_uuid", |lua, this, name: String| async move { + for (uuid, pname) in this.names.read().await.iter() { + if pname.eq_ignore_ascii_case(&name) { + return UuidUD(*uuid).into_lua(lua) + } + } + Ok(Value::Nil) + }); 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), @@ -109,58 +180,26 @@ impl mlua::UserData for WrappedServer { }); // events - methods.add_async_method("kick", |_, this, (UuidUD(uuid), msg)| async move { + methods.add_async_method("kick", |lua, this, (UuidUD(uuid), msg)| async move { + let msg = opt_value_to_chat(lua, msg)?; this.tx .send(PluginEvent::Kick(uuid, msg)) - .await - .map_err(|e| mlua::Error::RuntimeError(e.to_string())) + .await.unwrap(); + Ok(()) }); - 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 { + methods.add_async_method("send_msg", |lua, this, (msg, uuid): (Value, Option)| 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())) + .send(PluginEvent::System(msg, uuid.map(|u| u.0))) + .await.unwrap(); + Ok(()) }); - methods.add_async_method("broadcast_chat", |lua, this, (msg, name): (Value, Value)| async move { + methods.add_async_method("send_chat", |lua, this, (msg, uuid): (Value, Option)| 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)) + .send(PluginEvent::Chat(msg, uuid.map(|u| u.0))) + .await.unwrap(); + Ok(()) }); // logging @@ -172,6 +211,22 @@ impl mlua::UserData for WrappedServer { Ok(()) }); } + + // 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)) + }); + methods.add_function("parse_uuid", |lua, uuid: String| { + match Uuid::parse_str(&uuid) { + Ok(u) => UuidUD(u).into_lua(lua), + Err(_) => Ok(Value::Nil), + } + }); + } fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) { diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 3706127..40ce71a 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -2,7 +2,7 @@ use std::io::{Read, Write}; use uuid::Uuid; -use crate::{JsonValue, ser::Position}; +use crate::{JsonValue, ser::Position, command::Commands}; mod common; mod v1_20_1; @@ -91,6 +91,7 @@ pub enum ServerPacket { }, SystemMessage(JsonValue, bool), ChatMessage(JsonValue, JsonValue), + CommandData(Commands), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/src/protocol/v1_13_2.rs b/src/protocol/v1_13_2.rs index dcaab5a..6acb3f8 100644 --- a/src/protocol/v1_13_2.rs +++ b/src/protocol/v1_13_2.rs @@ -2,7 +2,7 @@ use std::io::{Write, Read}; use log::trace; -use crate::{ser::{Serializable, Deserializable}, varint::VarInt}; +use crate::{ser::{Serializable, Deserializable, PositionOld}, varint::VarInt, command::Commands}; use super::{ServerPacket, ClientPacket, Protocol, ProtocolState, common::COMMON}; @@ -35,6 +35,12 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> if overlay { 2u8 } else { 1u8 } // system or overlay .serialize(&mut w)?; }, + ServerPacket::CommandData(commands) => { + VarInt(0x11).serialize(&mut w)?; + VarInt(commands.len()).serialize(&mut w)?; + commands.serialize(&mut w, false)?; + VarInt(Commands::root()).serialize(&mut w)?; + }, ServerPacket::PluginMessage { channel, data } => { VarInt(0x19).serialize(&mut w)?; channel.serialize(&mut w)?; @@ -83,7 +89,7 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> } ServerPacket::SetDefaultSpawn { pos, angle: _ } => { VarInt(0x49).serialize(&mut w)?; - pos.serialize(&mut w)?; + PositionOld::from(pos).serialize(&mut w)?; }, _ => { (COMMON.encode)(w, state, ev)?; } } diff --git a/src/protocol/v1_14_4.rs b/src/protocol/v1_14_4.rs index 39750af..d1a04d8 100644 --- a/src/protocol/v1_14_4.rs +++ b/src/protocol/v1_14_4.rs @@ -2,7 +2,7 @@ use std::io::{Write, Read}; use log::trace; -use crate::{ser::{Serializable, Deserializable}, varint::VarInt}; +use crate::{ser::{Serializable, Deserializable}, varint::VarInt, command::Commands}; use super::{ServerPacket, ClientPacket, Protocol, ProtocolState, common::COMMON}; @@ -35,6 +35,12 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> if overlay { 2u8 } else { 1u8 } // system or overlay .serialize(&mut w)?; }, + ServerPacket::CommandData(commands) => { + VarInt(0x11).serialize(&mut w)?; + VarInt(commands.len()).serialize(&mut w)?; + commands.serialize(&mut w, false)?; + VarInt(Commands::root()).serialize(&mut w)?; + }, ServerPacket::PluginMessage { channel, data } => { VarInt(0x18).serialize(&mut w)?; channel.serialize(&mut w)?; diff --git a/src/protocol/v1_15_2.rs b/src/protocol/v1_15_2.rs index 84bc055..a9577b7 100644 --- a/src/protocol/v1_15_2.rs +++ b/src/protocol/v1_15_2.rs @@ -2,7 +2,7 @@ use std::io::{Write, Read}; use log::trace; -use crate::{ser::{Serializable, Deserializable}, varint::VarInt}; +use crate::{ser::{Serializable, Deserializable}, varint::VarInt, command::Commands}; use super::{ServerPacket, ClientPacket, Protocol, ProtocolState, common::COMMON}; @@ -35,6 +35,12 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> if overlay { 2u8 } else { 1u8 } // system or overlay .serialize(&mut w)?; }, + ServerPacket::CommandData(commands) => { + VarInt(0x12).serialize(&mut w)?; + VarInt(commands.len()).serialize(&mut w)?; + commands.serialize(&mut w, false)?; + VarInt(Commands::root()).serialize(&mut w)?; + }, ServerPacket::PluginMessage { channel, data } => { VarInt(0x19).serialize(&mut w)?; channel.serialize(&mut w)?; diff --git a/src/protocol/v1_16_5.rs b/src/protocol/v1_16_5.rs index e32281f..08b9048 100644 --- a/src/protocol/v1_16_5.rs +++ b/src/protocol/v1_16_5.rs @@ -2,7 +2,7 @@ use std::io::{Write, Read}; use log::trace; -use crate::{ser::{Serializable, Deserializable}, varint::VarInt}; +use crate::{ser::{Serializable, Deserializable}, varint::VarInt, command::Commands}; use super::{ServerPacket, ClientPacket, Protocol, ProtocolState, common::COMMON}; @@ -37,6 +37,12 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> .serialize(&mut w)?; 0u128.serialize(&mut w)?; // null uuid }, + ServerPacket::CommandData(commands) => { + VarInt(0x10).serialize(&mut w)?; + VarInt(commands.len()).serialize(&mut w)?; + commands.serialize(&mut w, false)?; + VarInt(Commands::root()).serialize(&mut w)?; + }, ServerPacket::PlayDisconnect(msg) => { VarInt(0x19).serialize(&mut w)?; msg.serialize(&mut w)?; diff --git a/src/protocol/v1_17_1.rs b/src/protocol/v1_17_1.rs index ab059c2..b646eeb 100644 --- a/src/protocol/v1_17_1.rs +++ b/src/protocol/v1_17_1.rs @@ -2,7 +2,7 @@ use std::io::{Write, Read}; use log::trace; -use crate::{ser::{Serializable, Deserializable}, varint::VarInt}; +use crate::{ser::{Serializable, Deserializable}, varint::VarInt, command::Commands}; use super::{ServerPacket, ClientPacket, Protocol, ProtocolState, common::COMMON}; @@ -37,6 +37,12 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> .serialize(&mut w)?; 0u128.serialize(&mut w)?; // null uuid }, + ServerPacket::CommandData(commands) => { + VarInt(0x12).serialize(&mut w)?; + VarInt(commands.len()).serialize(&mut w)?; + commands.serialize(&mut w, false)?; + VarInt(Commands::root()).serialize(&mut w)?; + }, ServerPacket::PluginMessage { channel, data } => { VarInt(0x18).serialize(&mut w)?; channel.serialize(&mut w)?; diff --git a/src/protocol/v1_18_2.rs b/src/protocol/v1_18_2.rs index 3143f29..8d8cc14 100644 --- a/src/protocol/v1_18_2.rs +++ b/src/protocol/v1_18_2.rs @@ -2,7 +2,7 @@ use std::io::{Write, Read}; use log::trace; -use crate::{ser::{Serializable, Deserializable}, varint::VarInt}; +use crate::{ser::{Serializable, Deserializable}, varint::VarInt, command::Commands}; use super::{ServerPacket, ClientPacket, Protocol, ProtocolState, common::COMMON}; @@ -37,6 +37,12 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> .serialize(&mut w)?; 0u128.serialize(&mut w)?; // null uuid }, + ServerPacket::CommandData(commands) => { + VarInt(0x12).serialize(&mut w)?; + VarInt(commands.len()).serialize(&mut w)?; + commands.serialize(&mut w, false)?; + VarInt(Commands::root()).serialize(&mut w)?; + }, ServerPacket::PluginMessage { channel, data } => { VarInt(0x18).serialize(&mut w)?; channel.serialize(&mut w)?; diff --git a/src/protocol/v1_19_4.rs b/src/protocol/v1_19_4.rs index 2283d44..dc7959a 100644 --- a/src/protocol/v1_19_4.rs +++ b/src/protocol/v1_19_4.rs @@ -3,7 +3,7 @@ use std::io::{Write, Read}; use log::trace; use uuid::Uuid; -use crate::{ser::{Serializable, Deserializable}, varint::VarInt}; +use crate::{ser::{Serializable, Deserializable}, varint::VarInt, command::Commands}; use super::{ServerPacket, ClientPacket, Protocol, ProtocolState, common::COMMON}; @@ -27,6 +27,12 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> // number of properties (0) VarInt(0x00).serialize(&mut w)?; }, + ServerPacket::CommandData(commands) => { + VarInt(0x10).serialize(&mut w)?; + VarInt(commands.len()).serialize(&mut w)?; + commands.serialize(&mut w, true)?; + VarInt(Commands::root()).serialize(&mut w)?; + }, ServerPacket::PlayDisconnect(msg) => { VarInt(0x1A).serialize(&mut w)?; msg.serialize(&mut w)?; diff --git a/src/protocol/v1_20_1.rs b/src/protocol/v1_20_1.rs index 806ad4a..dfc0cc5 100644 --- a/src/protocol/v1_20_1.rs +++ b/src/protocol/v1_20_1.rs @@ -3,7 +3,7 @@ use std::io::{Write, Read}; use log::trace; use uuid::Uuid; -use crate::{ser::{Serializable, Deserializable}, varint::VarInt}; +use crate::{ser::{Serializable, Deserializable}, varint::VarInt, command::Commands}; use super::{ServerPacket, ClientPacket, Protocol, ProtocolState, common::COMMON}; @@ -27,6 +27,12 @@ fn encode(mut w: Box<&mut dyn Write>, state: ProtocolState, ev: ServerPacket) -> // number of properties (0) VarInt(0x00).serialize(&mut w)?; }, + ServerPacket::CommandData(commands) => { + VarInt(0x10).serialize(&mut w)?; + VarInt(commands.len()).serialize(&mut w)?; + commands.serialize(&mut w, true)?; + VarInt(Commands::root()).serialize(&mut w)?; + }, ServerPacket::PluginMessage { channel, data } => { VarInt(0x17).serialize(&mut w)?; channel.serialize(&mut w)?; diff --git a/src/ser.rs b/src/ser.rs index d033656..ec6f0e0 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -214,19 +214,6 @@ impl Deserializable for Uuid { } } -// impl Serializable for nbt::Blob { -// fn serialize(&self, w: &mut impl Write) -> anyhow::Result<()> { -// self.to_writer(w)?; -// Ok(()) -// } -// } -// -// impl Deserializable for nbt::Blob { -// fn deserialize(r: &mut impl ReadExt) -> anyhow::Result { -// Ok(nbt::Blob::from_reader(r)?) -// } -// } - #[derive(Clone, Copy, Debug)] pub struct Position { pub x: i32, @@ -234,11 +221,34 @@ pub struct Position { pub z: i32, } + impl Serializable for Position { fn serialize(&self, w: &mut impl Write) -> anyhow::Result<()> { - let value = (((self.x & 0x3FFFFFF) as u64) << 38) | - (((self.z & 0x3FFFFFF) as u64) << 12) | - (((self.y & 0xFFF) as u64) << 38); + let value = (((self.x & 0x3FFFFFF) as u64) << 38) + | (((self.z & 0x3FFFFFF) as u64) << 12) + | ((self.y & 0xFFF) as u64); value.serialize(w) } } + +#[derive(Clone, Copy, Debug)] +pub struct PositionOld { + pub x: i32, + pub z: i32, + pub y: i16, +} + +impl Serializable for PositionOld { + fn serialize(&self, w: &mut impl Write) -> anyhow::Result<()> { + let value = (((self.x & 0x3FFFFFF) as u64) << 38) + | (((self.y & 0xFFF) as u64) << 26) + | ((self.z & 0x3FFFFFF) as u64); + value.serialize(w) + } +} + +impl From for PositionOld { + fn from(value: Position) -> Self { + Self { x: value.x, y: value.y, z: value.z } + } +} diff --git a/src/server.rs b/src/server.rs index a2b154a..b4f2c12 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,22 +1,27 @@ -use std::{collections::HashMap, time::Duration, net::{ToSocketAddrs, SocketAddr}}; +use std::{collections::HashMap, time::Duration, net::SocketAddr, sync::Arc}; use log::{info, error}; -use tokio::{net::{TcpListener, TcpStream}, sync::mpsc::{Sender, self}, select}; +use serde_json::json; +use tokio::{net::{TcpListener, TcpStream}, sync::{mpsc::{Sender, self}, RwLock}, select}; use uuid::Uuid; use anyhow::anyhow; -use crate::{client::run_client, Player, ClientInfo, client::event::{ServerEvent, ClientEvent}, plugin::{run_plugins, event::PluginEvent}}; +use crate::{client::{event::ClientInfo, run_client}, Player, client::event::{ServerEvent, ClientEvent}, plugin::{run_plugins, event::{PluginEvent, ConnectionInfo}}, command::Commands, CONFIG}; type PServerEvent = crate::plugin::event::ServerEvent; -pub struct Client { + +struct Client { tx: Sender, + remote_addr: SocketAddr, keepalive: Option, info: Option, player: Option, } + struct Server { + commands: Arc>, gc_tx: Sender<(i32, ClientEvent)>, ps_tx: Sender, next_id: i32, @@ -30,19 +35,22 @@ pub async fn run_server() -> anyhow::Result<()> { let (ps_tx, ps_rx) = mpsc::channel(256); let (pc_tx, mut pc_rx) = mpsc::channel(256); + let commands = Arc::new(RwLock::new(Commands::new())); + let pl_commands = commands.clone(); + tokio::spawn(async { - if let Err(e) = run_plugins(pc_tx, ps_rx).await { - println!("error in plugins: {e}"); + if let Err(e) = run_plugins(pc_tx, ps_rx, pl_commands).await { + error!("error in plugins: {e}"); } }); - let socket_addr = "0.0.0.0:25567".to_socket_addrs()? - .next().expect("invalid server address"); - let listener = TcpListener::bind(socket_addr).await?; + let server_addr = CONFIG.get().unwrap().address; + let listener = TcpListener::bind(server_addr).await?; let (gc_tx, mut gc_rx) = mpsc::channel(16); let mut server = Server { + commands, gc_tx, ps_tx, next_id: 0, clients: HashMap::new(), @@ -51,12 +59,12 @@ pub async fn run_server() -> anyhow::Result<()> { let mut keepalive = tokio::time::interval(Duration::from_secs(15)); - info!("listening on {socket_addr}"); + info!("listening on {server_addr}"); loop { select! { conn = listener.accept() => { let (stream, addr) = conn?; - accept_connection(&mut server, stream, addr).await?; + accept_connection(&mut server, stream, addr).await; }, _ = keepalive.tick() => handle_keepalive(&mut server).await?, ev = pc_rx.recv() => { @@ -74,15 +82,21 @@ pub async fn run_server() -> anyhow::Result<()> { } } } -async fn accept_connection(server: &mut Server, stream: TcpStream, addr: SocketAddr) -> anyhow::Result<()> { +async fn accept_connection(server: &mut Server, stream: TcpStream, addr: SocketAddr) { let gc_tx = server.gc_tx.clone(); - let id = server.next_id; - server.next_id = server.next_id.wrapping_add(1); + let id = loop { + let id = server.next_id; + server.next_id = server.next_id.wrapping_add(1); + if !server.clients.contains_key(&id) { + break id + } + }; let (gs_tx, gs_rx) = mpsc::channel(16); server.clients.insert(id, Client { tx: gs_tx, + remote_addr: addr, keepalive: None, info: None, player: None, @@ -96,13 +110,12 @@ async fn accept_connection(server: &mut Server, stream: TcpStream, addr: SocketA } 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?; + client.tx.send(ServerEvent::Disconnect(None)).await?; } else if client.player.is_some() { let data = 1; client.keepalive = Some(data); @@ -121,34 +134,42 @@ async fn handle_plugin_event(server: &mut Server, ev: PluginEvent) -> anyhow::Re .await?; } }, - PluginEvent::Chat(uuid, msg, name) => { + PluginEvent::Chat(msg, Some(uuid)) => { if let Some(id) = server.ids.get(&uuid) { server.clients[id].tx - .send(ServerEvent::ChatMessage(msg, name)) + .send(ServerEvent::ChatMessage(msg, serde_json::json!({"text":""}))) .await?; } }, - PluginEvent::BroadcastChat(msg, name) => { + PluginEvent::Chat(msg, None) => { for client in server.clients.values() { if client.player.is_some() { - client.tx.send(ServerEvent::ChatMessage(msg.clone(), name.clone())).await?; + client.tx.send(ServerEvent::ChatMessage(msg.clone(), serde_json::json!({"text":""}))).await?; } } }, - PluginEvent::System(uuid, msg, overlay) => { + PluginEvent::System(msg, Some(uuid)) => { if let Some(id) = server.ids.get(&uuid) { server.clients[id].tx - .send(ServerEvent::SystemMessage(msg, overlay)) + .send(ServerEvent::SystemMessage(msg, false)) .await?; } }, - PluginEvent::BroadcastSystem(msg, overlay) => { + PluginEvent::System(msg, None) => { for client in server.clients.values() { if client.player.is_some() { - client.tx.send(ServerEvent::SystemMessage(msg.clone(), overlay)).await?; + client.tx.send(ServerEvent::SystemMessage(msg.clone(), false)).await?; } } }, + PluginEvent::UpdateCommands => { + let commands = server.commands.read().await.clone(); + for client in server.clients.values() { + if client.player.is_some() { + client.tx.send(ServerEvent::UpdateCommands(commands.clone())).await?; + } + } + } } Ok(()) } @@ -165,13 +186,20 @@ async fn handle_client_event(server: &mut Server, id: i32, ev: ClientEvent) -> a .send(PServerEvent::Leave(other_client.player.unwrap())) .await?; other_client.tx - .send(ServerEvent::Disconnect(Some("logged in elsewhere".to_owned()))) + .send(ServerEvent::Disconnect(Some(json!({"translate": "multiplayer.disconnect.duplicate_login"})))) .await?; } let client = server.clients.get_mut(&id).unwrap(); client.player = Some(player.clone()); + client.tx.send(ServerEvent::UpdateCommands(server.commands.read().await.clone())).await?; server.ids.insert(player.uuid, id); - server.ps_tx.send(PServerEvent::Join(player, client.info.clone().unwrap())).await?; + let info = client.info.as_ref().unwrap(); + let conn_info = ConnectionInfo { + server_addr: (info.addr.to_owned(), info.port), + remote_addr: (client.remote_addr.ip().to_string(), client.remote_addr.port()), + protocol: (info.proto_version, info.proto_name.to_owned()) + }; + server.ps_tx.send(PServerEvent::Join(player, conn_info)).await?; }, ClientEvent::Disconnect => if let Some(client) = server.clients.remove(&id) { if let Some(player) = client.player {