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:
Bjorn Beishline
2026-03-09 10:12:48 -07:00
committed by GitHub
parent 7e1e411993
commit 576d76dfd6
47 changed files with 4826 additions and 89 deletions

1684
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

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

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

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

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

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

View 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,
}
}

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

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

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

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

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

View File

@@ -0,0 +1,5 @@
interface player {
resource player {
get-id: func () -> string;
}
}

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

View File

@@ -0,0 +1,12 @@
interface server {
enum difficulty {
peaceful,
easy,
normal,
hard,
}
resource server {
get-difficulty: func() -> difficulty;
}
}

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

View File

@@ -0,0 +1,5 @@
interface %world {
resource %world {
get-id: func () -> string;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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>,
}

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

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

View 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,
}
}
}

View File

@@ -0,0 +1 @@
pub mod v0_1_0;

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

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

View File

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