This commit is contained in:
TriMill 2023-08-07 22:24:28 -04:00
parent ff4423d593
commit 06a0beba24
25 changed files with 952 additions and 250 deletions

8
Cargo.lock generated
View file

@ -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",

View file

@ -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"] }

97
PLUGINS.md Normal file
View file

@ -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.

View file

@ -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

View file

@ -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,
}

View file

@ -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
}

View file

@ -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<String>),
Disconnect(Option<JsonValue>),
KeepAlive(i64),
SystemMessage(JsonValue, bool),
ChatMessage(JsonValue, JsonValue),
UpdateCommands(Commands),
}

View file

@ -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? {

263
src/command.rs Normal file
View file

@ -0,0 +1,263 @@
use crate::{ser::Serializable, varint::VarInt};
#[derive(Clone, Debug)]
pub struct Commands(Vec<Node>);
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<i32, &'static str> {
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<i32>,
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<i32>,
name: String,
executable: bool,
},
Argument {
redirect: Option<i32>,
name: String,
parser: Parser,
suggestion: Option<Suggestion>,
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<f32>, max: Option<f32> },
Double { min: Option<f64>, max: Option<f64> },
Integer { min: Option<i32>, max: Option<i32> },
Long { min: Option<i64>, max: Option<i64> },
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,
}

View file

@ -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<reqwest::Client> = OnceCell::new();
pub static CONFIG: OnceCell<ServerConfig> = OnceCell::new();
pub type ArcMutex<T> = Arc<Mutex<T>>;
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();
}

View file

@ -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<String>),
Chat(Uuid, JsonValue, JsonValue),
BroadcastChat(JsonValue, JsonValue),
System(Uuid, JsonValue, bool),
BroadcastSystem(JsonValue, bool),
Kick(Uuid, Option<JsonValue>),
Chat(JsonValue, Option<Uuid>),
System(JsonValue, Option<Uuid>),
UpdateCommands,
}
impl<'lua> FromLua<'lua> for PluginEvent {
fn from_lua(lua_value: Value<'lua>, lua: &'lua mlua::Lua) -> mlua::Result<Self> {
let table = Table::from_lua(lua_value, lua)?;
let ty: Box<str> = 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),

View file

@ -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<Self> {
fn from_lua(value: mlua::Value<'lua>, _lua: &'lua Lua) -> mlua::Result<Self> {
match value {
Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
v => Err(mlua::Error::FromLuaConversionError { from: v.type_name(), to: "Uuid", message: Some("Expected Uuid".to_owned()) })
}
}
}
}
impl mlua::UserData for UuidUD {
@ -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<Option<bool>> {
let srv: AnyUserData = lua.globals().get("srv")?;
let plugins: Table = srv.nth_user_value(1)?;
for (id, table) in plugins.pairs::<String, Table>().filter_map(Result::ok) {
if let Ok(f) = table.get::<_, Function>(fname) {
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<PluginEvent>, mut rx: Receiver<ServerEvent>) -> mlua::Result<()> {
fn run_event<'lua, F>(lua: &'lua Lua, fname: &str, id: &str, callback: F) -> mlua::Result<Option<mlua::Error>>
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<PluginEvent>, mut rx: Receiver<ServerEvent>, commands: Arc<RwLock<Commands>>) -> 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<PluginEvent>, mut rx: Receiver<ServerEvent>)
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::<Vec<_>>();
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(())

View file

@ -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<PluginData<'lua>> {
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<i32>>("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<Table> {
pub fn load_plugins(lua: &Lua) -> mlua::Result<(Table, Vec<String>)> {
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<Table> {
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<Table> {
let spath = path.to_str().unwrap_or("<non-unicode path>");
debug!("loading plugin at {spath}");
match load_plugin(&entry, lua) {
Ok((id, table)) => {
info!("loaded plugin {}", id);
plugins.set(id, table)?;
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))
}

View file

@ -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<Value<'lua>> {
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<JsonValue> {
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<Option<JsonValue>> {
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<PluginEvent>,
pub commands: Arc<RwLock<Commands>>,
pub command_ids: RwLock<HashMap<String, String>>,
pub names: RwLock<HashMap<Uuid, String>>,
pub info: RwLock<HashMap<Uuid, ClientInfo>>,
pub info: RwLock<HashMap<Uuid, ConnectionInfo>>,
}
#[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<Self> = 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<Self> = 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<bool>)| async move {
let msg = match msg {
Value::String(s) => json!{{"text": s.to_str()?}},
Value::Table(_) => lua.from_value(msg)?,
_ => return Err(mlua::Error::RuntimeError("()".to_owned())),
};
this.tx
.send(PluginEvent::System(uuid, msg, overlay == Some(true)))
.await
.map_err(|e| mlua::Error::RuntimeError(e.to_string()))
});
methods.add_async_method("broadcast_msg", |lua, this, (msg, overlay): (Value, Option<bool>)| async move {
let msg = match msg {
Value::String(s) => json!{{"text": s.to_str()?}},
Value::Table(_) => lua.from_value(msg)?,
_ => return Err(mlua::Error::RuntimeError("()".to_owned())),
};
this.tx
.send(PluginEvent::BroadcastSystem(msg, overlay == Some(true)))
.await
.map_err(|e| mlua::Error::RuntimeError(e.to_string()))
});
methods.add_async_method("send_chat", |lua, this, (UuidUD(uuid), msg, name): (_, Value, Value)| async move {
methods.add_async_method("send_msg", |lua, this, (msg, uuid): (Value, Option<UuidUD>)| 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<UuidUD>)| 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) {

View file

@ -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)]

View file

@ -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)?; }
}

View file

@ -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)?;

View file

@ -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)?;

View file

@ -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)?;

View file

@ -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)?;

View file

@ -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)?;

View file

@ -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)?;

View file

@ -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)?;

View file

@ -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<Self> {
// 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<Position> for PositionOld {
fn from(value: Position) -> Self {
Self { x: value.x, y: value.y, z: value.z }
}
}

View file

@ -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<ServerEvent>,
remote_addr: SocketAddr,
keepalive: Option<i64>,
info: Option<ClientInfo>,
player: Option<Player>,
}
struct Server {
commands: Arc<RwLock<Commands>>,
gc_tx: Sender<(i32, ClientEvent)>,
ps_tx: Sender<PServerEvent>,
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 {