433 lines
15 KiB
Rust
433 lines
15 KiB
Rust
//! Pumpkin plugin API.
|
|
#![warn(missing_docs)]
|
|
#![allow(
|
|
clippy::undocumented_unsafe_blocks,
|
|
clippy::option_if_let_else,
|
|
clippy::collection_is_never_read,
|
|
clippy::all,
|
|
clippy::pedantic,
|
|
clippy::nursery,
|
|
clippy::cargo,
|
|
clippy::panic
|
|
)]
|
|
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used, clippy::panic))]
|
|
//!
|
|
//! This crate provides everything needed to write a Pumpkin server plugin compiled
|
|
//! to WebAssembly. A plugin consists of a type that implements [`Plugin`], registered
|
|
//! with the [`register_plugin!`] macro.
|
|
//!
|
|
//! # Quick start
|
|
//!
|
|
//! ```rust,ignore
|
|
//! use pumpkin_plugin_api::{Plugin, PluginMetadata, Context, register_plugin, permissions::permissions};
|
|
//!
|
|
//! struct MyPlugin;
|
|
//!
|
|
//! impl Plugin for MyPlugin {
|
|
//! fn new() -> Self { MyPlugin }
|
|
//! fn metadata(&self) -> PluginMetadata {
|
|
//! PluginMetadata {
|
|
//! name: "my-plugin".into(),
|
|
//! version: "0.1.0".into(),
|
|
//! authors: vec!["you".into()],
|
|
//! description: "An example plugin.".into(),
|
|
//! dependencies: vec![],
|
|
//! permissions: vec![permissions::NETWORK_DNS.into()],
|
|
//! }
|
|
//! }
|
|
//! }
|
|
//!
|
|
//! register_plugin!(MyPlugin);
|
|
//! ```
|
|
//!
|
|
//! # Persisting data
|
|
//!
|
|
//! Plugins run as WebAssembly with WASI, so a plugin stores data that survives
|
|
//! server restarts by reading and writing its own files with your language's
|
|
//! normal file API (for example `std::fs` in Rust). There is no separate
|
|
//! storage API; the file system is the storage.
|
|
//!
|
|
//! Each plugin has a private data folder. To use it:
|
|
//!
|
|
//! 1. Request the `fs.read.data` and/or `fs.write.data` permissions
|
|
//! (`permissions::FS_READ_DATA` / `permissions::FS_WRITE_DATA`) in your
|
|
//! [`PluginMetadata`]. Without them the folder is not accessible.
|
|
//! 2. Get the folder path from the context's `get_data_folder` method inside
|
|
//! `on_load` or `on_unload`. The returned path is the folder as seen from
|
|
//! inside the WASI sandbox.
|
|
//! 3. Read and write files under that path with your normal file API.
|
|
//!
|
|
//! ```rust,ignore
|
|
//! fn on_load(&self, context: &Context) -> Result<(), String> {
|
|
//! let path = format!("{}/state.json", context.get_data_folder());
|
|
//! let saved = std::fs::read_to_string(&path).unwrap_or_default();
|
|
//! // ...parse and use `saved`, then later write it back...
|
|
//! Ok(())
|
|
//! }
|
|
//! ```
|
|
|
|
use crate::{
|
|
commands::COMMAND_HANDLERS, events::EVENT_HANDLERS, logging::WitSubscriber,
|
|
scheduler::TASK_HANDLERS, text::TextComponent,
|
|
};
|
|
|
|
/// Plugin command registration and handling utilities.
|
|
pub mod commands;
|
|
/// Display and interaction entity utilities and builders.
|
|
pub mod display;
|
|
/// Custom enchantment registration and builder utilities.
|
|
pub mod enchantment;
|
|
/// Event system and event handlers.
|
|
pub mod events;
|
|
mod ext;
|
|
/// Bedrock UI form builders.
|
|
pub mod forms;
|
|
/// Constants for plugin permissions.
|
|
///
|
|
/// Use these in your `PluginMetadata` to request access to specific host features.
|
|
pub mod permissions;
|
|
/// Custom recipe registration and builder utilities.
|
|
pub mod recipe;
|
|
/// Scheduler utilities.
|
|
pub mod scheduler;
|
|
/// Scoreboard team management and builder utilities.
|
|
pub mod team;
|
|
/// Custom world and chunk generation utilities and traits.
|
|
pub mod worldgen;
|
|
|
|
/// Command WIT API re-exports.
|
|
pub mod command {
|
|
pub use crate::wit::pumpkin::plugin::command::{
|
|
Arg, ArgumentType, Command, CommandError, CommandNode, CommandSender, ConsumedArgs,
|
|
StringType,
|
|
};
|
|
}
|
|
|
|
pub use wit::pumpkin::plugin::{
|
|
advancement as advancement_wit, bedrock_packets, block_entity, boss_bar,
|
|
command as command_wit, common,
|
|
context::{self, Context, MarketplaceMetadata, Server},
|
|
damage_types as damage_types_wit, data_components, display as display_wit,
|
|
enchantments as enchantments_wit, entity,
|
|
entity_types::EntityType,
|
|
event::{self as events_wit, EventType},
|
|
gui, i18n, ipc, item_stack, java_dialogs, java_packets, particles, permission, player,
|
|
recipe as recipe_wit, scoreboard, screens as screens_wit, server, statistics as statistics_wit,
|
|
text, uuid, world,
|
|
};
|
|
|
|
// Convenience re-exports of commonly-used plugin types so plugin authors can
|
|
// name them directly (e.g. build an `ItemStack` for a GUI or `/give`).
|
|
pub use damage_types_wit::DamageType;
|
|
pub use display::{
|
|
BillboardMode, BlockDisplayEntity, DisplayEntity, DisplayEntityExt, DisplayTransformation,
|
|
EntityDisplayExt, InteractionEntity, ItemDisplayEntity, ItemDisplayEntityExt, ItemDisplayMode,
|
|
Quaternionf, TextAlignment, TextDisplayEntity, TextDisplayEntityExt, TransformationBuilder,
|
|
Vector3f,
|
|
};
|
|
pub use enchantment::{
|
|
AttributeModifierSlot, CustomEnchantment, CustomEnchantmentValue, Enchantment,
|
|
EnchantmentBuilder, EnchantmentError, EnchantmentManager, RegistrableEnchantment,
|
|
};
|
|
pub use events::{EventHandler, FromIntoEvent};
|
|
pub use ext::player::PlayerEnderChestExt;
|
|
pub use recipe::{
|
|
CookingRecipeBuilder, Ingredient, RecipeCategory, RecipeError, RecipeManager,
|
|
RegistrableRecipe, ShapedRecipeBuilder, ShapelessRecipeBuilder,
|
|
};
|
|
pub use screens_wit::Screen;
|
|
pub use statistics_wit::{CustomStatistic, StatisticCategory};
|
|
pub use team::{PlayerTeamExt, ScoreboardTeamExt, Team, TeamSettingsBuilder};
|
|
pub use wit::pumpkin::plugin::item_stack::ItemStack;
|
|
pub use wit::pumpkin::plugin::player::Player;
|
|
pub use wit::pumpkin::plugin::scoreboard::{CollisionRule, NametagVisibility, TeamSettings};
|
|
pub use wit::pumpkin::plugin::server::Dimension;
|
|
pub use wit::pumpkin::plugin::world::World;
|
|
pub use worldgen::{ChunkBuffer, ChunkGenerator, GenerationPhase, GeneratorManager};
|
|
|
|
/// Advancement WIT API re-exports.
|
|
pub mod advancement {
|
|
pub use crate::wit::pumpkin::plugin::advancement::{
|
|
AdvancementDisplay, AdvancementInfo, AdvancementProgress, FrameType,
|
|
};
|
|
}
|
|
|
|
/// Java dialog WIT API re-exports.
|
|
pub mod java_dialog {
|
|
pub use crate::wit::pumpkin::plugin::java_dialogs::{ActionButton, DialogBody, DialogType};
|
|
}
|
|
|
|
/// WIT-based logging subscriber.
|
|
pub mod logging;
|
|
|
|
#[allow(clippy::too_many_arguments, missing_docs)]
|
|
mod wit {
|
|
wit_bindgen::generate!({
|
|
skip: ["init-plugin"],
|
|
path: "../pumpkin-plugin-wit/v0.1",
|
|
world: "plugin",
|
|
enable_method_chaining: true
|
|
});
|
|
|
|
use super::Component;
|
|
export!(Component);
|
|
}
|
|
|
|
struct Component;
|
|
|
|
/// Metadata that describes a plugin to the server.
|
|
pub struct PluginMetadata {
|
|
/// The human-readable name of the plugin.
|
|
pub name: String,
|
|
/// The plugin's version string (e.g. `"1.0.0"`).
|
|
pub version: String,
|
|
/// The list of plugin authors.
|
|
pub authors: Vec<String>,
|
|
/// A short description of what the plugin does.
|
|
pub description: String,
|
|
/// The list of plugin dependencies.
|
|
pub dependencies: Vec<String>,
|
|
/// The list of permissions requested by the plugin.
|
|
pub permissions: Vec<String>,
|
|
}
|
|
|
|
impl wit::exports::pumpkin::plugin::metadata::Guest for Component {
|
|
/// Returns the plugin metadata to the host.
|
|
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,
|
|
dependencies: metadata.dependencies,
|
|
permissions: metadata.permissions,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl wit::Guest for Component {
|
|
/// WIT entry point — delegates to [`Plugin::on_load`].
|
|
fn on_load(context: Context) -> Result<(), String> {
|
|
plugin().on_load(context)
|
|
}
|
|
|
|
/// WIT entry point — delegates to [`Plugin::on_unload`].
|
|
fn on_unload(context: Context) -> Result<(), String> {
|
|
plugin().on_unload(context)
|
|
}
|
|
|
|
/// WIT entry point — dispatches an incoming event to the registered handler for `event_id`.
|
|
///
|
|
/// Returns the event unchanged if no handler is registered for the given id.
|
|
fn handle_event(event_id: u32, server: Server, event: events::Event) -> events::Event {
|
|
let handlers = EVENT_HANDLERS.lock().unwrap_or_else(|e| e.into_inner());
|
|
if let Some(handler) = handlers.get(&event_id) {
|
|
handler.handle_erased(server, event)
|
|
} else {
|
|
event
|
|
}
|
|
}
|
|
|
|
/// WIT entry point — dispatches an incoming command invocation to the registered handler for `command_id`.
|
|
///
|
|
/// Returns a [`CommandError`](command::CommandError) if no handler is registered for the given id.
|
|
fn handle_command(
|
|
command_id: u32,
|
|
sender: command::CommandSender,
|
|
server: Server,
|
|
args: command::ConsumedArgs,
|
|
) -> Result<i32, command::CommandError> {
|
|
let handlers = COMMAND_HANDLERS.lock().unwrap_or_else(|e| e.into_inner());
|
|
handlers.get(&command_id).map_or_else(
|
|
|| {
|
|
Err(command::CommandError::CommandFailed(TextComponent::text(
|
|
&format!("no handler registered for command id {command_id}"),
|
|
)))
|
|
},
|
|
|handler| handler.handle(sender, server, args),
|
|
)
|
|
}
|
|
|
|
/// WIT entry point — dispatches a scheduled task invocation to the registered handler for `handler_id`.
|
|
fn handle_task(handler_id: u32, server: Server) {
|
|
let mut handlers = TASK_HANDLERS.lock().unwrap_or_else(|e| e.into_inner());
|
|
handlers.handle(handler_id, server);
|
|
}
|
|
|
|
fn handle_ai_goal_can_start(goal_id: u32, server: Server, entity: entity::Entity) -> bool {
|
|
let mut handlers = crate::ai::AI_GOAL_HANDLERS
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner());
|
|
if let Some(goal) = handlers.handlers.get_mut(&goal_id) {
|
|
goal.can_start(server, entity)
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn handle_ai_goal_should_continue(
|
|
goal_id: u32,
|
|
server: Server,
|
|
entity: entity::Entity,
|
|
) -> bool {
|
|
let mut handlers = crate::ai::AI_GOAL_HANDLERS
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner());
|
|
if let Some(goal) = handlers.handlers.get_mut(&goal_id) {
|
|
goal.should_continue(server, entity)
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn handle_ai_goal_start(goal_id: u32, server: Server, entity: entity::Entity) {
|
|
let mut handlers = crate::ai::AI_GOAL_HANDLERS
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner());
|
|
if let Some(goal) = handlers.handlers.get_mut(&goal_id) {
|
|
goal.start(server, entity);
|
|
}
|
|
}
|
|
|
|
fn handle_ai_goal_tick(goal_id: u32, server: Server, entity: entity::Entity) {
|
|
let mut handlers = crate::ai::AI_GOAL_HANDLERS
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner());
|
|
if let Some(goal) = handlers.handlers.get_mut(&goal_id) {
|
|
goal.tick(server, entity);
|
|
}
|
|
}
|
|
|
|
fn handle_ai_goal_stop(goal_id: u32, server: Server, entity: entity::Entity) {
|
|
let mut handlers = crate::ai::AI_GOAL_HANDLERS
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner());
|
|
if let Some(goal) = handlers.handlers.get_mut(&goal_id) {
|
|
goal.stop(server, entity);
|
|
}
|
|
}
|
|
|
|
fn handle_ipc_message(
|
|
sender: wit::PluginId,
|
|
message: wit::IpcMessage,
|
|
) -> Result<wit::IpcMessage, String> {
|
|
plugin().handle_ipc_message(sender, message)
|
|
}
|
|
|
|
fn handle_generate_phase(
|
|
generator_id: u32,
|
|
phase: wit::pumpkin::plugin::world::GenerationPhase,
|
|
chunk: wit::pumpkin::plugin::world::ChunkBuffer,
|
|
) {
|
|
let handlers = crate::worldgen::GENERATOR_HANDLERS
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner());
|
|
if let Some(generator) = handlers.get(&generator_id) {
|
|
let mut buffer = crate::worldgen::ChunkBuffer::new(chunk);
|
|
match phase {
|
|
wit::pumpkin::plugin::world::GenerationPhase::Biomes => {
|
|
generator.generate_biomes(&mut buffer);
|
|
}
|
|
wit::pumpkin::plugin::world::GenerationPhase::Noise => {
|
|
generator.generate_noise(&mut buffer);
|
|
}
|
|
wit::pumpkin::plugin::world::GenerationPhase::Surface => {
|
|
generator.generate_surface(&mut buffer);
|
|
}
|
|
wit::pumpkin::plugin::world::GenerationPhase::Features => {
|
|
generator.generate_features(&mut buffer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Convenience alias for `core::result::Result<T, String>` used throughout the plugin API.
|
|
pub type Result<T, E = String> = core::result::Result<T, E>;
|
|
|
|
/// The trait that every Pumpkin plugin must implement.
|
|
///
|
|
/// Use the [`register_plugin!`] macro to register your implementation with the runtime.
|
|
pub trait Plugin: Send + Sync {
|
|
/// Creates a new instance of the plugin.
|
|
///
|
|
/// Called once by the runtime before [`on_load`](Plugin::on_load).
|
|
fn new() -> Self
|
|
where
|
|
Self: Sized;
|
|
|
|
/// Returns the metadata for this plugin.
|
|
fn metadata(&self) -> PluginMetadata;
|
|
|
|
/// Called when the plugin is loaded by the server.
|
|
///
|
|
/// Use this to register event handlers, commands, and perform any setup work.
|
|
fn on_load(&mut self, _context: Context) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
/// Called when the plugin is unloaded by the server.
|
|
///
|
|
/// Use this to clean up any resources acquired during [`on_load`](Plugin::on_load).
|
|
fn on_unload(&mut self, _context: Context) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
/// Called when the plugin receives a message from another plugin.
|
|
fn handle_ipc_message(
|
|
&mut self,
|
|
_sender: wit::PluginId,
|
|
_message: wit::IpcMessage,
|
|
) -> Result<wit::IpcMessage, String> {
|
|
Err("This plugin cannot receive messages.".to_string())
|
|
}
|
|
}
|
|
|
|
#[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()) }
|
|
}
|
|
|
|
/// Returns a mutable reference to the currently loaded plugin instance.
|
|
///
|
|
/// # Panics
|
|
/// If called before [`register_plugin`] has initialized `PLUGIN`.
|
|
fn plugin() -> &'static mut dyn Plugin {
|
|
#[expect(static_mut_refs)]
|
|
#[allow(clippy::unwrap_used)]
|
|
unsafe {
|
|
PLUGIN.as_deref_mut().unwrap()
|
|
}
|
|
}
|
|
|
|
/// The singleton plugin instance, initialised by [`register_plugin`].
|
|
static mut PLUGIN: Option<Box<dyn Plugin>> = None;
|
|
|
|
/// Registers the provided type as a Pumpkin plugin.
|
|
///
|
|
/// This macro generates the WebAssembly export entry point that the server uses to
|
|
/// instantiate the plugin. The type must implement the [`Plugin`] trait.
|
|
///
|
|
/// # Example
|
|
/// ```rust,ignore
|
|
/// register_plugin!(MyPlugin);
|
|
/// ```
|
|
#[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()));
|
|
}
|
|
};
|
|
}
|
|
/// AI and mob goal utilities.
|
|
pub mod ai;
|
|
/// Persistent custom data containers (Bukkit-style `PersistentDataHolder`).
|
|
pub mod persistent_data;
|
|
pub use persistent_data::PersistentDataHolder;
|
|
/// Game rules definitions and values.
|
|
pub use wit::pumpkin::plugin::game_rules::{GameRule, GameRuleValue};
|