software
This commit is contained in:
parent
ff4423d593
commit
06a0beba24
25 changed files with 952 additions and 250 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -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",
|
||||
|
|
16
Cargo.toml
16
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"] }
|
||||
|
|
97
PLUGINS.md
Normal file
97
PLUGINS.md
Normal 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.
|
||||
|
10
README.md
10
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
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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
263
src/command.rs
Normal 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,
|
||||
}
|
55
src/main.rs
55
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<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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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)?; }
|
||||
}
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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)?;
|
||||
|
|
42
src/ser.rs
42
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<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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue