mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Use wasmtime to execute wasm components as veloren plugins
This commit is contained in:
parent
acd0f46fd5
commit
f56e1d84b5
@ -84,6 +84,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Camera no longer jumps on first mouse event after cursor grab is released on macos
|
||||
- Updated wgpu. Now supports OpenGL. Dx11 no longer supported.
|
||||
- Changes center_cursor to be reset_cursor_position so the cursor is effectively grabbed
|
||||
- Plugin interface based on WASI 0.2 WIT, wasmtime executes these components
|
||||
|
||||
### Removed
|
||||
- Medium and large potions from all loot tables
|
||||
|
1623
Cargo.lock
generated
1623
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -15,9 +15,6 @@ members = [
|
||||
"common/frontend",
|
||||
"client",
|
||||
"client/i18n",
|
||||
"plugin/api",
|
||||
"plugin/derive",
|
||||
"plugin/rt",
|
||||
"rtsim",
|
||||
"server",
|
||||
"server/agent",
|
||||
@ -156,6 +153,7 @@ image = { version = "0.24", default-features = false, features = ["png"] }
|
||||
rayon = { version = "1.5" }
|
||||
|
||||
clap = { version = "4.2", features = ["derive"]}
|
||||
async-trait = "0.1.42"
|
||||
|
||||
[patch.crates-io]
|
||||
vek = { git = "https://github.com/yoanlcq/vek.git", rev = "84d5cb65841d46599a986c5477341bea4456be26" }
|
||||
|
@ -6,7 +6,7 @@ version = "0.10.0"
|
||||
|
||||
[features]
|
||||
simd = ["vek/platform_intrinsics"]
|
||||
plugins = ["common-assets/plugins", "toml", "tar", "wasmer", "wasmer-wasix-types", "bincode", "plugin-api", "serde"]
|
||||
plugins = ["common-assets/plugins", "toml", "wasmtime", "tar", "bincode", "serde"]
|
||||
|
||||
default = ["simd"]
|
||||
|
||||
@ -33,11 +33,13 @@ scopeguard = "1.1.0"
|
||||
serde = { workspace = true, optional = true }
|
||||
toml = { version = "0.8", optional = true }
|
||||
tar = { version = "0.4.37", optional = true }
|
||||
wasmer = { version = "4.0.0", optional = true, default-features = false, features = ["sys", "wat", "cranelift"] }
|
||||
bincode = { workspace = true, optional = true }
|
||||
plugin-api = { package = "veloren-plugin-api", path = "../../plugin/api", optional = true }
|
||||
timer-queue = "0.1.0"
|
||||
wasmer-wasix-types = { version = "0.9.0", optional = true, default-features = false }
|
||||
wasmtime = { version = "17.0.0", optional = true , features = ["component-model", "async"] }
|
||||
wasmtime-wasi = "17.0.0"
|
||||
async-trait = { workspace = true }
|
||||
bytes = "^1"
|
||||
futures = "0.3.30"
|
||||
|
||||
# Tweak running code
|
||||
#inline_tweak = { version = "1.0.8", features = ["release_tweak"] }
|
||||
|
@ -1,5 +1,4 @@
|
||||
use bincode::ErrorKind;
|
||||
use wasmer::{CompileError, ExportError, InstantiationError, RuntimeError};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PluginError {
|
||||
@ -14,20 +13,21 @@ pub enum PluginError {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PluginModuleError {
|
||||
InstantiationError(Box<InstantiationError>),
|
||||
InvalidPointer,
|
||||
MemoryAllocation(MemoryAllocationError),
|
||||
MemoryUninit(ExportError),
|
||||
FindFunction(ExportError),
|
||||
RunFunction(RuntimeError),
|
||||
InvalidArgumentType(),
|
||||
Encoding(Box<ErrorKind>),
|
||||
CompileError(CompileError),
|
||||
Wasmtime(wasmtime::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MemoryAllocationError {
|
||||
InvalidReturnType,
|
||||
AllocatorNotFound(ExportError),
|
||||
CantAllocate(RuntimeError),
|
||||
pub enum EcsAccessError {
|
||||
EcsPointerNotAvailable,
|
||||
EcsComponentNotFound(common::uid::Uid, String),
|
||||
EcsResourceNotFound(String),
|
||||
EcsEntityNotFound(common::uid::Uid),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for EcsAccessError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Debug::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for EcsAccessError {}
|
||||
|
@ -1,139 +0,0 @@
|
||||
use crate::plugin::wasm_env::{HostFunctionEnvironment, HostFunctionException};
|
||||
use wasmer::{AsStoreMut, AsStoreRef, FunctionEnvMut, Memory32, Memory64, MemorySize, WasmPtr};
|
||||
// there is no WASI defined for wasm64, yet, so always use 32bit pointers
|
||||
type MemoryModel = wasmer::Memory32;
|
||||
|
||||
trait PtrConversion<T, M: MemorySize> {
|
||||
fn convert(self) -> WasmPtr<T, M>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
impl<T> PtrConversion<T, Memory64> for WasmPtr<T, Memory32> {
|
||||
fn convert(self) -> WasmPtr<T, Memory64>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
WasmPtr::new(self.offset().into())
|
||||
}
|
||||
}
|
||||
|
||||
fn print_impl(
|
||||
env: &HostFunctionEnvironment,
|
||||
store: &wasmer::StoreRef<'_>,
|
||||
ptr: WasmPtr<u8, MemoryModel>,
|
||||
len: <MemoryModel as wasmer::MemorySize>::Offset,
|
||||
) -> Result<(), wasmer_wasix_types::wasi::Errno> {
|
||||
env.read_bytes(store, ptr.convert(), len.into())
|
||||
.map_err(|error| {
|
||||
tracing::error!(
|
||||
"Logging message from plugin {} failed with {:?}!",
|
||||
env.name(),
|
||||
error
|
||||
);
|
||||
wasmer_wasix_types::wasi::Errno::Memviolation
|
||||
})
|
||||
.and_then(|bytes| {
|
||||
std::str::from_utf8(bytes.as_slice())
|
||||
.map_err(|error| {
|
||||
tracing::error!(
|
||||
"Logging message from plugin {} failed with {}!",
|
||||
env.name(),
|
||||
error
|
||||
);
|
||||
wasmer_wasix_types::wasi::Errno::Inval
|
||||
})
|
||||
.map(|msg| tracing::info!("[{}]: {}", env.name(), msg))
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct CioVec {
|
||||
buf: WasmPtr<u8, MemoryModel>,
|
||||
buf_len: <MemoryModel as wasmer::MemorySize>::Offset,
|
||||
}
|
||||
|
||||
// CioVec has no padding bytes, thus no action is necessary
|
||||
unsafe impl wasmer::ValueType for CioVec {
|
||||
fn zero_padding_bytes(&self, _bytes: &mut [std::mem::MaybeUninit<u8>]) {
|
||||
const _: () = assert!(
|
||||
core::mem::size_of::<CioVec>()
|
||||
== core::mem::size_of::<WasmPtr<u8, MemoryModel>>()
|
||||
+ core::mem::size_of::<<MemoryModel as wasmer::MemorySize>::Offset>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// fd_write(fd: fd, iovs: ciovec_array) -> Result<size, errno>
|
||||
pub(crate) fn wasi_fd_write(
|
||||
mut env: FunctionEnvMut<HostFunctionEnvironment>,
|
||||
fd: i32,
|
||||
iov_addr: WasmPtr<CioVec, MemoryModel>,
|
||||
iov_len: <MemoryModel as wasmer::MemorySize>::Offset,
|
||||
out_result: WasmPtr<<MemoryModel as wasmer::MemorySize>::Offset, MemoryModel>,
|
||||
) -> i32 {
|
||||
use wasmer_wasix_types::wasi::Errno;
|
||||
if fd != 1 && fd != 2 {
|
||||
Errno::Badf as i32
|
||||
} else {
|
||||
let memory = env.data().memory().clone();
|
||||
let mut written: u32 = 0;
|
||||
for i in 0..iov_len {
|
||||
let store = env.as_store_ref();
|
||||
let Ok(cio) = iov_addr
|
||||
.add_offset(i)
|
||||
.and_then(|p| p.read(&memory.view(&store)))
|
||||
else {
|
||||
return Errno::Memviolation as i32;
|
||||
};
|
||||
if let Err(e) = print_impl(env.data(), &store, cio.buf, cio.buf_len) {
|
||||
return e as i32;
|
||||
}
|
||||
written += cio.buf_len;
|
||||
}
|
||||
let store = env.as_store_mut();
|
||||
let mem = memory.view(&store);
|
||||
out_result
|
||||
.write(&mem, written)
|
||||
.map_or(Errno::Memviolation as i32, |()| Errno::Success as i32)
|
||||
}
|
||||
}
|
||||
|
||||
// environ_get(environ: Pointer<Pointer<u8>>, environ_buf: Pointer<u8>) ->
|
||||
// Result<(), errno>
|
||||
pub(crate) fn wasi_env_get(
|
||||
_env: FunctionEnvMut<HostFunctionEnvironment>,
|
||||
_environ: WasmPtr<WasmPtr<u8, MemoryModel>, MemoryModel>,
|
||||
_environ_buf: WasmPtr<u8, MemoryModel>,
|
||||
) -> i32 {
|
||||
// as the environment is always empty (0 bytes, 0 entries) this function will
|
||||
// just unconditionally return Success.
|
||||
wasmer_wasix_types::wasi::Errno::Success as i32
|
||||
}
|
||||
|
||||
// environ_sizes_get() -> Result<(size, size), errno>
|
||||
pub(crate) fn wasi_env_sizes_get(
|
||||
mut env: FunctionEnvMut<HostFunctionEnvironment>,
|
||||
numptr: WasmPtr<u32, MemoryModel>,
|
||||
bytesptr: WasmPtr<u32, MemoryModel>,
|
||||
) -> i32 {
|
||||
use wasmer_wasix_types::wasi::Errno;
|
||||
let memory = env.data().memory().clone();
|
||||
let store = env.as_store_mut();
|
||||
let mem = memory.view(&store);
|
||||
const NUMBER_OF_ENVIRONMENT_ENTRIES: u32 = 0;
|
||||
const NUMBER_OF_ENVIRONMENT_BYTES: u32 = 0;
|
||||
numptr
|
||||
.write(&mem, NUMBER_OF_ENVIRONMENT_ENTRIES)
|
||||
.and_then(|()| bytesptr.write(&mem, NUMBER_OF_ENVIRONMENT_BYTES))
|
||||
.map_or(Errno::Memviolation, |()| Errno::Success) as i32
|
||||
}
|
||||
|
||||
// proc_exit(rval: exitcode)
|
||||
pub(crate) fn wasi_proc_exit(
|
||||
_env: FunctionEnvMut<HostFunctionEnvironment>,
|
||||
exitcode: i32,
|
||||
) -> Result<(), HostFunctionException> {
|
||||
Err(HostFunctionException::ProcessExit(exitcode))
|
||||
}
|
@ -1,23 +1,11 @@
|
||||
use std::{
|
||||
mem::MaybeUninit,
|
||||
sync::atomic::{AtomicPtr, Ordering},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specs::{
|
||||
storage::GenericReadStorage, Component, Entities, Entity, Read, ReadStorage, WriteStorage,
|
||||
};
|
||||
use wasmer::{Memory, StoreMut, StoreRef, TypedFunction, WasmPtr};
|
||||
|
||||
use common::{
|
||||
comp::{Health, Player},
|
||||
uid::{IdMaps, Uid},
|
||||
};
|
||||
|
||||
use super::{
|
||||
errors::{MemoryAllocationError, PluginModuleError},
|
||||
MemoryModel,
|
||||
use specs::{
|
||||
storage::GenericReadStorage, Component, Entities, Entity, Read, ReadStorage, WriteStorage,
|
||||
};
|
||||
use std::sync::atomic::{AtomicPtr, Ordering};
|
||||
|
||||
pub struct EcsWorld<'a, 'b> {
|
||||
pub entities: &'b Entities<'a>,
|
||||
@ -105,134 +93,3 @@ impl EcsAccessManager {
|
||||
self.ecs_pointer.load(Ordering::Relaxed).as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// This function check if the buffer is wide enough if not it realloc the
|
||||
/// buffer calling the `wasm_prepare_buffer` function Note: There is
|
||||
/// probably optimizations that can be done using less restrictive
|
||||
/// ordering
|
||||
fn get_pointer(
|
||||
store: &mut StoreMut,
|
||||
object_length: <MemoryModel as wasmer::MemorySize>::Offset,
|
||||
allocator: &TypedFunction<
|
||||
<MemoryModel as wasmer::MemorySize>::Offset,
|
||||
WasmPtr<u8, MemoryModel>,
|
||||
>,
|
||||
) -> Result<WasmPtr<u8, MemoryModel>, MemoryAllocationError> {
|
||||
allocator
|
||||
.call(store, object_length)
|
||||
.map_err(MemoryAllocationError::CantAllocate)
|
||||
}
|
||||
|
||||
/// This functions wraps the serialization process
|
||||
fn serialize_data<T: Serialize>(object: &T) -> Result<Vec<u8>, PluginModuleError> {
|
||||
bincode::serialize(object).map_err(PluginModuleError::Encoding)
|
||||
}
|
||||
|
||||
/// This function writes an object to the wasm memory using the allocator if
|
||||
/// necessary using length padding.
|
||||
///
|
||||
/// With length padding the first bytes written are the length of the the
|
||||
/// following slice (The object serialized).
|
||||
pub(crate) fn write_serialized_with_length<T: Serialize>(
|
||||
store: &mut StoreMut,
|
||||
memory: &Memory,
|
||||
allocator: &TypedFunction<
|
||||
<MemoryModel as wasmer::MemorySize>::Offset,
|
||||
WasmPtr<u8, MemoryModel>,
|
||||
>,
|
||||
object: &T,
|
||||
) -> Result<WasmPtr<u8, MemoryModel>, PluginModuleError> {
|
||||
write_length_and_bytes(store, memory, allocator, &serialize_data(object)?)
|
||||
}
|
||||
|
||||
/// This function writes an raw bytes to WASM memory returning a pointer and
|
||||
/// a length. Will realloc the buffer is not wide enough.
|
||||
///
|
||||
/// As this function is often called after prepending a length to an existing
|
||||
/// object it accepts two slices and concatenates them to cut down copying in
|
||||
/// the caller.
|
||||
pub(crate) fn write_bytes(
|
||||
store: &mut StoreMut,
|
||||
memory: &Memory,
|
||||
allocator: &TypedFunction<
|
||||
<MemoryModel as wasmer::MemorySize>::Offset,
|
||||
WasmPtr<u8, MemoryModel>,
|
||||
>,
|
||||
bytes: (&[u8], &[u8]),
|
||||
) -> Result<
|
||||
(
|
||||
WasmPtr<u8, MemoryModel>,
|
||||
<MemoryModel as wasmer::MemorySize>::Offset,
|
||||
),
|
||||
PluginModuleError,
|
||||
> {
|
||||
let len = (bytes.0.len() + bytes.1.len()) as <MemoryModel as wasmer::MemorySize>::Offset;
|
||||
let ptr = get_pointer(store, len, allocator).map_err(PluginModuleError::MemoryAllocation)?;
|
||||
ptr.slice(
|
||||
&memory.view(store),
|
||||
len as <MemoryModel as wasmer::MemorySize>::Offset,
|
||||
)
|
||||
.and_then(|s| {
|
||||
s.subslice(0..bytes.0.len() as u64).write_slice(bytes.0)?;
|
||||
s.subslice(bytes.0.len() as u64..len).write_slice(bytes.1)
|
||||
})
|
||||
.map_err(|_| PluginModuleError::InvalidPointer)?;
|
||||
Ok((ptr, len))
|
||||
}
|
||||
|
||||
/// This function writes bytes to the wasm memory using the allocator if
|
||||
/// necessary using length padding.
|
||||
///
|
||||
/// With length padding the first bytes written are the length of the the
|
||||
/// following slice.
|
||||
pub(crate) fn write_length_and_bytes(
|
||||
store: &mut StoreMut,
|
||||
memory: &Memory,
|
||||
allocator: &TypedFunction<
|
||||
<MemoryModel as wasmer::MemorySize>::Offset,
|
||||
WasmPtr<u8, MemoryModel>,
|
||||
>,
|
||||
bytes: &[u8],
|
||||
) -> Result<WasmPtr<u8, MemoryModel>, PluginModuleError> {
|
||||
let len = bytes.len() as <MemoryModel as wasmer::MemorySize>::Offset;
|
||||
write_bytes(store, memory, allocator, (&len.to_le_bytes(), bytes)).map(|val| val.0)
|
||||
}
|
||||
|
||||
/// This function reads data from memory at a position with the array length and
|
||||
/// converts it to an object using bincode
|
||||
pub(crate) fn read_serialized<'a, T: for<'b> Deserialize<'b>>(
|
||||
memory: &'a Memory,
|
||||
store: &StoreRef,
|
||||
ptr: WasmPtr<u8, MemoryModel>,
|
||||
len: <MemoryModel as wasmer::MemorySize>::Offset,
|
||||
) -> Result<T, bincode::Error> {
|
||||
bincode::deserialize(
|
||||
&read_bytes(memory, store, ptr, len).map_err(|_| bincode::ErrorKind::SizeLimit)?,
|
||||
)
|
||||
}
|
||||
|
||||
/// This function reads raw bytes from memory at a position with the array
|
||||
/// length
|
||||
pub(crate) fn read_bytes(
|
||||
memory: &Memory,
|
||||
store: &StoreRef,
|
||||
ptr: WasmPtr<u8, MemoryModel>,
|
||||
len: <MemoryModel as wasmer::MemorySize>::Offset,
|
||||
) -> Result<Vec<u8>, PluginModuleError> {
|
||||
ptr.slice(&memory.view(store), len)
|
||||
.and_then(|s| s.read_to_vec())
|
||||
.map_err(|_| PluginModuleError::InvalidPointer)
|
||||
}
|
||||
|
||||
/// This function reads a constant amount of raw bytes from memory
|
||||
pub(crate) fn read_exact_bytes<const N: usize>(
|
||||
memory: &Memory,
|
||||
store: &StoreRef,
|
||||
ptr: WasmPtr<u8, MemoryModel>,
|
||||
) -> Result<[u8; N], PluginModuleError> {
|
||||
let mut result = MaybeUninit::uninit_array();
|
||||
ptr.slice(&memory.view(store), N.try_into().unwrap())
|
||||
.and_then(|s| s.read_slice_uninit(&mut result))
|
||||
.map_err(|_| PluginModuleError::InvalidPointer)?;
|
||||
unsafe { Ok(MaybeUninit::array_assume_init(result)) }
|
||||
}
|
||||
|
@ -1,11 +1,9 @@
|
||||
pub mod errors;
|
||||
pub mod exports;
|
||||
pub mod memory_manager;
|
||||
pub mod module;
|
||||
pub mod wasm_env;
|
||||
|
||||
use bincode::ErrorKind;
|
||||
use common::assets::ASSETS_PATH;
|
||||
use common::{assets::ASSETS_PATH, uid::Uid};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
@ -14,21 +12,13 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tracing::{error, info};
|
||||
use wasmer::Memory64;
|
||||
|
||||
use plugin_api::Event;
|
||||
|
||||
use self::{
|
||||
errors::PluginError,
|
||||
errors::{PluginError, PluginModuleError},
|
||||
memory_manager::EcsWorld,
|
||||
module::{PluginModule, PreparedEventQuery},
|
||||
wasm_env::HostFunctionException,
|
||||
module::PluginModule,
|
||||
};
|
||||
|
||||
use rayon::prelude::*;
|
||||
|
||||
pub type MemoryModel = Memory64;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PluginData {
|
||||
name: String,
|
||||
@ -90,53 +80,36 @@ impl Plugin {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn execute_prepared<T>(
|
||||
pub fn load_event(
|
||||
&mut self,
|
||||
ecs: &EcsWorld,
|
||||
event: &PreparedEventQuery<T>,
|
||||
) -> Result<Vec<T::Response>, PluginError>
|
||||
where
|
||||
T: Event,
|
||||
{
|
||||
mode: common::resources::GameMode,
|
||||
) -> Result<(), PluginModuleError> {
|
||||
self.modules
|
||||
.iter_mut()
|
||||
.flat_map(|module| {
|
||||
module.try_execute(ecs, event).map(|x| {
|
||||
x.map_err(|e| {
|
||||
if let errors::PluginModuleError::RunFunction(runtime_err) = &e {
|
||||
if let Some(host_except) =
|
||||
runtime_err.downcast_ref::<HostFunctionException>()
|
||||
{
|
||||
match host_except {
|
||||
HostFunctionException::ProcessExit(code) => {
|
||||
module.exit_code = Some(*code);
|
||||
tracing::warn!(
|
||||
"Module {} binary {} exited with {}",
|
||||
self.data.name,
|
||||
module.name(),
|
||||
*code
|
||||
);
|
||||
return PluginError::ProcessExit;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
PluginError::PluginModuleError(
|
||||
self.data.name.to_owned(),
|
||||
event.get_function_name().to_owned(),
|
||||
e,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| {
|
||||
if matches!(e, PluginError::ProcessExit) {
|
||||
// remove the executable from the module which called process exit
|
||||
self.modules.retain(|m| m.exit_code.is_none())
|
||||
}
|
||||
e
|
||||
})
|
||||
.try_for_each(|module| module.load_event(ecs, mode))
|
||||
}
|
||||
|
||||
pub fn command_event(
|
||||
&mut self,
|
||||
ecs: &EcsWorld,
|
||||
name: &str,
|
||||
args: &[String],
|
||||
player: common::uid::Uid,
|
||||
) -> Result<Vec<String>, CommandResults> {
|
||||
let mut result = Err(CommandResults::UnknownCommand);
|
||||
self.modules.iter_mut().for_each(|module| {
|
||||
match module.command_event(ecs, name, args, player) {
|
||||
Ok(res) => result = Ok(res),
|
||||
Err(CommandResults::UnknownCommand) => (),
|
||||
Err(err) => {
|
||||
if result.is_err() {
|
||||
result = Err(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,35 +126,6 @@ impl PluginMgr {
|
||||
Self::from_dir(assets_path)
|
||||
}
|
||||
|
||||
pub fn execute_prepared<T>(
|
||||
&mut self,
|
||||
ecs: &EcsWorld,
|
||||
event: &PreparedEventQuery<T>,
|
||||
) -> Result<Vec<T::Response>, PluginError>
|
||||
where
|
||||
T: Event,
|
||||
{
|
||||
Ok(self
|
||||
.plugins
|
||||
.par_iter_mut()
|
||||
.map(|plugin| plugin.execute_prepared(ecs, event))
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn execute_event<T>(
|
||||
&mut self,
|
||||
ecs: &EcsWorld,
|
||||
event: &T,
|
||||
) -> Result<Vec<T::Response>, PluginError>
|
||||
where
|
||||
T: Event,
|
||||
{
|
||||
self.execute_prepared(ecs, &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)?
|
||||
@ -224,4 +168,44 @@ impl PluginMgr {
|
||||
|
||||
Ok(Self { plugins })
|
||||
}
|
||||
|
||||
pub fn load_event(
|
||||
&mut self,
|
||||
ecs: &EcsWorld,
|
||||
mode: common::resources::GameMode,
|
||||
) -> Result<(), PluginModuleError> {
|
||||
self.plugins
|
||||
.iter_mut()
|
||||
.try_for_each(|plugin| plugin.load_event(ecs, mode))
|
||||
}
|
||||
|
||||
pub fn command_event(
|
||||
&mut self,
|
||||
ecs: &EcsWorld,
|
||||
name: &str,
|
||||
args: &[String],
|
||||
player: Uid,
|
||||
) -> Result<Vec<String>, CommandResults> {
|
||||
// return last value or last error
|
||||
let mut result = Err(CommandResults::UnknownCommand);
|
||||
self.plugins.iter_mut().for_each(|plugin| {
|
||||
match plugin.command_event(ecs, name, args, player) {
|
||||
Ok(val) => result = Ok(val),
|
||||
Err(CommandResults::UnknownCommand) => (),
|
||||
Err(err) => {
|
||||
if result.is_err() {
|
||||
result = Err(err);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned by plugin based server commands
|
||||
pub enum CommandResults {
|
||||
UnknownCommand,
|
||||
HostError(wasmtime::Error),
|
||||
PluginError(String),
|
||||
}
|
||||
|
@ -1,326 +1,314 @@
|
||||
use hashbrown::HashSet;
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
|
||||
use wasmer::{
|
||||
imports, AsStoreMut, AsStoreRef, Function, FunctionEnv, FunctionEnvMut, Instance, Memory,
|
||||
Module, Store, TypedFunction, WasmPtr,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{
|
||||
errors::{PluginError, PluginModuleError},
|
||||
exports,
|
||||
memory_manager::{self, EcsAccessManager, EcsWorld},
|
||||
wasm_env::HostFunctionEnvironment,
|
||||
MemoryModel,
|
||||
errors::{EcsAccessError, PluginModuleError},
|
||||
memory_manager::{EcsAccessManager, EcsWorld},
|
||||
CommandResults,
|
||||
};
|
||||
use hashbrown::HashSet;
|
||||
use wasmtime::{
|
||||
component::{Component, Linker},
|
||||
Config, Engine, Store,
|
||||
};
|
||||
use wasmtime_wasi::preview2::WasiView;
|
||||
|
||||
use plugin_api::{Action, EcsAccessError, Event, Retrieve, RetrieveError, RetrieveResult};
|
||||
wasmtime::component::bindgen!({
|
||||
path: "../../plugin/wit/veloren.wit",
|
||||
async: true,
|
||||
with: {
|
||||
"veloren:plugin/information@0.0.1/entity": Entity,
|
||||
},
|
||||
});
|
||||
|
||||
pub struct Entity {
|
||||
uid: common::uid::Uid,
|
||||
}
|
||||
|
||||
use veloren::plugin::{actions, information, types};
|
||||
|
||||
// #[derive(Clone)]
|
||||
/// This structure represent the WASM State of the plugin.
|
||||
pub struct PluginModule {
|
||||
ecs: Arc<EcsAccessManager>,
|
||||
wasm_state: Arc<Instance>,
|
||||
events: HashSet<String>,
|
||||
allocator: TypedFunction<<MemoryModel as wasmer::MemorySize>::Offset, WasmPtr<u8, MemoryModel>>,
|
||||
memory: Memory,
|
||||
store: Store,
|
||||
plugin: Plugin,
|
||||
store: wasmtime::Store<WasiHostCtx>,
|
||||
#[allow(dead_code)]
|
||||
name: String,
|
||||
pub(crate) exit_code: Option<i32>,
|
||||
}
|
||||
|
||||
struct WasiHostCtx {
|
||||
preview2_ctx: wasmtime_wasi::preview2::WasiCtx,
|
||||
preview2_table: wasmtime::component::ResourceTable,
|
||||
ecs: Arc<EcsAccessManager>,
|
||||
registered_commands: HashSet<String>,
|
||||
}
|
||||
|
||||
impl wasmtime_wasi::preview2::WasiView for WasiHostCtx {
|
||||
fn table(&self) -> &wasmtime::component::ResourceTable { &self.preview2_table }
|
||||
|
||||
fn ctx(&self) -> &wasmtime_wasi::preview2::WasiCtx { &self.preview2_ctx }
|
||||
|
||||
fn table_mut(&mut self) -> &mut wasmtime::component::ResourceTable { &mut self.preview2_table }
|
||||
|
||||
fn ctx_mut(&mut self) -> &mut wasmtime_wasi::preview2::WasiCtx { &mut self.preview2_ctx }
|
||||
}
|
||||
|
||||
impl information::Host for WasiHostCtx {}
|
||||
|
||||
impl types::Host for WasiHostCtx {}
|
||||
|
||||
#[wasmtime::component::__internal::async_trait]
|
||||
impl actions::Host for WasiHostCtx {
|
||||
async fn register_command(&mut self, name: String) -> wasmtime::Result<()> {
|
||||
tracing::info!("Plugin registers /{name}");
|
||||
self.registered_commands.insert(name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn player_send_message(
|
||||
&mut self,
|
||||
uid: actions::Uid,
|
||||
text: String,
|
||||
) -> wasmtime::Result<()> {
|
||||
tracing::info!("Plugin sends message {text} to player {uid:?}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[wasmtime::component::__internal::async_trait]
|
||||
impl information::HostEntity for WasiHostCtx {
|
||||
async fn find_entity(
|
||||
&mut self,
|
||||
uid: actions::Uid,
|
||||
) -> wasmtime::Result<Result<wasmtime::component::Resource<information::Entity>, ()>> {
|
||||
let entry = self.table_mut().push(Entity {
|
||||
uid: common::uid::Uid(uid),
|
||||
})?;
|
||||
Ok(Ok(entry))
|
||||
}
|
||||
|
||||
async fn health(
|
||||
&mut self,
|
||||
self_: wasmtime::component::Resource<information::Entity>,
|
||||
) -> wasmtime::Result<information::Health> {
|
||||
let uid = self.table().get(&self_)?.uid;
|
||||
// Safety: No reference is leaked out the function so it is safe.
|
||||
let world = unsafe {
|
||||
self.ecs
|
||||
.get()
|
||||
.ok_or(EcsAccessError::EcsPointerNotAvailable)?
|
||||
};
|
||||
let player = world
|
||||
.id_maps
|
||||
.uid_entity(uid)
|
||||
.ok_or(EcsAccessError::EcsEntityNotFound(uid))?;
|
||||
world
|
||||
.health
|
||||
.get(player)
|
||||
.map(|health| information::Health {
|
||||
current: health.current(),
|
||||
base_max: health.base_max(),
|
||||
maximum: health.maximum(),
|
||||
})
|
||||
.ok_or_else(|| EcsAccessError::EcsComponentNotFound(uid, "Health".to_owned()).into())
|
||||
}
|
||||
|
||||
async fn name(
|
||||
&mut self,
|
||||
self_: wasmtime::component::Resource<information::Entity>,
|
||||
) -> wasmtime::Result<String> {
|
||||
let uid = self.table().get(&self_)?.uid;
|
||||
// Safety: No reference is leaked out the function so it is safe.
|
||||
let world = unsafe {
|
||||
self.ecs
|
||||
.get()
|
||||
.ok_or(EcsAccessError::EcsPointerNotAvailable)?
|
||||
};
|
||||
let player = world
|
||||
.id_maps
|
||||
.uid_entity(uid)
|
||||
.ok_or(EcsAccessError::EcsEntityNotFound(uid))?;
|
||||
Ok(world
|
||||
.player
|
||||
.get(player)
|
||||
.ok_or_else(|| EcsAccessError::EcsComponentNotFound(uid, "Player".to_owned()))?
|
||||
.alias
|
||||
.to_owned())
|
||||
}
|
||||
|
||||
fn drop(
|
||||
&mut self,
|
||||
rep: wasmtime::component::Resource<information::Entity>,
|
||||
) -> wasmtime::Result<()> {
|
||||
Ok(self.table_mut().delete(rep).map(|_entity| ())?)
|
||||
}
|
||||
}
|
||||
|
||||
struct InfoStream(String);
|
||||
|
||||
impl wasmtime_wasi::preview2::HostOutputStream for InfoStream {
|
||||
fn write(&mut self, bytes: bytes::Bytes) -> wasmtime_wasi::preview2::StreamResult<()> {
|
||||
tracing::info!("{}: {}", self.0, String::from_utf8_lossy(bytes.as_ref()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> wasmtime_wasi::preview2::StreamResult<()> { Ok(()) }
|
||||
|
||||
fn check_write(&mut self) -> wasmtime_wasi::preview2::StreamResult<usize> { Ok(1024) }
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl wasmtime_wasi::preview2::Subscribe for InfoStream {
|
||||
async fn ready(&mut self) {}
|
||||
}
|
||||
|
||||
struct ErrorStream(String);
|
||||
|
||||
impl wasmtime_wasi::preview2::HostOutputStream for ErrorStream {
|
||||
fn write(&mut self, bytes: bytes::Bytes) -> wasmtime_wasi::preview2::StreamResult<()> {
|
||||
tracing::error!("{}: {}", self.0, String::from_utf8_lossy(bytes.as_ref()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> wasmtime_wasi::preview2::StreamResult<()> { Ok(()) }
|
||||
|
||||
fn check_write(&mut self) -> wasmtime_wasi::preview2::StreamResult<usize> { Ok(1024) }
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl wasmtime_wasi::preview2::Subscribe for ErrorStream {
|
||||
async fn ready(&mut self) {}
|
||||
}
|
||||
|
||||
struct LogStream(String, tracing::Level);
|
||||
|
||||
impl wasmtime_wasi::preview2::StdoutStream for LogStream {
|
||||
fn stream(&self) -> Box<dyn wasmtime_wasi::preview2::HostOutputStream> {
|
||||
if self.1 == tracing::Level::INFO {
|
||||
Box::new(InfoStream(self.0.clone()))
|
||||
} else {
|
||||
Box::new(ErrorStream(self.0.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
fn isatty(&self) -> bool { true }
|
||||
}
|
||||
|
||||
impl PluginModule {
|
||||
/// This function takes bytes from a WASM File and compile them
|
||||
pub fn new(name: String, wasm_data: &[u8]) -> Result<Self, PluginModuleError> {
|
||||
// The store contains all data for a specific instance, including the linear
|
||||
// memory
|
||||
let mut store = Store::default();
|
||||
// We are compiling the WASM file in the previously generated environement
|
||||
let module = Module::from_binary(store.engine(), wasm_data)
|
||||
.map_err(PluginModuleError::CompileError)?;
|
||||
|
||||
// This is the function imported into the wasm environement
|
||||
fn raw_emit_actions(
|
||||
env: FunctionEnvMut<HostFunctionEnvironment>,
|
||||
// store: &wasmer::StoreRef<'_>,
|
||||
ptr: WasmPtr<u8, MemoryModel>,
|
||||
len: <MemoryModel as wasmer::MemorySize>::Offset,
|
||||
) {
|
||||
handle_actions(
|
||||
match env.data().read_serialized(&env.as_store_ref(), ptr, len) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
tracing::error!(?e, "Can't decode action");
|
||||
return;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn raw_retrieve_action(
|
||||
mut env: FunctionEnvMut<HostFunctionEnvironment>,
|
||||
// store: &wasmer::StoreRef<'_>,
|
||||
ptr: WasmPtr<u8, MemoryModel>,
|
||||
len: <MemoryModel as wasmer::MemorySize>::Offset,
|
||||
) -> <MemoryModel as wasmer::MemorySize>::Offset {
|
||||
let out = match env.data().read_serialized(&env.as_store_ref(), ptr, len) {
|
||||
Ok(data) => retrieve_action(env.data().ecs(), data),
|
||||
Err(e) => Err(RetrieveError::BincodeError(e.to_string())),
|
||||
};
|
||||
|
||||
let data = env.data().clone();
|
||||
data.write_serialized_with_length(&mut env.as_store_mut(), &out)
|
||||
.unwrap_or_else(|_e|
|
||||
// return a null pointer so the WASM side can tell an error occured
|
||||
WasmPtr::null())
|
||||
.offset()
|
||||
}
|
||||
|
||||
fn dbg(a: i32) {
|
||||
println!("WASM DEBUG: {}", a);
|
||||
}
|
||||
|
||||
let ecs = Arc::new(EcsAccessManager::default());
|
||||
|
||||
// Environment to pass ecs and memory_manager to callbacks
|
||||
let env = FunctionEnv::new(
|
||||
&mut store,
|
||||
HostFunctionEnvironment::new(name.clone(), Arc::clone(&ecs)),
|
||||
);
|
||||
// Create an import object.
|
||||
let import_object = imports! {
|
||||
"env" => {
|
||||
"raw_emit_actions" => Function::new_typed_with_env(&mut store, &env, raw_emit_actions),
|
||||
"raw_retrieve_action" => Function::new_typed_with_env(&mut store, &env, raw_retrieve_action),
|
||||
"dbg" => Function::new_typed(&mut store, dbg),
|
||||
},
|
||||
"wasi_snapshot_preview1" => {
|
||||
"fd_write" => Function::new_typed_with_env(&mut store, &env, exports::wasi_fd_write),
|
||||
"environ_get" => Function::new_typed_with_env(&mut store, &env, exports::wasi_env_get),
|
||||
"environ_sizes_get" => Function::new_typed_with_env(&mut store, &env, exports::wasi_env_sizes_get),
|
||||
"proc_exit" => Function::new_typed_with_env(&mut store, &env, exports::wasi_proc_exit),
|
||||
},
|
||||
};
|
||||
// configure the wasm runtime
|
||||
let mut config = Config::new();
|
||||
config.async_support(true).wasm_component_model(true);
|
||||
|
||||
let engine = Engine::new(&config).map_err(PluginModuleError::Wasmtime)?;
|
||||
// create a WASI environment (std implementing system calls)
|
||||
let wasi = wasmtime_wasi::preview2::WasiCtxBuilder::new()
|
||||
.stdout(LogStream(name.clone(), tracing::Level::INFO))
|
||||
.stderr(LogStream(name.clone(), tracing::Level::ERROR))
|
||||
.build();
|
||||
let host_ctx = WasiHostCtx {
|
||||
preview2_ctx: wasi,
|
||||
preview2_table: wasmtime_wasi::preview2::ResourceTable::new(),
|
||||
ecs: Arc::clone(&ecs),
|
||||
registered_commands: HashSet::new(),
|
||||
};
|
||||
// the store contains all data of a wasm instance
|
||||
let mut store = Store::new(&engine, host_ctx);
|
||||
|
||||
// load wasm from binary
|
||||
let module =
|
||||
Component::from_binary(&engine, wasm_data).map_err(PluginModuleError::Wasmtime)?;
|
||||
|
||||
// register WASI and Veloren methods with the runtime
|
||||
let mut linker = Linker::new(&engine);
|
||||
wasmtime_wasi::preview2::command::add_to_linker(&mut linker)
|
||||
.map_err(PluginModuleError::Wasmtime)?;
|
||||
Plugin::add_to_linker(&mut linker, |x| x).map_err(PluginModuleError::Wasmtime)?;
|
||||
|
||||
let instance_fut = Plugin::instantiate_async(&mut store, &module, &linker);
|
||||
let (plugin, _instance) =
|
||||
futures::executor::block_on(instance_fut).map_err(PluginModuleError::Wasmtime)?;
|
||||
|
||||
// Create an instance (Code execution environement)
|
||||
let instance = Instance::new(&mut store, &module, &import_object)
|
||||
.map_err(|err| PluginModuleError::InstantiationError(Box::new(err)))?;
|
||||
let init_args = HostFunctionEnvironment::args_from_instance(&store, &instance)
|
||||
.map_err(PluginModuleError::FindFunction)?;
|
||||
env.as_mut(&mut store).init_with_instance(init_args);
|
||||
Ok(Self {
|
||||
plugin,
|
||||
ecs,
|
||||
memory: instance
|
||||
.exports
|
||||
.get_memory("memory")
|
||||
.map_err(PluginModuleError::MemoryUninit)?
|
||||
.clone(),
|
||||
allocator: instance
|
||||
.exports
|
||||
.get_typed_function(&store, "wasm_prepare_buffer")
|
||||
.map_err(PluginModuleError::MemoryUninit)?,
|
||||
events: instance
|
||||
.exports
|
||||
.iter()
|
||||
.map(|(name, _)| name.to_string())
|
||||
.collect(),
|
||||
wasm_state: Arc::new(instance),
|
||||
store,
|
||||
name,
|
||||
exit_code: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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>(
|
||||
&mut self,
|
||||
ecs: &EcsWorld,
|
||||
request: &PreparedEventQuery<T>,
|
||||
) -> Option<Result<T::Response, PluginModuleError>>
|
||||
where
|
||||
T: Event,
|
||||
{
|
||||
if !self.events.contains(&request.function_name) {
|
||||
return None;
|
||||
}
|
||||
// Store the ECS Pointer for later use in `retreives`
|
||||
let s_ecs = self.ecs.clone();
|
||||
let bytes = match s_ecs.execute_with(ecs, || {
|
||||
execute_raw(self, &request.function_name, &request.bytes)
|
||||
}) {
|
||||
Ok(e) => e,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
Some(bincode::deserialize(&bytes).map_err(PluginModuleError::Encoding))
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str { &self.name }
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
function_name: String,
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
// Implementation of the commands called from veloren and provided in plugins
|
||||
pub fn load_event(
|
||||
&mut self,
|
||||
ecs: &EcsWorld,
|
||||
mode: common::resources::GameMode,
|
||||
) -> Result<(), PluginModuleError> {
|
||||
let mode = match mode {
|
||||
common::resources::GameMode::Server => types::GameMode::Server,
|
||||
common::resources::GameMode::Client => types::GameMode::Client,
|
||||
common::resources::GameMode::Singleplayer => types::GameMode::SinglePlayer,
|
||||
};
|
||||
self.ecs
|
||||
.execute_with(ecs, || {
|
||||
let future = self
|
||||
.plugin
|
||||
.veloren_plugin_events()
|
||||
.call_load(&mut self.store, mode);
|
||||
futures::executor::block_on(future)
|
||||
})
|
||||
.map_err(PluginModuleError::Wasmtime)
|
||||
}
|
||||
|
||||
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(PluginError::Encoding)?,
|
||||
function_name: event.get_event_name(),
|
||||
_phantom: PhantomData,
|
||||
pub fn command_event(
|
||||
&mut self,
|
||||
ecs: &EcsWorld,
|
||||
name: &str,
|
||||
args: &[String],
|
||||
player: common::uid::Uid,
|
||||
) -> Result<Vec<String>, CommandResults> {
|
||||
if !self.store.data().registered_commands.contains(name) {
|
||||
return Err(CommandResults::UnknownCommand);
|
||||
}
|
||||
self.ecs.execute_with(ecs, || {
|
||||
let future = self.plugin.veloren_plugin_events().call_command(
|
||||
&mut self.store,
|
||||
name,
|
||||
args,
|
||||
player.0,
|
||||
);
|
||||
match futures::executor::block_on(future) {
|
||||
Err(err) => Err(CommandResults::HostError(err)),
|
||||
Ok(result) => result.map_err(CommandResults::PluginError),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_function_name(&self) -> &str { &self.function_name }
|
||||
}
|
||||
|
||||
// This function is not public because this function should not be used without
|
||||
// an interface to limit unsafe behaviours
|
||||
fn execute_raw(
|
||||
module: &mut PluginModule,
|
||||
// instance: &mut Instance,
|
||||
event_name: &str,
|
||||
bytes: &[u8],
|
||||
) -> Result<Vec<u8>, PluginModuleError> {
|
||||
// This write into memory `bytes` using allocation if necessary returning a
|
||||
// pointer and a length
|
||||
|
||||
let (ptr, len) = memory_manager::write_bytes(
|
||||
&mut module.store.as_store_mut(),
|
||||
&module.memory,
|
||||
&module.allocator,
|
||||
(bytes, &[]),
|
||||
)?;
|
||||
|
||||
// This gets the event function from module exports
|
||||
|
||||
let func: TypedFunction<
|
||||
(
|
||||
WasmPtr<u8, MemoryModel>,
|
||||
<MemoryModel as wasmer::MemorySize>::Offset,
|
||||
),
|
||||
WasmPtr<u8, MemoryModel>,
|
||||
> = module
|
||||
.wasm_state
|
||||
.exports
|
||||
.get_typed_function(&module.store.as_store_ref(), event_name)
|
||||
.map_err(PluginModuleError::MemoryUninit)?;
|
||||
|
||||
// We call the function with the pointer and the length
|
||||
|
||||
let result_ptr = func
|
||||
.call(&mut module.store.as_store_mut(), ptr, len)
|
||||
.map_err(PluginModuleError::RunFunction)?;
|
||||
|
||||
// The first bytes correspond to the length of the result
|
||||
let result_len: [u8; std::mem::size_of::<<MemoryModel as wasmer::MemorySize>::Offset>()] =
|
||||
memory_manager::read_exact_bytes(&module.memory, &module.store.as_store_ref(), result_ptr)
|
||||
.map_err(|_| PluginModuleError::InvalidPointer)?;
|
||||
let result_len = <MemoryModel as wasmer::MemorySize>::Offset::from_le_bytes(result_len);
|
||||
|
||||
// Read the result of the function with the pointer and the length
|
||||
let bytes = memory_manager::read_bytes(
|
||||
&module.memory,
|
||||
&module.store.as_store_ref(),
|
||||
WasmPtr::new(
|
||||
result_ptr.offset()
|
||||
+ std::mem::size_of::<<MemoryModel as wasmer::MemorySize>::Offset>()
|
||||
as <MemoryModel as wasmer::MemorySize>::Offset,
|
||||
),
|
||||
result_len,
|
||||
)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn retrieve_action(
|
||||
ecs: &EcsAccessManager,
|
||||
action: Retrieve,
|
||||
) -> Result<RetrieveResult, RetrieveError> {
|
||||
match action {
|
||||
Retrieve::GetPlayerName(e) => {
|
||||
// Safety: No reference is leaked out the function so it is safe.
|
||||
let world = unsafe {
|
||||
ecs.get().ok_or(RetrieveError::EcsAccessError(
|
||||
EcsAccessError::EcsPointerNotAvailable,
|
||||
))?
|
||||
};
|
||||
let player = world
|
||||
.id_maps
|
||||
.uid_entity(e)
|
||||
.ok_or(RetrieveError::EcsAccessError(
|
||||
EcsAccessError::EcsEntityNotFound(e),
|
||||
))?;
|
||||
|
||||
Ok(RetrieveResult::GetPlayerName(
|
||||
world
|
||||
.player
|
||||
.get(player)
|
||||
.ok_or_else(|| {
|
||||
RetrieveError::EcsAccessError(EcsAccessError::EcsComponentNotFound(
|
||||
e,
|
||||
"Player".to_owned(),
|
||||
))
|
||||
})?
|
||||
.alias
|
||||
.to_owned(),
|
||||
))
|
||||
},
|
||||
Retrieve::GetEntityHealth(e) => {
|
||||
// Safety: No reference is leaked out the function so it is safe.
|
||||
let world = unsafe {
|
||||
ecs.get().ok_or(RetrieveError::EcsAccessError(
|
||||
EcsAccessError::EcsPointerNotAvailable,
|
||||
))?
|
||||
};
|
||||
let player = world
|
||||
.id_maps
|
||||
.uid_entity(e)
|
||||
.ok_or(RetrieveError::EcsAccessError(
|
||||
EcsAccessError::EcsEntityNotFound(e),
|
||||
))?;
|
||||
Ok(RetrieveResult::GetEntityHealth(
|
||||
world
|
||||
.health
|
||||
.get(player)
|
||||
.ok_or_else(|| {
|
||||
RetrieveError::EcsAccessError(EcsAccessError::EcsComponentNotFound(
|
||||
e,
|
||||
"Health".to_owned(),
|
||||
))
|
||||
})?
|
||||
.clone(),
|
||||
))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_actions(actions: Vec<Action>) {
|
||||
for action in actions {
|
||||
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);
|
||||
},
|
||||
}
|
||||
pub fn player_join_event(
|
||||
&mut self,
|
||||
ecs: &EcsWorld,
|
||||
name: &str,
|
||||
uuid: common::uuid::Uuid,
|
||||
) -> types::JoinResult {
|
||||
self.ecs.execute_with(ecs, || {
|
||||
let future = self.plugin.veloren_plugin_events().call_join(
|
||||
&mut self.store,
|
||||
name,
|
||||
uuid.as_u64_pair(),
|
||||
);
|
||||
match futures::executor::block_on(future) {
|
||||
Ok(value) => {
|
||||
tracing::info!("JoinResult {value:?}");
|
||||
value
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!("join_event: {err:?}");
|
||||
types::JoinResult::None
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,119 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use wasmer::{ExportError, Instance, Memory, Store, StoreMut, StoreRef, TypedFunction, WasmPtr};
|
||||
|
||||
use super::{
|
||||
errors::PluginModuleError,
|
||||
memory_manager::{self, EcsAccessManager},
|
||||
MemoryModel,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HostFunctionEnvironment {
|
||||
ecs: Arc<EcsAccessManager>, /* This represent the pointer to the ECS object (set to
|
||||
* i32::MAX if to ECS is
|
||||
* availible) */
|
||||
memory: Option<Memory>, // This object represent the WASM Memory
|
||||
allocator: Option<
|
||||
TypedFunction<<MemoryModel as wasmer::MemorySize>::Offset, WasmPtr<u8, MemoryModel>>,
|
||||
>, /* Linked to: wasm_prepare_buffer */
|
||||
name: String, // This represent the plugin name
|
||||
}
|
||||
|
||||
pub struct HostFunctionEnvironmentInit {
|
||||
allocator: TypedFunction<<MemoryModel as wasmer::MemorySize>::Offset, WasmPtr<u8, MemoryModel>>,
|
||||
memory: Memory,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
// Exception thrown from a native wasm callback
|
||||
pub enum HostFunctionException {
|
||||
ProcessExit(i32),
|
||||
}
|
||||
|
||||
// needed for `std::error::Error`
|
||||
impl core::fmt::Display for HostFunctionException {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { write!(f, "{:?}", self) }
|
||||
}
|
||||
|
||||
impl std::error::Error for HostFunctionException {}
|
||||
|
||||
impl HostFunctionEnvironment {
|
||||
/// Create a new environment for functions providing functionality to WASM
|
||||
pub fn new(name: String, ecs: Arc<EcsAccessManager>) -> Self {
|
||||
Self {
|
||||
ecs,
|
||||
allocator: Default::default(),
|
||||
memory: Default::default(),
|
||||
name,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn ecs(&self) -> &Arc<EcsAccessManager> { &self.ecs }
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn memory(&self) -> &Memory { self.memory.as_ref().unwrap() }
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn allocator(
|
||||
&self,
|
||||
) -> &TypedFunction<<MemoryModel as wasmer::MemorySize>::Offset, WasmPtr<u8, MemoryModel>> {
|
||||
self.allocator.as_ref().unwrap()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn name(&self) -> &str { &self.name }
|
||||
|
||||
/// This function is a safe interface to WASM memory that serializes and
|
||||
/// writes an object to linear memory returning a pointer
|
||||
pub(crate) fn write_serialized_with_length<T: Serialize>(
|
||||
&self,
|
||||
store: &mut StoreMut,
|
||||
object: &T,
|
||||
) -> Result<WasmPtr<u8, MemoryModel>, PluginModuleError> {
|
||||
memory_manager::write_serialized_with_length(store, self.memory(), self.allocator(), object)
|
||||
}
|
||||
|
||||
/// This function is a safe interface to WASM memory that reads memory from
|
||||
/// pointer and length returning an object
|
||||
pub(crate) fn read_serialized<T: DeserializeOwned>(
|
||||
&self,
|
||||
store: &StoreRef,
|
||||
position: WasmPtr<u8, MemoryModel>,
|
||||
length: <MemoryModel as wasmer::MemorySize>::Offset,
|
||||
) -> Result<T, bincode::Error> {
|
||||
memory_manager::read_serialized(self.memory(), store, position, length)
|
||||
}
|
||||
|
||||
/// This function is a safe interface to WASM memory that reads memory from
|
||||
/// a pointer and a length and returns some bytes
|
||||
pub(crate) fn read_bytes(
|
||||
&self,
|
||||
store: &StoreRef,
|
||||
ptr: WasmPtr<u8, MemoryModel>,
|
||||
len: <MemoryModel as wasmer::MemorySize>::Offset,
|
||||
) -> Result<Vec<u8>, PluginModuleError> {
|
||||
memory_manager::read_bytes(self.memory(), store, ptr, len)
|
||||
}
|
||||
|
||||
/// This function creates the argument for init_with_instance() from
|
||||
/// exported symbol lookup
|
||||
pub fn args_from_instance(
|
||||
store: &Store,
|
||||
instance: &Instance,
|
||||
) -> Result<HostFunctionEnvironmentInit, ExportError> {
|
||||
let memory = instance.exports.get_memory("memory")?.clone();
|
||||
let allocator = instance
|
||||
.exports
|
||||
.get_typed_function(store, "wasm_prepare_buffer")?;
|
||||
Ok(HostFunctionEnvironmentInit { memory, allocator })
|
||||
}
|
||||
|
||||
/// Initialize the wasm exports in the environment
|
||||
pub fn init_with_instance(&mut self, args: HostFunctionEnvironmentInit) {
|
||||
self.memory = Some(args.memory);
|
||||
self.allocator = Some(args.allocator);
|
||||
}
|
||||
}
|
@ -349,11 +349,7 @@ impl State {
|
||||
id_maps: &ecs.read_resource::<IdMaps>().into(),
|
||||
player: ecs.read_component().into(),
|
||||
};
|
||||
if let Err(e) = plugin_mgr
|
||||
.execute_event(&ecs_world, &plugin_api::event::PluginLoadEvent {
|
||||
game_mode,
|
||||
})
|
||||
{
|
||||
if let Err(e) = plugin_mgr.load_event(&ecs_world, game_mode) {
|
||||
tracing::debug!(?e, "Failed to run plugin init");
|
||||
tracing::info!("Plugins disabled, enable debug logging for more information.");
|
||||
PluginMgr::default()
|
||||
|
@ -40,7 +40,7 @@ quinn = { version = "0.10", optional = true }
|
||||
rustls = "0.21"
|
||||
lz-fear = { version = "0.1.1", optional = true }
|
||||
# async traits
|
||||
async-trait = "0.1.42"
|
||||
async-trait = { workspace = true }
|
||||
bytes = "^1"
|
||||
# faster HashMaps
|
||||
hashbrown = { workspace = true }
|
||||
|
@ -22,7 +22,7 @@ prometheus = { workspace = true, optional = true }
|
||||
bitflags = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
# async traits
|
||||
async-trait = "0.1.42"
|
||||
async-trait = { workspace = true }
|
||||
bytes = "^1"
|
||||
hashbrown = { workspace = true }
|
||||
|
||||
|
@ -1,9 +0,0 @@
|
||||
[package]
|
||||
name = "veloren-plugin-api"
|
||||
version = "0.1.0"
|
||||
authors = ["ccgauche <gaucheron.laurent@gmail.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
common = { package = "veloren-common", path = "../../common", features = ["no-assets"] }
|
@ -1,71 +0,0 @@
|
||||
use common::uid::Uid;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum RetrieveError {
|
||||
EcsAccessError(EcsAccessError),
|
||||
OtherError(String),
|
||||
DataReadError,
|
||||
BincodeError(String),
|
||||
InvalidType,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for RetrieveError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
RetrieveError::EcsAccessError(e) => {
|
||||
write!(f, "RetrieveError: {}", e)
|
||||
},
|
||||
RetrieveError::OtherError(e) => {
|
||||
write!(f, "RetrieveError: Unknown error: {}", e)
|
||||
},
|
||||
RetrieveError::DataReadError => {
|
||||
write!(
|
||||
f,
|
||||
"RetrieveError: Can't pass data through WASM FFI: WASM Memory is corrupted"
|
||||
)
|
||||
},
|
||||
RetrieveError::BincodeError(e) => {
|
||||
write!(f, "RetrieveError: Bincode error: {}", e)
|
||||
},
|
||||
RetrieveError::InvalidType => {
|
||||
write!(
|
||||
f,
|
||||
"RetrieveError: This type wasn't expected as the result for this Retrieve"
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum EcsAccessError {
|
||||
EcsPointerNotAvailable,
|
||||
EcsComponentNotFound(Uid, String),
|
||||
EcsResourceNotFound(String),
|
||||
EcsEntityNotFound(Uid),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for EcsAccessError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
EcsAccessError::EcsPointerNotAvailable => {
|
||||
write!(f, "EcsAccessError can't read the ECS pointer")
|
||||
},
|
||||
EcsAccessError::EcsComponentNotFound(a, b) => {
|
||||
write!(
|
||||
f,
|
||||
"EcsAccessError can't find component {} for entity from UID {}",
|
||||
b, a
|
||||
)
|
||||
},
|
||||
EcsAccessError::EcsResourceNotFound(a) => {
|
||||
write!(f, "EcsAccessError can't find resource {}", a)
|
||||
},
|
||||
EcsAccessError::EcsEntityNotFound(a) => {
|
||||
write!(f, "EcsAccessError can't find entity from UID {}", a)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
@ -1,228 +0,0 @@
|
||||
pub extern crate common;
|
||||
|
||||
pub use common::comp::Health;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
pub use common::{resources::GameMode, uid::Uid};
|
||||
|
||||
mod errors;
|
||||
|
||||
pub use errors::*;
|
||||
pub use event::*;
|
||||
|
||||
/// The [`Action`] enum represents a push modification that will be made in the
|
||||
/// ECS in the next tick Note that all actions when sent are async and will not
|
||||
/// be executed in order like [`Retrieve`] that are sync. All actions sent will
|
||||
/// be executed in the send order in the ticking before the rest of the logic
|
||||
/// applies.
|
||||
///
|
||||
/// # Usage:
|
||||
/// ```rust
|
||||
/// # use veloren_plugin_api::*;
|
||||
/// # pub fn emit_action(action: Action) { emit_actions(vec![action]) }
|
||||
/// # pub fn emit_actions(_actions: Vec<Action>) {}
|
||||
/// // Packing actions is better than sending multiple ones at the same time!
|
||||
/// emit_actions(vec![
|
||||
/// Action::KillEntity(Uid(1)),
|
||||
/// Action::PlayerSendMessage(Uid(0), "This is a test message".to_owned()),
|
||||
/// ]);
|
||||
/// // You can also use this to only send one action
|
||||
/// emit_action(Action::KillEntity(Uid(1)));
|
||||
/// ```
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub enum Action {
|
||||
ServerClose,
|
||||
Print(String),
|
||||
PlayerSendMessage(Uid, String),
|
||||
KillEntity(Uid),
|
||||
}
|
||||
|
||||
/// The [`Retrieve`] enum represents read of the ECS is sync and blocking.
|
||||
/// This enum shouldn't be used by itself. You should always prefer `get`
|
||||
/// methods on Plugin API Types For instance, prefer this method:
|
||||
/// ```rust
|
||||
/// # use veloren_plugin_api::*;
|
||||
/// # let entityid = Player {id: Uid(0)};
|
||||
/// # trait G { fn get_entity_health(&self) -> Option<i64>; }
|
||||
/// # impl G for Player {fn get_entity_health(&self) -> Option<i64> {Some(1)}}
|
||||
/// let life = entityid.get_entity_health().unwrap();
|
||||
/// // Do something with life
|
||||
/// ```
|
||||
/// Over this one:
|
||||
/// ```rust
|
||||
/// # use common::comp::Body;
|
||||
/// # use common::comp::body::humanoid;
|
||||
/// # use veloren_plugin_api::*;
|
||||
/// # let entityid = Uid(0);
|
||||
/// # fn retrieve_action(r: &Retrieve) -> Result<RetrieveResult, RetrieveError> { Ok(RetrieveResult::GetEntityHealth(Health::new(Body::Humanoid(humanoid::Body::random()), 1))) }
|
||||
/// let life = if let RetrieveResult::GetEntityHealth(e) =
|
||||
/// retrieve_action(&Retrieve::GetEntityHealth(entityid)).unwrap()
|
||||
/// {
|
||||
/// e
|
||||
/// } else {
|
||||
/// unreachable!()
|
||||
/// };
|
||||
/// // Do something with life
|
||||
/// ```
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub enum Retrieve {
|
||||
GetPlayerName(Uid),
|
||||
GetEntityHealth(Uid),
|
||||
}
|
||||
|
||||
/// The [`RetrieveResult`] struct is generated while using the `retrieve_action`
|
||||
/// function
|
||||
///
|
||||
/// You should always prefer using `get` methods available in Plugin API types.
|
||||
///
|
||||
/// Example:
|
||||
/// ```rust
|
||||
/// # use common::comp::Body;
|
||||
/// # use common::comp::body::humanoid;
|
||||
/// # use veloren_plugin_api::*;
|
||||
/// # let entityid = Uid(0);
|
||||
/// # fn retrieve_action(r: &Retrieve) -> Result<RetrieveResult, RetrieveError> { Ok(RetrieveResult::GetEntityHealth(Health::new(Body::Humanoid(humanoid::Body::random()), 1)))}
|
||||
/// let life = if let RetrieveResult::GetEntityHealth(e) =
|
||||
/// retrieve_action(&Retrieve::GetEntityHealth(entityid)).unwrap()
|
||||
/// {
|
||||
/// e
|
||||
/// } else {
|
||||
/// unreachable!()
|
||||
/// };
|
||||
/// // Do something with life
|
||||
/// ```
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum RetrieveResult {
|
||||
GetPlayerName(String),
|
||||
GetEntityHealth(Health),
|
||||
}
|
||||
|
||||
/// This trait is implement by all events and ensure type safety of FFI.
|
||||
pub trait Event: Serialize + DeserializeOwned + Send + Sync {
|
||||
type Response: Serialize + DeserializeOwned + Send + Sync;
|
||||
|
||||
fn get_event_name(&self) -> String;
|
||||
}
|
||||
|
||||
/// This module contains all events from the api
|
||||
pub mod event {
|
||||
use super::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// This event is called when a chat command is run.
|
||||
/// Your event should be named `on_command_<Your command>`
|
||||
///
|
||||
/// If you return an Error the displayed message will be the error message
|
||||
/// in red You can return a Vec<String> that will be print to player
|
||||
/// chat as info
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// #[event_handler]
|
||||
/// pub fn on_command_testplugin(command: ChatCommandEvent) -> Result<Vec<String>, String> {
|
||||
/// Ok(vec![format!(
|
||||
/// "Player of id {:?} named {} with {:?} sent command with args {:?}",
|
||||
/// command.player.id,
|
||||
/// command
|
||||
/// .player
|
||||
/// .get_player_name()
|
||||
/// .expect("Can't get player name"),
|
||||
/// command
|
||||
/// .player
|
||||
/// .get_entity_health()
|
||||
/// .expect("Can't get player health"),
|
||||
/// command.command_args
|
||||
/// )])
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
pub struct ChatCommandEvent {
|
||||
pub command: String,
|
||||
pub command_args: Vec<String>,
|
||||
pub player: Player,
|
||||
}
|
||||
|
||||
impl Event for ChatCommandEvent {
|
||||
type Response = Result<Vec<String>, String>;
|
||||
|
||||
fn get_event_name(&self) -> String { format!("on_command_{}", self.command) }
|
||||
}
|
||||
|
||||
/// This struct represent a player
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Player {
|
||||
pub id: Uid,
|
||||
}
|
||||
|
||||
/// This event is called when a player connects.
|
||||
/// Your event should be named `on_join`
|
||||
///
|
||||
/// You can either return `CloseConnection` or `None`
|
||||
/// If `CloseConnection` is returned the player will be kicked
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// #[event_handler]
|
||||
/// pub fn on_join(command: PlayerJoinEvent) -> PlayerJoinResult {
|
||||
/// PlayerJoinResult::CloseConnection
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
pub struct PlayerJoinEvent {
|
||||
pub player_name: String,
|
||||
pub player_id: [u8; 16],
|
||||
}
|
||||
|
||||
impl Event for PlayerJoinEvent {
|
||||
type Response = PlayerJoinResult;
|
||||
|
||||
fn get_event_name(&self) -> String { "on_join".to_owned() }
|
||||
}
|
||||
|
||||
/// This is the return type of an `on_join` event. See [`PlayerJoinEvent`]
|
||||
///
|
||||
/// Variants:
|
||||
/// - `CloseConnection` will kick the player.
|
||||
/// - `None` will let the player join the server.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub enum PlayerJoinResult {
|
||||
Kick(String),
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for PlayerJoinResult {
|
||||
fn default() -> Self { Self::None }
|
||||
}
|
||||
|
||||
/// This event is called when the plugin is loaded
|
||||
/// Your event should be named `on_load`
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// #[event_handler]
|
||||
/// pub fn on_load(load: PluginLoadEvent) {
|
||||
/// match load.game_mode {
|
||||
/// GameMode::Server => emit_action(Action::Print("Hello, server!".to_owned())),
|
||||
/// GameMode::Client => emit_action(Action::Print("Hello, client!".to_owned())),
|
||||
/// GameMode::Singleplayer => emit_action(Action::Print("Hello, singleplayer!".to_owned())),
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
pub struct PluginLoadEvent {
|
||||
pub game_mode: GameMode,
|
||||
}
|
||||
|
||||
impl Event for PluginLoadEvent {
|
||||
type Response = ();
|
||||
|
||||
fn get_event_name(&self) -> String { "on_load".to_owned() }
|
||||
}
|
||||
|
||||
// impl Default for PlayerJoinResult {
|
||||
// fn default() -> Self {
|
||||
// Self::None
|
||||
// }
|
||||
// }
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "veloren-plugin-derive"
|
||||
version = "0.1.0"
|
||||
authors = ["ccgauche <gaucheron.laurent@gmail.com>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.24"
|
||||
syn = { version = "2", features = ["full","extra-traits"]}
|
||||
quote = "1.0.7"
|
@ -1,76 +0,0 @@
|
||||
extern crate proc_macro;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, ItemFn, ItemStruct};
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn global_state(_args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let parsed = parse_macro_input!(item as ItemStruct);
|
||||
let name = &parsed.ident;
|
||||
let out: proc_macro2::TokenStream = quote! {
|
||||
#parsed
|
||||
type PLUGIN_STATE_TYPE = #name;
|
||||
|
||||
static mut PLUGIN_STATE: Option<PLUGIN_STATE_TYPE> = None;
|
||||
|
||||
static PLUGIN_STATE_GUARD: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
};
|
||||
out.into()
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn event_handler(_args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let parsed = parse_macro_input!(item as ItemFn);
|
||||
let fn_body = parsed.block; // function body
|
||||
let sig = parsed.sig; // function signature
|
||||
let fn_name = sig.ident; // function name/identifier
|
||||
let fn_args = sig.inputs; // comma separated args
|
||||
let fn_return = sig.output; // comma separated args
|
||||
|
||||
let out: proc_macro2::TokenStream = if fn_args.len() == 1 {
|
||||
quote! {
|
||||
#[no_mangle]
|
||||
pub fn #fn_name(intern__ptr: i64, intern__len: i64) -> i64 {
|
||||
let input = ::veloren_plugin_rt::read_input(intern__ptr as _,intern__len as _).unwrap();
|
||||
#[inline]
|
||||
fn inner(#fn_args) #fn_return {
|
||||
#fn_body
|
||||
}
|
||||
// Artificially force the event handler to be type-correct
|
||||
fn force_event<E: ::veloren_plugin_rt::api::Event>(event: E, inner: fn(E) -> E::Response) -> E::Response {
|
||||
inner(event)
|
||||
}
|
||||
::veloren_plugin_rt::write_output(&force_event(input, inner))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#[no_mangle]
|
||||
pub fn #fn_name(intern__ptr: i64, intern__len: i64) -> i64 {
|
||||
let input = ::veloren_plugin_rt::read_input(intern__ptr as _,intern__len as _).unwrap();
|
||||
#[inline]
|
||||
fn inner(#fn_args) #fn_return {
|
||||
#fn_body
|
||||
}
|
||||
// Artificially force the event handler to be type-correct
|
||||
fn force_event<E: ::veloren_plugin_rt::api::Event>(event: E, inner: fn(E, &mut PLUGIN_STATE_TYPE) -> E::Response) -> E::Response {
|
||||
//let mut plugin_state = PLUGIN_STATE.lock().unwrap();
|
||||
|
||||
assert_eq!(PLUGIN_STATE_GUARD.swap(true, std::sync::atomic::Ordering::Acquire), false);
|
||||
unsafe {
|
||||
if PLUGIN_STATE.is_none() {
|
||||
PLUGIN_STATE = Some(PLUGIN_STATE_TYPE::default());
|
||||
}
|
||||
}
|
||||
let out = inner(event, unsafe {PLUGIN_STATE.as_mut().unwrap()});
|
||||
PLUGIN_STATE_GUARD.store(false, std::sync::atomic::Ordering::Release);
|
||||
out
|
||||
|
||||
}
|
||||
::veloren_plugin_rt::write_output(&force_event(input, inner))
|
||||
}
|
||||
}
|
||||
};
|
||||
out.into()
|
||||
}
|
25
plugin/examples/hello/Cargo.lock
generated
Normal file
25
plugin/examples/hello/Cargo.lock
generated
Normal file
@ -0,0 +1,25 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
|
||||
|
||||
[[package]]
|
||||
name = "hello"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b76f1d099678b4f69402a421e888bbe71bf20320c2f3f3565d0e7484dbe5bc20"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
22
plugin/examples/hello/Cargo.toml
Normal file
22
plugin/examples/hello/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "hello"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[workspace]
|
||||
|
||||
[package.metadata.component]
|
||||
package = "component:hello"
|
||||
|
||||
[package.metadata.component.target]
|
||||
path = "../../wit/veloren.wit"
|
||||
|
||||
[package.metadata.component.dependencies]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
wit-bindgen = { version = "0.16.0", default-features = false, features = ["realloc"] }
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
54
plugin/examples/hello/src/lib.rs
Normal file
54
plugin/examples/hello/src/lib.rs
Normal file
@ -0,0 +1,54 @@
|
||||
#![feature(atomic_bool_fetch_not)]
|
||||
|
||||
mod bindings;
|
||||
|
||||
use bindings::{
|
||||
exports::veloren::plugin::events::Guest,
|
||||
veloren::plugin::{
|
||||
actions,
|
||||
information::Entity,
|
||||
types::{GameMode, Health, JoinResult, PlayerId, Uid},
|
||||
},
|
||||
};
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Component {}
|
||||
|
||||
static COUNTER: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
impl Guest for Component {
|
||||
fn load(mode: GameMode) {
|
||||
actions::register_command("test");
|
||||
match mode {
|
||||
GameMode::Server => println!("Hello, server!"),
|
||||
GameMode::Client => println!("Hello, client!"),
|
||||
GameMode::SinglePlayer => println!("Hello, singleplayer!"),
|
||||
}
|
||||
}
|
||||
|
||||
fn join(player_name: wit_bindgen::rt::string::String, player_id: PlayerId) -> JoinResult {
|
||||
if COUNTER.fetch_not(Ordering::SeqCst) {
|
||||
JoinResult::Kick(format!("Rejected user {player_name}, id {player_id:?}"))
|
||||
} else {
|
||||
JoinResult::None
|
||||
}
|
||||
}
|
||||
|
||||
fn command(
|
||||
command: wit_bindgen::rt::string::String,
|
||||
command_args: wit_bindgen::rt::vec::Vec<wit_bindgen::rt::string::String>,
|
||||
player: Uid,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let entity: Result<Entity, ()> = Entity::find_entity(player);
|
||||
let health = entity.as_ref().map(|e| e.health()).unwrap_or(Health {
|
||||
base_max: 0.0,
|
||||
maximum: 0.0,
|
||||
current: 0.0,
|
||||
});
|
||||
Ok(vec![format!(
|
||||
"Player id {player:?} name {} with {health:?} command {command} args {command_args:?}",
|
||||
entity.map(|e| e.name()).unwrap_or_default(),
|
||||
)])
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
[package]
|
||||
name = "veloren-plugin-rt"
|
||||
version = "0.1.0"
|
||||
authors = ["Joshua Barretto <joshua.s.barretto@gmail.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
plugin-api = { package = "veloren-plugin-api", path = "../api" }
|
||||
plugin-derive = { package = "veloren-plugin-derive", path = "../derive"}
|
||||
serde = { workspace = true }
|
||||
bincode = { workspace = true }
|
||||
|
||||
[[example]]
|
||||
name = "hello"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dev-dependencies]
|
||||
plugin-derive = { package = "veloren-plugin-derive", path = "../derive"}
|
@ -1,46 +0,0 @@
|
||||
use veloren_plugin_rt::{
|
||||
api::{event::*, Action, GameMode},
|
||||
*,
|
||||
};
|
||||
|
||||
#[event_handler]
|
||||
pub fn on_load(load: PluginLoadEvent) {
|
||||
match load.game_mode {
|
||||
GameMode::Server => emit_action(Action::Print("Hello, server!".to_owned())),
|
||||
GameMode::Client => emit_action(Action::Print("Hello, client!".to_owned())),
|
||||
GameMode::Singleplayer => emit_action(Action::Print("Hello, singleplayer!".to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
#[event_handler]
|
||||
pub fn on_command_testplugin(command: ChatCommandEvent) -> Result<Vec<String>, String> {
|
||||
Ok(vec![format!(
|
||||
"Player of id {:?} named {} with {:?} sended command with args {:?}",
|
||||
command.player.id,
|
||||
command
|
||||
.player
|
||||
.get_player_name()
|
||||
.expect("Can't get player name"),
|
||||
command
|
||||
.player
|
||||
.get_entity_health()
|
||||
.expect("Can't get player health"),
|
||||
command.command_args
|
||||
)])
|
||||
}
|
||||
|
||||
#[global_state]
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
counter: bool,
|
||||
}
|
||||
|
||||
#[event_handler]
|
||||
pub fn on_join(input: PlayerJoinEvent, state: &mut State) -> PlayerJoinResult {
|
||||
state.counter = !state.counter;
|
||||
if !state.counter {
|
||||
PlayerJoinResult::Kick(format!("You are a cheater {:?}", input))
|
||||
} else {
|
||||
PlayerJoinResult::None
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
pub extern crate plugin_derive;
|
||||
|
||||
pub mod retrieve;
|
||||
|
||||
use api::RetrieveError;
|
||||
pub use retrieve::*;
|
||||
|
||||
use std::convert::TryInto;
|
||||
|
||||
pub use plugin_api as api;
|
||||
pub use plugin_derive::*;
|
||||
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
extern "C" {
|
||||
fn raw_emit_actions(ptr: i64, len: i64);
|
||||
fn raw_retrieve_action(ptr: i64, len: i64) -> i64;
|
||||
pub fn dbg(i: i32);
|
||||
}
|
||||
|
||||
pub fn retrieve_action<T: DeserializeOwned>(_actions: &api::Retrieve) -> Result<T, RetrieveError> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let ret = bincode::serialize(&_actions).expect("Can't serialize action in emit");
|
||||
unsafe {
|
||||
let ptr = raw_retrieve_action(to_i64(ret.as_ptr() as _), to_i64(ret.len() as _));
|
||||
let ptr = from_i64(ptr);
|
||||
let len =
|
||||
u64::from_le_bytes(std::slice::from_raw_parts(ptr as _, 8).try_into().unwrap());
|
||||
let a = ::std::slice::from_raw_parts((ptr + 8) as _, len as _);
|
||||
bincode::deserialize::<Result<T, RetrieveError>>(&a)
|
||||
.map_err(|x| RetrieveError::BincodeError(x.to_string()))?
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
pub fn emit_action(action: api::Action) { emit_actions(vec![action]) }
|
||||
|
||||
pub fn emit_actions(_actions: Vec<api::Action>) {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let ret = bincode::serialize(&_actions).expect("Can't serialize action in emit");
|
||||
unsafe {
|
||||
raw_emit_actions(to_i64(ret.as_ptr() as _), to_i64(ret.len() as _));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_input<T>(ptr: i64, len: i64) -> Result<T, &'static str>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let slice = unsafe { std::slice::from_raw_parts(from_i64(ptr) as _, from_i64(len) as _) };
|
||||
bincode::deserialize(slice).map_err(|_| "Failed to deserialize function input")
|
||||
}
|
||||
|
||||
/// This function split a u128 in two u64 encoding them as le bytes
|
||||
pub fn from_u128(i: u128) -> (u64, u64) {
|
||||
let i = i.to_le_bytes();
|
||||
(
|
||||
u64::from_le_bytes(i[0..8].try_into().unwrap()),
|
||||
u64::from_le_bytes(i[8..16].try_into().unwrap()),
|
||||
)
|
||||
}
|
||||
|
||||
/// This function merge two u64 encoded as le in one u128
|
||||
pub fn to_u128(a: u64, b: u64) -> u128 {
|
||||
let a = a.to_le_bytes();
|
||||
let b = b.to_le_bytes();
|
||||
u128::from_le_bytes([a, b].concat().try_into().unwrap())
|
||||
}
|
||||
|
||||
/// This function encode a u64 into a i64 using le bytes
|
||||
pub fn to_i64(i: u64) -> i64 { i64::from_le_bytes(i.to_le_bytes()) }
|
||||
|
||||
/// This function decode a i64 into a u64 using le bytes
|
||||
pub fn from_i64(i: i64) -> u64 { u64::from_le_bytes(i.to_le_bytes()) }
|
||||
|
||||
static mut VEC: Vec<u8> = vec![];
|
||||
static mut DATA: Vec<u8> = vec![];
|
||||
|
||||
pub fn write_output(value: impl Serialize) -> i64 {
|
||||
unsafe {
|
||||
VEC = bincode::serialize(&value).expect("Can't serialize event output");
|
||||
DATA = [
|
||||
(VEC.as_ptr() as u64).to_le_bytes(),
|
||||
(VEC.len() as u64).to_le_bytes(),
|
||||
]
|
||||
.concat();
|
||||
to_i64(DATA.as_ptr() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
static mut BUFFERS: Vec<u8> = Vec::new();
|
||||
|
||||
/// Allocate buffer from wasm linear memory
|
||||
/// # Safety
|
||||
/// This function should never be used only intended to by used by the host
|
||||
#[no_mangle]
|
||||
pub unsafe fn wasm_prepare_buffer(size: i64) -> i64 {
|
||||
BUFFERS = vec![0u8; size as usize];
|
||||
BUFFERS.as_ptr() as i64
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
use plugin_api::{Health, RetrieveError};
|
||||
|
||||
use crate::api::{Retrieve, RetrieveResult};
|
||||
|
||||
pub trait GetPlayerName {
|
||||
fn get_player_name(&self) -> Result<String, RetrieveError>;
|
||||
}
|
||||
|
||||
pub trait GetEntityHealth {
|
||||
fn get_entity_health(&self) -> Result<Health, RetrieveError>;
|
||||
}
|
||||
|
||||
impl GetEntityHealth for crate::api::event::Player {
|
||||
fn get_entity_health(&self) -> Result<Health, RetrieveError> {
|
||||
if let RetrieveResult::GetEntityHealth(e) =
|
||||
crate::retrieve_action(&Retrieve::GetEntityHealth(self.id))?
|
||||
{
|
||||
Ok(e)
|
||||
} else {
|
||||
Err(RetrieveError::InvalidType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GetPlayerName for crate::api::event::Player {
|
||||
fn get_player_name(&self) -> Result<String, RetrieveError> {
|
||||
if let RetrieveResult::GetPlayerName(e) =
|
||||
crate::retrieve_action(&Retrieve::GetPlayerName(self.id))?
|
||||
{
|
||||
Ok(e)
|
||||
} else {
|
||||
Err(RetrieveError::InvalidType)
|
||||
}
|
||||
}
|
||||
}
|
56
plugin/wit/veloren.wit
Normal file
56
plugin/wit/veloren.wit
Normal file
@ -0,0 +1,56 @@
|
||||
package veloren:plugin@0.0.1;
|
||||
|
||||
interface types {
|
||||
enum game-mode {
|
||||
server,
|
||||
client,
|
||||
single-player,
|
||||
}
|
||||
type uid = u64;
|
||||
type player-id = tuple<u64, u64>;
|
||||
|
||||
record health {
|
||||
current: f32,
|
||||
base-max: f32,
|
||||
maximum: f32,
|
||||
}
|
||||
|
||||
variant join-result {
|
||||
kick(string),
|
||||
none,
|
||||
}
|
||||
}
|
||||
|
||||
interface events {
|
||||
use types.{game-mode, uid, player-id, join-result};
|
||||
|
||||
load: func(mode: game-mode);
|
||||
join: func(player-name: string, player-id: player-id) -> join-result;
|
||||
command: func(command: string, command-args: list<string>, player: uid) -> result<list<string>, string>;
|
||||
}
|
||||
|
||||
interface actions {
|
||||
use types.{uid};
|
||||
|
||||
register-command: func(name: string);
|
||||
player-send-message: func(uid: uid, text: string);
|
||||
// for print use the normal WASI stdout
|
||||
}
|
||||
|
||||
interface information {
|
||||
use types.{uid, health};
|
||||
|
||||
resource entity {
|
||||
// fallible constructor
|
||||
find-entity: static func(uid: uid) -> result<entity>;
|
||||
|
||||
health: func() -> health;
|
||||
name: func() -> string;
|
||||
}
|
||||
}
|
||||
|
||||
world plugin {
|
||||
export events;
|
||||
import actions;
|
||||
import information;
|
||||
}
|
@ -67,6 +67,4 @@ censor = "0.3"
|
||||
rusqlite = { version = "0.28.0", features = ["array", "vtab", "bundled", "trace"] }
|
||||
refinery = { version = "0.8.8", features = ["rusqlite"] }
|
||||
|
||||
# Plugins
|
||||
plugin-api = { package = "veloren-plugin-api", path = "../plugin/api"}
|
||||
schnellru = "0.2.1"
|
||||
|
@ -7,9 +7,8 @@ use common::{
|
||||
link::Is,
|
||||
mounting::{Mounting, Rider, VolumeMounting, VolumeRider},
|
||||
rtsim::RtSimEntity,
|
||||
uid::IdMaps,
|
||||
uid::{IdMaps, Uid},
|
||||
};
|
||||
use plugin_api::Uid;
|
||||
use specs::WorldExt;
|
||||
|
||||
use crate::{rtsim::RtSim, state_ext::StateExt, Server};
|
||||
|
@ -1315,66 +1315,45 @@ impl Server {
|
||||
);
|
||||
return;
|
||||
};
|
||||
let rs = plugin_manager.execute_event(
|
||||
&ecs_world,
|
||||
&plugin_api::event::ChatCommandEvent {
|
||||
command: name.clone(),
|
||||
command_args: args.clone(),
|
||||
player: plugin_api::event::Player { id: uid },
|
||||
},
|
||||
);
|
||||
match rs {
|
||||
Ok(e) => {
|
||||
if e.is_empty() {
|
||||
self.notify_client(
|
||||
entity,
|
||||
ServerGeneral::server_msg(
|
||||
comp::ChatType::CommandError,
|
||||
format!(
|
||||
"Unknown command '/{}'.\nType '/help' for available \
|
||||
commands",
|
||||
name
|
||||
),
|
||||
match plugin_manager.command_event(&ecs_world, &name, args.as_slice(), uid) {
|
||||
Err(common_state::plugin::CommandResults::UnknownCommand) => self
|
||||
.notify_client(
|
||||
entity,
|
||||
ServerGeneral::server_msg(
|
||||
comp::ChatType::CommandError,
|
||||
format!(
|
||||
"Unknown command '/{name}'.\nType '/help' for available \
|
||||
commands",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
e.into_iter().for_each(|e| match e {
|
||||
Ok(e) => {
|
||||
if !e.is_empty() {
|
||||
self.notify_client(
|
||||
entity,
|
||||
ServerGeneral::server_msg(
|
||||
comp::ChatType::CommandInfo,
|
||||
e.join("\n"),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
self.notify_client(
|
||||
entity,
|
||||
ServerGeneral::server_msg(
|
||||
comp::ChatType::CommandError,
|
||||
format!(
|
||||
"Error occurred while executing command '/{}'.\n{}",
|
||||
name, e
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
),
|
||||
),
|
||||
Ok(value) => {
|
||||
self.notify_client(
|
||||
entity,
|
||||
ServerGeneral::server_msg(
|
||||
comp::ChatType::CommandInfo,
|
||||
value.join("\n"),
|
||||
),
|
||||
);
|
||||
},
|
||||
Err(e) => {
|
||||
error!(?e, "Can't execute command {} {:?}", name, args);
|
||||
Err(common_state::plugin::CommandResults::PluginError(err)) => {
|
||||
self.notify_client(
|
||||
entity,
|
||||
ServerGeneral::server_msg(
|
||||
comp::ChatType::CommandError,
|
||||
format!("Error occurred while executing command '/{name}'.\n{err}"),
|
||||
),
|
||||
);
|
||||
},
|
||||
Err(common_state::plugin::CommandResults::HostError(err)) => {
|
||||
error!(?err, ?name, ?args, "Can't execute command");
|
||||
self.notify_client(
|
||||
entity,
|
||||
ServerGeneral::server_msg(
|
||||
comp::ChatType::CommandError,
|
||||
format!(
|
||||
"Internal error while executing '/{}'.\nContact the server \
|
||||
administrator",
|
||||
name
|
||||
"Internal error {err:?} while executing '/{name}'.\nContact \
|
||||
the server administrator",
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -6,7 +6,7 @@ use crate::{
|
||||
EditableSettings, Settings,
|
||||
};
|
||||
use common::{
|
||||
comp::{self, Admin, Player, Stats},
|
||||
comp::{self, Admin, Health, Player, Stats},
|
||||
event::{ClientDisconnectEvent, EventBus, MakeAdminEvent},
|
||||
recipe::{default_component_recipe_book, default_recipe_book, default_repair_recipe_book},
|
||||
resources::TimeOfDay,
|
||||
@ -21,7 +21,6 @@ use common_net::msg::{
|
||||
};
|
||||
use hashbrown::{hash_map, HashMap};
|
||||
use itertools::Either;
|
||||
use plugin_api::Health;
|
||||
use rayon::prelude::*;
|
||||
use specs::{
|
||||
shred, Entities, Join, LendJoin, ParJoin, Read, ReadExpect, ReadStorage, SystemData,
|
||||
|
Loading…
Reference in New Issue
Block a user