changes
parent
162d337769
commit
0f067d6114
@ -1 +1,2 @@
|
||||
/target
|
||||
config.json
|
||||
|
@ -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
|
@ -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
|
@ -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)
|
||||
}
|
@ -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
|
@ -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,
|
||||
}
|
Loading…
Reference in New Issue