mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Moved plugin API to Uid instead of usize for entity IDs, hello plugin to an example
This commit is contained in:
@ -3,11 +3,12 @@ cargo-features = ["named-profiles","profile-overrides"]
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"common",
|
"common",
|
||||||
|
"common/net",
|
||||||
"common/sys",
|
"common/sys",
|
||||||
"plugin/rt",
|
"client",
|
||||||
"plugin/api",
|
"plugin/api",
|
||||||
"plugin/derive",
|
"plugin/derive",
|
||||||
"client",
|
"plugin/rt",
|
||||||
"server",
|
"server",
|
||||||
"server-cli",
|
"server-cli",
|
||||||
"voxygen",
|
"voxygen",
|
||||||
|
BIN
assets/plugins/hello.plugin.tar
(Stored with Git LFS)
BIN
assets/plugins/hello.plugin.tar
(Stored with Git LFS)
Binary file not shown.
@ -1,21 +0,0 @@
|
|||||||
use bincode::ErrorKind;
|
|
||||||
use wasmer_runtime::error::{ResolveError, RuntimeError};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum PluginError {
|
|
||||||
Io(std::io::Error),
|
|
||||||
Toml(toml::de::Error),
|
|
||||||
NoConfig,
|
|
||||||
NoSuchModule,
|
|
||||||
PluginModuleError(PluginModuleError)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum PluginModuleError {
|
|
||||||
FindFunction(String),
|
|
||||||
FunctionGet(ResolveError),
|
|
||||||
Compile(wasmer_runtime::error::CompileError),
|
|
||||||
Instantiate(wasmer_runtime::error::Error),
|
|
||||||
RunFunction(RuntimeError),
|
|
||||||
Encoding(Box<ErrorKind>),
|
|
||||||
}
|
|
@ -1,138 +0,0 @@
|
|||||||
|
|
||||||
pub mod errors;
|
|
||||||
pub mod module;
|
|
||||||
|
|
||||||
use common::assets::ASSETS_PATH;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{collections::{HashMap, HashSet}, fs, io::Read, path::{Path, PathBuf}};
|
|
||||||
use tracing::{error, info};
|
|
||||||
|
|
||||||
use plugin_api::Event;
|
|
||||||
|
|
||||||
use self::{ errors::PluginError, module::{PluginModule, PreparedEventQuery}};
|
|
||||||
|
|
||||||
use rayon::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct PluginData {
|
|
||||||
name: String,
|
|
||||||
modules: HashSet<PathBuf>,
|
|
||||||
dependencies: HashSet<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Plugin {
|
|
||||||
data: PluginData,
|
|
||||||
modules: Vec<PluginModule>,
|
|
||||||
files: HashMap<PathBuf, Vec<u8>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Plugin {
|
|
||||||
pub fn from_reader<R: Read>(mut reader: R) -> Result<Self, PluginError> {
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
reader.read_to_end(&mut buf).map_err(PluginError::Io)?;
|
|
||||||
|
|
||||||
let mut files = tar::Archive::new(&*buf)
|
|
||||||
.entries()
|
|
||||||
.map_err(PluginError::Io)?
|
|
||||||
.map(|e| {
|
|
||||||
e.and_then(|e| {
|
|
||||||
Ok((e.path()?.into_owned(), {
|
|
||||||
let offset = e.raw_file_position() as usize;
|
|
||||||
buf[offset..offset + e.size() as usize].to_vec()
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Result<HashMap<_, _>, _>>()
|
|
||||||
.map_err(PluginError::Io)?;
|
|
||||||
|
|
||||||
let data = toml::de::from_slice::<PluginData>(
|
|
||||||
&files
|
|
||||||
.get(Path::new("plugin.toml"))
|
|
||||||
.ok_or(PluginError::NoConfig)?,
|
|
||||||
)
|
|
||||||
.map_err(PluginError::Toml)?;
|
|
||||||
|
|
||||||
let modules = data
|
|
||||||
.modules
|
|
||||||
.iter()
|
|
||||||
.map(|path| {
|
|
||||||
let wasm_data = files.remove(path).ok_or(PluginError::NoSuchModule)?;
|
|
||||||
PluginModule::new(&wasm_data).map_err(|e| PluginError::PluginModuleError(e))
|
|
||||||
})
|
|
||||||
.collect::<Result<_, _>>()?;
|
|
||||||
|
|
||||||
Ok(Plugin {
|
|
||||||
data,
|
|
||||||
modules,
|
|
||||||
files,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn execute_prepared<T>(&self, event_name: &str, event: &PreparedEventQuery<T>) -> Result<Vec<T::Response>, PluginError> where T: Event {
|
|
||||||
self.modules.iter().flat_map(|module| {
|
|
||||||
module.try_execute(event_name, event).map(|x| x.map_err(|e| PluginError::PluginModuleError(e)))
|
|
||||||
}).collect::<Result<Vec<_>,_>>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
pub struct PluginMgr {
|
|
||||||
plugins: Vec<Plugin>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PluginMgr {
|
|
||||||
pub fn from_assets() -> Result<Self, PluginError> {
|
|
||||||
let mut assets_path = (&*ASSETS_PATH).clone();
|
|
||||||
assets_path.push("plugins");
|
|
||||||
info!("Searching {:?} for assets...", assets_path);
|
|
||||||
Self::from_dir(assets_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn execute_prepared<T>(&self, event_name: &str,event: &PreparedEventQuery<T>) -> Result<Vec<T::Response>, PluginError> where T: Event {
|
|
||||||
Ok(self.plugins.par_iter().map(|plugin| {
|
|
||||||
plugin.execute_prepared(event_name, event)
|
|
||||||
}).collect::<Result<Vec<_>,_>>()?.into_iter().flatten().collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn execute_event<T>(&self, event_name: &str,event: &T) -> Result<Vec<T::Response>, PluginError> where T: Event {
|
|
||||||
self.execute_prepared(event_name, &PreparedEventQuery::new(event)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_dir<P: AsRef<Path>>(path: P) -> Result<Self, PluginError> {
|
|
||||||
let plugins = fs::read_dir(path)
|
|
||||||
.map_err(PluginError::Io)?
|
|
||||||
.filter_map(|e| e.ok())
|
|
||||||
.map(|entry| {
|
|
||||||
if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false)
|
|
||||||
&& entry
|
|
||||||
.path()
|
|
||||||
.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.map(|s| s.ends_with(".plugin.tar"))
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
info!("Loading plugin at {:?}", entry.path());
|
|
||||||
Plugin::from_reader(fs::File::open(entry.path()).map_err(PluginError::Io)?)
|
|
||||||
.map(Some)
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter_map(Result::transpose)
|
|
||||||
.inspect(|p| {
|
|
||||||
let _ = p.as_ref().map_err(|e| error!(?e, "Failed to load plugin"));
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
|
|
||||||
for plugin in &plugins {
|
|
||||||
info!(
|
|
||||||
"Loaded plugin '{}' with {} module(s)",
|
|
||||||
plugin.data.name,
|
|
||||||
plugin.modules.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self { plugins })
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,150 +0,0 @@
|
|||||||
use std::{
|
|
||||||
cell::Cell,
|
|
||||||
collections::HashSet,
|
|
||||||
marker::PhantomData,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use error::RuntimeError;
|
|
||||||
use wasmer_runtime::*;
|
|
||||||
|
|
||||||
use super::errors::{PluginError, PluginModuleError};
|
|
||||||
use plugin_api::{Action, Event};
|
|
||||||
|
|
||||||
// This represent a WASM function interface
|
|
||||||
pub type Function<'a> = Func<'a, (i32, u32), i32>;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
// This structure represent the WASM State of the plugin.
|
|
||||||
pub struct PluginModule {
|
|
||||||
wasm_instance: Arc<Mutex<Instance>>,
|
|
||||||
events: HashSet<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PluginModule {
|
|
||||||
|
|
||||||
// This function takes bytes from a WASM File and compile them
|
|
||||||
pub fn new(wasm_data: &Vec<u8>) -> Result<Self,PluginModuleError> {
|
|
||||||
let module = compile(&wasm_data).map_err(|e| PluginModuleError::Compile(e))?;
|
|
||||||
let instance = module
|
|
||||||
.instantiate(&imports! {"env" => {
|
|
||||||
"raw_emit_actions" => func!(read_action),
|
|
||||||
}}).map_err(|e| PluginModuleError::Instantiate(e))?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
events: instance.exports.into_iter().map(|(name, _)| name).collect(),
|
|
||||||
wasm_instance: Arc::new(Mutex::new(instance)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function tries to execute an event for the current module. Will return None if the event doesn't exists
|
|
||||||
pub fn try_execute<T>(
|
|
||||||
&self,
|
|
||||||
event_name: &str,
|
|
||||||
request: &PreparedEventQuery<T>,
|
|
||||||
) -> Option<Result<T::Response,PluginModuleError>>
|
|
||||||
where
|
|
||||||
T: Event,
|
|
||||||
{
|
|
||||||
if !self.events.contains(event_name) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let bytes = {
|
|
||||||
let instance = self.wasm_instance.lock().unwrap();
|
|
||||||
let func = match instance.exports.get(event_name).map_err(|e| PluginModuleError::FunctionGet(e)) {
|
|
||||||
Ok(e) => e,
|
|
||||||
Err(e) => return Some(Err(e))
|
|
||||||
};
|
|
||||||
let mem = instance.context().memory(0);
|
|
||||||
match execute_raw(&mem, &func, &request.bytes).map_err(|e| PluginModuleError::RunFunction(e)) {
|
|
||||||
Ok(e) => e,
|
|
||||||
Err(e) => return Some(Err(e))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Some(bincode::deserialize(&bytes).map_err(|e| PluginModuleError::Encoding(e)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This structure represent a Pre-encoded event object (Useful to avoid reencoding for each module in every plugin)
|
|
||||||
pub struct PreparedEventQuery<T> {
|
|
||||||
bytes: Vec<u8>,
|
|
||||||
_phantom: PhantomData<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Event> PreparedEventQuery<T> {
|
|
||||||
// Create a prepared query from an event reference (Encode to bytes the struct)
|
|
||||||
// This Prepared Query is used by the `try_execute` method in `PluginModule`
|
|
||||||
pub fn new(event: &T) -> Result<Self, PluginError>
|
|
||||||
where
|
|
||||||
T: Event,
|
|
||||||
{
|
|
||||||
Ok(Self {
|
|
||||||
bytes: bincode::serialize(&event).map_err(|e| PluginError::PluginModuleError(PluginModuleError::Encoding(e)))?,
|
|
||||||
_phantom: PhantomData::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const MEMORY_POS: usize = 100000;
|
|
||||||
|
|
||||||
// This function is not public because this function should not be used without an interface to limit unsafe behaviours
|
|
||||||
fn execute_raw(
|
|
||||||
memory: &Memory,
|
|
||||||
function: &Function,
|
|
||||||
bytes: &[u8],
|
|
||||||
) -> Result<Vec<u8>, RuntimeError> {
|
|
||||||
let view = memory.view::<u8>();
|
|
||||||
let len = bytes.len();
|
|
||||||
for (cell, byte) in view[MEMORY_POS..len + MEMORY_POS].iter().zip(bytes.iter()) {
|
|
||||||
cell.set(*byte)
|
|
||||||
}
|
|
||||||
let start = function
|
|
||||||
.call(MEMORY_POS as i32, len as u32)? as usize;
|
|
||||||
let view = memory.view::<u8>();
|
|
||||||
let mut new_len_bytes = [0u8; 4];
|
|
||||||
// TODO: It is probably better to dirrectly make the new_len_bytes
|
|
||||||
for i in 0..4 {
|
|
||||||
new_len_bytes[i] = view.get(i + 1).map(Cell::get).unwrap_or(0);
|
|
||||||
}
|
|
||||||
let new_len = u32::from_ne_bytes(new_len_bytes) as usize;
|
|
||||||
Ok(view[start..start + new_len]
|
|
||||||
.iter()
|
|
||||||
.map(|c| c.get())
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_action(ctx: &mut Ctx, ptr: u32, len: u32) {
|
|
||||||
let memory = ctx.memory(0);
|
|
||||||
|
|
||||||
let memory = memory.view::<u8>();
|
|
||||||
|
|
||||||
let str_slice = &memory[ptr as usize..(ptr + len) as usize];
|
|
||||||
|
|
||||||
let bytes: Vec<u8> = str_slice.iter().map(|x| x.get()).collect();
|
|
||||||
|
|
||||||
let e: Vec<Action> = match bincode::deserialize(&bytes) {
|
|
||||||
Ok(e) => e,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!(?e, "Can't decode action");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for action in e {
|
|
||||||
match action {
|
|
||||||
Action::ServerClose => {
|
|
||||||
tracing::info!("Server closed by plugin");
|
|
||||||
std::process::exit(-1);
|
|
||||||
}
|
|
||||||
Action::Print(e) => {
|
|
||||||
tracing::info!("{}",e);
|
|
||||||
}
|
|
||||||
Action::PlayerSendMessage(a, b) => {
|
|
||||||
tracing::info!("SendMessage {} -> {}",a,b);
|
|
||||||
}
|
|
||||||
Action::KillEntity(e) => {
|
|
||||||
tracing::info!("Kill Entity {}",e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,3 @@
|
|||||||
#![allow(clippy::eval_order_dependence)]
|
|
||||||
|
|
||||||
use crate::commands::{Command, FileInfo, LocalCommand, RemoteInfo};
|
use crate::commands::{Command, FileInfo, LocalCommand, RemoteInfo};
|
||||||
use async_std::{
|
use async_std::{
|
||||||
fs,
|
fs,
|
||||||
|
@ -5,8 +5,8 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
|||||||
pub enum Action {
|
pub enum Action {
|
||||||
ServerClose,
|
ServerClose,
|
||||||
Print(String),
|
Print(String),
|
||||||
PlayerSendMessage(usize, String),
|
PlayerSendMessage(Uid, String),
|
||||||
KillEntity(usize),
|
KillEntity(Uid),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Event: Serialize + DeserializeOwned + Send + Sync {
|
pub trait Event: Serialize + DeserializeOwned + Send + Sync {
|
||||||
@ -38,7 +38,7 @@ pub mod event {
|
|||||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||||
pub struct PlayerJoinEvent {
|
pub struct PlayerJoinEvent {
|
||||||
pub player_name: String,
|
pub player_name: String,
|
||||||
pub player_id: usize,
|
pub player_id: Uid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Event for PlayerJoinEvent {
|
impl Event for PlayerJoinEvent {
|
||||||
|
@ -16,16 +16,16 @@ pub fn event_handler(_args: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
let out: proc_macro2::TokenStream = quote! {
|
let out: proc_macro2::TokenStream = quote! {
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub fn #fn_name(intern__ptr: i32, intern__len: u32) -> i32 {
|
pub fn #fn_name(intern__ptr: i32, intern__len: u32) -> i32 {
|
||||||
let input = ::plugin_rt::read_input(intern__ptr,intern__len).unwrap();
|
let input = ::veloren_plugin_rt::read_input(intern__ptr,intern__len).unwrap();
|
||||||
#[inline]
|
#[inline]
|
||||||
fn inner(#fn_args) #fn_return {
|
fn inner(#fn_args) #fn_return {
|
||||||
#fn_body
|
#fn_body
|
||||||
}
|
}
|
||||||
// Artificially force the event handler to be type-correct
|
// Artificially force the event handler to be type-correct
|
||||||
fn force_event<E: ::plugin_rt::api::Event>(event: E, inner: fn(E) -> E::Response) -> E::Response {
|
fn force_event<E: ::veloren_plugin_rt::api::Event>(event: E, inner: fn(E) -> E::Response) -> E::Response {
|
||||||
inner(event)
|
inner(event)
|
||||||
}
|
}
|
||||||
::plugin_rt::write_output(&force_event(input, inner))
|
::veloren_plugin_rt::write_output(&force_event(input, inner))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
out.into()
|
out.into()
|
||||||
|
2
plugin/hello/.gitignore
vendored
2
plugin/hello/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
# Rust
|
|
||||||
target
|
|
1276
plugin/hello/Cargo.lock
generated
1276
plugin/hello/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,17 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "hello"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["Joshua Barretto <joshua.s.barretto@gmail.com>"]
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
plugin-rt = { package = "veloren-plugin-rt", path = "../rt" }
|
|
||||||
|
|
||||||
[workspace]
|
|
||||||
|
|
||||||
[profile.dev]
|
|
||||||
opt-level = "s"
|
|
||||||
debug = false
|
|
@ -9,3 +9,10 @@ plugin-api = { package = "veloren-plugin-api", path = "../api" }
|
|||||||
plugin-derive = { package = "veloren-plugin-derive", path = "../derive"}
|
plugin-derive = { package = "veloren-plugin-derive", path = "../derive"}
|
||||||
serde = {version = "1.0.118", features = ["derive"]}
|
serde = {version = "1.0.118", features = ["derive"]}
|
||||||
bincode = "1.3.1"
|
bincode = "1.3.1"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "hello"
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
plugin-derive = { package = "veloren-plugin-derive", path = "../derive"}
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
extern crate plugin_rt;
|
use veloren_plugin_rt::{
|
||||||
|
|
||||||
use plugin_rt::{
|
|
||||||
api::{event::*, Action, GameMode},
|
api::{event::*, Action, GameMode},
|
||||||
*,
|
*,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[event_handler]
|
#[veloren_plugin_rt::event_handler]
|
||||||
pub fn on_load(load: PluginLoadEvent) {
|
pub fn on_load(load: PluginLoadEvent) {
|
||||||
match load.game_mode {
|
match load.game_mode {
|
||||||
GameMode::Server => emit_action(Action::Print("Hello, server!".to_owned())),
|
GameMode::Server => emit_action(Action::Print("Hello, server!".to_owned())),
|
Reference in New Issue
Block a user