mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'christof/wasmtime' into 'master'
Use wasmtime to execute wasm components as veloren plugins See merge request veloren/veloren!4302
This commit is contained in:
commit
ed2065cf6a
@ -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
|
- Camera no longer jumps on first mouse event after cursor grab is released on macos
|
||||||
- Updated wgpu. Now supports OpenGL. Dx11 no longer supported.
|
- Updated wgpu. Now supports OpenGL. Dx11 no longer supported.
|
||||||
- Changes center_cursor to be reset_cursor_position so the cursor is effectively grabbed
|
- 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
|
### Removed
|
||||||
- Medium and large potions from all loot tables
|
- 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",
|
"common/frontend",
|
||||||
"client",
|
"client",
|
||||||
"client/i18n",
|
"client/i18n",
|
||||||
"plugin/api",
|
|
||||||
"plugin/derive",
|
|
||||||
"plugin/rt",
|
|
||||||
"rtsim",
|
"rtsim",
|
||||||
"server",
|
"server",
|
||||||
"server/agent",
|
"server/agent",
|
||||||
@ -156,6 +153,7 @@ image = { version = "0.24", default-features = false, features = ["png"] }
|
|||||||
rayon = { version = "1.5" }
|
rayon = { version = "1.5" }
|
||||||
|
|
||||||
clap = { version = "4.2", features = ["derive"]}
|
clap = { version = "4.2", features = ["derive"]}
|
||||||
|
async-trait = "0.1.42"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
vek = { git = "https://github.com/yoanlcq/vek.git", rev = "84d5cb65841d46599a986c5477341bea4456be26" }
|
vek = { git = "https://github.com/yoanlcq/vek.git", rev = "84d5cb65841d46599a986c5477341bea4456be26" }
|
||||||
|
@ -6,7 +6,7 @@ version = "0.10.0"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
simd = ["vek/platform_intrinsics"]
|
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"]
|
default = ["simd"]
|
||||||
|
|
||||||
@ -33,11 +33,13 @@ scopeguard = "1.1.0"
|
|||||||
serde = { workspace = true, optional = true }
|
serde = { workspace = true, optional = true }
|
||||||
toml = { version = "0.8", optional = true }
|
toml = { version = "0.8", optional = true }
|
||||||
tar = { version = "0.4.37", 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 }
|
bincode = { workspace = true, optional = true }
|
||||||
plugin-api = { package = "veloren-plugin-api", path = "../../plugin/api", optional = true }
|
|
||||||
timer-queue = "0.1.0"
|
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
|
# Tweak running code
|
||||||
#inline_tweak = { version = "1.0.8", features = ["release_tweak"] }
|
#inline_tweak = { version = "1.0.8", features = ["release_tweak"] }
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
use bincode::ErrorKind;
|
use bincode::ErrorKind;
|
||||||
use wasmer::{CompileError, ExportError, InstantiationError, RuntimeError};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PluginError {
|
pub enum PluginError {
|
||||||
@ -14,20 +13,21 @@ pub enum PluginError {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PluginModuleError {
|
pub enum PluginModuleError {
|
||||||
InstantiationError(Box<InstantiationError>),
|
Wasmtime(wasmtime::Error),
|
||||||
InvalidPointer,
|
|
||||||
MemoryAllocation(MemoryAllocationError),
|
|
||||||
MemoryUninit(ExportError),
|
|
||||||
FindFunction(ExportError),
|
|
||||||
RunFunction(RuntimeError),
|
|
||||||
InvalidArgumentType(),
|
|
||||||
Encoding(Box<ErrorKind>),
|
|
||||||
CompileError(CompileError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum MemoryAllocationError {
|
pub enum EcsAccessError {
|
||||||
InvalidReturnType,
|
EcsPointerNotAvailable,
|
||||||
AllocatorNotFound(ExportError),
|
EcsComponentNotFound(common::uid::Uid, String),
|
||||||
CantAllocate(RuntimeError),
|
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::{
|
use common::{
|
||||||
comp::{Health, Player},
|
comp::{Health, Player},
|
||||||
uid::{IdMaps, Uid},
|
uid::{IdMaps, Uid},
|
||||||
};
|
};
|
||||||
|
use specs::{
|
||||||
use super::{
|
storage::GenericReadStorage, Component, Entities, Entity, Read, ReadStorage, WriteStorage,
|
||||||
errors::{MemoryAllocationError, PluginModuleError},
|
|
||||||
MemoryModel,
|
|
||||||
};
|
};
|
||||||
|
use std::sync::atomic::{AtomicPtr, Ordering};
|
||||||
|
|
||||||
pub struct EcsWorld<'a, 'b> {
|
pub struct EcsWorld<'a, 'b> {
|
||||||
pub entities: &'b Entities<'a>,
|
pub entities: &'b Entities<'a>,
|
||||||
@ -105,134 +93,3 @@ impl EcsAccessManager {
|
|||||||
self.ecs_pointer.load(Ordering::Relaxed).as_ref()
|
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 errors;
|
||||||
pub mod exports;
|
|
||||||
pub mod memory_manager;
|
pub mod memory_manager;
|
||||||
pub mod module;
|
pub mod module;
|
||||||
pub mod wasm_env;
|
|
||||||
|
|
||||||
use bincode::ErrorKind;
|
use bincode::ErrorKind;
|
||||||
use common::assets::ASSETS_PATH;
|
use common::{assets::ASSETS_PATH, uid::Uid};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
@ -14,21 +12,13 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use wasmer::Memory64;
|
|
||||||
|
|
||||||
use plugin_api::Event;
|
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
errors::PluginError,
|
errors::{PluginError, PluginModuleError},
|
||||||
memory_manager::EcsWorld,
|
memory_manager::EcsWorld,
|
||||||
module::{PluginModule, PreparedEventQuery},
|
module::PluginModule,
|
||||||
wasm_env::HostFunctionException,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use rayon::prelude::*;
|
|
||||||
|
|
||||||
pub type MemoryModel = Memory64;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct PluginData {
|
pub struct PluginData {
|
||||||
name: String,
|
name: String,
|
||||||
@ -90,53 +80,36 @@ impl Plugin {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute_prepared<T>(
|
pub fn load_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
ecs: &EcsWorld,
|
ecs: &EcsWorld,
|
||||||
event: &PreparedEventQuery<T>,
|
mode: common::resources::GameMode,
|
||||||
) -> Result<Vec<T::Response>, PluginError>
|
) -> Result<(), PluginModuleError> {
|
||||||
where
|
|
||||||
T: Event,
|
|
||||||
{
|
|
||||||
self.modules
|
self.modules
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.flat_map(|module| {
|
.try_for_each(|module| module.load_event(ecs, mode))
|
||||||
module.try_execute(ecs, event).map(|x| {
|
}
|
||||||
x.map_err(|e| {
|
|
||||||
if let errors::PluginModuleError::RunFunction(runtime_err) = &e {
|
pub fn command_event(
|
||||||
if let Some(host_except) =
|
&mut self,
|
||||||
runtime_err.downcast_ref::<HostFunctionException>()
|
ecs: &EcsWorld,
|
||||||
{
|
name: &str,
|
||||||
match host_except {
|
args: &[String],
|
||||||
HostFunctionException::ProcessExit(code) => {
|
player: common::uid::Uid,
|
||||||
module.exit_code = Some(*code);
|
) -> Result<Vec<String>, CommandResults> {
|
||||||
tracing::warn!(
|
let mut result = Err(CommandResults::UnknownCommand);
|
||||||
"Module {} binary {} exited with {}",
|
self.modules.iter_mut().for_each(|module| {
|
||||||
self.data.name,
|
match module.command_event(ecs, name, args, player) {
|
||||||
module.name(),
|
Ok(res) => result = Ok(res),
|
||||||
*code
|
Err(CommandResults::UnknownCommand) => (),
|
||||||
);
|
Err(err) => {
|
||||||
return PluginError::ProcessExit;
|
if result.is_err() {
|
||||||
},
|
result = Err(err)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
PluginError::PluginModuleError(
|
});
|
||||||
self.data.name.to_owned(),
|
result
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,35 +126,6 @@ impl PluginMgr {
|
|||||||
Self::from_dir(assets_path)
|
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> {
|
pub fn from_dir<P: AsRef<Path>>(path: P) -> Result<Self, PluginError> {
|
||||||
let plugins = fs::read_dir(path)
|
let plugins = fs::read_dir(path)
|
||||||
.map_err(PluginError::Io)?
|
.map_err(PluginError::Io)?
|
||||||
@ -224,4 +168,44 @@ impl PluginMgr {
|
|||||||
|
|
||||||
Ok(Self { plugins })
|
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::sync::Arc;
|
||||||
use std::{marker::PhantomData, sync::Arc};
|
|
||||||
|
|
||||||
use wasmer::{
|
|
||||||
imports, AsStoreMut, AsStoreRef, Function, FunctionEnv, FunctionEnvMut, Instance, Memory,
|
|
||||||
Module, Store, TypedFunction, WasmPtr,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
errors::{PluginError, PluginModuleError},
|
errors::{EcsAccessError, PluginModuleError},
|
||||||
exports,
|
memory_manager::{EcsAccessManager, EcsWorld},
|
||||||
memory_manager::{self, EcsAccessManager, EcsWorld},
|
CommandResults,
|
||||||
wasm_env::HostFunctionEnvironment,
|
|
||||||
MemoryModel,
|
|
||||||
};
|
};
|
||||||
|
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.
|
/// This structure represent the WASM State of the plugin.
|
||||||
pub struct PluginModule {
|
pub struct PluginModule {
|
||||||
ecs: Arc<EcsAccessManager>,
|
ecs: Arc<EcsAccessManager>,
|
||||||
wasm_state: Arc<Instance>,
|
plugin: Plugin,
|
||||||
events: HashSet<String>,
|
store: wasmtime::Store<WasiHostCtx>,
|
||||||
allocator: TypedFunction<<MemoryModel as wasmer::MemorySize>::Offset, WasmPtr<u8, MemoryModel>>,
|
|
||||||
memory: Memory,
|
|
||||||
store: Store,
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
name: String,
|
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 {
|
impl PluginModule {
|
||||||
/// This function takes bytes from a WASM File and compile them
|
/// This function takes bytes from a WASM File and compile them
|
||||||
pub fn new(name: String, wasm_data: &[u8]) -> Result<Self, PluginModuleError> {
|
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());
|
let ecs = Arc::new(EcsAccessManager::default());
|
||||||
|
|
||||||
// Environment to pass ecs and memory_manager to callbacks
|
// configure the wasm runtime
|
||||||
let env = FunctionEnv::new(
|
let mut config = Config::new();
|
||||||
&mut store,
|
config.async_support(true).wasm_component_model(true);
|
||||||
HostFunctionEnvironment::new(name.clone(), Arc::clone(&ecs)),
|
|
||||||
);
|
let engine = Engine::new(&config).map_err(PluginModuleError::Wasmtime)?;
|
||||||
// Create an import object.
|
// create a WASI environment (std implementing system calls)
|
||||||
let import_object = imports! {
|
let wasi = wasmtime_wasi::preview2::WasiCtxBuilder::new()
|
||||||
"env" => {
|
.stdout(LogStream(name.clone(), tracing::Level::INFO))
|
||||||
"raw_emit_actions" => Function::new_typed_with_env(&mut store, &env, raw_emit_actions),
|
.stderr(LogStream(name.clone(), tracing::Level::ERROR))
|
||||||
"raw_retrieve_action" => Function::new_typed_with_env(&mut store, &env, raw_retrieve_action),
|
.build();
|
||||||
"dbg" => Function::new_typed(&mut store, dbg),
|
let host_ctx = WasiHostCtx {
|
||||||
},
|
preview2_ctx: wasi,
|
||||||
"wasi_snapshot_preview1" => {
|
preview2_table: wasmtime_wasi::preview2::ResourceTable::new(),
|
||||||
"fd_write" => Function::new_typed_with_env(&mut store, &env, exports::wasi_fd_write),
|
ecs: Arc::clone(&ecs),
|
||||||
"environ_get" => Function::new_typed_with_env(&mut store, &env, exports::wasi_env_get),
|
registered_commands: HashSet::new(),
|
||||||
"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),
|
// 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 {
|
Ok(Self {
|
||||||
|
plugin,
|
||||||
ecs,
|
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,
|
store,
|
||||||
name,
|
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 }
|
pub fn name(&self) -> &str { &self.name }
|
||||||
}
|
|
||||||
|
|
||||||
/// This structure represent a Pre-encoded event object (Useful to avoid
|
// Implementation of the commands called from veloren and provided in plugins
|
||||||
/// reencoding for each module in every plugin)
|
pub fn load_event(
|
||||||
pub struct PreparedEventQuery<T> {
|
&mut self,
|
||||||
bytes: Vec<u8>,
|
ecs: &EcsWorld,
|
||||||
function_name: String,
|
mode: common::resources::GameMode,
|
||||||
_phantom: PhantomData<T>,
|
) -> 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> {
|
pub fn command_event(
|
||||||
/// Create a prepared query from an event reference (Encode to bytes the
|
&mut self,
|
||||||
/// struct) This Prepared Query is used by the `try_execute` method in
|
ecs: &EcsWorld,
|
||||||
/// `PluginModule`
|
name: &str,
|
||||||
pub fn new(event: &T) -> Result<Self, PluginError>
|
args: &[String],
|
||||||
where
|
player: common::uid::Uid,
|
||||||
T: Event,
|
) -> Result<Vec<String>, CommandResults> {
|
||||||
{
|
if !self.store.data().registered_commands.contains(name) {
|
||||||
Ok(Self {
|
return Err(CommandResults::UnknownCommand);
|
||||||
bytes: bincode::serialize(&event).map_err(PluginError::Encoding)?,
|
}
|
||||||
function_name: event.get_event_name(),
|
self.ecs.execute_with(ecs, || {
|
||||||
_phantom: PhantomData,
|
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 }
|
pub fn player_join_event(
|
||||||
}
|
&mut self,
|
||||||
|
ecs: &EcsWorld,
|
||||||
// This function is not public because this function should not be used without
|
name: &str,
|
||||||
// an interface to limit unsafe behaviours
|
uuid: common::uuid::Uuid,
|
||||||
fn execute_raw(
|
) -> types::JoinResult {
|
||||||
module: &mut PluginModule,
|
self.ecs.execute_with(ecs, || {
|
||||||
// instance: &mut Instance,
|
let future = self.plugin.veloren_plugin_events().call_join(
|
||||||
event_name: &str,
|
&mut self.store,
|
||||||
bytes: &[u8],
|
name,
|
||||||
) -> Result<Vec<u8>, PluginModuleError> {
|
uuid.as_u64_pair(),
|
||||||
// This write into memory `bytes` using allocation if necessary returning a
|
);
|
||||||
// pointer and a length
|
match futures::executor::block_on(future) {
|
||||||
|
Ok(value) => {
|
||||||
let (ptr, len) = memory_manager::write_bytes(
|
tracing::info!("JoinResult {value:?}");
|
||||||
&mut module.store.as_store_mut(),
|
value
|
||||||
&module.memory,
|
},
|
||||||
&module.allocator,
|
Err(err) => {
|
||||||
(bytes, &[]),
|
tracing::error!("join_event: {err:?}");
|
||||||
)?;
|
types::JoinResult::None
|
||||||
|
},
|
||||||
// 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);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
id_maps: &ecs.read_resource::<IdMaps>().into(),
|
||||||
player: ecs.read_component().into(),
|
player: ecs.read_component().into(),
|
||||||
};
|
};
|
||||||
if let Err(e) = plugin_mgr
|
if let Err(e) = plugin_mgr.load_event(&ecs_world, game_mode) {
|
||||||
.execute_event(&ecs_world, &plugin_api::event::PluginLoadEvent {
|
|
||||||
game_mode,
|
|
||||||
})
|
|
||||||
{
|
|
||||||
tracing::debug!(?e, "Failed to run plugin init");
|
tracing::debug!(?e, "Failed to run plugin init");
|
||||||
tracing::info!("Plugins disabled, enable debug logging for more information.");
|
tracing::info!("Plugins disabled, enable debug logging for more information.");
|
||||||
PluginMgr::default()
|
PluginMgr::default()
|
||||||
|
@ -40,7 +40,7 @@ quinn = { version = "0.10", optional = true }
|
|||||||
rustls = "0.21"
|
rustls = "0.21"
|
||||||
lz-fear = { version = "0.1.1", optional = true }
|
lz-fear = { version = "0.1.1", optional = true }
|
||||||
# async traits
|
# async traits
|
||||||
async-trait = "0.1.42"
|
async-trait = { workspace = true }
|
||||||
bytes = "^1"
|
bytes = "^1"
|
||||||
# faster HashMaps
|
# faster HashMaps
|
||||||
hashbrown = { workspace = true }
|
hashbrown = { workspace = true }
|
||||||
|
@ -22,7 +22,7 @@ prometheus = { workspace = true, optional = true }
|
|||||||
bitflags = { workspace = true }
|
bitflags = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
# async traits
|
# async traits
|
||||||
async-trait = "0.1.42"
|
async-trait = { workspace = true }
|
||||||
bytes = "^1"
|
bytes = "^1"
|
||||||
hashbrown = { workspace = true }
|
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"] }
|
rusqlite = { version = "0.28.0", features = ["array", "vtab", "bundled", "trace"] }
|
||||||
refinery = { version = "0.8.8", features = ["rusqlite"] }
|
refinery = { version = "0.8.8", features = ["rusqlite"] }
|
||||||
|
|
||||||
# Plugins
|
|
||||||
plugin-api = { package = "veloren-plugin-api", path = "../plugin/api"}
|
|
||||||
schnellru = "0.2.1"
|
schnellru = "0.2.1"
|
||||||
|
@ -7,9 +7,8 @@ use common::{
|
|||||||
link::Is,
|
link::Is,
|
||||||
mounting::{Mounting, Rider, VolumeMounting, VolumeRider},
|
mounting::{Mounting, Rider, VolumeMounting, VolumeRider},
|
||||||
rtsim::RtSimEntity,
|
rtsim::RtSimEntity,
|
||||||
uid::IdMaps,
|
uid::{IdMaps, Uid},
|
||||||
};
|
};
|
||||||
use plugin_api::Uid;
|
|
||||||
use specs::WorldExt;
|
use specs::WorldExt;
|
||||||
|
|
||||||
use crate::{rtsim::RtSim, state_ext::StateExt, Server};
|
use crate::{rtsim::RtSim, state_ext::StateExt, Server};
|
||||||
|
@ -1315,66 +1315,45 @@ impl Server {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let rs = plugin_manager.execute_event(
|
match plugin_manager.command_event(&ecs_world, &name, args.as_slice(), uid) {
|
||||||
&ecs_world,
|
Err(common_state::plugin::CommandResults::UnknownCommand) => self
|
||||||
&plugin_api::event::ChatCommandEvent {
|
.notify_client(
|
||||||
command: name.clone(),
|
entity,
|
||||||
command_args: args.clone(),
|
ServerGeneral::server_msg(
|
||||||
player: plugin_api::event::Player { id: uid },
|
comp::ChatType::CommandError,
|
||||||
},
|
format!(
|
||||||
);
|
"Unknown command '/{name}'.\nType '/help' for available \
|
||||||
match rs {
|
commands",
|
||||||
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
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
} else {
|
),
|
||||||
e.into_iter().for_each(|e| match e {
|
Ok(value) => {
|
||||||
Ok(e) => {
|
self.notify_client(
|
||||||
if !e.is_empty() {
|
entity,
|
||||||
self.notify_client(
|
ServerGeneral::server_msg(
|
||||||
entity,
|
comp::ChatType::CommandInfo,
|
||||||
ServerGeneral::server_msg(
|
value.join("\n"),
|
||||||
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
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(common_state::plugin::CommandResults::PluginError(err)) => {
|
||||||
error!(?e, "Can't execute command {} {:?}", name, args);
|
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(
|
self.notify_client(
|
||||||
entity,
|
entity,
|
||||||
ServerGeneral::server_msg(
|
ServerGeneral::server_msg(
|
||||||
comp::ChatType::CommandError,
|
comp::ChatType::CommandError,
|
||||||
format!(
|
format!(
|
||||||
"Internal error while executing '/{}'.\nContact the server \
|
"Internal error {err:?} while executing '/{name}'.\nContact \
|
||||||
administrator",
|
the server administrator",
|
||||||
name
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -6,7 +6,7 @@ use crate::{
|
|||||||
EditableSettings, Settings,
|
EditableSettings, Settings,
|
||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
comp::{self, Admin, Player, Stats},
|
comp::{self, Admin, Health, Player, Stats},
|
||||||
event::{ClientDisconnectEvent, EventBus, MakeAdminEvent},
|
event::{ClientDisconnectEvent, EventBus, MakeAdminEvent},
|
||||||
recipe::{default_component_recipe_book, default_recipe_book, default_repair_recipe_book},
|
recipe::{default_component_recipe_book, default_recipe_book, default_repair_recipe_book},
|
||||||
resources::TimeOfDay,
|
resources::TimeOfDay,
|
||||||
@ -21,7 +21,6 @@ use common_net::msg::{
|
|||||||
};
|
};
|
||||||
use hashbrown::{hash_map, HashMap};
|
use hashbrown::{hash_map, HashMap};
|
||||||
use itertools::Either;
|
use itertools::Either;
|
||||||
use plugin_api::Health;
|
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use specs::{
|
use specs::{
|
||||||
shred, Entities, Join, LendJoin, ParJoin, Read, ReadExpect, ReadStorage, SystemData,
|
shred, Entities, Join, LendJoin, ParJoin, Read, ReadExpect, ReadStorage, SystemData,
|
||||||
|
Loading…
Reference in New Issue
Block a user