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:
Christof Petig 2024-02-10 09:35:04 +00:00
commit ed2065cf6a
30 changed files with 1458 additions and 2207 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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" }

View File

@ -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"] }

View File

@ -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 {}

View File

@ -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))
}

View File

@ -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)) }
}

View File

@ -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)
}
}, },
} }
} });
} result
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
})
} }
} }
@ -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),
} }

View File

@ -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,
(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) => { Err(err) => {
// Safety: No reference is leaked out the function so it is safe. tracing::error!("join_event: {err:?}");
let world = unsafe { types::JoinResult::None
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);
}, },
} }
})
} }
} }

View File

@ -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);
}
}

View File

@ -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()

View File

@ -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 }

View File

@ -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 }

View File

@ -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"] }

View File

@ -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)
},
}
}
}

View File

@ -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
// }
// }
}

View File

@ -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"

View File

@ -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
View 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",
]

View 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"]

View 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(),
)])
}
}

View File

@ -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"}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
View 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;
}

View File

@ -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"

View File

@ -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};

View File

@ -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(),
command_args: args.clone(),
player: plugin_api::event::Player { id: uid },
},
);
match rs {
Ok(e) => {
if e.is_empty() {
self.notify_client(
entity, entity,
ServerGeneral::server_msg( ServerGeneral::server_msg(
comp::ChatType::CommandError, comp::ChatType::CommandError,
format!( format!(
"Unknown command '/{}'.\nType '/help' for available \ "Unknown command '/{name}'.\nType '/help' for available \
commands", commands",
name
), ),
), ),
); ),
} else { Ok(value) => {
e.into_iter().for_each(|e| match e {
Ok(e) => {
if !e.is_empty() {
self.notify_client( self.notify_client(
entity, entity,
ServerGeneral::server_msg( ServerGeneral::server_msg(
comp::ChatType::CommandInfo, comp::ChatType::CommandInfo,
e.join("\n"), value.join("\n"),
), ),
); );
}
}, },
Err(e) => { 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( self.notify_client(
entity, entity,
ServerGeneral::server_msg( ServerGeneral::server_msg(
comp::ChatType::CommandError, comp::ChatType::CommandError,
format!( format!(
"Error occurred while executing command '/{}'.\n{}", "Internal error {err:?} while executing '/{name}'.\nContact \
name, e the server administrator",
),
),
);
},
});
}
},
Err(e) => {
error!(?e, "Can't execute command {} {:?}", name, args);
self.notify_client(
entity,
ServerGeneral::server_msg(
comp::ChatType::CommandError,
format!(
"Internal error while executing '/{}'.\nContact the server \
administrator",
name
), ),
), ),
); );

View File

@ -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,