571 lines
17 KiB
Rust
571 lines
17 KiB
Rust
use log::{debug, error, info, warn};
|
|
use regex::Regex;
|
|
use rhai::{packages::Package, CustomType, Engine, EvalAltResult, FnPtr, Scope, TypeBuilder, AST};
|
|
use rhai_rand::RandomPackage;
|
|
use std::{
|
|
collections::HashMap,
|
|
ffi::OsStr,
|
|
fmt, fs,
|
|
path::Path,
|
|
sync::{Arc, Mutex},
|
|
};
|
|
|
|
use crate::{
|
|
error::AppError,
|
|
player::Player,
|
|
utils::{send_chat_message, stream_write_short, write_chat_stream},
|
|
world::World,
|
|
};
|
|
|
|
pub struct Extensions {
|
|
extensions: Vec<Extension>,
|
|
players: PlayersWrapper,
|
|
}
|
|
|
|
impl Extensions {
|
|
pub fn run_command(
|
|
&self,
|
|
key: String,
|
|
player: u8,
|
|
argv: Vec<String>,
|
|
) -> 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 mut res_data: Vec<u8> = Vec::new();
|
|
res_data.extend_from_slice(&write_chat_stream("Extension listing".to_string()));
|
|
|
|
for extension in &self.extensions {
|
|
res_data.extend_from_slice(&write_chat_stream(format!(
|
|
"&a{} &bv{}",
|
|
extension.metadata.name, extension.metadata.version
|
|
)));
|
|
}
|
|
|
|
self.players.0.lock().unwrap()[player as usize]
|
|
.outgoing_data
|
|
.extend_from_slice(&res_data);
|
|
|
|
return Ok(true);
|
|
}
|
|
|
|
// Reserve command listing command
|
|
if &key == "help" {
|
|
let mut res_data: Vec<u8> = Vec::new();
|
|
|
|
res_data.extend_from_slice(&write_chat_stream("Command listing".to_string()));
|
|
|
|
res_data.extend_from_slice(&write_chat_stream(format!(
|
|
"&c{} &a[{}]",
|
|
"help", "Builtin"
|
|
)));
|
|
res_data.extend_from_slice(&write_chat_stream(format!(
|
|
"&c{} &a[{}]",
|
|
"extensions", "Builtin"
|
|
)));
|
|
res_data.extend_from_slice(&write_chat_stream(format!(
|
|
"&c{} &a[{}]",
|
|
"kick", "Builtin"
|
|
)));
|
|
res_data.extend_from_slice(&write_chat_stream(format!("&c{} &a[{}]", "tp", "Builtin")));
|
|
|
|
for extension in &self.extensions {
|
|
for command in extension.commands.keys() {
|
|
res_data.extend_from_slice(&write_chat_stream(format!(
|
|
"&c{} &a[{}]",
|
|
command, extension.metadata.name
|
|
)));
|
|
}
|
|
}
|
|
|
|
self.players.0.lock().unwrap()[player as usize]
|
|
.outgoing_data
|
|
.extend_from_slice(&res_data);
|
|
|
|
return Ok(true);
|
|
}
|
|
|
|
for extension in &self.extensions {
|
|
if let Some(key_value) = extension.commands.get(&key) {
|
|
// Vector transfer number 2 (get the shit joke?)
|
|
let mut argv_rhai_array: rhai::Array = rhai::Array::new();
|
|
for arg in &argv {
|
|
argv_rhai_array.push(arg.into());
|
|
}
|
|
|
|
key_value.call::<()>(
|
|
&extension.engine,
|
|
&extension.ast,
|
|
(player, argv_rhai_array),
|
|
)?;
|
|
return Ok(true);
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
pub fn run_event(&self, event_type: EventType, event: Event) -> Event {
|
|
let mut is_cancelled = false;
|
|
|
|
for extension in &self.extensions {
|
|
if let Some(key_value) = extension.event_listeners.get(&event_type) {
|
|
is_cancelled =
|
|
match key_value.call::<Event>(&extension.engine, &extension.ast, (event,)) {
|
|
Ok(result) => result.is_cancelled,
|
|
Err(err) => {
|
|
error!("{} raised: {}", extension.metadata.name, err);
|
|
break;
|
|
}
|
|
};
|
|
}
|
|
}
|
|
let mut response = Event::new();
|
|
response.is_cancelled = is_cancelled;
|
|
response
|
|
}
|
|
}
|
|
|
|
pub struct Extension {
|
|
ast: AST,
|
|
engine: Engine,
|
|
commands: HashMap<String, FnPtr>,
|
|
event_listeners: HashMap<EventType, FnPtr>,
|
|
metadata: ExtensionMetadata,
|
|
}
|
|
|
|
////// BEGIN RHAI DEFINITIONS //////
|
|
fn info(msg: &str) {
|
|
info!("{}", msg);
|
|
}
|
|
|
|
fn warn(msg: &str) {
|
|
warn!("{}", msg);
|
|
}
|
|
|
|
fn error(msg: &str) {
|
|
error!("{}", msg);
|
|
}
|
|
|
|
fn debug(msg: &str) {
|
|
debug!("{}", msg);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Version {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.display())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq, CustomType)]
|
|
#[rhai_type(name = "Metadata", extra = Self::build_extra)]
|
|
struct ExtensionMetadata {
|
|
name: String,
|
|
author: String,
|
|
version: Version,
|
|
}
|
|
|
|
impl ExtensionMetadata {
|
|
fn new(name: String, author: String, version: Version) -> Self {
|
|
Self {
|
|
name,
|
|
author,
|
|
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 send_all(self, message: String) -> Result<(), Box<EvalAltResult>> {
|
|
let mut players = self.0.lock().unwrap();
|
|
|
|
let data = &send_chat_message(255, "".to_string(), message);
|
|
|
|
for i in 0..255 {
|
|
if players[i].id != 255 {
|
|
players[i].outgoing_data.extend_from_slice(data);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn username(self, player: u8) -> String {
|
|
let players = self.0.lock().unwrap();
|
|
|
|
players[player as usize].username.clone()
|
|
}
|
|
|
|
fn build_extra(builder: &mut TypeBuilder<Self>) {
|
|
builder.with_fn("send_message", Self::send_message);
|
|
builder.with_fn("send_all", Self::send_all);
|
|
builder.with_fn("username", Self::username);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, CustomType)]
|
|
#[rhai_type(name = "WorldWrapper", extra=Self::build_extra)]
|
|
pub struct WorldWrapper(Arc<Mutex<World>>);
|
|
|
|
impl WorldWrapper {
|
|
pub fn new(world: Arc<Mutex<World>>) -> Self {
|
|
Self(world)
|
|
}
|
|
|
|
pub fn set_block(self, players_wrapper: PlayersWrapper, position: Vec3, block_type: u8) {
|
|
let mut world_dat = self.0.lock().unwrap();
|
|
|
|
let world_offset: u32 = position.x as u32
|
|
+ (position.z as u32 * world_dat.size_x as u32)
|
|
+ (position.y as u32 * world_dat.size_x as u32 * world_dat.size_z as u32);
|
|
|
|
world_dat.data[world_offset as usize] = block_type;
|
|
|
|
let mut update_block_bytes: Vec<u8> = Vec::new();
|
|
update_block_bytes.push(0x06);
|
|
update_block_bytes.extend_from_slice(&stream_write_short(position.x));
|
|
update_block_bytes.extend_from_slice(&stream_write_short(position.y));
|
|
update_block_bytes.extend_from_slice(&stream_write_short(position.z));
|
|
update_block_bytes.push(block_type);
|
|
|
|
let mut players = players_wrapper.0.lock().unwrap();
|
|
for i in 0..players.len() {
|
|
if players[i].id != 255 {
|
|
players[i]
|
|
.outgoing_data
|
|
.extend_from_slice(&update_block_bytes);
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Finish this
|
|
// pub fn get_block(&self, position: Vec3) -> u8 {
|
|
// let mut world = self.0.lock().unwrap();
|
|
// }
|
|
|
|
fn build_extra(builder: &mut TypeBuilder<Self>) {
|
|
builder.with_fn("set_block", Self::set_block);
|
|
// builder.with_fn("get_block", Self::get_block);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, CustomType)]
|
|
#[rhai_type(name = "Context", extra = Self::build_extra)]
|
|
struct Context {
|
|
#[rhai_type(skip)]
|
|
commands: HashMap<String, FnPtr>,
|
|
#[rhai_type(skip)]
|
|
event_listener: HashMap<EventType, FnPtr>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, CustomType)]
|
|
#[rhai_type(name = "Vec3", extra = Self::build_extra)]
|
|
pub struct Vec3 {
|
|
pub x: i16,
|
|
pub y: i16,
|
|
pub z: i16,
|
|
}
|
|
|
|
// Custom type API
|
|
impl Vec3 {
|
|
fn new(x: i64, y: i64, z: i64) -> Self {
|
|
Self {
|
|
x: x.try_into().unwrap(),
|
|
y: y.try_into().unwrap(),
|
|
z: z.try_into().unwrap(),
|
|
}
|
|
}
|
|
|
|
fn build_extra(builder: &mut TypeBuilder<Self>) {
|
|
builder.with_fn("Vec3", Self::new);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
|
pub enum EventType {
|
|
BlockBreak,
|
|
PlayerLeave,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, CustomType)]
|
|
#[rhai_type(name = "Event", extra = Self::build_extra)]
|
|
pub struct Event {
|
|
pub player: u8,
|
|
pub position: Vec3,
|
|
pub selected_block: u8,
|
|
pub is_cancelled: bool,
|
|
}
|
|
|
|
impl Event {
|
|
pub fn new() -> Event {
|
|
Event {
|
|
player: 255,
|
|
position: (Vec3 { x: 0, y: 0, z: 0 }),
|
|
selected_block: 0,
|
|
is_cancelled: false,
|
|
}
|
|
}
|
|
|
|
pub fn cancel(&mut self) {
|
|
self.is_cancelled = true;
|
|
}
|
|
|
|
fn build_extra(builder: &mut TypeBuilder<Self>) {
|
|
builder.with_fn("Event", Self::new);
|
|
builder.with_fn("cancel", Self::cancel);
|
|
}
|
|
}
|
|
|
|
impl Context {
|
|
fn new() -> Self {
|
|
Self {
|
|
commands: HashMap::new(),
|
|
event_listener: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
fn register_command(&mut self, name: String, callback: FnPtr) {
|
|
self.commands.insert(name, callback);
|
|
}
|
|
|
|
fn add_event_listener(&mut self, event: &str, callback: FnPtr) {
|
|
let event_listener: EventType = match event {
|
|
"block_break" => EventType::BlockBreak,
|
|
"player_leave" => EventType::PlayerLeave,
|
|
_ => {
|
|
warn!("An event listener was created with invalid type: {}", event);
|
|
return;
|
|
}
|
|
};
|
|
self.event_listener.insert(event_listener, callback);
|
|
}
|
|
|
|
fn build_extra(builder: &mut TypeBuilder<Self>) {
|
|
builder.with_fn("Context", Self::new);
|
|
builder.with_fn("register_command", Self::register_command);
|
|
builder.with_fn("add_event_listener", Self::add_event_listener);
|
|
}
|
|
}
|
|
////// END RHAI DEFINITIONS //////
|
|
|
|
impl Extensions {
|
|
pub fn init(players: PlayersWrapper, world: WorldWrapper) -> 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(),
|
|
players: players.clone(),
|
|
};
|
|
|
|
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();
|
|
let random = RandomPackage::new();
|
|
random.register_into_engine(&mut engine);
|
|
engine.set_max_expr_depths(50, 50);
|
|
engine.build_type::<Version>();
|
|
engine.build_type::<ExtensionMetadata>();
|
|
engine.build_type::<RhaiPlayer>();
|
|
engine.build_type::<PlayersWrapper>();
|
|
engine.build_type::<WorldWrapper>();
|
|
engine.build_type::<Context>();
|
|
engine.build_type::<Vec3>();
|
|
engine.build_type::<Event>();
|
|
engine.register_fn("info", info);
|
|
engine.register_fn("warn", warn);
|
|
engine.register_fn("error", error);
|
|
engine.register_fn("debug", debug);
|
|
|
|
let ast = match engine.compile_file(extension_path.clone().into()) {
|
|
Ok(result) => result,
|
|
Err(error) => {
|
|
error!(
|
|
"Rhai plugin compilation failed for {}, reason: {}",
|
|
extension_path.display(),
|
|
error
|
|
);
|
|
continue;
|
|
}
|
|
};
|
|
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
|
|
);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let mut current_extension = Extension {
|
|
ast,
|
|
engine,
|
|
commands: HashMap::new(),
|
|
event_listeners: 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()),
|
|
WorldWrapper::new(world.0.clone()),
|
|
),
|
|
) {
|
|
Ok(result) => result,
|
|
Err(error) => {
|
|
error!(
|
|
"Plugin {} failed to init: {}",
|
|
current_extension.metadata.name, error
|
|
);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
for (key, value) in ctx.commands.iter() {
|
|
current_extension
|
|
.commands
|
|
.insert(key.to_string(), value.clone());
|
|
}
|
|
|
|
for (key, value) in ctx.event_listener.iter() {
|
|
current_extension
|
|
.event_listeners
|
|
.insert(key.clone(), value.clone());
|
|
}
|
|
|
|
info!(
|
|
"Loaded {} v{}",
|
|
current_extension.metadata.name, current_extension.metadata.version,
|
|
);
|
|
|
|
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, command_name
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(extensions)
|
|
}
|
|
}
|