324 lines
9.7 KiB
Rust
324 lines
9.7 KiB
Rust
use log::{error, info};
|
|
use regex::Regex;
|
|
use rhai::{CustomType, Engine, EvalAltResult, FnPtr, Scope, TypeBuilder, AST};
|
|
use std::{
|
|
collections::HashMap,
|
|
ffi::OsStr,
|
|
fs,
|
|
net::TcpStream,
|
|
path::Path,
|
|
sync::{Arc, Mutex},
|
|
};
|
|
|
|
use crate::{
|
|
error::AppError,
|
|
player::Player,
|
|
utils::{send_chat_message, write_chat_stream},
|
|
};
|
|
|
|
pub struct Extensions {
|
|
extensions: Vec<Extension>,
|
|
}
|
|
|
|
impl Extensions {
|
|
pub fn run_command(
|
|
&self,
|
|
key: String,
|
|
player: u8,
|
|
stream: &mut TcpStream,
|
|
) -> Result<bool, AppError> {
|
|
// Here I'm calling write_chat_stream multiple times. This is because the stock minecraft
|
|
// chat has a length limit of 64 characters, which is pathetically small. There is a
|
|
// classic extension to support an unlimited number of characters, but it's not guaranteed
|
|
// that the client will support it, so the next best option is to just send multiple
|
|
// messages, they're newline seperated anyway. I am aware that repeated stream writes are
|
|
// not the best option however, and that at some point I should switch to buffered streams.
|
|
// TODO: Use buffered streams (That's everywhere not just here)
|
|
|
|
// Reserve extension listing command
|
|
if &key == "extensions" {
|
|
let _ = write_chat_stream(stream, "Extension listing".to_string());
|
|
|
|
for extension in &self.extensions {
|
|
let _ = write_chat_stream(
|
|
stream,
|
|
format!(
|
|
"&a{} &bv{}",
|
|
extension.metadata.name,
|
|
extension.metadata.version.display()
|
|
),
|
|
);
|
|
}
|
|
|
|
return Ok(true);
|
|
}
|
|
|
|
// Reserve command listing command
|
|
if &key == "commands" {
|
|
let _ = write_chat_stream(stream, "Command listing".to_string());
|
|
|
|
for extension in &self.extensions {
|
|
for command in extension.commands.keys() {
|
|
let _ = write_chat_stream(
|
|
stream,
|
|
format!(
|
|
"&c{} &a[{}]",
|
|
command,
|
|
extension.metadata.name
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
return Ok(true);
|
|
}
|
|
|
|
for extension in &self.extensions {
|
|
if let Some(key_value) = extension.commands.get(&key) {
|
|
key_value.call::<()>(&extension.engine, &extension.ast, (player,))?;
|
|
return Ok(true);
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
}
|
|
|
|
pub struct Extension {
|
|
ast: AST,
|
|
engine: Engine,
|
|
commands: HashMap<String, FnPtr>,
|
|
metadata: ExtensionMetadata,
|
|
}
|
|
|
|
////// BEGIN RHAI DEFINITIONS //////
|
|
#[derive(Debug, Clone, Eq, PartialEq, CustomType)]
|
|
#[rhai_type(name = "Version", extra = Self::build_extra)]
|
|
struct Version {
|
|
major: u16,
|
|
minor: u16,
|
|
patch: u16,
|
|
prerelease: String,
|
|
build: String,
|
|
}
|
|
|
|
impl Version {
|
|
pub fn display(&self) -> String {
|
|
let mut base = format!("{}.{}.{}", self.major, self.minor, self.patch);
|
|
|
|
if !self.prerelease.is_empty() {
|
|
base.push_str(&format!("-{}", self.prerelease).to_string());
|
|
}
|
|
|
|
if !self.build.is_empty() {
|
|
base.push_str(&format!("+{}", self.build).to_string());
|
|
}
|
|
|
|
base
|
|
}
|
|
|
|
fn parse(version_string: String) -> Result<Self, Box<EvalAltResult>> {
|
|
let Ok(re) = Regex::new(
|
|
r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$",
|
|
) else {
|
|
return Err("Failed to create regex".into());
|
|
};
|
|
|
|
let Some(version_parts) = re.captures(&version_string) else {
|
|
return Err("Invalid Extension Version".into());
|
|
};
|
|
|
|
let mut prerelease: String = "".to_string();
|
|
let mut build: String = "".to_string();
|
|
|
|
if version_parts.name("prerelease").is_some() {
|
|
prerelease = version_parts["prerelease"].to_string();
|
|
}
|
|
if version_parts.name("buildmetadata").is_some() {
|
|
build = version_parts["buildmetadata"].to_string();
|
|
}
|
|
|
|
Ok(Version {
|
|
major: version_parts["major"].parse::<u16>().unwrap(),
|
|
minor: version_parts["minor"].parse::<u16>().unwrap(),
|
|
patch: version_parts["patch"].parse::<u16>().unwrap(),
|
|
prerelease,
|
|
build,
|
|
})
|
|
}
|
|
|
|
fn build_extra(builder: &mut TypeBuilder<Self>) {
|
|
// Register constructor function
|
|
builder.with_fn("Version", Self::parse);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq, CustomType)]
|
|
#[rhai_type(name = "Metadata", extra = Self::build_extra)]
|
|
struct ExtensionMetadata {
|
|
name: String,
|
|
version: Version,
|
|
}
|
|
|
|
impl ExtensionMetadata {
|
|
fn new(name: String, version: Version) -> Self {
|
|
Self { name, version }
|
|
}
|
|
|
|
fn build_extra(builder: &mut TypeBuilder<Self>) {
|
|
// Register constructor function
|
|
builder.with_fn("Metadata", Self::new);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, CustomType)]
|
|
#[rhai_type(name = "Player")]
|
|
struct RhaiPlayer {
|
|
id: u8,
|
|
}
|
|
|
|
#[derive(Debug, Clone, CustomType)]
|
|
#[rhai_type(name = "PlayersWrapper", extra=Self::build_extra)]
|
|
pub struct PlayersWrapper(Arc<Mutex<[Player; 255]>>);
|
|
|
|
impl PlayersWrapper {
|
|
pub fn new(players: Arc<Mutex<[Player; 255]>>) -> Self {
|
|
Self(players)
|
|
}
|
|
|
|
fn send_message(self, player: u8, message: String) -> Result<(), Box<EvalAltResult>> {
|
|
let mut players = self.0.lock().unwrap();
|
|
|
|
players[player as usize]
|
|
.outgoing_data
|
|
.extend_from_slice(&send_chat_message(255, "".to_string(), message));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn build_extra(builder: &mut TypeBuilder<Self>) {
|
|
builder.with_fn("send_message", Self::send_message);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, CustomType)]
|
|
#[rhai_type(name = "Context", extra = Self::build_extra)]
|
|
struct Context {
|
|
#[rhai_type(skip)]
|
|
commands: HashMap<String, FnPtr>,
|
|
}
|
|
|
|
impl Context {
|
|
fn new() -> Self {
|
|
Self {
|
|
commands: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
fn register_command(&mut self, name: String, callback: FnPtr) {
|
|
self.commands.insert(name, callback);
|
|
}
|
|
|
|
fn build_extra(builder: &mut TypeBuilder<Self>) {
|
|
builder.with_fn("Context", Self::new);
|
|
builder.with_fn("register_command", Self::register_command);
|
|
}
|
|
}
|
|
////// END RHAI DEFINITIONS //////
|
|
|
|
impl Extensions {
|
|
pub fn init(players: PlayersWrapper) -> Result<Extensions, AppError> {
|
|
if !Path::new("./extensions/").exists() {
|
|
let _ = fs::create_dir("./extensions/");
|
|
}
|
|
|
|
let extensions_listing = fs::read_dir("./extensions")?;
|
|
|
|
let mut extensions = Extensions {
|
|
extensions: Vec::new(),
|
|
};
|
|
|
|
for extension in extensions_listing {
|
|
let extension_path = extension?.path();
|
|
|
|
if extension_path.extension() != Some(OsStr::new("rhai")) {
|
|
break;
|
|
}
|
|
info!("Loading extension {}", extension_path.display());
|
|
|
|
let mut engine = Engine::new();
|
|
engine.build_type::<Version>();
|
|
engine.build_type::<ExtensionMetadata>();
|
|
engine.build_type::<RhaiPlayer>();
|
|
engine.build_type::<PlayersWrapper>();
|
|
engine.build_type::<Context>();
|
|
|
|
let ast = engine.compile_file(extension_path.clone().into())?;
|
|
let mut scope = Scope::new();
|
|
|
|
let extension_metadata =
|
|
match engine.call_fn::<ExtensionMetadata>(&mut scope, &ast, "metadata", ()) {
|
|
Ok(result) => result,
|
|
Err(error) => {
|
|
error!(
|
|
"Rhai plugin with path {} missing critical section metadata! {}",
|
|
extension_path.display(),
|
|
error
|
|
);
|
|
break;
|
|
}
|
|
};
|
|
|
|
let mut current_extension = Extension {
|
|
ast,
|
|
engine,
|
|
commands: HashMap::new(),
|
|
metadata: extension_metadata,
|
|
};
|
|
|
|
let ctx = match current_extension.engine.call_fn::<Context>(
|
|
&mut scope,
|
|
¤t_extension.ast,
|
|
"init",
|
|
(PlayersWrapper::new(players.0.clone()),),
|
|
) {
|
|
Ok(result) => result,
|
|
Err(error) => {
|
|
error!(
|
|
"Plugin {} failed to init: {}",
|
|
current_extension.metadata.name, error
|
|
);
|
|
break;
|
|
}
|
|
};
|
|
|
|
for (key, value) in ctx.commands.iter() {
|
|
current_extension
|
|
.commands
|
|
.insert(key.to_string(), value.clone());
|
|
}
|
|
|
|
info!(
|
|
"Loaded {} v{}",
|
|
current_extension.metadata.name,
|
|
current_extension.metadata.version.display()
|
|
);
|
|
|
|
extensions.extensions.push(current_extension);
|
|
}
|
|
|
|
for extension in &extensions.extensions {
|
|
for command_name in extension.commands.keys() {
|
|
info!(
|
|
"Extension {} v{} has reserved command: {}",
|
|
extension.metadata.name,
|
|
extension.metadata.version.display(),
|
|
command_name
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(extensions)
|
|
}
|
|
}
|