changes
This commit is contained in:
parent
162d337769
commit
0f067d6114
15 changed files with 401 additions and 63 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
/target
|
/target
|
||||||
|
config.json
|
||||||
|
|
16
README.md
Normal file
16
README.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Quectocraft
|
||||||
|
|
||||||
|
Quectocraft is a minimal, extensible, efficient Minecraft server implementation written in Rust and Lua.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Minimal: By default, Quectocraft does very little by itself. It accepts connections, encodes and decodes packets, and handles the login sequence for you, but everything else must be done via plugins.
|
||||||
|
- Extensible: Via its Lua plugin system, Quectocraft can be configured to do a variety of things.
|
||||||
|
- Efficient: The vanilla Minecraft server, and even more efficient servers like Spigot and Paper, all use significant amounts of CPU even while idling with no players connected. Due to its low CPU and memory usage, Quectocraft is suitable for running on lower-end systems, or alongside another server without causing additional lag.
|
||||||
|
|
||||||
|
## Why?
|
||||||
|
|
||||||
|
I'm mostly just writing this for fun, but here are some potential applications:
|
||||||
|
- A lobby for a server network
|
||||||
|
- A queue that players have to wait in before joining another server
|
||||||
|
- A server to send players to if they are AFK for too long
|
23
plugins/testcmd.lua
Normal file
23
plugins/testcmd.lua
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
local plugin = {
|
||||||
|
id = "testcmd",
|
||||||
|
name = "TestCmd",
|
||||||
|
description = "eufdahjklfhjakl",
|
||||||
|
authors = { "trimill" },
|
||||||
|
version = "0.1.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
local logger = nil
|
||||||
|
|
||||||
|
function plugin.init()
|
||||||
|
logger = server.initLogger(plugin)
|
||||||
|
end
|
||||||
|
|
||||||
|
function plugin.registerCommands(registry)
|
||||||
|
registry.addCommand("test")
|
||||||
|
end
|
||||||
|
|
||||||
|
function plugin.command(command, args, name, uuid)
|
||||||
|
logger.info("player " .. name .. " ran /" .. command .. " " .. args)
|
||||||
|
end
|
||||||
|
|
||||||
|
return plugin
|
14
src/config.rs
Normal file
14
src/config.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
use std::{net::IpAddr, fs::OpenOptions};
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub addr: IpAddr,
|
||||||
|
pub port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_config() -> Result<Config, Box<dyn std::error::Error>> {
|
||||||
|
let config = serde_json::from_reader(OpenOptions::new().read(true).open("./config.json")?)?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
10
src/main.rs
10
src/main.rs
|
@ -1,4 +1,5 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::net::SocketAddr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
|
@ -9,11 +10,14 @@ use mlua::Lua;
|
||||||
use network::NetworkServer;
|
use network::NetworkServer;
|
||||||
use plugins::Plugins;
|
use plugins::Plugins;
|
||||||
|
|
||||||
|
use crate::config::load_config;
|
||||||
|
|
||||||
|
mod config;
|
||||||
mod plugins;
|
mod plugins;
|
||||||
mod protocol;
|
mod protocol;
|
||||||
mod network;
|
mod network;
|
||||||
|
|
||||||
pub const VERSION: &'static str = std::env!("CARGO_PKG_VERSION");
|
pub const VERSION: &str = std::env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
env_logger::Builder::from_env(
|
env_logger::Builder::from_env(
|
||||||
|
@ -37,6 +41,8 @@ fn main() {
|
||||||
writeln!(buf, "\x1b[90m[\x1b[37m{} {color}{}\x1b[37m {}\x1b[90m]\x1b[0m {}", now, record.level(), target, record.args())
|
writeln!(buf, "\x1b[90m[\x1b[37m{} {color}{}\x1b[37m {}\x1b[90m]\x1b[0m {}", now, record.level(), target, record.args())
|
||||||
}).init();
|
}).init();
|
||||||
|
|
||||||
|
let config = load_config().expect("Failed to load config");
|
||||||
|
let addr = SocketAddr::new(config.addr, config.port);
|
||||||
|
|
||||||
info!("Starting Quectocraft version {}", VERSION);
|
info!("Starting Quectocraft version {}", VERSION);
|
||||||
|
|
||||||
|
@ -45,7 +51,7 @@ fn main() {
|
||||||
std::fs::create_dir_all("plugins").expect("Couldn't create the plugins directory");
|
std::fs::create_dir_all("plugins").expect("Couldn't create the plugins directory");
|
||||||
plugins.load_plugins();
|
plugins.load_plugins();
|
||||||
|
|
||||||
let mut server = NetworkServer::new("127.0.0.1:25565".to_owned(), plugins);
|
let mut server = NetworkServer::new(addr, plugins);
|
||||||
let sleep_dur = Duration::from_millis(5);
|
let sleep_dur = Duration::from_millis(5);
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
loop {
|
loop {
|
||||||
|
|
|
@ -1,32 +1,37 @@
|
||||||
use std::{net::TcpListener, thread, sync::mpsc::{Receiver, Sender, channel}, collections::HashSet};
|
use std::{net::{TcpListener, SocketAddr}, thread, sync::mpsc::{Receiver, Sender, channel}, collections::HashSet};
|
||||||
|
|
||||||
use log::{info, warn, debug};
|
use log::{info, warn, debug};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::{protocol::{data::PacketEncoder, serverbound::*, clientbound::*}, plugins::{Plugins, Response}, VERSION};
|
use crate::{protocol::{data::PacketEncoder, serverbound::*, clientbound::*, command::{Commands, CommandNodeType}}, plugins::{Plugins, Response}, VERSION};
|
||||||
|
|
||||||
use super::{client::NetworkClient, Player};
|
use super::{client::NetworkClient, Player};
|
||||||
|
|
||||||
pub struct NetworkServer<'lua> {
|
pub struct NetworkServer<'lua> {
|
||||||
plugins: Plugins<'lua>,
|
plugins: Plugins<'lua>,
|
||||||
|
commands: Commands,
|
||||||
new_clients: Receiver<NetworkClient>,
|
new_clients: Receiver<NetworkClient>,
|
||||||
clients: Vec<NetworkClient>,
|
clients: Vec<NetworkClient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl <'lua> NetworkServer<'lua> {
|
impl <'lua> NetworkServer<'lua> {
|
||||||
pub fn new(addr: String, plugins: Plugins<'lua>) -> Self {
|
pub fn new(addr: SocketAddr, mut plugins: Plugins<'lua>) -> Self {
|
||||||
let (send, recv) = channel();
|
let (send, recv) = channel();
|
||||||
info!("Initializing plugins");
|
info!("Initializing plugins");
|
||||||
plugins.init();
|
plugins.init();
|
||||||
|
let mut commands = Commands::new();
|
||||||
|
commands.create_simple_cmd("qc");
|
||||||
|
let commands = plugins.register_commands(commands).unwrap();
|
||||||
thread::spawn(move || Self::listen(&addr, send));
|
thread::spawn(move || Self::listen(&addr, send));
|
||||||
Self {
|
Self {
|
||||||
plugins,
|
plugins,
|
||||||
|
commands,
|
||||||
new_clients: recv,
|
new_clients: recv,
|
||||||
clients: Vec::new(),
|
clients: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn listen(addr: &str, send_clients: Sender<NetworkClient>) {
|
fn listen(addr: &SocketAddr, send_clients: Sender<NetworkClient>) {
|
||||||
info!("Listening on {}", addr);
|
info!("Listening on {}", addr);
|
||||||
let listener = TcpListener::bind(addr).unwrap();
|
let listener = TcpListener::bind(addr).unwrap();
|
||||||
for (id, stream) in listener.incoming().enumerate() {
|
for (id, stream) in listener.incoming().enumerate() {
|
||||||
|
@ -57,7 +62,8 @@ impl <'lua> NetworkServer<'lua> {
|
||||||
let mut closed = Vec::new();
|
let mut closed = Vec::new();
|
||||||
for client in self.clients.iter_mut() {
|
for client in self.clients.iter_mut() {
|
||||||
if client.play {
|
if client.play {
|
||||||
if let Err(_) = client.send_packet(ClientBoundPacket::KeepAlive(0)) {
|
let result = client.send_packet(ClientBoundPacket::KeepAlive(0));
|
||||||
|
if result.is_err() {
|
||||||
client.close();
|
client.close();
|
||||||
if let Some(pl) = &client.player {
|
if let Some(pl) = &client.player {
|
||||||
self.plugins.player_leave(pl);
|
self.plugins.player_leave(pl);
|
||||||
|
@ -77,7 +83,8 @@ impl <'lua> NetworkServer<'lua> {
|
||||||
};
|
};
|
||||||
let mut alive = true;
|
let mut alive = true;
|
||||||
while let Some(packet) = client.recv_packet(&mut alive) {
|
while let Some(packet) = client.recv_packet(&mut alive) {
|
||||||
if let Err(_) = self.handle_packet(client, packet) {
|
let result = self.handle_packet(client, packet);
|
||||||
|
if result.is_err() {
|
||||||
alive = false;
|
alive = false;
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -162,6 +169,20 @@ impl <'lua> NetworkServer<'lua> {
|
||||||
ServerBoundPacket::ChatMessage(msg) => {
|
ServerBoundPacket::ChatMessage(msg) => {
|
||||||
self.plugins.chat_message(client.player.as_ref().unwrap(), &msg.message);
|
self.plugins.chat_message(client.player.as_ref().unwrap(), &msg.message);
|
||||||
}
|
}
|
||||||
|
ServerBoundPacket::ChatCommand(msg) => {
|
||||||
|
let mut parts = msg.message.splitn(1, " ");
|
||||||
|
if let Some(cmd) = parts.next() {
|
||||||
|
if cmd == "qc" {
|
||||||
|
client.send_packet(ClientBoundPacket::SystemChatMessage(json!({
|
||||||
|
"text": format!("QuectoCraft version {}", VERSION),
|
||||||
|
"color": "green"
|
||||||
|
}), false))?;
|
||||||
|
} else {
|
||||||
|
let args = parts.next().unwrap_or_default();
|
||||||
|
self.plugins.command(client.player.as_ref().unwrap(), cmd, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -173,11 +194,11 @@ impl <'lua> NetworkServer<'lua> {
|
||||||
gamemode: 1,
|
gamemode: 1,
|
||||||
prev_gamemode: 1,
|
prev_gamemode: 1,
|
||||||
dimensions: vec![
|
dimensions: vec![
|
||||||
"minecraft:world".to_owned(),
|
"qc:world".to_owned(),
|
||||||
],
|
],
|
||||||
registry_codec: include_bytes!("../resources/registry_codec.nbt").to_vec(),
|
registry_codec: include_bytes!("../resources/registry_codec.nbt").to_vec(),
|
||||||
dimension_type: "minecraft:the_end".to_owned(),
|
dimension_type: "minecraft:the_end".to_owned(),
|
||||||
dimension_name: "minecraft:world".to_owned(),
|
dimension_name: "qc:world".to_owned(),
|
||||||
seed_hash: 0,
|
seed_hash: 0,
|
||||||
max_players: 0,
|
max_players: 0,
|
||||||
view_distance: 8,
|
view_distance: 8,
|
||||||
|
@ -196,6 +217,7 @@ impl <'lua> NetworkServer<'lua> {
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
}))?;
|
}))?;
|
||||||
|
client.send_packet(ClientBoundPacket::Commands(self.commands.clone()))?;
|
||||||
let mut chunk_data: Vec<u8> = Vec::new();
|
let mut chunk_data: Vec<u8> = Vec::new();
|
||||||
for _ in 0..(384 / 16) {
|
for _ in 0..(384 / 16) {
|
||||||
// number of non-air blocks
|
// number of non-air blocks
|
||||||
|
|
38
src/plugins/init.lua
Normal file
38
src/plugins/init.lua
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
server = { players = {} }
|
||||||
|
_qc = { responses = {} }
|
||||||
|
|
||||||
|
local function to_chat(message, default)
|
||||||
|
if message == nil then
|
||||||
|
if default ~= nil then
|
||||||
|
return default
|
||||||
|
else
|
||||||
|
error("message must be a string or table")
|
||||||
|
end
|
||||||
|
elseif type(message) == "table" then
|
||||||
|
return message
|
||||||
|
elseif type(message) == "string" then
|
||||||
|
return { text = message }
|
||||||
|
elseif default == nil then
|
||||||
|
error("message must be a string or table")
|
||||||
|
else
|
||||||
|
error("message must be a string, table, or nil for the default message")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function server.sendMessage(player, message)
|
||||||
|
if type(player) ~= "string" then
|
||||||
|
error("player must be a string")
|
||||||
|
end
|
||||||
|
local message = assert(to_chat(message))
|
||||||
|
table.insert(_qc.responses, {type = "message", player = player, message = message})
|
||||||
|
end
|
||||||
|
|
||||||
|
function server.broadcast(message)
|
||||||
|
local message = assert(to_chat(message))
|
||||||
|
table.insert(_qc.responses, { type = "broadcast", message = message })
|
||||||
|
end
|
||||||
|
|
||||||
|
function server.disconnect(player, reason)
|
||||||
|
local reason = assert(to_chat(reason, { translate = "multiplayer.disconnect.generic" }))
|
||||||
|
table.insert(_qc.responses, { type = "disconnect", player = player, reason = reason })
|
||||||
|
end
|
|
@ -1,8 +1,9 @@
|
||||||
use log::{info, warn, trace, error, debug};
|
use log::{info, warn, trace, error, debug};
|
||||||
use mlua::{Lua, chunk};
|
use mlua::{Lua, chunk};
|
||||||
|
use crate::VERSION;
|
||||||
|
|
||||||
|
|
||||||
pub fn init<'lua>(lua: &'lua Lua) -> Result<(), mlua::Error> {
|
pub fn init(lua: &Lua) -> Result<(), mlua::Error> {
|
||||||
macro_rules! log_any {
|
macro_rules! log_any {
|
||||||
($level:tt) => {
|
($level:tt) => {
|
||||||
lua.create_function(|_, args: (String, String)| {
|
lua.create_function(|_, args: (String, String)| {
|
||||||
|
@ -16,9 +17,8 @@ pub fn init<'lua>(lua: &'lua Lua) -> Result<(), mlua::Error> {
|
||||||
let log_info = log_any!(info)?;
|
let log_info = log_any!(info)?;
|
||||||
let log_warn = log_any!(warn)?;
|
let log_warn = log_any!(warn)?;
|
||||||
let log_error = log_any!(error)?;
|
let log_error = log_any!(error)?;
|
||||||
|
lua.load(include_str!("init.lua")).exec()?;
|
||||||
lua.load(chunk!{
|
lua.load(chunk!{
|
||||||
server = { players = {} }
|
|
||||||
_qc = { responses = {} }
|
|
||||||
function server.initLogger(plugin)
|
function server.initLogger(plugin)
|
||||||
local id = "pl::" .. assert(plugin["id"])
|
local id = "pl::" .. assert(plugin["id"])
|
||||||
return {
|
return {
|
||||||
|
@ -30,41 +30,8 @@ pub fn init<'lua>(lua: &'lua Lua) -> Result<(), mlua::Error> {
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
local function to_chat(message, default)
|
server.version = $VERSION
|
||||||
if message == nil then
|
|
||||||
if default ~= nil then
|
|
||||||
return default
|
|
||||||
else
|
|
||||||
error("message must be a string or table")
|
|
||||||
end
|
|
||||||
elseif type(message) == "table" then
|
|
||||||
return message
|
|
||||||
elseif type(message) == "string" then
|
|
||||||
return { text = message }
|
|
||||||
elseif default == nil then
|
|
||||||
error("message must be a string or table")
|
|
||||||
else
|
|
||||||
error("message must be a string, table, or nil for the default message")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function server.sendMessage(player, message)
|
|
||||||
if type(player) ~= "string" then
|
|
||||||
error("player must be a string")
|
|
||||||
end
|
|
||||||
local message = assert(to_chat(message))
|
|
||||||
table.insert(_qc.responses, {type = "message", player = player, message = message})
|
|
||||||
end
|
|
||||||
|
|
||||||
function server.broadcast(message)
|
|
||||||
local message = assert(to_chat(message))
|
|
||||||
table.insert(_qc.responses, { type = "broadcast", message = message })
|
|
||||||
end
|
|
||||||
|
|
||||||
function server.disconnect(player, reason)
|
|
||||||
local reason = assert(to_chat(reason, { translate = "multiplayer.disconnect.generic" }))
|
|
||||||
table.insert(_qc.responses, { type = "disconnect", player = player, reason = reason })
|
|
||||||
end
|
|
||||||
}).exec()?;
|
}).exec()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use std::fs::read_dir;
|
use std::{fs::read_dir, rc::Rc, cell::RefCell, collections::HashMap};
|
||||||
|
|
||||||
use log::{warn, info};
|
use log::{warn, info};
|
||||||
use mlua::{Lua, Table, LuaSerdeExt};
|
use mlua::{Lua, Table, LuaSerdeExt};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::network::Player;
|
use crate::{network::Player, protocol::command::Commands};
|
||||||
|
|
||||||
use self::plugin::Plugin;
|
use self::plugin::Plugin;
|
||||||
|
|
||||||
|
@ -26,7 +26,8 @@ pub enum Response {
|
||||||
|
|
||||||
pub struct Plugins<'lua> {
|
pub struct Plugins<'lua> {
|
||||||
lua: &'lua Lua,
|
lua: &'lua Lua,
|
||||||
plugins: Vec<Plugin<'lua>>
|
plugins: Vec<Plugin<'lua>>,
|
||||||
|
cmd_owners: HashMap<String, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl <'lua> Plugins<'lua> {
|
impl <'lua> Plugins<'lua> {
|
||||||
|
@ -35,6 +36,7 @@ impl <'lua> Plugins<'lua> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
lua,
|
lua,
|
||||||
plugins: Vec::new(),
|
plugins: Vec::new(),
|
||||||
|
cmd_owners: HashMap::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +51,7 @@ impl <'lua> Plugins<'lua> {
|
||||||
} else {
|
} else {
|
||||||
file.path()
|
file.path()
|
||||||
};
|
};
|
||||||
let pl = Plugin::load(&path, &self.lua).expect("error loading plugin");
|
let pl = Plugin::load(&path, self.lua).expect("error loading plugin");
|
||||||
self.plugins.push(pl);
|
self.plugins.push(pl);
|
||||||
info!("Loaded plugin '{}'", file.file_name().to_string_lossy());
|
info!("Loaded plugin '{}'", file.file_name().to_string_lossy());
|
||||||
}
|
}
|
||||||
|
@ -82,6 +84,38 @@ impl <'lua> Plugins<'lua> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn register_commands(&mut self, commands: Commands) -> Result<Commands, mlua::Error> {
|
||||||
|
let commands = Rc::new(RefCell::new(commands));
|
||||||
|
let cmd_owners = Rc::new(RefCell::new(HashMap::new()));
|
||||||
|
for (i, pl) in self.plugins.iter().enumerate() {
|
||||||
|
let commands_2 = commands.clone();
|
||||||
|
let cmd_owners_2 = cmd_owners.clone();
|
||||||
|
let pl_id = pl.id.clone();
|
||||||
|
let add_command = self.lua.create_function(move |_, name: String| {
|
||||||
|
let scoped_name = format!("{}:{}", pl_id, name);
|
||||||
|
let mut cmds = commands_2.borrow_mut();
|
||||||
|
let id1 = cmds.create_simple_cmd(&name);
|
||||||
|
let id2 = cmds.create_simple_cmd(&scoped_name);
|
||||||
|
if id1.is_none() || id2.is_none() {
|
||||||
|
return Ok(mlua::Nil)
|
||||||
|
}
|
||||||
|
cmd_owners_2.borrow_mut().insert(name, i);
|
||||||
|
cmd_owners_2.borrow_mut().insert(scoped_name, i);
|
||||||
|
Ok(mlua::Nil)
|
||||||
|
})?;
|
||||||
|
let registry = self.lua.create_table()?;
|
||||||
|
registry.set("addCommand", add_command)?;
|
||||||
|
if let Some(init) = &pl.event_handlers.register_commands {
|
||||||
|
if let Err(e) = init.call::<_, ()>((registry.clone(),)) {
|
||||||
|
warn!("Error in plugin {}: {}", pl.name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cb = commands.borrow();
|
||||||
|
self.cmd_owners = (*cmd_owners.borrow()).clone();
|
||||||
|
Ok((*cb).clone())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn player_join(&self, player: &Player) {
|
pub fn player_join(&self, player: &Player) {
|
||||||
if let Err(e) = self.add_player(player) {
|
if let Err(e) = self.add_player(player) {
|
||||||
warn!("Error adding player: {}", e);
|
warn!("Error adding player: {}", e);
|
||||||
|
@ -133,4 +167,17 @@ impl <'lua> Plugins<'lua> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn command(&self, player: &Player, command: &str, args: &str) {
|
||||||
|
if let Some(owner) = self.cmd_owners.get(command) {
|
||||||
|
let pl = &self.plugins[*owner];
|
||||||
|
if let Some(func) = &pl.event_handlers.command {
|
||||||
|
if let Err(e) = func.call::<_, ()>((command, args, player.name.as_str(), player.uuid.to_string())) {
|
||||||
|
warn!("Error in plugin {}: {}", pl.name, e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("Plugin {} registered a command but no command handler was found", pl.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,11 @@ use mlua::{Function, Table, Lua};
|
||||||
|
|
||||||
pub struct EventHandlers<'lua> {
|
pub struct EventHandlers<'lua> {
|
||||||
pub init: Option<Function<'lua>>,
|
pub init: Option<Function<'lua>>,
|
||||||
|
pub register_commands: Option<Function<'lua>>,
|
||||||
pub player_join: Option<Function<'lua>>,
|
pub player_join: Option<Function<'lua>>,
|
||||||
pub player_leave: Option<Function<'lua>>,
|
pub player_leave: Option<Function<'lua>>,
|
||||||
pub chat_message: Option<Function<'lua>>,
|
pub chat_message: Option<Function<'lua>>,
|
||||||
|
pub command: Option<Function<'lua>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Plugin<'lua> {
|
pub struct Plugin<'lua> {
|
||||||
|
@ -26,11 +28,13 @@ impl <'lua> Plugin<'lua> {
|
||||||
let version: String = module.get("version").unwrap_or_else(|_| "?".to_owned());
|
let version: String = module.get("version").unwrap_or_else(|_| "?".to_owned());
|
||||||
|
|
||||||
let init: Option<Function<'lua>> = module.get("init").ok();
|
let init: Option<Function<'lua>> = module.get("init").ok();
|
||||||
|
let register_commands: Option<Function<'lua>> = module.get("registerCommands").ok();
|
||||||
let player_join: Option<Function<'lua>> = module.get("playerJoin").ok();
|
let player_join: Option<Function<'lua>> = module.get("playerJoin").ok();
|
||||||
let player_leave: Option<Function<'lua>> = module.get("playerLeave").ok();
|
let player_leave: Option<Function<'lua>> = module.get("playerLeave").ok();
|
||||||
let chat_message: Option<Function<'lua>> = module.get("chatMessage").ok();
|
let chat_message: Option<Function<'lua>> = module.get("chatMessage").ok();
|
||||||
|
let command: Option<Function<'lua>> = module.get("command").ok();
|
||||||
|
|
||||||
let event_handlers = EventHandlers { init, player_join, player_leave, chat_message };
|
let event_handlers = EventHandlers { init, register_commands, player_join, player_leave, chat_message, command };
|
||||||
Ok(Plugin { id, name, version, event_handlers })
|
Ok(Plugin { id, name, version, event_handlers })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::{data::{PacketEncoder, finalize_packet}, Position};
|
use super::{data::{PacketEncoder, finalize_packet}, Position, command::Commands};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct LoginSuccess {
|
pub struct LoginSuccess {
|
||||||
|
@ -147,8 +147,9 @@ pub enum ClientBoundPacket {
|
||||||
// play
|
// play
|
||||||
LoginPlay(LoginPlay),
|
LoginPlay(LoginPlay),
|
||||||
PluginMessage(PluginMessage),
|
PluginMessage(PluginMessage),
|
||||||
SyncPlayerPosition(SyncPlayerPosition),
|
Commands(Commands),
|
||||||
ChunkData(ChunkData),
|
ChunkData(ChunkData),
|
||||||
|
SyncPlayerPosition(SyncPlayerPosition),
|
||||||
KeepAlive(i64),
|
KeepAlive(i64),
|
||||||
PlayerAbilities(i8, f32, f32),
|
PlayerAbilities(i8, f32, f32),
|
||||||
Disconnect(serde_json::Value),
|
Disconnect(serde_json::Value),
|
||||||
|
@ -190,14 +191,18 @@ impl ClientBoundPacket {
|
||||||
plugin_message.encode(&mut packet);
|
plugin_message.encode(&mut packet);
|
||||||
finalize_packet(packet, 22)
|
finalize_packet(packet, 22)
|
||||||
}
|
}
|
||||||
Self::SyncPlayerPosition(sync_player_position) => {
|
Self::Commands(commands) => {
|
||||||
sync_player_position.encode(&mut packet);
|
commands.encode(&mut packet);
|
||||||
finalize_packet(packet, 57)
|
finalize_packet(packet, 15)
|
||||||
}
|
}
|
||||||
Self::ChunkData(chunk_data) => {
|
Self::ChunkData(chunk_data) => {
|
||||||
chunk_data.encode(&mut packet);
|
chunk_data.encode(&mut packet);
|
||||||
finalize_packet(packet, 33)
|
finalize_packet(packet, 33)
|
||||||
}
|
}
|
||||||
|
Self::SyncPlayerPosition(sync_player_position) => {
|
||||||
|
sync_player_position.encode(&mut packet);
|
||||||
|
finalize_packet(packet, 57)
|
||||||
|
}
|
||||||
Self::KeepAlive(n) => {
|
Self::KeepAlive(n) => {
|
||||||
packet.write_long(n);
|
packet.write_long(n);
|
||||||
finalize_packet(packet, 32)
|
finalize_packet(packet, 32)
|
||||||
|
|
191
src/protocol/command.rs
Normal file
191
src/protocol/command.rs
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
use super::data::PacketEncoder;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Commands {
|
||||||
|
nodes: Vec<CommandNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Commands {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let root = CommandNode {
|
||||||
|
executable: false,
|
||||||
|
redirect: None,
|
||||||
|
children: Vec::new(),
|
||||||
|
suggestion: None,
|
||||||
|
type_data: CommandNodeType::Root
|
||||||
|
};
|
||||||
|
let simple_cmd_arg = CommandNode {
|
||||||
|
executable: true,
|
||||||
|
redirect: None,
|
||||||
|
children: Vec::new(),
|
||||||
|
suggestion: None,
|
||||||
|
type_data: CommandNodeType::Argument { name: "[args]".to_owned(), parser: Parser::String { kind: StringKind::Greedy } }
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
nodes: vec![root, simple_cmd_arg],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_node(
|
||||||
|
&mut self,
|
||||||
|
parent: i32,
|
||||||
|
type_data: CommandNodeType,
|
||||||
|
executable: bool,
|
||||||
|
redirect: Option<i32>,
|
||||||
|
suggestion: Option<String>
|
||||||
|
) -> Option<i32> {
|
||||||
|
if parent < 0 || parent >= self.nodes.len() as i32 {
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
if let Some(redirect) = redirect {
|
||||||
|
if redirect < 0 || redirect >= self.nodes.len() as i32 {
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let CommandNodeType::Root = type_data {
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
let id = self.nodes.len() as i32;
|
||||||
|
self.nodes.push(CommandNode {
|
||||||
|
executable,
|
||||||
|
redirect,
|
||||||
|
children: Vec::new(),
|
||||||
|
suggestion,
|
||||||
|
type_data,
|
||||||
|
});
|
||||||
|
self.nodes[parent as usize].children.push(id);
|
||||||
|
Some(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_simple_cmd(&mut self, name: &str) -> Option<i32> {
|
||||||
|
let id = self.create_node(0, CommandNodeType::Literal { name: name.to_owned() }, true, None, None)?;
|
||||||
|
self.add_child(id, 1);
|
||||||
|
Some(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_child(&mut self, node: i32, child: i32) {
|
||||||
|
self.nodes[node as usize].children.push(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(&self, encoder: &mut impl PacketEncoder) {
|
||||||
|
encoder.write_varint(self.nodes.len() as i32);
|
||||||
|
for node in &self.nodes {
|
||||||
|
node.encode(encoder);
|
||||||
|
}
|
||||||
|
// root node
|
||||||
|
encoder.write_varint(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum NodeError {
|
||||||
|
InvalidParent(i32),
|
||||||
|
InvalidRedirect(i32),
|
||||||
|
BadTypeData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CommandNode {
|
||||||
|
executable: bool,
|
||||||
|
redirect: Option<i32>,
|
||||||
|
children: Vec<i32>,
|
||||||
|
suggestion: Option<String>,
|
||||||
|
type_data: CommandNodeType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum CommandNodeType {
|
||||||
|
Root,
|
||||||
|
Literal { name: String },
|
||||||
|
Argument { name: String, parser: Parser }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandNode {
|
||||||
|
pub fn encode(&self, encoder: &mut impl PacketEncoder) {
|
||||||
|
let mut flags = match self.type_data {
|
||||||
|
CommandNodeType::Root => 0,
|
||||||
|
CommandNodeType::Literal{..} => 1,
|
||||||
|
CommandNodeType::Argument{..} => 2,
|
||||||
|
};
|
||||||
|
if self.executable { flags |= 4; }
|
||||||
|
if self.redirect.is_some() { flags |= 8; }
|
||||||
|
if self.suggestion.is_some() { flags |= 16; }
|
||||||
|
encoder.write_byte(flags);
|
||||||
|
encoder.write_varint(self.children.len() as i32);
|
||||||
|
for child in &self.children {
|
||||||
|
encoder.write_varint(*child);
|
||||||
|
}
|
||||||
|
if let Some(redirect) = &self.redirect {
|
||||||
|
encoder.write_varint(*redirect);
|
||||||
|
}
|
||||||
|
match &self.type_data {
|
||||||
|
CommandNodeType::Root => (),
|
||||||
|
CommandNodeType::Literal { name } => {
|
||||||
|
encoder.write_string(32767, &name);
|
||||||
|
}
|
||||||
|
CommandNodeType::Argument { name, parser } => {
|
||||||
|
encoder.write_string(32767, &name);
|
||||||
|
parser.encode(encoder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(suggestion) = &self.suggestion {
|
||||||
|
encoder.write_string(32767, &suggestion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Parser {
|
||||||
|
Bool,
|
||||||
|
Float { min: Option<f32>, max: Option<f32>, },
|
||||||
|
Double { min: Option<f64>, max: Option<f64>, },
|
||||||
|
Int { min: Option<i32>, max: Option<i32>, },
|
||||||
|
Long { min: Option<i64>, max: Option<i64>, },
|
||||||
|
String { kind: StringKind },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parser {
|
||||||
|
pub fn encode(&self, encoder: &mut impl PacketEncoder) {
|
||||||
|
match self {
|
||||||
|
Self::Bool => encoder.write_varint(0),
|
||||||
|
Self::Float{ min, max } => {
|
||||||
|
encoder.write_varint(1);
|
||||||
|
encoder.write_byte( if min.is_some() { 1 } else { 0 } + if max.is_some() { 2 } else { 0 } );
|
||||||
|
if let Some(min) = min { encoder.write_float(*min) };
|
||||||
|
if let Some(max) = max { encoder.write_float(*max) };
|
||||||
|
},
|
||||||
|
Self::Double{ min, max } => {
|
||||||
|
encoder.write_varint(2);
|
||||||
|
encoder.write_byte( if min.is_some() { 1 } else { 0 } + if max.is_some() { 2 } else { 0 } );
|
||||||
|
if let Some(min) = min { encoder.write_double(*min) };
|
||||||
|
if let Some(max) = max { encoder.write_double(*max) };
|
||||||
|
},
|
||||||
|
Self::Int{ min, max } => {
|
||||||
|
encoder.write_varint(3);
|
||||||
|
encoder.write_byte( if min.is_some() { 1 } else { 0 } + if max.is_some() { 2 } else { 0 } );
|
||||||
|
if let Some(min) = min { encoder.write_int(*min) };
|
||||||
|
if let Some(max) = max { encoder.write_int(*max) };
|
||||||
|
},
|
||||||
|
Self::Long{ min, max } => {
|
||||||
|
encoder.write_varint(4);
|
||||||
|
encoder.write_byte( if min.is_some() { 1 } else { 0 } + if max.is_some() { 2 } else { 0 } );
|
||||||
|
if let Some(min) = min { encoder.write_long(*min) };
|
||||||
|
if let Some(max) = max { encoder.write_long(*max) };
|
||||||
|
},
|
||||||
|
Self::String{ kind } => {
|
||||||
|
encoder.write_varint(5);
|
||||||
|
encoder.write_varint(match kind {
|
||||||
|
StringKind::Single => 0,
|
||||||
|
StringKind::Quoted => 1,
|
||||||
|
StringKind::Greedy => 2,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum StringKind {
|
||||||
|
Single,
|
||||||
|
Quoted,
|
||||||
|
Greedy,
|
||||||
|
}
|
|
@ -53,7 +53,7 @@ pub trait PacketEncoder: Write {
|
||||||
fn write_varint(&mut self, mut data: i32) {
|
fn write_varint(&mut self, mut data: i32) {
|
||||||
loop {
|
loop {
|
||||||
let mut byte = (data & 0b11111111) as u8;
|
let mut byte = (data & 0b11111111) as u8;
|
||||||
data = data >> 7;
|
data >>= 7;
|
||||||
if data != 0 {
|
if data != 0 {
|
||||||
byte |= 0b10000000;
|
byte |= 0b10000000;
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ pub trait PacketEncoder: Write {
|
||||||
fn write_varlong(&mut self, mut data: i64) {
|
fn write_varlong(&mut self, mut data: i64) {
|
||||||
loop {
|
loop {
|
||||||
let mut byte = (data & 0b11111111) as u8;
|
let mut byte = (data & 0b11111111) as u8;
|
||||||
data = data >> 7;
|
data >>= 7;
|
||||||
if data != 0 {
|
if data != 0 {
|
||||||
byte |= 0b10000000;
|
byte |= 0b10000000;
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ fn encode_varint(mut data: i32) -> Vec<u8> {
|
||||||
let mut res = Vec::new();
|
let mut res = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
let mut byte = (data & 0b11111111) as u8;
|
let mut byte = (data & 0b11111111) as u8;
|
||||||
data = data >> 7;
|
data >>= 7;
|
||||||
if data != 0 {
|
if data != 0 {
|
||||||
byte |= 0b10000000;
|
byte |= 0b10000000;
|
||||||
}
|
}
|
||||||
|
@ -148,11 +148,11 @@ impl PacketDecoder {
|
||||||
packet_id: 0
|
packet_id: 0
|
||||||
};
|
};
|
||||||
decoder.packet_id = decoder.read_varint();
|
decoder.packet_id = decoder.read_varint();
|
||||||
return Ok(decoder);
|
Ok(decoder)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn packet_id(&self) -> i32 {
|
pub fn packet_id(&self) -> i32 {
|
||||||
return self.packet_id;
|
self.packet_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_bytes(&mut self, n: usize) -> &[u8] {
|
pub fn read_bytes(&mut self, n: usize) -> &[u8] {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
|
||||||
|
pub mod command;
|
||||||
pub mod data;
|
pub mod data;
|
||||||
pub mod serverbound;
|
pub mod serverbound;
|
||||||
pub mod clientbound;
|
pub mod clientbound;
|
||||||
|
|
|
@ -85,6 +85,7 @@ pub enum ServerBoundPacket {
|
||||||
LoginStart(LoginStart),
|
LoginStart(LoginStart),
|
||||||
// play
|
// play
|
||||||
ChatMessage(ChatMessage),
|
ChatMessage(ChatMessage),
|
||||||
|
ChatCommand(ChatMessage),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerBoundPacket {
|
impl ServerBoundPacket {
|
||||||
|
@ -108,6 +109,7 @@ impl ServerBoundPacket {
|
||||||
*state = NS::Play;
|
*state = NS::Play;
|
||||||
ServerBoundPacket::LoginStart(LoginStart::decode(decoder))
|
ServerBoundPacket::LoginStart(LoginStart::decode(decoder))
|
||||||
},
|
},
|
||||||
|
(NS::Play, 4) => ServerBoundPacket::ChatCommand(ChatMessage::decode(decoder)),
|
||||||
(NS::Play, 5) => ServerBoundPacket::ChatMessage(ChatMessage::decode(decoder)),
|
(NS::Play, 5) => ServerBoundPacket::ChatMessage(ChatMessage::decode(decoder)),
|
||||||
(NS::Play, id @ (18 | 20 | 21 | 22 | 30)) => ServerBoundPacket::Ignored(id),
|
(NS::Play, id @ (18 | 20 | 21 | 22 | 30)) => ServerBoundPacket::Ignored(id),
|
||||||
(_, id) => ServerBoundPacket::Unknown(id),
|
(_, id) => ServerBoundPacket::Unknown(id),
|
||||||
|
|
Loading…
Reference in a new issue