This commit is contained in:
TriMill 2022-12-17 00:10:01 -05:00
parent 162d337769
commit 0f067d6114
15 changed files with 401 additions and 63 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target
config.json

16
README.md Normal file
View 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
View 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
View 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)
}

View file

@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::net::SocketAddr;
use std::time::Duration;
use std::io::Write;
@ -9,11 +10,14 @@ use mlua::Lua;
use network::NetworkServer;
use plugins::Plugins;
use crate::config::load_config;
mod config;
mod plugins;
mod protocol;
mod network;
pub const VERSION: &'static str = std::env!("CARGO_PKG_VERSION");
pub const VERSION: &str = std::env!("CARGO_PKG_VERSION");
fn main() {
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())
}).init();
let config = load_config().expect("Failed to load config");
let addr = SocketAddr::new(config.addr, config.port);
info!("Starting Quectocraft version {}", VERSION);
@ -45,7 +51,7 @@ fn main() {
std::fs::create_dir_all("plugins").expect("Couldn't create the plugins directory");
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 mut i = 0;
loop {

View file

@ -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 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};
pub struct NetworkServer<'lua> {
plugins: Plugins<'lua>,
commands: Commands,
new_clients: Receiver<NetworkClient>,
clients: Vec<NetworkClient>,
}
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();
info!("Initializing plugins");
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));
Self {
plugins,
commands,
new_clients: recv,
clients: Vec::new(),
}
}
fn listen(addr: &str, send_clients: Sender<NetworkClient>) {
fn listen(addr: &SocketAddr, send_clients: Sender<NetworkClient>) {
info!("Listening on {}", addr);
let listener = TcpListener::bind(addr).unwrap();
for (id, stream) in listener.incoming().enumerate() {
@ -57,7 +62,8 @@ impl <'lua> NetworkServer<'lua> {
let mut closed = Vec::new();
for client in self.clients.iter_mut() {
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();
if let Some(pl) = &client.player {
self.plugins.player_leave(pl);
@ -77,7 +83,8 @@ impl <'lua> NetworkServer<'lua> {
};
let mut alive = true;
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;
break
}
@ -162,6 +169,20 @@ impl <'lua> NetworkServer<'lua> {
ServerBoundPacket::ChatMessage(msg) => {
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(())
}
@ -173,11 +194,11 @@ impl <'lua> NetworkServer<'lua> {
gamemode: 1,
prev_gamemode: 1,
dimensions: vec![
"minecraft:world".to_owned(),
"qc:world".to_owned(),
],
registry_codec: include_bytes!("../resources/registry_codec.nbt").to_vec(),
dimension_type: "minecraft:the_end".to_owned(),
dimension_name: "minecraft:world".to_owned(),
dimension_name: "qc:world".to_owned(),
seed_hash: 0,
max_players: 0,
view_distance: 8,
@ -196,6 +217,7 @@ impl <'lua> NetworkServer<'lua> {
data
}
}))?;
client.send_packet(ClientBoundPacket::Commands(self.commands.clone()))?;
let mut chunk_data: Vec<u8> = Vec::new();
for _ in 0..(384 / 16) {
// number of non-air blocks

38
src/plugins/init.lua Normal file
View 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

View file

@ -1,8 +1,9 @@
use log::{info, warn, trace, error, debug};
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 {
($level:tt) => {
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_warn = log_any!(warn)?;
let log_error = log_any!(error)?;
lua.load(include_str!("init.lua")).exec()?;
lua.load(chunk!{
server = { players = {} }
_qc = { responses = {} }
function server.initLogger(plugin)
local id = "pl::" .. assert(plugin["id"])
return {
@ -30,41 +30,8 @@ pub fn init<'lua>(lua: &'lua Lua) -> Result<(), mlua::Error> {
}
end
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
server.version = $VERSION
}).exec()?;
Ok(())
}

View file

@ -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 mlua::{Lua, Table, LuaSerdeExt};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::network::Player;
use crate::{network::Player, protocol::command::Commands};
use self::plugin::Plugin;
@ -26,7 +26,8 @@ pub enum Response {
pub struct Plugins<'lua> {
lua: &'lua Lua,
plugins: Vec<Plugin<'lua>>
plugins: Vec<Plugin<'lua>>,
cmd_owners: HashMap<String, usize>,
}
impl <'lua> Plugins<'lua> {
@ -35,6 +36,7 @@ impl <'lua> Plugins<'lua> {
Ok(Self {
lua,
plugins: Vec::new(),
cmd_owners: HashMap::new(),
})
}
@ -49,7 +51,7 @@ impl <'lua> Plugins<'lua> {
} else {
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);
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) {
if let Err(e) = self.add_player(player) {
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);
}
}
}
}

View file

@ -4,9 +4,11 @@ use mlua::{Function, Table, Lua};
pub struct EventHandlers<'lua> {
pub init: Option<Function<'lua>>,
pub register_commands: Option<Function<'lua>>,
pub player_join: Option<Function<'lua>>,
pub player_leave: Option<Function<'lua>>,
pub chat_message: Option<Function<'lua>>,
pub command: Option<Function<'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 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_leave: Option<Function<'lua>> = module.get("playerLeave").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 })
}
}

View file

@ -1,6 +1,6 @@
use uuid::Uuid;
use super::{data::{PacketEncoder, finalize_packet}, Position};
use super::{data::{PacketEncoder, finalize_packet}, Position, command::Commands};
#[derive(Debug)]
pub struct LoginSuccess {
@ -147,8 +147,9 @@ pub enum ClientBoundPacket {
// play
LoginPlay(LoginPlay),
PluginMessage(PluginMessage),
SyncPlayerPosition(SyncPlayerPosition),
Commands(Commands),
ChunkData(ChunkData),
SyncPlayerPosition(SyncPlayerPosition),
KeepAlive(i64),
PlayerAbilities(i8, f32, f32),
Disconnect(serde_json::Value),
@ -190,14 +191,18 @@ impl ClientBoundPacket {
plugin_message.encode(&mut packet);
finalize_packet(packet, 22)
}
Self::SyncPlayerPosition(sync_player_position) => {
sync_player_position.encode(&mut packet);
finalize_packet(packet, 57)
Self::Commands(commands) => {
commands.encode(&mut packet);
finalize_packet(packet, 15)
}
Self::ChunkData(chunk_data) => {
chunk_data.encode(&mut packet);
finalize_packet(packet, 33)
}
Self::SyncPlayerPosition(sync_player_position) => {
sync_player_position.encode(&mut packet);
finalize_packet(packet, 57)
}
Self::KeepAlive(n) => {
packet.write_long(n);
finalize_packet(packet, 32)

191
src/protocol/command.rs Normal file
View 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,
}

View file

@ -53,7 +53,7 @@ pub trait PacketEncoder: Write {
fn write_varint(&mut self, mut data: i32) {
loop {
let mut byte = (data & 0b11111111) as u8;
data = data >> 7;
data >>= 7;
if data != 0 {
byte |= 0b10000000;
}
@ -67,7 +67,7 @@ pub trait PacketEncoder: Write {
fn write_varlong(&mut self, mut data: i64) {
loop {
let mut byte = (data & 0b11111111) as u8;
data = data >> 7;
data >>= 7;
if data != 0 {
byte |= 0b10000000;
}
@ -109,7 +109,7 @@ fn encode_varint(mut data: i32) -> Vec<u8> {
let mut res = Vec::new();
loop {
let mut byte = (data & 0b11111111) as u8;
data = data >> 7;
data >>= 7;
if data != 0 {
byte |= 0b10000000;
}
@ -148,11 +148,11 @@ impl PacketDecoder {
packet_id: 0
};
decoder.packet_id = decoder.read_varint();
return Ok(decoder);
Ok(decoder)
}
pub fn packet_id(&self) -> i32 {
return self.packet_id;
self.packet_id
}
pub fn read_bytes(&mut self, n: usize) -> &[u8] {

View file

@ -1,3 +1,5 @@
pub mod command;
pub mod data;
pub mod serverbound;
pub mod clientbound;

View file

@ -85,6 +85,7 @@ pub enum ServerBoundPacket {
LoginStart(LoginStart),
// play
ChatMessage(ChatMessage),
ChatCommand(ChatMessage),
}
impl ServerBoundPacket {
@ -108,6 +109,7 @@ impl ServerBoundPacket {
*state = NS::Play;
ServerBoundPacket::LoginStart(LoginStart::decode(decoder))
},
(NS::Play, 4) => ServerBoundPacket::ChatCommand(ChatMessage::decode(decoder)),
(NS::Play, 5) => ServerBoundPacket::ChatMessage(ChatMessage::decode(decoder)),
(NS::Play, id @ (18 | 20 | 21 | 22 | 30)) => ServerBoundPacket::Ignored(id),
(_, id) => ServerBoundPacket::Unknown(id),