mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat: add wasm plugin api (#1675)
* feat: wasm plugin api base * fix: make metadata macro work * chore: fix clippy lints * Implement guest registering of events * chore: remove hardcoded v0.1.0 dep in wasm_host * feat: add events to wasm * feat: add caching of components * fix: clippy lints * chore: update Cargo.lock * fix: add feature flag for link section * feat: add base for command wit definitions * feat: implement arg variants for command wit interface * feat: add commented out command-tree arg variant * feat: add wit definitions for text-component and command tree Introduces the text interface with a builder-pattern resource, argument-type variant, command-node resource, and register-command on context. Migrates event and command-sender to use the new text-component resource. * feat: add stubs for wasm host * chore: update wasmtime to v42 * feat: implement three methods of wit textcomponent * Add consume function * feat: implement all text-component host methods Replace todo!() stubs with working implementations for all HostTextComponent trait methods: style setters, click events, hover events, add_text, get_text, and encode. * feat: allow events to be modified * feat: add some of base for commands * chore: fix spelling mistake * chore: remove unused dep * feat: implement all argument-type to consumer mappings * Add command exports * log plugin load errors * Some commands work * Some more Commands work * Update how commands are registered * Implement locale * Use handler id with require * chore: fix some warnings * Add PlayerLeaveEvent * chore: fix all clippy warnings * fix: add more descriptive error message --------- Co-authored-by: Purdze <r.s.sutton@hotmail.co.uk> Co-authored-by: Alexander Medvedev <lilalexmed@proton.me>
This commit is contained in:
14
pumpkin-plugin-api/Cargo.toml
Normal file
14
pumpkin-plugin-api/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "pumpkin-plugin-api"
|
||||
# This should be different then the pumpkin server verion
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
wit-bindgen = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
postcard = { workspace = true }
|
||||
tracing-serde-structured = { workspace = true }
|
||||
|
||||
[package.metadata.component]
|
||||
target = { path = "../pumpkin-plugin-wit" }
|
||||
59
pumpkin-plugin-api/src/commands.rs
Normal file
59
pumpkin-plugin-api/src/commands.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
sync::{
|
||||
Mutex,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
pub use crate::wit::pumpkin::plugin::command::Command;
|
||||
use crate::{
|
||||
Result, Server,
|
||||
command::CommandNode,
|
||||
wit::pumpkin::plugin::command::{CommandError, CommandSender, ConsumedArgs},
|
||||
};
|
||||
|
||||
pub(crate) static NEXT_COMMAND_ID: AtomicU32 = AtomicU32::new(0);
|
||||
pub(crate) static COMMAND_HANDLERS: Mutex<BTreeMap<u32, Box<dyn CommandHandler>>> =
|
||||
Mutex::new(BTreeMap::new());
|
||||
|
||||
pub trait CommandHandler: Send + Sync {
|
||||
fn handle(
|
||||
&self,
|
||||
sender: CommandSender,
|
||||
server: Server,
|
||||
args: ConsumedArgs,
|
||||
) -> Result<i32, CommandError>;
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Registers a command handler with the plugin.
|
||||
pub fn execute<H: CommandHandler + Send + Sync + 'static>(self, handler: H) -> Command {
|
||||
let id = NEXT_COMMAND_ID.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
COMMAND_HANDLERS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id, Box::new(handler));
|
||||
|
||||
self.execute_with_handler_id(id);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandNode {
|
||||
/// Registers a command handler with the plugin.
|
||||
pub fn execute<H: CommandHandler + Send + Sync + 'static>(self, handler: H) -> CommandNode {
|
||||
let id = NEXT_COMMAND_ID.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
COMMAND_HANDLERS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id, Box::new(handler));
|
||||
|
||||
self.execute_with_handler_id(id);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
107
pumpkin-plugin-api/src/events.rs
Normal file
107
pumpkin-plugin-api/src/events.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
Mutex,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
pub use crate::wit::pumpkin::plugin::event::{
|
||||
Event, EventPriority, PlayerJoinEventData, PlayerLeaveEventData,
|
||||
};
|
||||
use crate::{Context, Result, Server, wit::pumpkin::plugin::event::EventType};
|
||||
|
||||
pub(crate) static NEXT_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
|
||||
pub(crate) static EVENT_HANDLERS: Mutex<BTreeMap<u32, Box<dyn ErasedEventHandler>>> =
|
||||
Mutex::new(BTreeMap::new());
|
||||
|
||||
pub trait FromIntoEvent: Sized {
|
||||
const EVENT_TYPE: EventType;
|
||||
|
||||
fn from_event(event: Event) -> Self;
|
||||
fn into_event(self) -> Event;
|
||||
}
|
||||
|
||||
impl FromIntoEvent for PlayerJoinEventData {
|
||||
const EVENT_TYPE: EventType = EventType::PlayerJoinEvent;
|
||||
|
||||
fn from_event(event: Event) -> Self {
|
||||
match event {
|
||||
Event::PlayerJoinEvent(data) => data,
|
||||
_ => panic!("unexpected event"),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_event(self) -> Event {
|
||||
Event::PlayerJoinEvent(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIntoEvent for PlayerLeaveEventData {
|
||||
const EVENT_TYPE: EventType = EventType::PlayerLeaveEvent;
|
||||
|
||||
fn from_event(event: Event) -> Self {
|
||||
match event {
|
||||
Event::PlayerLeaveEvent(data) => data,
|
||||
_ => panic!("unexpected event"),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_event(self) -> Event {
|
||||
Event::PlayerLeaveEvent(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||
pub trait EventHandler<E> {
|
||||
fn handle(&self, server: Server, event: E) -> E;
|
||||
}
|
||||
|
||||
pub(crate) trait ErasedEventHandler: Send + Sync {
|
||||
fn handle_erased(&self, server: Server, event: Event) -> Event;
|
||||
}
|
||||
|
||||
struct HandlerWrapper<E, H> {
|
||||
handler: H,
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<E: FromIntoEvent + Send + Sync, H: EventHandler<E> + Send + Sync> ErasedEventHandler
|
||||
for HandlerWrapper<E, H>
|
||||
{
|
||||
fn handle_erased(&self, server: Server, event: Event) -> Event {
|
||||
let specific_event = E::from_event(event);
|
||||
self.handler.handle(server, specific_event).into_event()
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Registers an event handler with the plugin.
|
||||
///
|
||||
/// The handler must implement the [`EventHandler`] trait.
|
||||
/// If the event is blocking, returning an event from the handler will modify the event.
|
||||
pub fn register_event_handler<
|
||||
E: FromIntoEvent + Send + Sync + 'static,
|
||||
H: EventHandler<E> + Send + Sync + 'static,
|
||||
>(
|
||||
&self,
|
||||
handler: H,
|
||||
event_priority: EventPriority,
|
||||
blocking: bool,
|
||||
) -> Result<u32> {
|
||||
let id = NEXT_HANDLER_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let wrapped = HandlerWrapper {
|
||||
handler,
|
||||
_phantom: PhantomData::<E>,
|
||||
};
|
||||
EVENT_HANDLERS
|
||||
.lock()
|
||||
.map_err(|e| e.to_string())?
|
||||
.insert(id, Box::new(wrapped));
|
||||
|
||||
self.register_event(id, E::EVENT_TYPE, event_priority, blocking);
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
141
pumpkin-plugin-api/src/lib.rs
Normal file
141
pumpkin-plugin-api/src/lib.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use crate::{
|
||||
commands::COMMAND_HANDLERS, events::EVENT_HANDLERS, logging::WitSubscriber, text::TextComponent,
|
||||
};
|
||||
|
||||
pub mod commands;
|
||||
pub mod events;
|
||||
|
||||
pub mod command {
|
||||
pub use crate::wit::pumpkin::plugin::command::{
|
||||
Command, CommandError, CommandNode, CommandSender, ConsumedArgs,
|
||||
};
|
||||
}
|
||||
|
||||
pub use wit::pumpkin::plugin::{
|
||||
context::{Context, Server},
|
||||
text,
|
||||
};
|
||||
|
||||
pub mod logging;
|
||||
|
||||
mod wit {
|
||||
wit_bindgen::generate!({
|
||||
skip: ["init-plugin"],
|
||||
path: "../pumpkin-plugin-wit/v0.1.0",
|
||||
world: "plugin",
|
||||
});
|
||||
|
||||
use super::Component;
|
||||
export!(Component);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[unsafe(link_section = "pumpkin:api-version")]
|
||||
#[used]
|
||||
static API_VERSION: [u8; 5] = *b"0.1.0";
|
||||
|
||||
struct Component;
|
||||
pub struct PluginMetadata {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub authors: Vec<String>,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl wit::exports::pumpkin::plugin::metadata::Guest for Component {
|
||||
fn get_metadata() -> wit::exports::pumpkin::plugin::metadata::PluginMetadata {
|
||||
let metadata = plugin().metadata();
|
||||
wit::exports::pumpkin::plugin::metadata::PluginMetadata {
|
||||
name: metadata.name,
|
||||
version: metadata.version,
|
||||
authors: metadata.authors,
|
||||
description: metadata.description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl wit::Guest for Component {
|
||||
fn on_load(context: Context) -> Result<(), String> {
|
||||
plugin().on_load(context)
|
||||
}
|
||||
|
||||
fn on_unload(context: Context) -> Result<(), String> {
|
||||
plugin().on_unload(context)
|
||||
}
|
||||
|
||||
fn handle_event(event_id: u32, server: Server, event: events::Event) -> events::Event {
|
||||
let handlers = EVENT_HANDLERS.lock().unwrap();
|
||||
if let Some(handler) = handlers.get(&event_id) {
|
||||
handler.handle_erased(server, event)
|
||||
} else {
|
||||
event
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_command(
|
||||
command_id: u32,
|
||||
sender: command::CommandSender,
|
||||
server: Server,
|
||||
args: command::ConsumedArgs,
|
||||
) -> Result<i32, command::CommandError> {
|
||||
let handlers = COMMAND_HANDLERS.lock().unwrap();
|
||||
if let Some(handler) = handlers.get(&command_id) {
|
||||
handler.handle(sender, server, args)
|
||||
} else {
|
||||
Err(command::CommandError::CommandFailed(TextComponent::text(
|
||||
&format!("no handler registered for command id {command_id}"),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T, E = String> = core::result::Result<T, E>;
|
||||
|
||||
/// The trait that every Pumpkin plugin must implement.
|
||||
pub trait Plugin: Send + Sync {
|
||||
/// Create a new instance of the plugin.
|
||||
fn new() -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Define the metadata for the plugin.
|
||||
fn metadata(&self) -> PluginMetadata;
|
||||
|
||||
/// Called when the plugin is loaded by the server.
|
||||
fn on_load(&mut self, _context: Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Called when the plugin is unloaded by the server.
|
||||
fn on_unload(&mut self, _context: Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn register_plugin(build_plugin: fn() -> Box<dyn Plugin>) {
|
||||
let _ = tracing::subscriber::set_global_default(WitSubscriber::new());
|
||||
unsafe { PLUGIN = Some((build_plugin)()) }
|
||||
}
|
||||
|
||||
fn plugin() -> &'static mut dyn Plugin {
|
||||
#[expect(static_mut_refs)]
|
||||
unsafe {
|
||||
PLUGIN.as_deref_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
static mut PLUGIN: Option<Box<dyn Plugin>> = None;
|
||||
|
||||
/// Registers the provided type as a Pumpkin plugin.
|
||||
///
|
||||
/// The type must implement the [`Plugin`] trait.
|
||||
#[macro_export]
|
||||
macro_rules! register_plugin {
|
||||
($plugin_type:ty) => {
|
||||
#[unsafe(export_name = "init-plugin")]
|
||||
pub extern "C" fn __init_plugin() {
|
||||
$crate::register_plugin(|| Box::new(<$plugin_type as $crate::Plugin>::new()));
|
||||
}
|
||||
};
|
||||
}
|
||||
68
pumpkin-plugin-api/src/logging.rs
Normal file
68
pumpkin-plugin-api/src/logging.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use tracing_serde_structured::AsSerde;
|
||||
|
||||
use crate::wit;
|
||||
|
||||
pub(crate) struct WitSubscriber {
|
||||
next_id: AtomicU64,
|
||||
}
|
||||
|
||||
impl WitSubscriber {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
next_id: AtomicU64::new(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl tracing::Subscriber for WitSubscriber {
|
||||
fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn new_span(&self, _attrs: &tracing::span::Attributes<'_>) -> tracing::span::Id {
|
||||
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
tracing::span::Id::from_u64(id)
|
||||
}
|
||||
|
||||
fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {}
|
||||
|
||||
fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {}
|
||||
|
||||
fn event(&self, event: &tracing::Event<'_>) {
|
||||
let serialized =
|
||||
postcard::to_allocvec(&event.as_serde()).expect("failed to serialize tracing event");
|
||||
wit::pumpkin::plugin::logging::log_tracing(&serialized);
|
||||
}
|
||||
|
||||
fn enter(&self, _span: &tracing::span::Id) {}
|
||||
|
||||
fn exit(&self, _span: &tracing::span::Id) {}
|
||||
}
|
||||
|
||||
/// The log severity level.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LogLevel {
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
fn to_wit(self) -> wit::pumpkin::plugin::logging::Level {
|
||||
match self {
|
||||
LogLevel::Trace => wit::pumpkin::plugin::logging::Level::Trace,
|
||||
LogLevel::Debug => wit::pumpkin::plugin::logging::Level::Debug,
|
||||
LogLevel::Info => wit::pumpkin::plugin::logging::Level::Info,
|
||||
LogLevel::Warn => wit::pumpkin::plugin::logging::Level::Warn,
|
||||
LogLevel::Error => wit::pumpkin::plugin::logging::Level::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log(level: LogLevel, message: &str) {
|
||||
wit::pumpkin::plugin::logging::log(level.to_wit(), message);
|
||||
}
|
||||
Reference in New Issue
Block a user