mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat: add wasm plugin api (#1675)
* feat: wasm plugin api base * fix: make metadata macro work * chore: fix clippy lints * Implement guest registering of events * chore: remove hardcoded v0.1.0 dep in wasm_host * feat: add events to wasm * feat: add caching of components * fix: clippy lints * chore: update Cargo.lock * fix: add feature flag for link section * feat: add base for command wit definitions * feat: implement arg variants for command wit interface * feat: add commented out command-tree arg variant * feat: add wit definitions for text-component and command tree Introduces the text interface with a builder-pattern resource, argument-type variant, command-node resource, and register-command on context. Migrates event and command-sender to use the new text-component resource. * feat: add stubs for wasm host * chore: update wasmtime to v42 * feat: implement three methods of wit textcomponent * Add consume function * feat: implement all text-component host methods Replace todo!() stubs with working implementations for all HostTextComponent trait methods: style setters, click events, hover events, add_text, get_text, and encode. * feat: allow events to be modified * feat: add some of base for commands * chore: fix spelling mistake * chore: remove unused dep * feat: implement all argument-type to consumer mappings * Add command exports * log plugin load errors * Some commands work * Some more Commands work * Update how commands are registered * Implement locale * Use handler id with require * chore: fix some warnings * Add PlayerLeaveEvent * chore: fix all clippy warnings * fix: add more descriptive error message --------- Co-authored-by: Purdze <r.s.sutton@hotmail.co.uk> Co-authored-by: Alexander Medvedev <lilalexmed@proton.me>
This commit is contained in:
1684
Cargo.lock
generated
1684
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ members = [
|
||||
"pumpkin-world",
|
||||
"pumpkin/",
|
||||
"pumpkin-data",
|
||||
"pumpkin-plugin-api",
|
||||
]
|
||||
exclude = ["pumpkin-codegen"]
|
||||
|
||||
@@ -171,3 +172,11 @@ arc-swap = "1.8"
|
||||
tokio-util = "0.7.18"
|
||||
toml = "1.0"
|
||||
ureq = "3.2.0"
|
||||
|
||||
wasmtime = "42.0"
|
||||
wasmtime-wasi = "42.0"
|
||||
wit-bindgen = "0.53"
|
||||
wasmparser = "0.245"
|
||||
|
||||
postcard = { version = "1.0", features = ["alloc"] }
|
||||
tracing-serde-structured = "0.4"
|
||||
|
||||
@@ -81,12 +81,14 @@ pub fn plugin_impl(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
std::sync::LazyLock::new(|| std::sync::Arc::new(tokio::runtime::Runtime::new().unwrap()));
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub static METADATA: pumpkin::plugin::PluginMetadata = pumpkin::plugin::PluginMetadata {
|
||||
name: env!("CARGO_PKG_NAME"),
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
authors: env!("CARGO_PKG_AUTHORS"),
|
||||
description: env!("CARGO_PKG_DESCRIPTION"),
|
||||
};
|
||||
pub static METADATA: std::sync::LazyLock<pumpkin::plugin::PluginMetadata> = std::sync::LazyLock::new(|| {
|
||||
pumpkin::plugin::PluginMetadata {
|
||||
name: env!("CARGO_PKG_NAME").to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
authors: env!("CARGO_PKG_AUTHORS").split(',').map(String::from).collect(),
|
||||
description: env!("CARGO_PKG_DESCRIPTION").to_string(),
|
||||
}
|
||||
});
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub static PUMPKIN_API_VERSION: u32 = pumpkin::plugin::PLUGIN_API_VERSION;
|
||||
|
||||
14
pumpkin-plugin-api/Cargo.toml
Normal file
14
pumpkin-plugin-api/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "pumpkin-plugin-api"
|
||||
# This should be different then the pumpkin server verion
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
wit-bindgen = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
postcard = { workspace = true }
|
||||
tracing-serde-structured = { workspace = true }
|
||||
|
||||
[package.metadata.component]
|
||||
target = { path = "../pumpkin-plugin-wit" }
|
||||
59
pumpkin-plugin-api/src/commands.rs
Normal file
59
pumpkin-plugin-api/src/commands.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
sync::{
|
||||
Mutex,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
pub use crate::wit::pumpkin::plugin::command::Command;
|
||||
use crate::{
|
||||
Result, Server,
|
||||
command::CommandNode,
|
||||
wit::pumpkin::plugin::command::{CommandError, CommandSender, ConsumedArgs},
|
||||
};
|
||||
|
||||
pub(crate) static NEXT_COMMAND_ID: AtomicU32 = AtomicU32::new(0);
|
||||
pub(crate) static COMMAND_HANDLERS: Mutex<BTreeMap<u32, Box<dyn CommandHandler>>> =
|
||||
Mutex::new(BTreeMap::new());
|
||||
|
||||
pub trait CommandHandler: Send + Sync {
|
||||
fn handle(
|
||||
&self,
|
||||
sender: CommandSender,
|
||||
server: Server,
|
||||
args: ConsumedArgs,
|
||||
) -> Result<i32, CommandError>;
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Registers a command handler with the plugin.
|
||||
pub fn execute<H: CommandHandler + Send + Sync + 'static>(self, handler: H) -> Command {
|
||||
let id = NEXT_COMMAND_ID.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
COMMAND_HANDLERS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id, Box::new(handler));
|
||||
|
||||
self.execute_with_handler_id(id);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandNode {
|
||||
/// Registers a command handler with the plugin.
|
||||
pub fn execute<H: CommandHandler + Send + Sync + 'static>(self, handler: H) -> CommandNode {
|
||||
let id = NEXT_COMMAND_ID.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
COMMAND_HANDLERS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id, Box::new(handler));
|
||||
|
||||
self.execute_with_handler_id(id);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
107
pumpkin-plugin-api/src/events.rs
Normal file
107
pumpkin-plugin-api/src/events.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
Mutex,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
pub use crate::wit::pumpkin::plugin::event::{
|
||||
Event, EventPriority, PlayerJoinEventData, PlayerLeaveEventData,
|
||||
};
|
||||
use crate::{Context, Result, Server, wit::pumpkin::plugin::event::EventType};
|
||||
|
||||
pub(crate) static NEXT_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
|
||||
pub(crate) static EVENT_HANDLERS: Mutex<BTreeMap<u32, Box<dyn ErasedEventHandler>>> =
|
||||
Mutex::new(BTreeMap::new());
|
||||
|
||||
pub trait FromIntoEvent: Sized {
|
||||
const EVENT_TYPE: EventType;
|
||||
|
||||
fn from_event(event: Event) -> Self;
|
||||
fn into_event(self) -> Event;
|
||||
}
|
||||
|
||||
impl FromIntoEvent for PlayerJoinEventData {
|
||||
const EVENT_TYPE: EventType = EventType::PlayerJoinEvent;
|
||||
|
||||
fn from_event(event: Event) -> Self {
|
||||
match event {
|
||||
Event::PlayerJoinEvent(data) => data,
|
||||
_ => panic!("unexpected event"),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_event(self) -> Event {
|
||||
Event::PlayerJoinEvent(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIntoEvent for PlayerLeaveEventData {
|
||||
const EVENT_TYPE: EventType = EventType::PlayerLeaveEvent;
|
||||
|
||||
fn from_event(event: Event) -> Self {
|
||||
match event {
|
||||
Event::PlayerLeaveEvent(data) => data,
|
||||
_ => panic!("unexpected event"),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_event(self) -> Event {
|
||||
Event::PlayerLeaveEvent(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||
pub trait EventHandler<E> {
|
||||
fn handle(&self, server: Server, event: E) -> E;
|
||||
}
|
||||
|
||||
pub(crate) trait ErasedEventHandler: Send + Sync {
|
||||
fn handle_erased(&self, server: Server, event: Event) -> Event;
|
||||
}
|
||||
|
||||
struct HandlerWrapper<E, H> {
|
||||
handler: H,
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<E: FromIntoEvent + Send + Sync, H: EventHandler<E> + Send + Sync> ErasedEventHandler
|
||||
for HandlerWrapper<E, H>
|
||||
{
|
||||
fn handle_erased(&self, server: Server, event: Event) -> Event {
|
||||
let specific_event = E::from_event(event);
|
||||
self.handler.handle(server, specific_event).into_event()
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Registers an event handler with the plugin.
|
||||
///
|
||||
/// The handler must implement the [`EventHandler`] trait.
|
||||
/// If the event is blocking, returning an event from the handler will modify the event.
|
||||
pub fn register_event_handler<
|
||||
E: FromIntoEvent + Send + Sync + 'static,
|
||||
H: EventHandler<E> + Send + Sync + 'static,
|
||||
>(
|
||||
&self,
|
||||
handler: H,
|
||||
event_priority: EventPriority,
|
||||
blocking: bool,
|
||||
) -> Result<u32> {
|
||||
let id = NEXT_HANDLER_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let wrapped = HandlerWrapper {
|
||||
handler,
|
||||
_phantom: PhantomData::<E>,
|
||||
};
|
||||
EVENT_HANDLERS
|
||||
.lock()
|
||||
.map_err(|e| e.to_string())?
|
||||
.insert(id, Box::new(wrapped));
|
||||
|
||||
self.register_event(id, E::EVENT_TYPE, event_priority, blocking);
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
141
pumpkin-plugin-api/src/lib.rs
Normal file
141
pumpkin-plugin-api/src/lib.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use crate::{
|
||||
commands::COMMAND_HANDLERS, events::EVENT_HANDLERS, logging::WitSubscriber, text::TextComponent,
|
||||
};
|
||||
|
||||
pub mod commands;
|
||||
pub mod events;
|
||||
|
||||
pub mod command {
|
||||
pub use crate::wit::pumpkin::plugin::command::{
|
||||
Command, CommandError, CommandNode, CommandSender, ConsumedArgs,
|
||||
};
|
||||
}
|
||||
|
||||
pub use wit::pumpkin::plugin::{
|
||||
context::{Context, Server},
|
||||
text,
|
||||
};
|
||||
|
||||
pub mod logging;
|
||||
|
||||
mod wit {
|
||||
wit_bindgen::generate!({
|
||||
skip: ["init-plugin"],
|
||||
path: "../pumpkin-plugin-wit/v0.1.0",
|
||||
world: "plugin",
|
||||
});
|
||||
|
||||
use super::Component;
|
||||
export!(Component);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[unsafe(link_section = "pumpkin:api-version")]
|
||||
#[used]
|
||||
static API_VERSION: [u8; 5] = *b"0.1.0";
|
||||
|
||||
struct Component;
|
||||
pub struct PluginMetadata {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub authors: Vec<String>,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl wit::exports::pumpkin::plugin::metadata::Guest for Component {
|
||||
fn get_metadata() -> wit::exports::pumpkin::plugin::metadata::PluginMetadata {
|
||||
let metadata = plugin().metadata();
|
||||
wit::exports::pumpkin::plugin::metadata::PluginMetadata {
|
||||
name: metadata.name,
|
||||
version: metadata.version,
|
||||
authors: metadata.authors,
|
||||
description: metadata.description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl wit::Guest for Component {
|
||||
fn on_load(context: Context) -> Result<(), String> {
|
||||
plugin().on_load(context)
|
||||
}
|
||||
|
||||
fn on_unload(context: Context) -> Result<(), String> {
|
||||
plugin().on_unload(context)
|
||||
}
|
||||
|
||||
fn handle_event(event_id: u32, server: Server, event: events::Event) -> events::Event {
|
||||
let handlers = EVENT_HANDLERS.lock().unwrap();
|
||||
if let Some(handler) = handlers.get(&event_id) {
|
||||
handler.handle_erased(server, event)
|
||||
} else {
|
||||
event
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_command(
|
||||
command_id: u32,
|
||||
sender: command::CommandSender,
|
||||
server: Server,
|
||||
args: command::ConsumedArgs,
|
||||
) -> Result<i32, command::CommandError> {
|
||||
let handlers = COMMAND_HANDLERS.lock().unwrap();
|
||||
if let Some(handler) = handlers.get(&command_id) {
|
||||
handler.handle(sender, server, args)
|
||||
} else {
|
||||
Err(command::CommandError::CommandFailed(TextComponent::text(
|
||||
&format!("no handler registered for command id {command_id}"),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T, E = String> = core::result::Result<T, E>;
|
||||
|
||||
/// The trait that every Pumpkin plugin must implement.
|
||||
pub trait Plugin: Send + Sync {
|
||||
/// Create a new instance of the plugin.
|
||||
fn new() -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Define the metadata for the plugin.
|
||||
fn metadata(&self) -> PluginMetadata;
|
||||
|
||||
/// Called when the plugin is loaded by the server.
|
||||
fn on_load(&mut self, _context: Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Called when the plugin is unloaded by the server.
|
||||
fn on_unload(&mut self, _context: Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn register_plugin(build_plugin: fn() -> Box<dyn Plugin>) {
|
||||
let _ = tracing::subscriber::set_global_default(WitSubscriber::new());
|
||||
unsafe { PLUGIN = Some((build_plugin)()) }
|
||||
}
|
||||
|
||||
fn plugin() -> &'static mut dyn Plugin {
|
||||
#[expect(static_mut_refs)]
|
||||
unsafe {
|
||||
PLUGIN.as_deref_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
static mut PLUGIN: Option<Box<dyn Plugin>> = None;
|
||||
|
||||
/// Registers the provided type as a Pumpkin plugin.
|
||||
///
|
||||
/// The type must implement the [`Plugin`] trait.
|
||||
#[macro_export]
|
||||
macro_rules! register_plugin {
|
||||
($plugin_type:ty) => {
|
||||
#[unsafe(export_name = "init-plugin")]
|
||||
pub extern "C" fn __init_plugin() {
|
||||
$crate::register_plugin(|| Box::new(<$plugin_type as $crate::Plugin>::new()));
|
||||
}
|
||||
};
|
||||
}
|
||||
68
pumpkin-plugin-api/src/logging.rs
Normal file
68
pumpkin-plugin-api/src/logging.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use tracing_serde_structured::AsSerde;
|
||||
|
||||
use crate::wit;
|
||||
|
||||
pub(crate) struct WitSubscriber {
|
||||
next_id: AtomicU64,
|
||||
}
|
||||
|
||||
impl WitSubscriber {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
next_id: AtomicU64::new(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl tracing::Subscriber for WitSubscriber {
|
||||
fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn new_span(&self, _attrs: &tracing::span::Attributes<'_>) -> tracing::span::Id {
|
||||
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
tracing::span::Id::from_u64(id)
|
||||
}
|
||||
|
||||
fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {}
|
||||
|
||||
fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {}
|
||||
|
||||
fn event(&self, event: &tracing::Event<'_>) {
|
||||
let serialized =
|
||||
postcard::to_allocvec(&event.as_serde()).expect("failed to serialize tracing event");
|
||||
wit::pumpkin::plugin::logging::log_tracing(&serialized);
|
||||
}
|
||||
|
||||
fn enter(&self, _span: &tracing::span::Id) {}
|
||||
|
||||
fn exit(&self, _span: &tracing::span::Id) {}
|
||||
}
|
||||
|
||||
/// The log severity level.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LogLevel {
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
fn to_wit(self) -> wit::pumpkin::plugin::logging::Level {
|
||||
match self {
|
||||
LogLevel::Trace => wit::pumpkin::plugin::logging::Level::Trace,
|
||||
LogLevel::Debug => wit::pumpkin::plugin::logging::Level::Debug,
|
||||
LogLevel::Info => wit::pumpkin::plugin::logging::Level::Info,
|
||||
LogLevel::Warn => wit::pumpkin::plugin::logging::Level::Warn,
|
||||
LogLevel::Error => wit::pumpkin::plugin::logging::Level::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log(level: LogLevel, message: &str) {
|
||||
wit::pumpkin::plugin::logging::log(level.to_wit(), message);
|
||||
}
|
||||
189
pumpkin-plugin-wit/v0.1.0/command.wit
Normal file
189
pumpkin-plugin-wit/v0.1.0/command.wit
Normal file
@@ -0,0 +1,189 @@
|
||||
interface command {
|
||||
use player.{player};
|
||||
use %world.{%world};
|
||||
use entity.{command-block-entity, entity};
|
||||
use common.{position, block-position, locale};
|
||||
use server.{server, difficulty};
|
||||
use text.{text-component};
|
||||
|
||||
enum gamemode {
|
||||
survival,
|
||||
creative,
|
||||
adventure,
|
||||
spectator,
|
||||
}
|
||||
|
||||
enum bossbar-color {
|
||||
pink,
|
||||
blue,
|
||||
red,
|
||||
green,
|
||||
yellow,
|
||||
purple,
|
||||
white,
|
||||
}
|
||||
|
||||
enum bossbar-style {
|
||||
no-division,
|
||||
notches6,
|
||||
notches10,
|
||||
notches12,
|
||||
notches20,
|
||||
}
|
||||
|
||||
enum sound-category {
|
||||
master,
|
||||
music,
|
||||
records,
|
||||
weather,
|
||||
blocks,
|
||||
hostile,
|
||||
neutral,
|
||||
players,
|
||||
ambient,
|
||||
voice,
|
||||
ui,
|
||||
}
|
||||
|
||||
enum entity-anchor {
|
||||
feet,
|
||||
eyes,
|
||||
}
|
||||
|
||||
variant number {
|
||||
float64(f64),
|
||||
float32(f32),
|
||||
int32(s32),
|
||||
int64(s64),
|
||||
}
|
||||
|
||||
variant not-in-bounds {
|
||||
lower-bound(tuple<number, number>),
|
||||
upper-bound(tuple<number, number>),
|
||||
}
|
||||
|
||||
variant arg {
|
||||
entities(list<entity>),
|
||||
entity(entity),
|
||||
players(list<player>),
|
||||
block-pos(block-position),
|
||||
pos3d(position),
|
||||
pos2d(tuple<f64, f64>),
|
||||
rotation(tuple<f32, bool, f32, bool>),
|
||||
game-mode(gamemode),
|
||||
difficulty(difficulty),
|
||||
item(string),
|
||||
item-predicate(string),
|
||||
resource-location(string),
|
||||
block(string),
|
||||
block-predicate(string),
|
||||
bossbar-color(bossbar-color),
|
||||
bossbar-style(bossbar-style),
|
||||
particle(string),
|
||||
msg(string),
|
||||
text-component(text-component),
|
||||
time(s32),
|
||||
num(result<number, not-in-bounds>),
|
||||
%bool(bool),
|
||||
simple(string),
|
||||
sound-category(sound-category),
|
||||
damage-type(string),
|
||||
effect(string),
|
||||
enchantment(string),
|
||||
entity-anchor(entity-anchor),
|
||||
}
|
||||
|
||||
resource consumed-args {
|
||||
get-value: func(key: string) -> arg;
|
||||
}
|
||||
|
||||
variant command-sender-type {
|
||||
rcon(list<string>),
|
||||
console,
|
||||
player(player),
|
||||
command-block(tuple<command-block-entity, %world>),
|
||||
dummy,
|
||||
}
|
||||
|
||||
enum permission-level {
|
||||
zero,
|
||||
one,
|
||||
two,
|
||||
three,
|
||||
four,
|
||||
}
|
||||
|
||||
resource command-sender {
|
||||
get-command-sender-type: func() -> command-sender-type;
|
||||
send-message: func(text: text-component);
|
||||
set-success-count: func(count: s32);
|
||||
is-player: func() -> bool;
|
||||
is-console: func() -> bool;
|
||||
as-player: func() -> option<player>;
|
||||
permission-level: func() -> permission-level;
|
||||
has-permission-level: func(level: permission-level) -> bool;
|
||||
has-permission: func(server: borrow<server>, node: string) -> bool;
|
||||
position: func() -> option<position>;
|
||||
%world: func() -> option<%world>;
|
||||
get-locale: func() -> locale;
|
||||
should-receive-feedback: func() -> bool;
|
||||
should-broadcast-console-to-ops: func() -> bool;
|
||||
should-track-output: func() -> bool;
|
||||
}
|
||||
|
||||
enum string-type {
|
||||
single-word,
|
||||
quotable,
|
||||
greedy,
|
||||
}
|
||||
|
||||
variant argument-type {
|
||||
%bool,
|
||||
float(tuple<option<f32>, option<f32>>),
|
||||
double(tuple<option<f64>, option<f64>>),
|
||||
integer(tuple<option<s32>, option<s32>>),
|
||||
long(tuple<option<s64>, option<s64>>),
|
||||
%string(string-type),
|
||||
entities,
|
||||
entity,
|
||||
%players,
|
||||
game-profile,
|
||||
block-pos,
|
||||
position3d,
|
||||
position2d,
|
||||
block-state,
|
||||
block-predicate,
|
||||
item,
|
||||
item-predicate,
|
||||
component,
|
||||
rotation,
|
||||
%resource-location,
|
||||
entity-anchor,
|
||||
gamemode,
|
||||
difficulty,
|
||||
time(s32),
|
||||
%resource(string),
|
||||
}
|
||||
|
||||
variant command-error {
|
||||
invalid-consumption(option<string>),
|
||||
invalid-requirement,
|
||||
permission-denied,
|
||||
command-failed(text-component),
|
||||
}
|
||||
|
||||
resource command-node {
|
||||
literal: static func(name: string) -> command-node;
|
||||
argument: static func(name: string, arg-type: argument-type) -> command-node;
|
||||
then: func(node: command-node);
|
||||
execute-with-handler-id: func(handler-id: u32);
|
||||
require-with-handler-id: func(handler-id: u32);
|
||||
}
|
||||
|
||||
resource command {
|
||||
/// First name is primary, rest are aliases
|
||||
constructor(names: list<string>, description: string);
|
||||
then: func(node: command-node);
|
||||
execute-with-handler-id: func(handler-id: u32);
|
||||
}
|
||||
}
|
||||
170
pumpkin-plugin-wit/v0.1.0/common.wit
Normal file
170
pumpkin-plugin-wit/v0.1.0/common.wit
Normal file
@@ -0,0 +1,170 @@
|
||||
interface common {
|
||||
/// Serialized text component as a postcard byte array.
|
||||
/// Deprecated: use the text-component resource from the text interface instead.
|
||||
type raw-text-component = list<u8>;
|
||||
type block-position = tuple<s32, s32, s32>;
|
||||
type position = tuple<f64, f64, f64>;
|
||||
|
||||
enum named-color {
|
||||
black,
|
||||
dark-blue,
|
||||
dark-green,
|
||||
dark-aqua,
|
||||
dark-red,
|
||||
dark-purple,
|
||||
gold,
|
||||
gray,
|
||||
dark-gray,
|
||||
blue,
|
||||
green,
|
||||
aqua,
|
||||
red,
|
||||
light-purple,
|
||||
yellow,
|
||||
white,
|
||||
}
|
||||
|
||||
record rgb-color {
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
}
|
||||
|
||||
record argb-color {
|
||||
a: u8,
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
}
|
||||
|
||||
enum locale {
|
||||
af-za,
|
||||
ar-sa,
|
||||
ast-es,
|
||||
az-az,
|
||||
ba-ru,
|
||||
bar,
|
||||
be-by,
|
||||
bg-bg,
|
||||
br-fr,
|
||||
brb,
|
||||
bs-ba,
|
||||
ca-es,
|
||||
cs-cz,
|
||||
cy-gb,
|
||||
da-dk,
|
||||
de-at,
|
||||
de-ch,
|
||||
de-de,
|
||||
el-gr,
|
||||
en-au,
|
||||
en-ca,
|
||||
en-gb,
|
||||
en-nz,
|
||||
en-pt,
|
||||
en-ud,
|
||||
en-us,
|
||||
enp,
|
||||
enws,
|
||||
eo-uy,
|
||||
es-ar,
|
||||
es-cl,
|
||||
es-ec,
|
||||
es-es,
|
||||
es-mx,
|
||||
es-uy,
|
||||
es-ve,
|
||||
esan,
|
||||
et-ee,
|
||||
eu-es,
|
||||
fa-ir,
|
||||
fi-fi,
|
||||
fil-ph,
|
||||
fo-fo,
|
||||
fr-ca,
|
||||
fr-fr,
|
||||
fra-de,
|
||||
fur-it,
|
||||
fy-nl,
|
||||
ga-ie,
|
||||
gd-gb,
|
||||
gl-es,
|
||||
haw-us,
|
||||
he-il,
|
||||
hi-in,
|
||||
hr-hr,
|
||||
hu-hu,
|
||||
hy-am,
|
||||
id-id,
|
||||
ig-ng,
|
||||
io-en,
|
||||
is-is,
|
||||
isv,
|
||||
it-it,
|
||||
ja-jp,
|
||||
jbo-en,
|
||||
ka-ge,
|
||||
kk-kz,
|
||||
kn-in,
|
||||
ko-kr,
|
||||
ksh,
|
||||
kw-gb,
|
||||
la-la,
|
||||
lb-lu,
|
||||
li-li,
|
||||
lmo,
|
||||
lo-la,
|
||||
lol-us,
|
||||
lt-lt,
|
||||
lv-lv,
|
||||
lzh,
|
||||
mk-mk,
|
||||
mn-mn,
|
||||
ms-my,
|
||||
mt-mt,
|
||||
nah,
|
||||
nds-de,
|
||||
nl-be,
|
||||
nl-nl,
|
||||
nn-no,
|
||||
no-no,
|
||||
oc-fr,
|
||||
ovd,
|
||||
pl-pl,
|
||||
pt-br,
|
||||
pt-pt,
|
||||
qya-aa,
|
||||
ro-ro,
|
||||
rpr,
|
||||
ru-ru,
|
||||
ry-ua,
|
||||
sah-sah,
|
||||
se-no,
|
||||
sk-sk,
|
||||
sl-si,
|
||||
so-so,
|
||||
sq-al,
|
||||
sr-cs,
|
||||
sr-sp,
|
||||
sv-se,
|
||||
sxu,
|
||||
szl,
|
||||
ta-in,
|
||||
th-th,
|
||||
tl-ph,
|
||||
tlh-aa,
|
||||
tok,
|
||||
tr-tr,
|
||||
tt-ru,
|
||||
uk-ua,
|
||||
val-es,
|
||||
vec-it,
|
||||
vi-vn,
|
||||
yi-de,
|
||||
yo-ng,
|
||||
zh-cn,
|
||||
zh-hk,
|
||||
zh-tw,
|
||||
zlm-arab,
|
||||
}
|
||||
}
|
||||
11
pumpkin-plugin-wit/v0.1.0/context.wit
Normal file
11
pumpkin-plugin-wit/v0.1.0/context.wit
Normal file
@@ -0,0 +1,11 @@
|
||||
interface context {
|
||||
use server.{server};
|
||||
use event.{event-type, event-priority};
|
||||
use command.{command};
|
||||
|
||||
resource context {
|
||||
register-event: func(handler-id: u32, event-type: event-type, event-priority: event-priority, blocking: bool);
|
||||
register-command: func(command: command, permission: string);
|
||||
get-server: func() -> server;
|
||||
}
|
||||
}
|
||||
36
pumpkin-plugin-wit/v0.1.0/entity.wit
Normal file
36
pumpkin-plugin-wit/v0.1.0/entity.wit
Normal file
@@ -0,0 +1,36 @@
|
||||
interface entity {
|
||||
use common.{block-position};
|
||||
|
||||
resource block-entity {
|
||||
resource-location: func() -> string;
|
||||
get-position: func() -> block-position;
|
||||
get-id: func() -> u32;
|
||||
// write-nbt: func(nbt: nbt-compound);
|
||||
// from-nbt: func(nbt: nbt-compound, position: block-position) -> entity;
|
||||
// tick: func(world: simple-world);
|
||||
// write-internal: func(nbt: nbt-compound);
|
||||
// chunk-data-nbt: func() -> option<nbt-compound;
|
||||
// get-inventory: func() -> option<inventory>;
|
||||
// set-block-state: func(block-state: block-state-id);
|
||||
// on-block-replaced: func(world: simple-world, position: block-position);
|
||||
is-dirty: func() -> bool;
|
||||
clear-dirty: func();
|
||||
// to-property-delegate: func() -> option<property-delegate>;
|
||||
// to-experience-container: func() -> option<experience-container>;
|
||||
}
|
||||
|
||||
resource command-block-entity {
|
||||
get-block-entity: func () -> block-entity;
|
||||
last-output: func() -> string;
|
||||
track-output: func() -> bool;
|
||||
success-count: func() -> u32;
|
||||
command: func() -> string;
|
||||
auto: func() -> bool;
|
||||
condition-met: func() -> bool;
|
||||
powered: func() -> bool;
|
||||
}
|
||||
|
||||
variant entity {
|
||||
command-block-entity(command-block-entity),
|
||||
}
|
||||
}
|
||||
34
pumpkin-plugin-wit/v0.1.0/event.wit
Normal file
34
pumpkin-plugin-wit/v0.1.0/event.wit
Normal file
@@ -0,0 +1,34 @@
|
||||
interface event {
|
||||
use player.{player};
|
||||
use text.{text-component};
|
||||
|
||||
enum event-priority {
|
||||
highest,
|
||||
high,
|
||||
normal,
|
||||
low,
|
||||
lowest,
|
||||
}
|
||||
|
||||
record player-join-event-data {
|
||||
player: player,
|
||||
join-message: text-component,
|
||||
cancelled: bool
|
||||
}
|
||||
|
||||
record player-leave-event-data {
|
||||
player: player,
|
||||
leave-message: text-component,
|
||||
cancelled: bool
|
||||
}
|
||||
|
||||
enum event-type {
|
||||
player-join-event,
|
||||
player-leave-event,
|
||||
}
|
||||
|
||||
variant event {
|
||||
player-join-event(player-join-event-data),
|
||||
player-leave-event(player-leave-event-data),
|
||||
}
|
||||
}
|
||||
14
pumpkin-plugin-wit/v0.1.0/log.wit
Normal file
14
pumpkin-plugin-wit/v0.1.0/log.wit
Normal file
@@ -0,0 +1,14 @@
|
||||
interface logging {
|
||||
enum level {
|
||||
trace,
|
||||
debug,
|
||||
info,
|
||||
warn,
|
||||
error,
|
||||
}
|
||||
|
||||
/// log any general purpose message
|
||||
log: func(level: level, message: string);
|
||||
/// This function is meant to be used by the tracing crate.
|
||||
log-tracing: func(event: list<u8>);
|
||||
}
|
||||
15
pumpkin-plugin-wit/v0.1.0/metadata.wit
Normal file
15
pumpkin-plugin-wit/v0.1.0/metadata.wit
Normal file
@@ -0,0 +1,15 @@
|
||||
/// Plugin metadata describing the plugin and its compatibility.
|
||||
interface metadata {
|
||||
record plugin-metadata {
|
||||
/// Name of the plugin.
|
||||
name: string,
|
||||
/// Plugin version (semver).
|
||||
version: string,
|
||||
/// Plugin authors.
|
||||
authors: list<string>,
|
||||
/// Short description of the plugin.
|
||||
description: string,
|
||||
}
|
||||
|
||||
get-metadata: func() -> plugin-metadata;
|
||||
}
|
||||
5
pumpkin-plugin-wit/v0.1.0/player.wit
Normal file
5
pumpkin-plugin-wit/v0.1.0/player.wit
Normal file
@@ -0,0 +1,5 @@
|
||||
interface player {
|
||||
resource player {
|
||||
get-id: func () -> string;
|
||||
}
|
||||
}
|
||||
24
pumpkin-plugin-wit/v0.1.0/plugin.wit
Normal file
24
pumpkin-plugin-wit/v0.1.0/plugin.wit
Normal file
@@ -0,0 +1,24 @@
|
||||
package pumpkin:plugin;
|
||||
|
||||
world plugin {
|
||||
use context.{context};
|
||||
use event.{event};
|
||||
use server.{server as server-instance};
|
||||
use command.{command-sender, consumed-args, command-error};
|
||||
|
||||
// This is what the host should provide to the plugin
|
||||
import logging;
|
||||
import server;
|
||||
import text;
|
||||
import command;
|
||||
import context;
|
||||
|
||||
// This is what the plugin should provide
|
||||
export init-plugin: func();
|
||||
export on-load: func(context: context) -> result<_, string>;
|
||||
export on-unload: func(context: context) -> result<_, string>;
|
||||
export metadata;
|
||||
export common;
|
||||
export handle-event: func(event-id: u32, server: server-instance, event: event) -> event;
|
||||
export handle-command: func(command-id: u32, sender: command-sender, server: server-instance, args: consumed-args) -> result<s32, command-error>;
|
||||
}
|
||||
12
pumpkin-plugin-wit/v0.1.0/server.wit
Normal file
12
pumpkin-plugin-wit/v0.1.0/server.wit
Normal file
@@ -0,0 +1,12 @@
|
||||
interface server {
|
||||
enum difficulty {
|
||||
peaceful,
|
||||
easy,
|
||||
normal,
|
||||
hard,
|
||||
}
|
||||
|
||||
resource server {
|
||||
get-difficulty: func() -> difficulty;
|
||||
}
|
||||
}
|
||||
41
pumpkin-plugin-wit/v0.1.0/text.wit
Normal file
41
pumpkin-plugin-wit/v0.1.0/text.wit
Normal file
@@ -0,0 +1,41 @@
|
||||
interface text {
|
||||
use common.{named-color, rgb-color, argb-color};
|
||||
|
||||
resource text-component {
|
||||
text: static func(plain: string) -> text-component;
|
||||
translate: static func(key: string, %with: list<text-component>) -> text-component;
|
||||
|
||||
add-child: func(child: text-component);
|
||||
add-text: func(text: string);
|
||||
get-text: func() -> string;
|
||||
encode: func() -> list<u8>;
|
||||
|
||||
// Style
|
||||
|
||||
color-named: func(color: named-color);
|
||||
color-rgb: func(color: rgb-color);
|
||||
bold: func(value: bool);
|
||||
italic: func(value: bool);
|
||||
underlined: func(value: bool);
|
||||
strikethrough: func(value: bool);
|
||||
obfuscated: func(value: bool);
|
||||
/// Text inserted into chat when shift-clicked
|
||||
insertion: func(text: string);
|
||||
font: func(font: string);
|
||||
shadow-color: func(color: argb-color);
|
||||
|
||||
// Click events
|
||||
|
||||
click-open-url: func(url: string);
|
||||
click-run-command: func(command: string);
|
||||
click-suggest-command: func(command: string);
|
||||
click-copy-to-clipboard: func(text: string);
|
||||
|
||||
// Hover events
|
||||
|
||||
hover-show-text: func(text: text-component);
|
||||
/// Item data as SNBT string
|
||||
hover-show-item: func(item: string);
|
||||
hover-show-entity: func(entity-type: string, id: string, name: option<text-component>);
|
||||
}
|
||||
}
|
||||
5
pumpkin-plugin-wit/v0.1.0/world.wit
Normal file
5
pumpkin-plugin-wit/v0.1.0/world.wit
Normal file
@@ -0,0 +1,5 @@
|
||||
interface %world {
|
||||
resource %world {
|
||||
get-id: func () -> string;
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,13 @@ tokio-util = { workspace = true, features = ["rt"] }
|
||||
flate2.workspace = true
|
||||
console-subscriber = { workspace = true, optional = true }
|
||||
|
||||
wasmtime = { workspace = true }
|
||||
wasmtime-wasi = { workspace = true }
|
||||
wasmparser = { workspace = true }
|
||||
|
||||
postcard = { workspace = true }
|
||||
tracing-serde-structured = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
|
||||
@@ -46,13 +46,15 @@ impl CommandExecutor for ListExecutor {
|
||||
|
||||
for (i, metadata) in plugins.iter().enumerate() {
|
||||
let fmt = if i == plugins.len() - 1 {
|
||||
metadata.name.to_string()
|
||||
metadata.name.clone()
|
||||
} else {
|
||||
format!("{}, ", metadata.name)
|
||||
};
|
||||
let hover_text = format!(
|
||||
"Version: {}\nAuthors: {}\nDescription: {}",
|
||||
metadata.version, metadata.authors, metadata.description
|
||||
metadata.version,
|
||||
metadata.authors.join(", "),
|
||||
metadata.description
|
||||
);
|
||||
let component = TextComponent::text(fmt)
|
||||
.color_named(NamedColor::Green)
|
||||
|
||||
@@ -31,13 +31,15 @@ impl CommandExecutor for Executor {
|
||||
|
||||
for (i, metadata) in plugins.clone().into_iter().enumerate() {
|
||||
let fmt = if i == plugins.len() - 1 {
|
||||
metadata.name.to_string()
|
||||
metadata.name.clone()
|
||||
} else {
|
||||
format!("{}, ", metadata.name)
|
||||
};
|
||||
let hover_text = format!(
|
||||
"Version: {}\nAuthors: {}\nDescription: {}",
|
||||
metadata.version, metadata.authors, metadata.description
|
||||
metadata.version,
|
||||
metadata.authors.join(", "),
|
||||
metadata.description
|
||||
);
|
||||
let component = TextComponent::text(fmt)
|
||||
.color_named(NamedColor::Green)
|
||||
|
||||
@@ -148,7 +148,7 @@ async fn handle_packet(
|
||||
.active_plugins()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|meta| meta.name.to_string())
|
||||
.map(|meta| meta.name)
|
||||
.reduce(|acc, name| format!("{acc}, {name}"))
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::{
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use crate::{LoggerOption, command::client_suggestions, plugin_log};
|
||||
use crate::{LoggerOption, command::client_suggestions, plugin::PluginMetadata, plugin_log};
|
||||
use pumpkin_util::{
|
||||
PermissionLvl,
|
||||
permission::{Permission, PermissionManager},
|
||||
@@ -20,7 +20,7 @@ use crate::{
|
||||
|
||||
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
use super::{EventPriority, Payload, PluginMetadata};
|
||||
use super::{EventPriority, Payload};
|
||||
|
||||
/// The `Context` struct represents the context of a plugin, containing metadata,
|
||||
/// a server reference, and event handlers.
|
||||
@@ -30,7 +30,7 @@ use super::{EventPriority, Payload, PluginMetadata};
|
||||
/// - `server`: A reference to the server on which the plugin operates.
|
||||
/// - `handlers`: A map of event handlers, protected by a read-write lock for safe access across threads.
|
||||
pub struct Context {
|
||||
metadata: PluginMetadata<'static>,
|
||||
metadata: PluginMetadata,
|
||||
pub server: Arc<Server>,
|
||||
pub handlers: Arc<RwLock<HandlerMap>>,
|
||||
pub plugin_manager: Arc<PluginManager>,
|
||||
@@ -49,7 +49,7 @@ impl Context {
|
||||
/// A new instance of `Context`.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
metadata: PluginMetadata<'static>,
|
||||
metadata: PluginMetadata,
|
||||
server: Arc<Server>,
|
||||
handlers: Arc<RwLock<HandlerMap>>,
|
||||
plugin_manager: Arc<PluginManager>,
|
||||
@@ -72,7 +72,7 @@ impl Context {
|
||||
/// A string representing the path to the data folder.
|
||||
#[must_use]
|
||||
pub fn get_data_folder(&self) -> PathBuf {
|
||||
let path = Path::new("./plugins").join(self.metadata.name);
|
||||
let path = Path::new("./plugins").join(&self.metadata.name);
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(&path).unwrap();
|
||||
}
|
||||
@@ -156,13 +156,12 @@ impl Context {
|
||||
tree: crate::command::tree::CommandTree,
|
||||
permission: P,
|
||||
) {
|
||||
let plugin_name = self.metadata.name;
|
||||
let permission = permission.into();
|
||||
|
||||
let full_permission_node = if permission.contains(':') {
|
||||
permission
|
||||
} else {
|
||||
format!("{plugin_name}:{permission}")
|
||||
format!("{}:{permission}", self.metadata.name)
|
||||
};
|
||||
|
||||
{
|
||||
@@ -209,12 +208,13 @@ impl Context {
|
||||
/// Register a permission for this plugin
|
||||
pub async fn register_permission(&self, permission: Permission) -> Result<(), String> {
|
||||
// Ensure the permission has the correct namespace
|
||||
let plugin_name = self.metadata.name;
|
||||
|
||||
if !permission.node.starts_with(&format!("{plugin_name}:")) {
|
||||
if !permission
|
||||
.node
|
||||
.starts_with(&format!("{}:", self.metadata.name))
|
||||
{
|
||||
return Err(format!(
|
||||
"Permission {} must use the plugin's namespace ({})",
|
||||
permission.node, plugin_name
|
||||
permission.node, self.metadata.name
|
||||
));
|
||||
}
|
||||
|
||||
@@ -339,6 +339,6 @@ impl Context {
|
||||
} else {
|
||||
Level::INFO
|
||||
};
|
||||
plugin_log!(level, self.metadata.name, "{}", message);
|
||||
plugin_log!(level, &self.metadata.name, "{}", message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,15 @@ pub use events::*;
|
||||
/// version, authors, and a description. It is generic over a lifetime `'s` to allow
|
||||
/// for string slices that are valid for the lifetime of the plugin metadata.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PluginMetadata<'s> {
|
||||
pub struct PluginMetadata {
|
||||
/// The name of the plugin.
|
||||
pub name: &'s str,
|
||||
pub name: String,
|
||||
/// The version of the plugin.
|
||||
pub version: &'s str,
|
||||
pub version: String,
|
||||
/// The authors of the plugin.
|
||||
pub authors: &'s str,
|
||||
pub authors: Vec<String>,
|
||||
/// A description of the plugin.
|
||||
pub description: &'s str,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// This type represents a future for the plugin.
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
use crate::plugin::api::{Plugin, PluginMetadata};
|
||||
use crate::plugin::{PluginMetadata, api::Plugin, loader::wasm::wasm_host::PluginInitError};
|
||||
use std::{any::Any, path::Path, pin::Pin};
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod native;
|
||||
pub mod wasm;
|
||||
|
||||
pub type PluginLoadFuture<'a> = Pin<
|
||||
Box<
|
||||
dyn Future<
|
||||
Output = Result<
|
||||
(
|
||||
Box<dyn Plugin>,
|
||||
PluginMetadata<'static>,
|
||||
Box<dyn Any + Send + Sync>,
|
||||
),
|
||||
(Box<dyn Plugin>, PluginMetadata, Box<dyn Any + Send + Sync>),
|
||||
LoaderError,
|
||||
>,
|
||||
> + Send
|
||||
@@ -69,4 +66,7 @@ pub enum LoaderError {
|
||||
plugin_version: u32,
|
||||
server_version: u32,
|
||||
},
|
||||
|
||||
#[error("Wasm plugin initialization error: {0}")]
|
||||
WasmInitializationError(#[from] PluginInitError),
|
||||
}
|
||||
|
||||
65
pumpkin/src/plugin/loader/wasm/mod.rs
Normal file
65
pumpkin/src/plugin/loader/wasm/mod.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use std::{any::Any, path::Path, sync::Arc};
|
||||
|
||||
use wasm_host::{PluginRuntime, WasmPlugin};
|
||||
|
||||
use crate::plugin::{
|
||||
Context, Plugin, PluginFuture,
|
||||
loader::{PluginLoadFuture, PluginLoader, PluginUnloadFuture},
|
||||
};
|
||||
|
||||
pub mod wasm_host;
|
||||
|
||||
impl Plugin for Arc<WasmPlugin> {
|
||||
fn on_load(&mut self, context: Arc<Context>) -> PluginFuture<'_, Result<(), String>> {
|
||||
Box::pin(async move {
|
||||
self.as_ref()
|
||||
.on_load(context)
|
||||
.await
|
||||
.map_err(|err| err.to_string())?
|
||||
})
|
||||
}
|
||||
|
||||
fn on_unload(&mut self, context: Arc<Context>) -> PluginFuture<'_, Result<(), String>> {
|
||||
Box::pin(async move {
|
||||
self.as_ref()
|
||||
.on_unload(context)
|
||||
.await
|
||||
.map_err(|err| err.to_string())?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WasmPluginLoader;
|
||||
impl PluginLoader for WasmPluginLoader {
|
||||
fn load<'a>(&'a self, path: &'a Path) -> PluginLoadFuture<'a> {
|
||||
Box::pin(async {
|
||||
let path = path.to_owned();
|
||||
|
||||
let runtime = PluginRuntime::new(&path)?;
|
||||
let (plugin, metadata) = runtime.init_plugin(&path).await?;
|
||||
|
||||
Ok((
|
||||
Box::new(plugin) as Box<dyn Plugin>,
|
||||
metadata,
|
||||
Box::new(()) as Box<dyn Any + Send + Sync>,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn can_load(&self, path: &Path) -> bool {
|
||||
let ext = path
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or_default();
|
||||
|
||||
ext.eq_ignore_ascii_case("wasm")
|
||||
}
|
||||
|
||||
fn unload(&self, _data: Box<dyn Any + Send + Sync>) -> PluginUnloadFuture<'_> {
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn can_unload(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
85
pumpkin/src/plugin/loader/wasm/wasm_host/args.rs
Normal file
85
pumpkin/src/plugin/loader/wasm/wasm_host/args.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use pumpkin_util::text::TextComponent;
|
||||
|
||||
use crate::{command::tree::CommandTree, entity::player::Player};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum OwnedArg {
|
||||
Entities(Vec<Arc<dyn crate::entity::EntityBase>>),
|
||||
Entity(Arc<dyn crate::entity::EntityBase>),
|
||||
Players(Vec<Arc<Player>>),
|
||||
BlockPos(pumpkin_util::math::position::BlockPos),
|
||||
Pos3D(pumpkin_util::math::vector3::Vector3<f64>),
|
||||
Pos2D(pumpkin_util::math::vector2::Vector2<f64>),
|
||||
Rotation(f32, bool, f32, bool),
|
||||
GameMode(pumpkin_util::GameMode),
|
||||
Difficulty(pumpkin_util::Difficulty),
|
||||
CommandTree(CommandTree),
|
||||
Item(String),
|
||||
ItemPredicate(String),
|
||||
ResourceLocation(String),
|
||||
Block(String),
|
||||
BlockPredicate(String),
|
||||
BossbarColor(crate::world::bossbar::BossbarColor),
|
||||
BossbarStyle(crate::world::bossbar::BossbarDivisions),
|
||||
Particle(pumpkin_data::particle::Particle),
|
||||
Msg(String),
|
||||
TextComponent(TextComponent),
|
||||
Time(i32),
|
||||
Num(
|
||||
Result<
|
||||
crate::command::args::bounded_num::Number,
|
||||
crate::command::args::bounded_num::NotInBounds,
|
||||
>,
|
||||
),
|
||||
Bool(bool),
|
||||
Simple(String),
|
||||
SoundCategory(pumpkin_data::sound::SoundCategory),
|
||||
DamageType(pumpkin_data::damage::DamageType),
|
||||
Effect(&'static pumpkin_data::effect::StatusEffect),
|
||||
Enchantment(&'static pumpkin_data::Enchantment),
|
||||
EntityAnchor(crate::command::args::EntityAnchor),
|
||||
}
|
||||
|
||||
impl OwnedArg {
|
||||
#[must_use]
|
||||
pub fn from_arg(arg: &crate::command::args::Arg<'_>) -> Self {
|
||||
use crate::command::args::Arg;
|
||||
match arg {
|
||||
Arg::Entities(v) => Self::Entities(v.clone()),
|
||||
Arg::Entity(e) => Self::Entity(e.clone()),
|
||||
Arg::Players(v) => Self::Players(v.clone()),
|
||||
Arg::BlockPos(p) => Self::BlockPos(*p),
|
||||
Arg::Pos3D(v) => Self::Pos3D(*v),
|
||||
Arg::Pos2D(v) => Self::Pos2D(*v),
|
||||
Arg::Rotation(a, b, c, d) => Self::Rotation(*a, *b, *c, *d),
|
||||
Arg::GameMode(g) => Self::GameMode(*g),
|
||||
Arg::Difficulty(d) => Self::Difficulty(*d),
|
||||
Arg::CommandTree(t) => Self::CommandTree(t.clone()),
|
||||
Arg::Item(s) => Self::Item(s.to_string()),
|
||||
Arg::ItemPredicate(s) => Self::ItemPredicate(s.to_string()),
|
||||
Arg::ResourceLocation(s) => Self::ResourceLocation(s.to_string()),
|
||||
Arg::Block(s) => Self::Block(s.to_string()),
|
||||
Arg::BlockPredicate(s) => Self::BlockPredicate(s.to_string()),
|
||||
Arg::BossbarColor(c) => Self::BossbarColor(c.clone()),
|
||||
Arg::BossbarStyle(s) => Self::BossbarStyle(s.clone()),
|
||||
Arg::Particle(p) => Self::Particle(*p),
|
||||
Arg::Msg(m) => Self::Msg(m.clone()),
|
||||
Arg::TextComponent(t) => Self::TextComponent(t.clone()),
|
||||
Arg::Time(t) => Self::Time(*t),
|
||||
Arg::Num(n) => Self::Num(*n),
|
||||
Arg::Bool(b) => Self::Bool(*b),
|
||||
Arg::Simple(s) => Self::Simple(s.to_string()),
|
||||
Arg::SoundCategory(s) => Self::SoundCategory(*s),
|
||||
Arg::DamageType(d) => Self::DamageType(*d),
|
||||
Arg::Effect(e) => Self::Effect(e),
|
||||
Arg::Enchantment(e) => Self::Enchantment(e),
|
||||
Arg::EntityAnchor(a) => Self::EntityAnchor(*a),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConsumedArgsResource {
|
||||
pub provider: HashMap<String, OwnedArg>,
|
||||
}
|
||||
114
pumpkin/src/plugin/loader/wasm/wasm_host/logging.rs
Normal file
114
pumpkin/src/plugin/loader/wasm/wasm_host/logging.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use tracing_serde_structured::{DebugRecord, SerializeValue};
|
||||
|
||||
pub async fn log_tracing(event_bytes: Vec<u8>) {
|
||||
let event: tracing_serde_structured::SerializeEvent<'_> =
|
||||
match postcard::from_bytes(&event_bytes) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
tracing::error!("[plugin] failed to deserialize tracing event: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let message = match &event.fields {
|
||||
tracing_serde_structured::SerializeRecordFields::De(map) => map
|
||||
.get(&tracing_serde_structured::CowString::Borrowed("message"))
|
||||
.map(|v| match v {
|
||||
SerializeValue::Debug(d) => match d {
|
||||
DebugRecord::De(s) => s.as_str().to_string(),
|
||||
DebugRecord::Ser(_) => String::new(),
|
||||
},
|
||||
SerializeValue::Str(s) => s.as_str().to_string(),
|
||||
_ => String::new(),
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
tracing_serde_structured::SerializeRecordFields::Ser(_) => String::new(),
|
||||
};
|
||||
|
||||
let target = event.metadata.target.as_str().to_string();
|
||||
let module_path = event
|
||||
.metadata
|
||||
.module_path
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let file = event
|
||||
.metadata
|
||||
.file
|
||||
.as_deref()
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let line = event.metadata.line.unwrap_or(0);
|
||||
|
||||
let extra: Vec<(&str, &tracing_serde_structured::SerializeValue)> = match &event.fields {
|
||||
tracing_serde_structured::SerializeRecordFields::De(map) => map
|
||||
.iter()
|
||||
.filter(|(k, _)| k.as_str() != "message")
|
||||
.map(|(k, v)| (k.as_str(), v))
|
||||
.collect(),
|
||||
tracing_serde_structured::SerializeRecordFields::Ser(_) => vec![],
|
||||
};
|
||||
|
||||
let fields_str = if extra.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
extra
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{k}={}", format_value(v)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
)
|
||||
};
|
||||
|
||||
macro_rules! emit {
|
||||
($level:expr) => {
|
||||
match &fields_str {
|
||||
None => {
|
||||
tracing::event!(
|
||||
$level,
|
||||
plugin.target = %target,
|
||||
plugin.module = %module_path,
|
||||
plugin.file = %file,
|
||||
plugin.line = line,
|
||||
"{message}"
|
||||
);
|
||||
}
|
||||
Some(fields) => {
|
||||
tracing::event!(
|
||||
$level,
|
||||
plugin.target = %target,
|
||||
plugin.module = %module_path,
|
||||
plugin.file = %file,
|
||||
plugin.line = line,
|
||||
plugin.fields = %fields,
|
||||
"{message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
match event.metadata.level {
|
||||
tracing_serde_structured::SerializeLevel::Trace => emit!(tracing::Level::TRACE),
|
||||
tracing_serde_structured::SerializeLevel::Debug => emit!(tracing::Level::DEBUG),
|
||||
tracing_serde_structured::SerializeLevel::Info => emit!(tracing::Level::INFO),
|
||||
tracing_serde_structured::SerializeLevel::Warn => emit!(tracing::Level::WARN),
|
||||
tracing_serde_structured::SerializeLevel::Error => emit!(tracing::Level::ERROR),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_value(v: &tracing_serde_structured::SerializeValue) -> String {
|
||||
match v {
|
||||
SerializeValue::Debug(d) => match d {
|
||||
DebugRecord::De(s) => s.as_str().to_string(),
|
||||
DebugRecord::Ser(args) => format!("{args:?}"),
|
||||
},
|
||||
SerializeValue::Str(s) => s.as_str().to_string(),
|
||||
SerializeValue::F64(f) => f.to_string(),
|
||||
SerializeValue::I64(i) => i.to_string(),
|
||||
SerializeValue::U64(u) => u.to_string(),
|
||||
SerializeValue::Bool(b) => b.to_string(),
|
||||
_ => String::from("<unknown>"),
|
||||
}
|
||||
}
|
||||
186
pumpkin/src/plugin/loader/wasm/wasm_host/mod.rs
Normal file
186
pumpkin/src/plugin/loader/wasm/wasm_host/mod.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use std::{fs, path::Path, sync::Arc};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::Mutex;
|
||||
use wasmtime::{Cache, CacheConfig, Engine, Store, component::Component};
|
||||
|
||||
use crate::plugin::{Context, PluginMetadata, loader::wasm::wasm_host::state::PluginHostState};
|
||||
|
||||
pub mod args;
|
||||
pub mod logging;
|
||||
pub mod state;
|
||||
pub mod wit;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PluginInitError {
|
||||
#[error("Engine creation failed")]
|
||||
EngineCreationFailed(wasmtime::Error),
|
||||
#[error("Failed to setup linker")]
|
||||
LinkerSetupFailed(wasmtime::Error),
|
||||
#[error("plugin API version mismatch received plugin with version `{0}`")]
|
||||
ApiVersionMismatch(String),
|
||||
#[error("plugin missing pumpkin:api-version custom section")]
|
||||
MissingApiVersionSection,
|
||||
#[error("failed to read payload for plugin")]
|
||||
FailedToReadPayload(#[from] wasmparser::BinaryReaderError),
|
||||
#[error("failed to read plugin bytes")]
|
||||
FailedToReadPluginBytes(#[from] std::io::Error),
|
||||
#[error("plugin failed to load with error: {0}")]
|
||||
PluginFailedToLoad(#[from] wasmtime::Error),
|
||||
}
|
||||
|
||||
pub struct PluginRuntime {
|
||||
engine: Engine,
|
||||
cache_dir: std::path::PathBuf,
|
||||
linker_v0_1_0: wasmtime::component::Linker<PluginHostState>,
|
||||
}
|
||||
|
||||
pub enum PluginInstance {
|
||||
V0_1_0(wit::v0_1_0::Plugin),
|
||||
}
|
||||
|
||||
pub struct WasmPlugin {
|
||||
pub plugin_instance: PluginInstance,
|
||||
pub store: Mutex<Store<PluginHostState>>,
|
||||
}
|
||||
|
||||
impl PluginRuntime {
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, PluginInitError> {
|
||||
let mut config = wasmtime::Config::new();
|
||||
config.wasm_component_model(true);
|
||||
let mut path = std::path::absolute(path.as_ref()).expect("Failed to get absolute path");
|
||||
path.pop();
|
||||
path.push("cache");
|
||||
let mut cache_config = CacheConfig::new();
|
||||
cache_config.with_directory(&path);
|
||||
config.cache(Some(
|
||||
Cache::new(cache_config).expect("Failed to create cache"),
|
||||
));
|
||||
let engine = Engine::new(&config).map_err(PluginInitError::EngineCreationFailed)?;
|
||||
|
||||
let linker_v0_1_0 =
|
||||
wit::v0_1_0::setup_linker(&engine).map_err(PluginInitError::LinkerSetupFailed)?;
|
||||
|
||||
Ok(Self {
|
||||
engine,
|
||||
cache_dir: path,
|
||||
linker_v0_1_0,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn init_plugin<P: AsRef<Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
) -> Result<(Arc<WasmPlugin>, PluginMetadata), PluginInitError> {
|
||||
let wasm_bytes = std::fs::read(&path)?;
|
||||
|
||||
let api_version = probe_api_version_from_bytes(&wasm_bytes)?;
|
||||
|
||||
if api_version != "0.1.0" {
|
||||
return Err(PluginInitError::ApiVersionMismatch(api_version));
|
||||
}
|
||||
|
||||
let component = load_component(&self.engine, &wasm_bytes, path.as_ref(), &self.cache_dir)?;
|
||||
|
||||
let (wasm_plugin, metadata) = match api_version.as_str() {
|
||||
"0.1.0" => {
|
||||
wit::v0_1_0::init_plugin(&self.engine, &self.linker_v0_1_0, component).await?
|
||||
}
|
||||
_ => return Err(PluginInitError::ApiVersionMismatch(api_version)),
|
||||
};
|
||||
let wasm_plugin = Arc::new(wasm_plugin);
|
||||
wasm_plugin.store.lock().await.data_mut().plugin = Some(Arc::downgrade(&wasm_plugin));
|
||||
|
||||
Ok((wasm_plugin, metadata))
|
||||
}
|
||||
}
|
||||
|
||||
fn cache_key(wasm_path: &Path) -> Result<String, std::io::Error> {
|
||||
let metadata = fs::metadata(wasm_path)?;
|
||||
let file_name = wasm_path.file_stem().unwrap().to_string_lossy();
|
||||
let len = metadata.len();
|
||||
let modified = metadata
|
||||
.modified()?
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
Ok(format!(
|
||||
"{file_name}-{len}-{modified}-{}.cwasm",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
))
|
||||
}
|
||||
|
||||
fn load_component(
|
||||
engine: &Engine,
|
||||
wasm_bytes: &[u8],
|
||||
wasm_path: &Path,
|
||||
cache_dir: &Path,
|
||||
) -> Result<Component, PluginInitError> {
|
||||
let cache_name = cache_key(wasm_path)?;
|
||||
let cache_path = cache_dir.join(cache_name);
|
||||
|
||||
if cache_path.exists() {
|
||||
match unsafe { Component::deserialize_file(engine, &cache_path) } {
|
||||
Ok(component) => return Ok(component),
|
||||
Err(_) => {
|
||||
let _ = fs::remove_file(&cache_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let component = Component::new(engine, wasm_bytes)?;
|
||||
fs::write(&cache_path, component.serialize()?)?;
|
||||
Ok(component)
|
||||
}
|
||||
|
||||
/// Kind of a dumb solution, but in order to get the API version from a component, we define a custom section inside of the wasm binary itself, we then
|
||||
/// parse the value in that section to get the API version.
|
||||
fn probe_api_version_from_bytes(wasm_bytes: &[u8]) -> Result<String, PluginInitError> {
|
||||
let parser = wasmparser::Parser::new(0);
|
||||
for payload in parser.parse_all(wasm_bytes) {
|
||||
if let wasmparser::Payload::CustomSection(reader) = payload?
|
||||
&& reader.name() == "pumpkin:api-version"
|
||||
{
|
||||
return Ok(String::from_utf8_lossy(reader.data()).to_string());
|
||||
}
|
||||
}
|
||||
Err(PluginInitError::MissingApiVersionSection)
|
||||
}
|
||||
|
||||
impl WasmPlugin {
|
||||
pub async fn on_load(
|
||||
&self,
|
||||
context: Arc<Context>,
|
||||
) -> Result<Result<(), String>, wasmtime::Error> {
|
||||
let mut store = self.store.lock().await;
|
||||
|
||||
store.data_mut().server = Some(context.server.clone());
|
||||
|
||||
match self.plugin_instance {
|
||||
PluginInstance::V0_1_0(ref plugin) => {
|
||||
let context = store.data_mut().add_context(context)?;
|
||||
plugin.call_on_load(&mut *store, context).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn on_unload(
|
||||
&self,
|
||||
context: Arc<Context>,
|
||||
) -> Result<Result<(), String>, wasmtime::Error> {
|
||||
let mut store = self.store.lock().await;
|
||||
|
||||
match self.plugin_instance {
|
||||
PluginInstance::V0_1_0(ref plugin) => {
|
||||
let context = store.data_mut().add_context(context)?;
|
||||
plugin.call_on_unload(&mut *store, context).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DowncastResourceExt<E> {
|
||||
fn downcast_ref<'a>(&'a self, state: &'a mut PluginHostState) -> &'a E;
|
||||
fn downcast_mut<'a>(&'a self, state: &'a mut PluginHostState) -> &'a mut E;
|
||||
fn consume(self, state: &mut PluginHostState) -> E;
|
||||
}
|
||||
146
pumpkin/src/plugin/loader/wasm/wasm_host/state.rs
Normal file
146
pumpkin/src/plugin/loader/wasm/wasm_host/state.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
use pumpkin_util::text::TextComponent;
|
||||
use wasmtime::component::ResourceTable;
|
||||
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
|
||||
|
||||
use crate::{
|
||||
command::{
|
||||
CommandSender,
|
||||
args::ConsumedArgs,
|
||||
tree::{CommandTree, builder::NonLeafNodeBuilder},
|
||||
},
|
||||
entity::player::Player,
|
||||
plugin::{
|
||||
Context,
|
||||
loader::wasm::wasm_host::{WasmPlugin, args::OwnedArg},
|
||||
},
|
||||
server::Server,
|
||||
};
|
||||
|
||||
pub struct WasmResource<T> {
|
||||
pub provider: T,
|
||||
}
|
||||
|
||||
pub type ServerResource = WasmResource<Arc<Server>>;
|
||||
pub type ContextResource = WasmResource<Arc<Context>>;
|
||||
pub type PlayerResource = WasmResource<Arc<Player>>;
|
||||
pub type TextComponentResource = WasmResource<TextComponent>;
|
||||
pub type CommandResource = WasmResource<CommandTree>;
|
||||
pub type CommandSenderResource = WasmResource<CommandSender>;
|
||||
pub type ConsumedArgsResource = WasmResource<OwnedConsumedArgs>;
|
||||
pub type CommandNodeResource = WasmResource<NonLeafNodeBuilder>;
|
||||
|
||||
pub type OwnedConsumedArgs = HashMap<String, OwnedArg>;
|
||||
|
||||
pub struct PluginHostState {
|
||||
pub wasi_ctx: WasiCtx,
|
||||
pub resource_table: ResourceTable,
|
||||
pub plugin: Option<Weak<WasmPlugin>>,
|
||||
pub server: Option<Arc<Server>>,
|
||||
}
|
||||
|
||||
impl Default for PluginHostState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginHostState {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
let resource_table = ResourceTable::new();
|
||||
Self {
|
||||
wasi_ctx: WasiCtxBuilder::new().build(),
|
||||
resource_table,
|
||||
plugin: None,
|
||||
server: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_server<T>(
|
||||
&mut self,
|
||||
provider: Arc<Server>,
|
||||
) -> wasmtime::Result<wasmtime::component::Resource<T>> {
|
||||
let resource = self.resource_table.push(ServerResource { provider })?;
|
||||
Ok(wasmtime::component::Resource::new_own(resource.rep()))
|
||||
}
|
||||
|
||||
pub fn add_context<T>(
|
||||
&mut self,
|
||||
provider: Arc<Context>,
|
||||
) -> wasmtime::Result<wasmtime::component::Resource<T>> {
|
||||
let resource = self.resource_table.push(ContextResource { provider })?;
|
||||
Ok(wasmtime::component::Resource::new_own(resource.rep()))
|
||||
}
|
||||
|
||||
pub fn add_player<T>(
|
||||
&mut self,
|
||||
provider: Arc<Player>,
|
||||
) -> wasmtime::Result<wasmtime::component::Resource<T>> {
|
||||
let resource = self.resource_table.push(PlayerResource { provider })?;
|
||||
Ok(wasmtime::component::Resource::new_own(resource.rep()))
|
||||
}
|
||||
|
||||
pub fn add_text_component<T>(
|
||||
&mut self,
|
||||
provider: TextComponent,
|
||||
) -> wasmtime::Result<wasmtime::component::Resource<T>> {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.push(TextComponentResource { provider })?;
|
||||
Ok(wasmtime::component::Resource::new_own(resource.rep()))
|
||||
}
|
||||
|
||||
pub fn add_command<T>(
|
||||
&mut self,
|
||||
provider: CommandTree,
|
||||
) -> wasmtime::Result<wasmtime::component::Resource<T>> {
|
||||
let resource = self.resource_table.push(CommandResource { provider })?;
|
||||
Ok(wasmtime::component::Resource::new_own(resource.rep()))
|
||||
}
|
||||
|
||||
pub fn add_command_sender<T>(
|
||||
&mut self,
|
||||
command_sender: CommandSender,
|
||||
) -> wasmtime::Result<wasmtime::component::Resource<T>> {
|
||||
let resource = self.resource_table.push(CommandSenderResource {
|
||||
provider: command_sender,
|
||||
})?;
|
||||
Ok(wasmtime::component::Resource::new_own(resource.rep()))
|
||||
}
|
||||
|
||||
pub fn add_consumed_args<T>(
|
||||
&mut self,
|
||||
provider: &ConsumedArgs<'_>,
|
||||
) -> wasmtime::Result<wasmtime::component::Resource<T>> {
|
||||
let owned: HashMap<String, OwnedArg> = provider
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), OwnedArg::from_arg(v)))
|
||||
.collect();
|
||||
let resource = self
|
||||
.resource_table
|
||||
.push(ConsumedArgsResource { provider: owned })?;
|
||||
Ok(wasmtime::component::Resource::new_own(resource.rep()))
|
||||
}
|
||||
|
||||
pub fn add_command_node<T>(
|
||||
&mut self,
|
||||
provider: NonLeafNodeBuilder,
|
||||
) -> wasmtime::Result<wasmtime::component::Resource<T>> {
|
||||
let resource = self.resource_table.push(CommandNodeResource { provider })?;
|
||||
Ok(wasmtime::component::Resource::new_own(resource.rep()))
|
||||
}
|
||||
}
|
||||
|
||||
impl WasiView for PluginHostState {
|
||||
fn ctx(&mut self) -> WasiCtxView<'_> {
|
||||
WasiCtxView {
|
||||
ctx: &mut self.wasi_ctx,
|
||||
table: &mut self.resource_table,
|
||||
}
|
||||
}
|
||||
}
|
||||
1
pumpkin/src/plugin/loader/wasm/wasm_host/wit/mod.rs
Normal file
1
pumpkin/src/plugin/loader/wasm/wasm_host/wit/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod v0_1_0;
|
||||
@@ -0,0 +1,80 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use pumpkin_util::text::{
|
||||
TextComponent,
|
||||
color::{Color, NamedColor},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
command::{CommandExecutor, dispatcher::CommandError},
|
||||
plugin::loader::wasm::wasm_host::{
|
||||
DowncastResourceExt, PluginInstance, WasmPlugin,
|
||||
wit::v0_1_0::pumpkin::plugin::command::CommandError as CommandErrorWit,
|
||||
},
|
||||
server::Server,
|
||||
};
|
||||
|
||||
pub struct WasmCommandExecutor {
|
||||
pub handler_id: u32,
|
||||
pub plugin: Arc<WasmPlugin>,
|
||||
pub server: Arc<Server>,
|
||||
}
|
||||
|
||||
impl CommandExecutor for WasmCommandExecutor {
|
||||
fn execute<'a>(
|
||||
&'a self,
|
||||
sender: &'a crate::command::CommandSender,
|
||||
_server: &'a crate::server::Server,
|
||||
args: &'a crate::command::args::ConsumedArgs<'a>,
|
||||
) -> crate::command::CommandResult<'a> {
|
||||
Box::pin(async move {
|
||||
let mut store = self.plugin.store.lock().await;
|
||||
|
||||
let sender_resource = store.data_mut().add_command_sender(sender.clone()).unwrap();
|
||||
let server_resource = store.data_mut().add_server(self.server.clone()).unwrap();
|
||||
let args_resource = store.data_mut().add_consumed_args(args).unwrap();
|
||||
|
||||
match self.plugin.plugin_instance {
|
||||
PluginInstance::V0_1_0(ref plugin) => {
|
||||
let result = plugin
|
||||
.call_handle_command(
|
||||
&mut *store,
|
||||
self.handler_id,
|
||||
sender_resource,
|
||||
server_resource,
|
||||
args_resource,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
CommandError::CommandFailed(
|
||||
TextComponent::text(format!(
|
||||
"Wasm command failed with following error: {e}"
|
||||
))
|
||||
.color(Color::Named(NamedColor::Red)),
|
||||
)
|
||||
})?;
|
||||
|
||||
match result {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) => match err {
|
||||
CommandErrorWit::InvalidConsumption(value) => {
|
||||
Err(CommandError::InvalidConsumption(value))
|
||||
}
|
||||
CommandErrorWit::InvalidRequirement => {
|
||||
Err(CommandError::InvalidRequirement)
|
||||
}
|
||||
CommandErrorWit::PermissionDenied => {
|
||||
Err(CommandError::PermissionDenied)
|
||||
}
|
||||
CommandErrorWit::CommandFailed(resource) => {
|
||||
Err(CommandError::CommandFailed(
|
||||
resource.consume(store.data_mut()).provider,
|
||||
))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,656 @@
|
||||
use wasmtime::component::Resource;
|
||||
|
||||
use crate::{
|
||||
command::{
|
||||
args::{
|
||||
GetClientSideArgParser,
|
||||
block::{BlockArgumentConsumer, BlockPredicateArgumentConsumer},
|
||||
bool::BoolArgConsumer,
|
||||
bounded_num::{BoundedNumArgumentConsumer, ToFromNumber},
|
||||
difficulty::DifficultyArgumentConsumer,
|
||||
entities::EntitiesArgumentConsumer,
|
||||
entity::EntityArgumentConsumer,
|
||||
entity_anchor::EntityAnchorArgumentConsumer,
|
||||
gamemode::GamemodeArgumentConsumer,
|
||||
message::MsgArgConsumer,
|
||||
players::PlayersArgumentConsumer,
|
||||
position_2d::Position2DArgumentConsumer,
|
||||
position_3d::Position3DArgumentConsumer,
|
||||
position_block::BlockPosArgumentConsumer,
|
||||
resource::item::{ItemArgumentConsumer, ItemPredicateArgumentConsumer},
|
||||
resource_location::ResourceLocationArgumentConsumer,
|
||||
rotation::RotationArgumentConsumer,
|
||||
simple::SimpleArgConsumer,
|
||||
textcomponent::TextComponentArgConsumer,
|
||||
time::TimeArgumentConsumer,
|
||||
},
|
||||
tree::{
|
||||
CommandTree,
|
||||
builder::{argument, literal},
|
||||
},
|
||||
},
|
||||
plugin::loader::wasm::wasm_host::{
|
||||
DowncastResourceExt,
|
||||
state::{
|
||||
CommandNodeResource, CommandSenderResource, PluginHostState, TextComponentResource,
|
||||
},
|
||||
wit::v0_1_0::{
|
||||
commands::executor::WasmCommandExecutor,
|
||||
pumpkin::{
|
||||
self,
|
||||
plugin::{
|
||||
command::{
|
||||
Arg, ArgumentType, Command, CommandNode, CommandSender, CommandSenderType,
|
||||
ConsumedArgs, PermissionLevel, StringType,
|
||||
},
|
||||
common::{Locale, Position},
|
||||
player::Player,
|
||||
server::Server,
|
||||
text::TextComponent,
|
||||
world::World,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
pub mod executor;
|
||||
|
||||
impl pumpkin::plugin::command::Host for PluginHostState {}
|
||||
|
||||
impl pumpkin::plugin::command::HostConsumedArgs for PluginHostState {
|
||||
async fn get_value(&mut self, _consumed_args: Resource<ConsumedArgs>, _key: String) -> Arg {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn drop(&mut self, rep: Resource<ConsumedArgs>) -> wasmtime::Result<()> {
|
||||
self.resource_table
|
||||
.delete::<crate::plugin::loader::wasm::wasm_host::state::ConsumedArgsResource>(
|
||||
Resource::new_own(rep.rep()),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl pumpkin::plugin::command::HostCommand for PluginHostState {
|
||||
async fn new(&mut self, names: Vec<String>, description: String) -> Resource<Command> {
|
||||
self.add_command(CommandTree::new(names, description))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn then(&mut self, command: Resource<Command>, node: Resource<CommandNode>) -> () {
|
||||
let node_resource = node.consume(self);
|
||||
let command_resource = self
|
||||
.resource_table
|
||||
.get_mut::<crate::plugin::loader::wasm::wasm_host::state::CommandResource>(
|
||||
&Resource::new_own(command.rep()),
|
||||
)
|
||||
.expect("invalid command resource handle");
|
||||
|
||||
command_resource.provider = command_resource
|
||||
.provider
|
||||
.clone()
|
||||
.then(node_resource.provider);
|
||||
}
|
||||
|
||||
async fn execute_with_handler_id(&mut self, command: Resource<Command>, handler_id: u32) -> () {
|
||||
let plugin = self
|
||||
.plugin
|
||||
.as_ref()
|
||||
.expect("plugin should always be initialized here")
|
||||
.upgrade()
|
||||
.expect("plugin has been dropped");
|
||||
|
||||
let server = self
|
||||
.server
|
||||
.clone()
|
||||
.expect("server should be set before command registration");
|
||||
|
||||
let executor = WasmCommandExecutor {
|
||||
handler_id,
|
||||
plugin,
|
||||
server,
|
||||
};
|
||||
|
||||
let command_resource = self
|
||||
.resource_table
|
||||
.get_mut::<crate::plugin::loader::wasm::wasm_host::state::CommandResource>(
|
||||
&Resource::new_own(command.rep()),
|
||||
)
|
||||
.expect("invalid command resource handle");
|
||||
|
||||
command_resource.provider = command_resource.provider.clone().execute(executor);
|
||||
}
|
||||
|
||||
async fn drop(&mut self, rep: Resource<Command>) -> wasmtime::Result<()> {
|
||||
self.resource_table
|
||||
.delete::<crate::plugin::loader::wasm::wasm_host::state::CommandResource>(
|
||||
Resource::new_own(rep.rep()),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl pumpkin::plugin::command::HostCommandSender for PluginHostState {
|
||||
async fn get_command_sender_type(
|
||||
&mut self,
|
||||
_command_sender: Resource<CommandSender>,
|
||||
) -> CommandSenderType {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn send_message(
|
||||
&mut self,
|
||||
command_sender: Resource<CommandSender>,
|
||||
text: Resource<TextComponent>,
|
||||
) -> () {
|
||||
let text_resource = self
|
||||
.resource_table
|
||||
.get::<TextComponentResource>(&Resource::new_own(text.rep()))
|
||||
.expect("invalid text-component resource handle");
|
||||
let component = text_resource.provider.clone();
|
||||
|
||||
let sender_resource = self
|
||||
.resource_table
|
||||
.get::<CommandSenderResource>(&Resource::new_own(command_sender.rep()))
|
||||
.expect("invalid command-sender resource handle");
|
||||
|
||||
sender_resource.provider.send_message(component).await;
|
||||
}
|
||||
|
||||
async fn set_success_count(&mut self, command_sender: Resource<CommandSender>, count: i32) {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.get_mut::<CommandSenderResource>(&Resource::new_own(command_sender.rep()))
|
||||
.expect("invalid command-sender resource handle");
|
||||
|
||||
resource.provider.set_success_count(count as u32);
|
||||
}
|
||||
|
||||
async fn is_player(&mut self, command_sender: Resource<CommandSender>) -> bool {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.get::<CommandSenderResource>(&Resource::new_own(command_sender.rep()))
|
||||
.expect("invalid command-sender resource handle");
|
||||
|
||||
matches!(resource.provider, crate::command::CommandSender::Player(_))
|
||||
}
|
||||
|
||||
async fn is_console(&mut self, command_sender: Resource<CommandSender>) -> bool {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.get::<CommandSenderResource>(&Resource::new_own(command_sender.rep()))
|
||||
.expect("invalid command-sender resource handle");
|
||||
|
||||
matches!(
|
||||
resource.provider,
|
||||
crate::command::CommandSender::Console | crate::command::CommandSender::Rcon(_)
|
||||
)
|
||||
}
|
||||
|
||||
async fn as_player(
|
||||
&mut self,
|
||||
command_sender: Resource<CommandSender>,
|
||||
) -> Option<Resource<Player>> {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.get::<CommandSenderResource>(&Resource::new_own(command_sender.rep()))
|
||||
.expect("invalid command-sender resource handle");
|
||||
|
||||
if let crate::command::CommandSender::Player(player) = &resource.provider {
|
||||
let player = player.clone();
|
||||
Some(self.add_player(player).unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn permission_level(
|
||||
&mut self,
|
||||
command_sender: Resource<CommandSender>,
|
||||
) -> PermissionLevel {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.get::<CommandSenderResource>(&Resource::new_own(command_sender.rep()))
|
||||
.expect("invalid command-sender resource handle");
|
||||
|
||||
match resource.provider.permission_lvl() {
|
||||
pumpkin_util::PermissionLvl::Zero => PermissionLevel::Zero,
|
||||
pumpkin_util::PermissionLvl::One => PermissionLevel::One,
|
||||
pumpkin_util::PermissionLvl::Two => PermissionLevel::Two,
|
||||
pumpkin_util::PermissionLvl::Three => PermissionLevel::Three,
|
||||
pumpkin_util::PermissionLvl::Four => PermissionLevel::Four,
|
||||
}
|
||||
}
|
||||
|
||||
async fn has_permission_level(
|
||||
&mut self,
|
||||
command_sender: Resource<CommandSender>,
|
||||
level: PermissionLevel,
|
||||
) -> bool {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.get::<CommandSenderResource>(&Resource::new_own(command_sender.rep()))
|
||||
.expect("invalid command-sender resource handle");
|
||||
|
||||
let required = match level {
|
||||
PermissionLevel::Zero => pumpkin_util::PermissionLvl::Zero,
|
||||
PermissionLevel::One => pumpkin_util::PermissionLvl::One,
|
||||
PermissionLevel::Two => pumpkin_util::PermissionLvl::Two,
|
||||
PermissionLevel::Three => pumpkin_util::PermissionLvl::Three,
|
||||
PermissionLevel::Four => pumpkin_util::PermissionLvl::Four,
|
||||
};
|
||||
|
||||
resource.provider.permission_lvl() >= required
|
||||
}
|
||||
|
||||
async fn has_permission(
|
||||
&mut self,
|
||||
command_sender: Resource<CommandSender>,
|
||||
server: Resource<Server>,
|
||||
node: String,
|
||||
) -> bool {
|
||||
let sender_resource = self
|
||||
.resource_table
|
||||
.get::<CommandSenderResource>(&Resource::new_own(command_sender.rep()))
|
||||
.expect("invalid command-sender resource handle");
|
||||
|
||||
let server_resource = self
|
||||
.resource_table
|
||||
.get::<crate::plugin::loader::wasm::wasm_host::state::ServerResource>(
|
||||
&Resource::new_own(server.rep()),
|
||||
)
|
||||
.expect("invalid server resource handle");
|
||||
|
||||
sender_resource
|
||||
.provider
|
||||
.has_permission(&server_resource.provider, &node)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn position(&mut self, command_sender: Resource<CommandSender>) -> Option<Position> {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.get::<CommandSenderResource>(&Resource::new_own(command_sender.rep()))
|
||||
.expect("invalid command-sender resource handle");
|
||||
|
||||
resource
|
||||
.provider
|
||||
.position()
|
||||
.map(|pos| (pos.x, pos.y, pos.z))
|
||||
}
|
||||
|
||||
async fn world(&mut self, command_sender: Resource<CommandSender>) -> Option<Resource<World>> {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.get::<CommandSenderResource>(&Resource::new_own(command_sender.rep()))
|
||||
.expect("invalid command-sender resource handle");
|
||||
|
||||
if let Some(world) = resource.provider.world() {
|
||||
Some(
|
||||
self.resource_table
|
||||
.push(
|
||||
crate::plugin::loader::wasm::wasm_host::state::WasmResource {
|
||||
provider: world,
|
||||
},
|
||||
)
|
||||
.map(|r| wasmtime::component::Resource::new_own(r.rep()))
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn get_locale(&mut self, command_sender: Resource<CommandSender>) -> Locale {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.get::<CommandSenderResource>(&Resource::new_own(command_sender.rep()))
|
||||
.expect("invalid command-sender resource handle");
|
||||
|
||||
match resource.provider.get_locale() {
|
||||
pumpkin_util::translation::Locale::AfZa => Locale::AfZa,
|
||||
pumpkin_util::translation::Locale::ArSa => Locale::ArSa,
|
||||
pumpkin_util::translation::Locale::AstEs => Locale::AstEs,
|
||||
pumpkin_util::translation::Locale::AzAz => Locale::AzAz,
|
||||
pumpkin_util::translation::Locale::BaRu => Locale::BaRu,
|
||||
pumpkin_util::translation::Locale::Bar => Locale::Bar,
|
||||
pumpkin_util::translation::Locale::BeBy => Locale::BeBy,
|
||||
pumpkin_util::translation::Locale::BgBg => Locale::BgBg,
|
||||
pumpkin_util::translation::Locale::BrFr => Locale::BrFr,
|
||||
pumpkin_util::translation::Locale::Brb => Locale::Brb,
|
||||
pumpkin_util::translation::Locale::BsBa => Locale::BsBa,
|
||||
pumpkin_util::translation::Locale::CaEs => Locale::CaEs,
|
||||
pumpkin_util::translation::Locale::CsCz => Locale::CsCz,
|
||||
pumpkin_util::translation::Locale::CyGb => Locale::CyGb,
|
||||
pumpkin_util::translation::Locale::DaDk => Locale::DaDk,
|
||||
pumpkin_util::translation::Locale::DeAt => Locale::DeAt,
|
||||
pumpkin_util::translation::Locale::DeCh => Locale::DeCh,
|
||||
pumpkin_util::translation::Locale::DeDe => Locale::DeDe,
|
||||
pumpkin_util::translation::Locale::ElGr => Locale::ElGr,
|
||||
pumpkin_util::translation::Locale::EnAu => Locale::EnAu,
|
||||
pumpkin_util::translation::Locale::EnCa => Locale::EnCa,
|
||||
pumpkin_util::translation::Locale::EnGb => Locale::EnGb,
|
||||
pumpkin_util::translation::Locale::EnNz => Locale::EnNz,
|
||||
pumpkin_util::translation::Locale::EnPt => Locale::EnPt,
|
||||
pumpkin_util::translation::Locale::EnUd => Locale::EnUd,
|
||||
pumpkin_util::translation::Locale::EnUs => Locale::EnUs,
|
||||
pumpkin_util::translation::Locale::Enp => Locale::Enp,
|
||||
pumpkin_util::translation::Locale::Enws => Locale::Enws,
|
||||
pumpkin_util::translation::Locale::EoUy => Locale::EoUy,
|
||||
pumpkin_util::translation::Locale::EsAr => Locale::EsAr,
|
||||
pumpkin_util::translation::Locale::EsCl => Locale::EsCl,
|
||||
pumpkin_util::translation::Locale::EsEc => Locale::EsEc,
|
||||
pumpkin_util::translation::Locale::EsEs => Locale::EsEs,
|
||||
pumpkin_util::translation::Locale::EsMx => Locale::EsMx,
|
||||
pumpkin_util::translation::Locale::EsUy => Locale::EsUy,
|
||||
pumpkin_util::translation::Locale::EsVe => Locale::EsVe,
|
||||
pumpkin_util::translation::Locale::Esan => Locale::Esan,
|
||||
pumpkin_util::translation::Locale::EtEe => Locale::EtEe,
|
||||
pumpkin_util::translation::Locale::EuEs => Locale::EuEs,
|
||||
pumpkin_util::translation::Locale::FaIr => Locale::FaIr,
|
||||
pumpkin_util::translation::Locale::FiFi => Locale::FiFi,
|
||||
pumpkin_util::translation::Locale::FilPh => Locale::FilPh,
|
||||
pumpkin_util::translation::Locale::FoFo => Locale::FoFo,
|
||||
pumpkin_util::translation::Locale::FrCa => Locale::FrCa,
|
||||
pumpkin_util::translation::Locale::FrFr => Locale::FrFr,
|
||||
pumpkin_util::translation::Locale::FraDe => Locale::FraDe,
|
||||
pumpkin_util::translation::Locale::FurIt => Locale::FurIt,
|
||||
pumpkin_util::translation::Locale::FyNl => Locale::FyNl,
|
||||
pumpkin_util::translation::Locale::GaIe => Locale::GaIe,
|
||||
pumpkin_util::translation::Locale::GdGb => Locale::GdGb,
|
||||
pumpkin_util::translation::Locale::GlEs => Locale::GlEs,
|
||||
pumpkin_util::translation::Locale::HawUs => Locale::HawUs,
|
||||
pumpkin_util::translation::Locale::HeIl => Locale::HeIl,
|
||||
pumpkin_util::translation::Locale::HiIn => Locale::HiIn,
|
||||
pumpkin_util::translation::Locale::HrHr => Locale::HrHr,
|
||||
pumpkin_util::translation::Locale::HuHu => Locale::HuHu,
|
||||
pumpkin_util::translation::Locale::HyAm => Locale::HyAm,
|
||||
pumpkin_util::translation::Locale::IdId => Locale::IdId,
|
||||
pumpkin_util::translation::Locale::IgNg => Locale::IgNg,
|
||||
pumpkin_util::translation::Locale::IoEn => Locale::IoEn,
|
||||
pumpkin_util::translation::Locale::IsIs => Locale::IsIs,
|
||||
pumpkin_util::translation::Locale::Isv => Locale::Isv,
|
||||
pumpkin_util::translation::Locale::ItIt => Locale::ItIt,
|
||||
pumpkin_util::translation::Locale::JaJp => Locale::JaJp,
|
||||
pumpkin_util::translation::Locale::JboEn => Locale::JboEn,
|
||||
pumpkin_util::translation::Locale::KaGe => Locale::KaGe,
|
||||
pumpkin_util::translation::Locale::KkKz => Locale::KkKz,
|
||||
pumpkin_util::translation::Locale::KnIn => Locale::KnIn,
|
||||
pumpkin_util::translation::Locale::KoKr => Locale::KoKr,
|
||||
pumpkin_util::translation::Locale::Ksh => Locale::Ksh,
|
||||
pumpkin_util::translation::Locale::KwGb => Locale::KwGb,
|
||||
pumpkin_util::translation::Locale::LaLa => Locale::LaLa,
|
||||
pumpkin_util::translation::Locale::LbLu => Locale::LbLu,
|
||||
pumpkin_util::translation::Locale::LiLi => Locale::LiLi,
|
||||
pumpkin_util::translation::Locale::Lmo => Locale::Lmo,
|
||||
pumpkin_util::translation::Locale::LoLa => Locale::LoLa,
|
||||
pumpkin_util::translation::Locale::LolUs => Locale::LolUs,
|
||||
pumpkin_util::translation::Locale::LtLt => Locale::LtLt,
|
||||
pumpkin_util::translation::Locale::LvLv => Locale::LvLv,
|
||||
pumpkin_util::translation::Locale::Lzh => Locale::Lzh,
|
||||
pumpkin_util::translation::Locale::MkMk => Locale::MkMk,
|
||||
pumpkin_util::translation::Locale::MnMn => Locale::MnMn,
|
||||
pumpkin_util::translation::Locale::MsMy => Locale::MsMy,
|
||||
pumpkin_util::translation::Locale::MtMt => Locale::MtMt,
|
||||
pumpkin_util::translation::Locale::Nah => Locale::Nah,
|
||||
pumpkin_util::translation::Locale::NdsDe => Locale::NdsDe,
|
||||
pumpkin_util::translation::Locale::NlBe => Locale::NlBe,
|
||||
pumpkin_util::translation::Locale::NlNl => Locale::NlNl,
|
||||
pumpkin_util::translation::Locale::NnNo => Locale::NnNo,
|
||||
pumpkin_util::translation::Locale::NoNo => Locale::NoNo,
|
||||
pumpkin_util::translation::Locale::OcFr => Locale::OcFr,
|
||||
pumpkin_util::translation::Locale::Ovd => Locale::Ovd,
|
||||
pumpkin_util::translation::Locale::PlPl => Locale::PlPl,
|
||||
pumpkin_util::translation::Locale::PtBr => Locale::PtBr,
|
||||
pumpkin_util::translation::Locale::PtPt => Locale::PtPt,
|
||||
pumpkin_util::translation::Locale::QyaAa => Locale::QyaAa,
|
||||
pumpkin_util::translation::Locale::RoRo => Locale::RoRo,
|
||||
pumpkin_util::translation::Locale::Rpr => Locale::Rpr,
|
||||
pumpkin_util::translation::Locale::RuRu => Locale::RuRu,
|
||||
pumpkin_util::translation::Locale::RyUa => Locale::RyUa,
|
||||
pumpkin_util::translation::Locale::SahSah => Locale::SahSah,
|
||||
pumpkin_util::translation::Locale::SeNo => Locale::SeNo,
|
||||
pumpkin_util::translation::Locale::SkSk => Locale::SkSk,
|
||||
pumpkin_util::translation::Locale::SlSi => Locale::SlSi,
|
||||
pumpkin_util::translation::Locale::SoSo => Locale::SoSo,
|
||||
pumpkin_util::translation::Locale::SqAl => Locale::SqAl,
|
||||
pumpkin_util::translation::Locale::SrCs => Locale::SrCs,
|
||||
pumpkin_util::translation::Locale::SrSp => Locale::SrSp,
|
||||
pumpkin_util::translation::Locale::SvSe => Locale::SvSe,
|
||||
pumpkin_util::translation::Locale::Sxu => Locale::Sxu,
|
||||
pumpkin_util::translation::Locale::Szl => Locale::Szl,
|
||||
pumpkin_util::translation::Locale::TaIn => Locale::TaIn,
|
||||
pumpkin_util::translation::Locale::ThTh => Locale::ThTh,
|
||||
pumpkin_util::translation::Locale::TlPh => Locale::TlPh,
|
||||
pumpkin_util::translation::Locale::TlhAa => Locale::TlhAa,
|
||||
pumpkin_util::translation::Locale::Tok => Locale::Tok,
|
||||
pumpkin_util::translation::Locale::TrTr => Locale::TrTr,
|
||||
pumpkin_util::translation::Locale::TtRu => Locale::TtRu,
|
||||
pumpkin_util::translation::Locale::UkUa => Locale::UkUa,
|
||||
pumpkin_util::translation::Locale::ValEs => Locale::ValEs,
|
||||
pumpkin_util::translation::Locale::VecIt => Locale::VecIt,
|
||||
pumpkin_util::translation::Locale::ViVn => Locale::ViVn,
|
||||
pumpkin_util::translation::Locale::YiDe => Locale::YiDe,
|
||||
pumpkin_util::translation::Locale::YoNg => Locale::YoNg,
|
||||
pumpkin_util::translation::Locale::ZhCn => Locale::ZhCn,
|
||||
pumpkin_util::translation::Locale::ZhHk => Locale::ZhHk,
|
||||
pumpkin_util::translation::Locale::ZhTw => Locale::ZhTw,
|
||||
pumpkin_util::translation::Locale::ZlmArab => Locale::ZlmArab,
|
||||
}
|
||||
}
|
||||
|
||||
async fn should_receive_feedback(&mut self, command_sender: Resource<CommandSender>) -> bool {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.get::<CommandSenderResource>(&Resource::new_own(command_sender.rep()))
|
||||
.expect("invalid command-sender resource handle");
|
||||
|
||||
resource.provider.should_receive_feedback()
|
||||
}
|
||||
|
||||
async fn should_broadcast_console_to_ops(
|
||||
&mut self,
|
||||
command_sender: Resource<CommandSender>,
|
||||
) -> bool {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.get::<CommandSenderResource>(&Resource::new_own(command_sender.rep()))
|
||||
.expect("invalid command-sender resource handle");
|
||||
|
||||
resource.provider.should_broadcast_console_to_ops()
|
||||
}
|
||||
|
||||
async fn should_track_output(&mut self, command_sender: Resource<CommandSender>) -> bool {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.get::<CommandSenderResource>(&Resource::new_own(command_sender.rep()))
|
||||
.expect("invalid command-sender resource handle");
|
||||
|
||||
resource.provider.should_track_output()
|
||||
}
|
||||
|
||||
async fn drop(&mut self, rep: Resource<CommandSender>) -> wasmtime::Result<()> {
|
||||
self.resource_table
|
||||
.delete::<CommandSenderResource>(Resource::new_own(rep.rep()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl DowncastResourceExt<CommandNodeResource> for Resource<CommandNode> {
|
||||
fn downcast_ref<'a>(&'a self, state: &'a mut PluginHostState) -> &'a CommandNodeResource {
|
||||
state
|
||||
.resource_table
|
||||
.get_any_mut(self.rep())
|
||||
.expect("invalid command-node resource handle")
|
||||
.downcast_ref()
|
||||
.expect("resource type mismatch")
|
||||
}
|
||||
|
||||
fn downcast_mut<'a>(&'a self, state: &'a mut PluginHostState) -> &'a mut CommandNodeResource {
|
||||
state
|
||||
.resource_table
|
||||
.get_any_mut(self.rep())
|
||||
.expect("invalid command-node resource handle")
|
||||
.downcast_mut()
|
||||
.expect("resource type mismatch")
|
||||
}
|
||||
|
||||
fn consume(self, state: &mut PluginHostState) -> CommandNodeResource {
|
||||
state
|
||||
.resource_table
|
||||
.delete(Resource::new_own(self.rep()))
|
||||
.expect("invalid command-node resource handle")
|
||||
}
|
||||
}
|
||||
|
||||
fn bounded_num_argument<T: ToFromNumber + 'static>(
|
||||
state: &mut PluginHostState,
|
||||
name: String,
|
||||
min: Option<T>,
|
||||
max: Option<T>,
|
||||
) -> Resource<CommandNode>
|
||||
where
|
||||
BoundedNumArgumentConsumer<T>: GetClientSideArgParser,
|
||||
{
|
||||
let mut consumer = BoundedNumArgumentConsumer::<T>::new();
|
||||
if let Some(min) = min {
|
||||
consumer = consumer.min(min);
|
||||
}
|
||||
if let Some(max) = max {
|
||||
consumer = consumer.max(max);
|
||||
}
|
||||
state.add_command_node(argument(name, consumer)).unwrap()
|
||||
}
|
||||
|
||||
impl pumpkin::plugin::command::HostCommandNode for PluginHostState {
|
||||
async fn literal(&mut self, name: String) -> Resource<CommandNode> {
|
||||
self.add_command_node(literal(name)).unwrap()
|
||||
}
|
||||
|
||||
async fn argument(&mut self, name: String, arg_type: ArgumentType) -> Resource<CommandNode> {
|
||||
match arg_type {
|
||||
ArgumentType::Bool => self
|
||||
.add_command_node(argument(name, BoolArgConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::Float((min, max)) => bounded_num_argument(self, name, min, max),
|
||||
ArgumentType::Double((min, max)) => bounded_num_argument(self, name, min, max),
|
||||
ArgumentType::Integer((min, max)) => bounded_num_argument(self, name, min, max),
|
||||
ArgumentType::Long((min, max)) => bounded_num_argument(self, name, min, max),
|
||||
ArgumentType::String(string_type) => match string_type {
|
||||
StringType::SingleWord | StringType::Quotable => self
|
||||
.add_command_node(argument(name, SimpleArgConsumer))
|
||||
.unwrap(),
|
||||
StringType::Greedy => self
|
||||
.add_command_node(argument(name, MsgArgConsumer))
|
||||
.unwrap(),
|
||||
},
|
||||
ArgumentType::Entities => self
|
||||
.add_command_node(argument(name, EntitiesArgumentConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::Entity => self
|
||||
.add_command_node(argument(name, EntityArgumentConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::Players | ArgumentType::GameProfile => self
|
||||
.add_command_node(argument(name, PlayersArgumentConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::BlockPos => self
|
||||
.add_command_node(argument(name, BlockPosArgumentConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::Position3d => self
|
||||
.add_command_node(argument(name, Position3DArgumentConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::Position2d => self
|
||||
.add_command_node(argument(name, Position2DArgumentConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::BlockState => self
|
||||
.add_command_node(argument(name, BlockArgumentConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::BlockPredicate => self
|
||||
.add_command_node(argument(name, BlockPredicateArgumentConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::Item => self
|
||||
.add_command_node(argument(name, ItemArgumentConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::ItemPredicate => self
|
||||
.add_command_node(argument(name, ItemPredicateArgumentConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::Component => self
|
||||
.add_command_node(argument(name, TextComponentArgConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::Rotation => self
|
||||
.add_command_node(argument(name, RotationArgumentConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::ResourceLocation | ArgumentType::Resource(_) => self
|
||||
.add_command_node(argument(name, ResourceLocationArgumentConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::EntityAnchor => self
|
||||
.add_command_node(argument(name, EntityAnchorArgumentConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::Gamemode => self
|
||||
.add_command_node(argument(name, GamemodeArgumentConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::Difficulty => self
|
||||
.add_command_node(argument(name, DifficultyArgumentConsumer))
|
||||
.unwrap(),
|
||||
ArgumentType::Time(_) => self
|
||||
.add_command_node(argument(name, TimeArgumentConsumer))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn then(
|
||||
&mut self,
|
||||
self_command_node: Resource<CommandNode>,
|
||||
node: Resource<CommandNode>,
|
||||
) {
|
||||
let child_resource = node.consume(self);
|
||||
let parent_resource = self_command_node.downcast_mut(self);
|
||||
let builder = std::mem::replace(&mut parent_resource.provider, literal(""));
|
||||
parent_resource.provider = builder.then(child_resource.provider);
|
||||
}
|
||||
|
||||
async fn execute_with_handler_id(
|
||||
&mut self,
|
||||
command_node: Resource<CommandNode>,
|
||||
handler_id: u32,
|
||||
) {
|
||||
let plugin = self
|
||||
.plugin
|
||||
.as_ref()
|
||||
.expect("plugin should always be initialized here")
|
||||
.upgrade()
|
||||
.expect("plugin has been dropped");
|
||||
|
||||
let server = self
|
||||
.server
|
||||
.clone()
|
||||
.expect("server should be set before command registration");
|
||||
|
||||
let executor = WasmCommandExecutor {
|
||||
handler_id,
|
||||
plugin,
|
||||
server,
|
||||
};
|
||||
|
||||
let resource = command_node.downcast_mut(self);
|
||||
// Unless we make the native command registration code less convenient to use, this is our best option
|
||||
let builder = std::mem::replace(&mut resource.provider, literal(""));
|
||||
resource.provider = builder.execute(executor);
|
||||
}
|
||||
|
||||
async fn require_with_handler_id(
|
||||
&mut self,
|
||||
_command_node: Resource<CommandNode>,
|
||||
_handler_id: u32,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn drop(&mut self, rep: Resource<CommandNode>) -> wasmtime::Result<()> {
|
||||
self.resource_table
|
||||
.delete::<CommandNodeResource>(Resource::new_own(rep.rep()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
use crate::plugin::loader::wasm::wasm_host::{state::PluginHostState, wit::v0_1_0::pumpkin};
|
||||
|
||||
impl pumpkin::plugin::common::Host for PluginHostState {}
|
||||
119
pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/context.rs
Normal file
119
pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/context.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use wasmtime::component::Resource;
|
||||
|
||||
use crate::plugin::loader::wasm::wasm_host::{
|
||||
state::{CommandResource, ContextResource, PluginHostState},
|
||||
wit::v0_1_0::{
|
||||
events::WasmPluginV0_1_0EventHandler,
|
||||
pumpkin::{
|
||||
self,
|
||||
plugin::{
|
||||
command::Command,
|
||||
context::Context,
|
||||
event::{EventPriority, EventType},
|
||||
server::Server,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
impl pumpkin::plugin::context::Host for PluginHostState {}
|
||||
|
||||
impl pumpkin::plugin::context::HostContext for PluginHostState {
|
||||
async fn drop(&mut self, rep: Resource<Context>) -> wasmtime::Result<()> {
|
||||
let _ = self
|
||||
.resource_table
|
||||
.delete::<ContextResource>(Resource::new_own(rep.rep()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_server(&mut self, context: Resource<Context>) -> Resource<Server> {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.get_any_mut(context.rep())
|
||||
.expect("invalid context resource handle")
|
||||
.downcast_ref::<ContextResource>()
|
||||
.expect("resource type mismatch");
|
||||
let server_provider = resource.provider.server.clone();
|
||||
self.add_server(server_provider)
|
||||
.expect("failed to add server resource")
|
||||
}
|
||||
|
||||
async fn register_event(
|
||||
&mut self,
|
||||
context: Resource<Context>,
|
||||
handler_id: u32,
|
||||
event_type: EventType,
|
||||
event_priority: EventPriority,
|
||||
blocking: bool,
|
||||
) {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.get_any_mut(context.rep())
|
||||
.expect("invalid context resource handle")
|
||||
.downcast_ref::<ContextResource>()
|
||||
.expect("resource type mismatch");
|
||||
|
||||
let priority = match event_priority {
|
||||
EventPriority::Highest => crate::plugin::EventPriority::Highest,
|
||||
EventPriority::High => crate::plugin::EventPriority::High,
|
||||
EventPriority::Normal => crate::plugin::EventPriority::Normal,
|
||||
EventPriority::Low => crate::plugin::EventPriority::Low,
|
||||
EventPriority::Lowest => crate::plugin::EventPriority::Lowest,
|
||||
};
|
||||
|
||||
let plugin = self
|
||||
.plugin
|
||||
.as_ref()
|
||||
.expect("plugin should always be initialized here")
|
||||
.upgrade()
|
||||
.expect("plugin has been dropped");
|
||||
|
||||
let handler = Arc::new(WasmPluginV0_1_0EventHandler { handler_id, plugin });
|
||||
|
||||
match event_type {
|
||||
EventType::PlayerJoinEvent => {
|
||||
resource
|
||||
.provider
|
||||
.register_event::<crate::plugin::player::player_join::PlayerJoinEvent, _>(
|
||||
handler, priority, blocking,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
EventType::PlayerLeaveEvent => {
|
||||
resource
|
||||
.provider
|
||||
.register_event::<crate::plugin::player::player_leave::PlayerLeaveEvent, _>(
|
||||
handler, priority, blocking,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn register_command(
|
||||
&mut self,
|
||||
context: Resource<Context>,
|
||||
command: Resource<Command>,
|
||||
permission: String,
|
||||
) {
|
||||
let command = self
|
||||
.resource_table
|
||||
.delete::<CommandResource>(Resource::new_own(command.rep()))
|
||||
.expect("invalid command resource handle")
|
||||
.provider;
|
||||
|
||||
let context_resource = self
|
||||
.resource_table
|
||||
.get_any_mut(context.rep())
|
||||
.expect("invalid context resource handle")
|
||||
.downcast_ref::<ContextResource>()
|
||||
.expect("resource type mismatch");
|
||||
|
||||
context_resource
|
||||
.provider
|
||||
.register_command(command, permission)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use wasmtime::component::Resource;
|
||||
|
||||
use crate::plugin::loader::wasm::wasm_host::{
|
||||
state::PluginHostState,
|
||||
wit::v0_1_0::pumpkin::{
|
||||
self,
|
||||
plugin::{
|
||||
common::BlockPosition,
|
||||
entity::{BlockEntity, CommandBlockEntity},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
impl pumpkin::plugin::entity::Host for PluginHostState {}
|
||||
|
||||
impl pumpkin::plugin::entity::HostBlockEntity for PluginHostState {
|
||||
async fn resource_location(&mut self, _block_entity: Resource<BlockEntity>) -> String {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_position(&mut self, _block_entity: Resource<BlockEntity>) -> BlockPosition {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_id(&mut self, _block_entity: Resource<BlockEntity>) -> u32 {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn is_dirty(&mut self, _block_entity: Resource<BlockEntity>) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn clear_dirty(&mut self, _block_entity: Resource<BlockEntity>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn drop(&mut self, _rep: Resource<BlockEntity>) -> wasmtime::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl pumpkin::plugin::entity::HostCommandBlockEntity for PluginHostState {
|
||||
async fn get_block_entity(
|
||||
&mut self,
|
||||
_command_block_entity: Resource<CommandBlockEntity>,
|
||||
) -> Resource<BlockEntity> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn last_output(&mut self, _command_block_entity: Resource<CommandBlockEntity>) -> String {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn track_output(&mut self, _command_block_entity: Resource<CommandBlockEntity>) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn success_count(&mut self, _command_block_entity: Resource<CommandBlockEntity>) -> u32 {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn command(&mut self, _command_block_entity: Resource<CommandBlockEntity>) -> String {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn auto(&mut self, _command_block_entity: Resource<CommandBlockEntity>) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn condition_met(&mut self, _command_block_entity: Resource<CommandBlockEntity>) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn powered(&mut self, _command_block_entity: Resource<CommandBlockEntity>) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn drop(&mut self, _rep: Resource<CommandBlockEntity>) -> wasmtime::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
plugin::{
|
||||
BoxFuture, EventHandler, Payload,
|
||||
loader::wasm::wasm_host::{
|
||||
PluginInstance, WasmPlugin,
|
||||
state::PluginHostState,
|
||||
wit::{self, v0_1_0::pumpkin},
|
||||
},
|
||||
},
|
||||
server::Server,
|
||||
};
|
||||
|
||||
pub mod player;
|
||||
|
||||
impl pumpkin::plugin::event::Host for PluginHostState {}
|
||||
|
||||
pub struct WasmPluginV0_1_0EventHandler {
|
||||
pub handler_id: u32,
|
||||
pub plugin: Arc<WasmPlugin>,
|
||||
}
|
||||
|
||||
pub trait ToFromV0_1_0WasmEvent {
|
||||
fn to_v0_1_0_wasm_event(
|
||||
&self,
|
||||
state: &mut PluginHostState,
|
||||
) -> wit::v0_1_0::pumpkin::plugin::event::Event;
|
||||
|
||||
fn from_v0_1_0_wasm_event(
|
||||
event: wit::v0_1_0::pumpkin::plugin::event::Event,
|
||||
state: &mut PluginHostState,
|
||||
) -> Self;
|
||||
}
|
||||
|
||||
impl<E: Payload + ToFromV0_1_0WasmEvent> EventHandler<E> for WasmPluginV0_1_0EventHandler {
|
||||
fn handle<'a>(&'a self, server: &'a Arc<Server>, event: &'a E) -> BoxFuture<'a, ()> {
|
||||
Box::pin(async {
|
||||
let mut store = self.plugin.store.lock().await;
|
||||
let event = event.to_v0_1_0_wasm_event(store.data_mut());
|
||||
match self.plugin.plugin_instance {
|
||||
PluginInstance::V0_1_0(ref plugin) => {
|
||||
let server = store.data_mut().add_server(server.clone()).unwrap();
|
||||
plugin
|
||||
.call_handle_event(&mut *store, self.handler_id, server, event)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_blocking<'a>(
|
||||
&'a self,
|
||||
server: &'a Arc<Server>,
|
||||
event: &'a mut E,
|
||||
) -> BoxFuture<'a, ()> {
|
||||
Box::pin(async {
|
||||
let mut store = self.plugin.store.lock().await;
|
||||
let wasm_event = event.to_v0_1_0_wasm_event(store.data_mut());
|
||||
match self.plugin.plugin_instance {
|
||||
PluginInstance::V0_1_0(ref plugin) => {
|
||||
let server = store.data_mut().add_server(server.clone()).unwrap();
|
||||
let returned_event = plugin
|
||||
.call_handle_event(&mut *store, self.handler_id, server, wasm_event)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
*event = E::from_v0_1_0_wasm_event(returned_event, store.data_mut());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
use wasmtime::component::Resource;
|
||||
|
||||
use crate::plugin::{
|
||||
loader::wasm::wasm_host::{
|
||||
state::{PlayerResource, PluginHostState, TextComponentResource},
|
||||
wit::v0_1_0::{
|
||||
events::ToFromV0_1_0WasmEvent,
|
||||
pumpkin::plugin::event::{Event, PlayerJoinEventData, PlayerLeaveEventData},
|
||||
},
|
||||
},
|
||||
player::{player_join::PlayerJoinEvent, player_leave::PlayerLeaveEvent},
|
||||
};
|
||||
|
||||
impl ToFromV0_1_0WasmEvent for PlayerJoinEvent {
|
||||
fn to_v0_1_0_wasm_event(&self, state: &mut PluginHostState) -> Event {
|
||||
let player_resource = state
|
||||
.add_player(self.player.clone())
|
||||
.expect("failed to add player resource");
|
||||
|
||||
let text_component_resource = state.add_text_component(self.join_message.clone()).unwrap();
|
||||
|
||||
Event::PlayerJoinEvent(PlayerJoinEventData {
|
||||
player: player_resource,
|
||||
join_message: text_component_resource,
|
||||
cancelled: self.cancelled,
|
||||
})
|
||||
}
|
||||
|
||||
fn from_v0_1_0_wasm_event(
|
||||
event: crate::plugin::loader::wasm::wasm_host::wit::v0_1_0::pumpkin::plugin::event::Event,
|
||||
state: &mut PluginHostState,
|
||||
) -> Self {
|
||||
#[allow(clippy::match_wildcard_for_single_variants)]
|
||||
match event {
|
||||
Event::PlayerJoinEvent(data) => {
|
||||
let player_resource = state
|
||||
.resource_table
|
||||
.delete::<PlayerResource>(Resource::new_own(data.player.rep()))
|
||||
.unwrap();
|
||||
|
||||
let text_component_resource = state
|
||||
.resource_table
|
||||
.delete::<TextComponentResource>(Resource::new_own(data.join_message.rep()))
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
player: player_resource.provider,
|
||||
join_message: text_component_resource.provider,
|
||||
cancelled: data.cancelled,
|
||||
}
|
||||
}
|
||||
_ => panic!("unexpected event type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToFromV0_1_0WasmEvent for PlayerLeaveEvent {
|
||||
fn to_v0_1_0_wasm_event(&self, state: &mut PluginHostState) -> Event {
|
||||
let player_resource = state
|
||||
.add_player(self.player.clone())
|
||||
.expect("failed to add player resource");
|
||||
|
||||
let text_component_resource = state
|
||||
.add_text_component(self.leave_message.clone())
|
||||
.unwrap();
|
||||
|
||||
Event::PlayerLeaveEvent(PlayerLeaveEventData {
|
||||
player: player_resource,
|
||||
leave_message: text_component_resource,
|
||||
cancelled: self.cancelled,
|
||||
})
|
||||
}
|
||||
|
||||
fn from_v0_1_0_wasm_event(
|
||||
event: crate::plugin::loader::wasm::wasm_host::wit::v0_1_0::pumpkin::plugin::event::Event,
|
||||
state: &mut PluginHostState,
|
||||
) -> Self {
|
||||
#[allow(clippy::match_wildcard_for_single_variants)]
|
||||
match event {
|
||||
Event::PlayerLeaveEvent(data) => {
|
||||
let player_resource = state
|
||||
.resource_table
|
||||
.delete::<PlayerResource>(Resource::new_own(data.player.rep()))
|
||||
.unwrap();
|
||||
|
||||
let text_component_resource = state
|
||||
.resource_table
|
||||
.delete::<TextComponentResource>(Resource::new_own(data.leave_message.rep()))
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
player: player_resource.provider,
|
||||
leave_message: text_component_resource.provider,
|
||||
cancelled: data.cancelled,
|
||||
}
|
||||
}
|
||||
_ => panic!("unexpected event type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use crate::plugin::loader::wasm::wasm_host::{
|
||||
logging::log_tracing, state::PluginHostState, wit::v0_1_0::pumpkin,
|
||||
};
|
||||
|
||||
impl pumpkin::plugin::logging::Host for PluginHostState {
|
||||
async fn log(&mut self, level: pumpkin::plugin::logging::Level, message: String) {
|
||||
match level {
|
||||
pumpkin::plugin::logging::Level::Trace => tracing::trace!("[plugin] {message}"),
|
||||
pumpkin::plugin::logging::Level::Debug => tracing::debug!("[plugin] {message}"),
|
||||
pumpkin::plugin::logging::Level::Info => tracing::info!("[plugin] {message}"),
|
||||
pumpkin::plugin::logging::Level::Warn => tracing::warn!("[plugin] {message}"),
|
||||
pumpkin::plugin::logging::Level::Error => tracing::error!("[plugin] {message}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn log_tracing(&mut self, event: Vec<u8>) {
|
||||
log_tracing(event).await;
|
||||
}
|
||||
}
|
||||
71
pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/mod.rs
Normal file
71
pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/mod.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use crate::plugin::{
|
||||
PluginMetadata,
|
||||
loader::wasm::wasm_host::{PluginInstance, WasmPlugin, state::PluginHostState},
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use wasmtime::component::{Component, HasData, Linker, bindgen};
|
||||
use wasmtime::{Engine, Store};
|
||||
|
||||
pub mod commands;
|
||||
pub mod common;
|
||||
pub mod context;
|
||||
pub mod entity;
|
||||
pub mod events;
|
||||
pub mod logging;
|
||||
pub mod player;
|
||||
pub mod server;
|
||||
pub mod text;
|
||||
pub mod world;
|
||||
|
||||
bindgen!({
|
||||
path: "../pumpkin-plugin-wit/v0.1.0",
|
||||
world: "plugin",
|
||||
imports: { default: async },
|
||||
exports: { default: async },
|
||||
});
|
||||
|
||||
struct PluginHostComponent;
|
||||
|
||||
impl HasData for PluginHostComponent {
|
||||
type Data<'a> = &'a mut PluginHostState;
|
||||
}
|
||||
|
||||
pub fn setup_linker(engine: &Engine) -> wasmtime::Result<Linker<PluginHostState>> {
|
||||
let mut linker = Linker::new(engine);
|
||||
wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
|
||||
Plugin::add_to_linker::<_, PluginHostComponent>(&mut linker, |state: &mut PluginHostState| {
|
||||
state
|
||||
})?;
|
||||
Ok(linker)
|
||||
}
|
||||
|
||||
pub async fn init_plugin(
|
||||
engine: &Engine,
|
||||
linker: &Linker<PluginHostState>,
|
||||
component: Component,
|
||||
) -> wasmtime::Result<(WasmPlugin, PluginMetadata)> {
|
||||
let mut store = Store::new(engine, PluginHostState::new());
|
||||
let plugin = Plugin::instantiate_async(&mut store, &component, linker).await?;
|
||||
|
||||
plugin.call_init_plugin(&mut store).await?;
|
||||
|
||||
let metadata = plugin
|
||||
.pumpkin_plugin_metadata()
|
||||
.call_get_metadata(&mut store)
|
||||
.await?;
|
||||
|
||||
let metadata = PluginMetadata {
|
||||
name: metadata.name,
|
||||
version: metadata.version,
|
||||
authors: metadata.authors,
|
||||
description: metadata.description,
|
||||
};
|
||||
|
||||
Ok((
|
||||
WasmPlugin {
|
||||
plugin_instance: PluginInstance::V0_1_0(plugin),
|
||||
store: Mutex::new(store),
|
||||
},
|
||||
metadata,
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use wasmtime::component::Resource;
|
||||
|
||||
use crate::plugin::loader::wasm::wasm_host::{
|
||||
state::{PlayerResource, PluginHostState},
|
||||
wit::v0_1_0::pumpkin::{self, plugin::player::Player},
|
||||
};
|
||||
|
||||
impl pumpkin::plugin::player::Host for PluginHostState {}
|
||||
impl pumpkin::plugin::player::HostPlayer for PluginHostState {
|
||||
async fn drop(&mut self, rep: Resource<Player>) -> wasmtime::Result<()> {
|
||||
let _ = self
|
||||
.resource_table
|
||||
.delete::<PlayerResource>(Resource::new_own(rep.rep()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_id(&mut self, player: Resource<Player>) -> String {
|
||||
let resource = self
|
||||
.resource_table
|
||||
.get_any_mut(player.rep())
|
||||
.expect("invalid player resource handle")
|
||||
.downcast_ref::<PlayerResource>()
|
||||
.expect("resource type mismatch");
|
||||
resource.provider.gameprofile.id.to_string()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use wasmtime::component::Resource;
|
||||
|
||||
use crate::plugin::loader::wasm::wasm_host::{
|
||||
state::{PluginHostState, ServerResource},
|
||||
wit::v0_1_0::pumpkin::{
|
||||
self,
|
||||
plugin::server::{Difficulty, Server},
|
||||
},
|
||||
};
|
||||
|
||||
impl pumpkin::plugin::server::Host for PluginHostState {}
|
||||
|
||||
impl pumpkin::plugin::server::HostServer for PluginHostState {
|
||||
async fn drop(&mut self, rep: Resource<Server>) -> wasmtime::Result<()> {
|
||||
let _ = self
|
||||
.resource_table
|
||||
.delete::<ServerResource>(Resource::new_own(rep.rep()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_difficulty(&mut self, server: Resource<Server>) -> Difficulty {
|
||||
let resource: &ServerResource = self
|
||||
.resource_table
|
||||
.get_any_mut(server.rep())
|
||||
.expect("invalid server resource handle")
|
||||
.downcast_ref::<ServerResource>()
|
||||
.expect("resource type mismatch");
|
||||
|
||||
match resource.provider.get_difficulty() {
|
||||
pumpkin_util::Difficulty::Peaceful => Difficulty::Peaceful,
|
||||
pumpkin_util::Difficulty::Easy => Difficulty::Easy,
|
||||
pumpkin_util::Difficulty::Normal => Difficulty::Normal,
|
||||
pumpkin_util::Difficulty::Hard => Difficulty::Hard,
|
||||
}
|
||||
}
|
||||
}
|
||||
292
pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/text.rs
Normal file
292
pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/text.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use wasmtime::component::Resource;
|
||||
|
||||
use crate::plugin::loader::wasm::wasm_host::{
|
||||
DowncastResourceExt,
|
||||
state::{PluginHostState, TextComponentResource},
|
||||
wit::v0_1_0::pumpkin::{
|
||||
self,
|
||||
plugin::text::{ArgbColor, NamedColor, RgbColor, TextComponent},
|
||||
},
|
||||
};
|
||||
|
||||
use pumpkin_util::text::{
|
||||
click::ClickEvent,
|
||||
color::{self, Color},
|
||||
hover::HoverEvent,
|
||||
};
|
||||
|
||||
impl pumpkin::plugin::text::Host for PluginHostState {}
|
||||
|
||||
// TODO - Change the pumpkin_util::text::TextComponent to use &mut self instead of self for the builder pattern.
|
||||
// right now we have to do a bunch of cloning due to the fact that the builder pattern doesn't accept &mut self.
|
||||
impl DowncastResourceExt<TextComponentResource> for Resource<TextComponent> {
|
||||
fn downcast_ref<'a>(&'a self, state: &'a mut PluginHostState) -> &'a TextComponentResource {
|
||||
state
|
||||
.resource_table
|
||||
.get_any_mut(self.rep())
|
||||
.expect("invalid text-component resource handle")
|
||||
.downcast_ref::<TextComponentResource>()
|
||||
.expect("resource type mismatch")
|
||||
}
|
||||
|
||||
fn downcast_mut<'a>(&'a self, state: &'a mut PluginHostState) -> &'a mut TextComponentResource {
|
||||
state
|
||||
.resource_table
|
||||
.get_any_mut(self.rep())
|
||||
.expect("invalid text-component resource handle")
|
||||
.downcast_mut::<TextComponentResource>()
|
||||
.expect("resource type mismatch")
|
||||
}
|
||||
|
||||
fn consume(self, state: &mut PluginHostState) -> TextComponentResource {
|
||||
state
|
||||
.resource_table
|
||||
.delete::<TextComponentResource>(Resource::new_own(self.rep()))
|
||||
.expect("invalid text-component resource handle")
|
||||
}
|
||||
}
|
||||
|
||||
const fn map_named_color(color: NamedColor) -> color::NamedColor {
|
||||
match color {
|
||||
NamedColor::Black => color::NamedColor::Black,
|
||||
NamedColor::DarkBlue => color::NamedColor::DarkBlue,
|
||||
NamedColor::DarkGreen => color::NamedColor::DarkGreen,
|
||||
NamedColor::DarkAqua => color::NamedColor::DarkAqua,
|
||||
NamedColor::DarkRed => color::NamedColor::DarkRed,
|
||||
NamedColor::DarkPurple => color::NamedColor::DarkPurple,
|
||||
NamedColor::Gold => color::NamedColor::Gold,
|
||||
NamedColor::Gray => color::NamedColor::Gray,
|
||||
NamedColor::DarkGray => color::NamedColor::DarkGray,
|
||||
NamedColor::Blue => color::NamedColor::Blue,
|
||||
NamedColor::Green => color::NamedColor::Green,
|
||||
NamedColor::Aqua => color::NamedColor::Aqua,
|
||||
NamedColor::Red => color::NamedColor::Red,
|
||||
NamedColor::LightPurple => color::NamedColor::LightPurple,
|
||||
NamedColor::Yellow => color::NamedColor::Yellow,
|
||||
NamedColor::White => color::NamedColor::White,
|
||||
}
|
||||
}
|
||||
|
||||
impl pumpkin::plugin::text::HostTextComponent for PluginHostState {
|
||||
async fn text(&mut self, plain: String) -> Resource<TextComponent> {
|
||||
let tc = pumpkin_util::text::TextComponent::text(plain);
|
||||
self.add_text_component(tc).unwrap()
|
||||
}
|
||||
|
||||
async fn translate(
|
||||
&mut self,
|
||||
key: String,
|
||||
with: Vec<Resource<TextComponent>>,
|
||||
) -> Resource<TextComponent> {
|
||||
let with: Vec<pumpkin_util::text::TextComponent> =
|
||||
with.into_iter().map(|r| r.consume(self).provider).collect();
|
||||
let tc = pumpkin_util::text::TextComponent::translate(key, with);
|
||||
self.add_text_component(tc).unwrap()
|
||||
}
|
||||
|
||||
async fn add_child(
|
||||
&mut self,
|
||||
text_component: Resource<TextComponent>,
|
||||
child: Resource<TextComponent>,
|
||||
) {
|
||||
let child = child.consume(self).provider;
|
||||
let parent = &mut text_component.downcast_mut(self).provider;
|
||||
*parent = parent.clone().add_child(child);
|
||||
}
|
||||
|
||||
async fn add_text(&mut self, text_component: Resource<TextComponent>, text: String) {
|
||||
let parent = &mut text_component.downcast_mut(self).provider;
|
||||
*parent = parent.clone().add_text(text);
|
||||
}
|
||||
|
||||
async fn get_text(&mut self, text_component: Resource<TextComponent>) -> String {
|
||||
text_component
|
||||
.downcast_ref(self)
|
||||
.provider
|
||||
.clone()
|
||||
.get_text()
|
||||
}
|
||||
|
||||
async fn encode(&mut self, text_component: Resource<TextComponent>) -> Vec<u8> {
|
||||
text_component
|
||||
.downcast_ref(self)
|
||||
.provider
|
||||
.encode()
|
||||
.into_vec()
|
||||
}
|
||||
|
||||
async fn color_named(&mut self, text_component: Resource<TextComponent>, color: NamedColor) {
|
||||
text_component.downcast_mut(self).provider.0.style.color =
|
||||
Some(Color::Named(map_named_color(color)));
|
||||
}
|
||||
|
||||
async fn color_rgb(&mut self, text_component: Resource<TextComponent>, color: RgbColor) {
|
||||
text_component.downcast_mut(self).provider.0.style.color =
|
||||
Some(Color::Rgb(color::RGBColor::new(color.r, color.g, color.b)));
|
||||
}
|
||||
|
||||
async fn bold(&mut self, text_component: Resource<TextComponent>, value: bool) {
|
||||
text_component.downcast_mut(self).provider.0.style.bold = Some(value);
|
||||
}
|
||||
|
||||
async fn italic(&mut self, text_component: Resource<TextComponent>, value: bool) {
|
||||
text_component.downcast_mut(self).provider.0.style.italic = Some(value);
|
||||
}
|
||||
|
||||
async fn underlined(&mut self, text_component: Resource<TextComponent>, value: bool) {
|
||||
text_component
|
||||
.downcast_mut(self)
|
||||
.provider
|
||||
.0
|
||||
.style
|
||||
.underlined = Some(value);
|
||||
}
|
||||
|
||||
async fn strikethrough(&mut self, text_component: Resource<TextComponent>, value: bool) {
|
||||
text_component
|
||||
.downcast_mut(self)
|
||||
.provider
|
||||
.0
|
||||
.style
|
||||
.strikethrough = Some(value);
|
||||
}
|
||||
|
||||
async fn obfuscated(&mut self, text_component: Resource<TextComponent>, value: bool) {
|
||||
text_component
|
||||
.downcast_mut(self)
|
||||
.provider
|
||||
.0
|
||||
.style
|
||||
.obfuscated = Some(value);
|
||||
}
|
||||
|
||||
async fn insertion(&mut self, text_component: Resource<TextComponent>, text: String) {
|
||||
text_component.downcast_mut(self).provider.0.style.insertion = Some(text);
|
||||
}
|
||||
|
||||
async fn font(&mut self, text_component: Resource<TextComponent>, font: String) {
|
||||
text_component.downcast_mut(self).provider.0.style.font = Some(font);
|
||||
}
|
||||
|
||||
async fn shadow_color(&mut self, text_component: Resource<TextComponent>, color: ArgbColor) {
|
||||
text_component
|
||||
.downcast_mut(self)
|
||||
.provider
|
||||
.0
|
||||
.style
|
||||
.shadow_color = Some(color::ARGBColor::new(color.a, color.r, color.g, color.b));
|
||||
}
|
||||
|
||||
async fn click_open_url(&mut self, text_component: Resource<TextComponent>, url: String) {
|
||||
text_component
|
||||
.downcast_mut(self)
|
||||
.provider
|
||||
.0
|
||||
.style
|
||||
.click_event = Some(ClickEvent::OpenUrl {
|
||||
url: Cow::Owned(url),
|
||||
});
|
||||
}
|
||||
|
||||
async fn click_run_command(
|
||||
&mut self,
|
||||
text_component: Resource<TextComponent>,
|
||||
command: String,
|
||||
) {
|
||||
text_component
|
||||
.downcast_mut(self)
|
||||
.provider
|
||||
.0
|
||||
.style
|
||||
.click_event = Some(ClickEvent::RunCommand {
|
||||
command: Cow::Owned(command),
|
||||
});
|
||||
}
|
||||
|
||||
async fn click_suggest_command(
|
||||
&mut self,
|
||||
text_component: Resource<TextComponent>,
|
||||
command: String,
|
||||
) {
|
||||
text_component
|
||||
.downcast_mut(self)
|
||||
.provider
|
||||
.0
|
||||
.style
|
||||
.click_event = Some(ClickEvent::SuggestCommand {
|
||||
command: Cow::Owned(command),
|
||||
});
|
||||
}
|
||||
|
||||
async fn click_copy_to_clipboard(
|
||||
&mut self,
|
||||
text_component: Resource<TextComponent>,
|
||||
text: String,
|
||||
) {
|
||||
text_component
|
||||
.downcast_mut(self)
|
||||
.provider
|
||||
.0
|
||||
.style
|
||||
.click_event = Some(ClickEvent::CopyToClipboard {
|
||||
value: Cow::Owned(text),
|
||||
});
|
||||
}
|
||||
|
||||
async fn hover_show_text(
|
||||
&mut self,
|
||||
text_component: Resource<TextComponent>,
|
||||
text: Resource<TextComponent>,
|
||||
) {
|
||||
let hover_tc = text.consume(self).provider;
|
||||
text_component
|
||||
.downcast_mut(self)
|
||||
.provider
|
||||
.0
|
||||
.style
|
||||
.hover_event = Some(HoverEvent::ShowText {
|
||||
value: vec![hover_tc.0],
|
||||
});
|
||||
}
|
||||
|
||||
async fn hover_show_item(&mut self, text_component: Resource<TextComponent>, item: String) {
|
||||
text_component
|
||||
.downcast_mut(self)
|
||||
.provider
|
||||
.0
|
||||
.style
|
||||
.hover_event = Some(HoverEvent::ShowItem {
|
||||
id: Cow::Owned(item),
|
||||
count: None,
|
||||
});
|
||||
}
|
||||
|
||||
async fn hover_show_entity(
|
||||
&mut self,
|
||||
text_component: Resource<TextComponent>,
|
||||
entity_type: String,
|
||||
id: String,
|
||||
name: Option<Resource<TextComponent>>,
|
||||
) {
|
||||
let name = name.map(|r| vec![r.consume(self).provider.0]);
|
||||
text_component
|
||||
.downcast_mut(self)
|
||||
.provider
|
||||
.0
|
||||
.style
|
||||
.hover_event = Some(HoverEvent::ShowEntity {
|
||||
id: Cow::Owned(entity_type),
|
||||
uuid: Cow::Owned(id),
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
async fn drop(&mut self, rep: Resource<TextComponent>) -> wasmtime::Result<()> {
|
||||
let _ = self
|
||||
.resource_table
|
||||
.delete::<TextComponentResource>(Resource::new_own(rep.rep()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
18
pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/world.rs
Normal file
18
pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1_0/world.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use wasmtime::component::Resource;
|
||||
|
||||
use crate::plugin::loader::wasm::wasm_host::{
|
||||
state::PluginHostState,
|
||||
wit::v0_1_0::pumpkin::{self, plugin::world::World},
|
||||
};
|
||||
|
||||
impl pumpkin::plugin::world::Host for PluginHostState {}
|
||||
|
||||
impl pumpkin::plugin::world::HostWorld for PluginHostState {
|
||||
async fn get_id(&mut self, _world: Resource<World>) -> String {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn drop(&mut self, _rep: Resource<World>) -> wasmtime::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use tracing::{error, info};
|
||||
pub mod api;
|
||||
pub mod loader;
|
||||
|
||||
use crate::{LOGGER_IMPL, server::Server};
|
||||
use crate::{LOGGER_IMPL, plugin::loader::wasm::WasmPluginLoader, server::Server};
|
||||
pub use api::*;
|
||||
|
||||
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||
@@ -176,7 +176,7 @@ pub struct PluginManager {
|
||||
/// OS specific issues
|
||||
/// - Windows: Plugin cannot be unloaded, it can be only active or not
|
||||
struct LoadedPlugin {
|
||||
metadata: PluginMetadata<'static>,
|
||||
metadata: PluginMetadata,
|
||||
instance: Option<Box<dyn Plugin>>,
|
||||
loader: Arc<dyn PluginLoader>,
|
||||
loader_data: Option<Box<dyn Any + Send + Sync>>,
|
||||
@@ -207,7 +207,10 @@ impl Default for PluginManager {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
plugins: RwLock::new(Vec::new()),
|
||||
loaders: RwLock::new(vec![Arc::new(NativePluginLoader)]),
|
||||
loaders: RwLock::new(vec![
|
||||
Arc::new(NativePluginLoader),
|
||||
Arc::new(WasmPluginLoader),
|
||||
]),
|
||||
server: RwLock::new(None),
|
||||
handlers: Arc::new(RwLock::new(HashMap::new())),
|
||||
unloaded_files: RwLock::new(HashSet::new()),
|
||||
@@ -233,7 +236,7 @@ impl PluginManager {
|
||||
plugins
|
||||
.iter()
|
||||
.filter(|p| p.is_active)
|
||||
.map(|p| p.metadata.name.to_string())
|
||||
.map(|p| p.metadata.name.clone())
|
||||
.collect()
|
||||
};
|
||||
|
||||
@@ -309,8 +312,9 @@ impl PluginManager {
|
||||
}
|
||||
|
||||
// Start loading plugin concurrently
|
||||
if let Ok(task) = self.start_loading_plugin(&path).await {
|
||||
load_tasks.push(task);
|
||||
match self.start_loading_plugin(&path).await {
|
||||
Ok(task) => load_tasks.push(task),
|
||||
Err(err) => error!("{}", err),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,7 +338,7 @@ impl PluginManager {
|
||||
self.plugin_states
|
||||
.write()
|
||||
.await
|
||||
.insert(metadata.name.to_string(), PluginState::Loading);
|
||||
.insert(metadata.name.clone(), PluginState::Loading);
|
||||
|
||||
let self_ref = self
|
||||
.self_ref
|
||||
@@ -380,7 +384,7 @@ impl PluginManager {
|
||||
// Spawn async task for plugin initialization
|
||||
let self_ref_clone = Arc::clone(&self_ref);
|
||||
let state_notify = Arc::clone(&self.state_notify);
|
||||
let plugin_name = metadata.name.to_string();
|
||||
let plugin_name = metadata.name.clone();
|
||||
let loader_clone = loader.clone();
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
@@ -503,7 +507,7 @@ impl PluginManager {
|
||||
|
||||
/// Get list of active plugins
|
||||
#[must_use]
|
||||
pub async fn active_plugins(&self) -> Vec<PluginMetadata<'static>> {
|
||||
pub async fn active_plugins(&self) -> Vec<PluginMetadata> {
|
||||
let plugins = self.plugins.read().await;
|
||||
plugins
|
||||
.iter()
|
||||
@@ -521,7 +525,7 @@ impl PluginManager {
|
||||
|
||||
/// Get list of loaded plugins
|
||||
#[must_use]
|
||||
pub async fn loaded_plugins(&self) -> Vec<PluginMetadata<'static>> {
|
||||
pub async fn loaded_plugins(&self) -> Vec<PluginMetadata> {
|
||||
let plugins = self.plugins.read().await;
|
||||
plugins.iter().map(|p| p.metadata.clone()).collect()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user