Skip to content

Commit

Permalink
add Plugin system (Pumpkin-MC#339)
Browse files Browse the repository at this point in the history
* Add pumpkin-api crate for API definitions

* Add proc-macro definitions for pumpkin-api

* Add pumpkin-api and pumpkin-api-macros to workspace

* Add basic PluginManager implementation plugin loading

* Update .gitignore to include specific plugin file types

* Add example plugin

* Cargo fmt

* Fix clippy issues

* Refactoring to prevent a circular import

* Add base impl for plugin hooks

* Move plugin manager to server

* Make metadata have static lifetime

* Add default implementations to events

* Add hooks to proc macro

* Update example

* Fix formatting

* Fix clippy warnings

* Load metadata from Cargo.toml

* Mark plugins as an implemented feature :D

* Implement new event handling

* Create a static global reference to the plugin manager

* Emit player join and leave events

* Update macro generation

* Update example

* Fix formatting

* Fix clippy issue

* Simplify event handling

* Add plugin command to list plugins

* Make handlers async

* Update macros

* Update example

* Fix formatting and clippy issues

* Better styling on plugins command

* Fix clippy issues

* Cargo fmt

* Disable doctest for lib target on pumpkin crate

* New API for plugins

* Update api macros

* Update plugin example

* A bit of clean up

* Some QoL (and performance) improvements

* Cargo fmt and clippy fixes

* refactoring, better event handling, new context functions

* Async on_load and on_unload

* Fix mutex lock never going out of scope

* Async plugin loading

* Add plugin management command

* Fix clippy issues

* Fix import issues

* Move TcpConnection out of client

* Move packet encoding out of client

* Allow plugins to register commands

* Fix fmt and clippy

* Implement plugin list in query

* Make arguments public so that plugins can use them

* Update proc_macros to handle runtime

* Make tree_builder public for use in plugins

* Make FindArg trait public for use in plugins

* Update example plugin

* Fix merge related issues

* Fix cargo fmt

* Post-merge fixes (also 69th commit, nice)

* New event system

* cargo fmt

* Impl block break event

* cargo fmt and clippy

* Reduced plugin size

* Reduce dependency count

* Old code cleanup

* cargo fmt

* Add event priority and blocking options to event registration and handling

* Refactor event handling to support blocking execution and improve event registration

* Fix post-merge errors

* Update example plugin

* increase mpsc channel capacity and handle write errors gracefully

* Use Arc<Player> instead of &Player

* add missing import

* Update example plugin with playing sounds

* Remove example plugin

* Create plugins dir if doesnt exist

* Remove unused vars

* Remove unnecessary clones

* Fix impl

* cargo fmt

* Make server pub in context
  • Loading branch information
vyPal authored Jan 24, 2025
1 parent 4ac2db5 commit a0ae1ae
Show file tree
Hide file tree
Showing 48 changed files with 1,588 additions and 164 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ Cargo.lock
#.idea/

# === PROJECT SPECIFIC ===
plugins/*
plugins/**/*.so
plugins/**/*.dylib
plugins/**/*.dll
world/*

# docker-compose
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ and customizable experience. It prioritizes performance and player enjoyment whi
- [x] Entity AI
- [ ] Boss
- Server
- [ ] Plugins
- [x] Plugins
- [x] Query
- [x] RCON
- [x] Inventories
Expand Down
12 changes: 12 additions & 0 deletions pumpkin-api-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "pumpkin-api-macros"
version.workspace = true
edition.workspace = true

[lib]
proc-macro = true

[dependencies]
syn = { version = "2.0.89", features = ["full"] }
quote = "1.0.37"
proc-macro2 = "1.0.92"
128 changes: 128 additions & 0 deletions pumpkin-api-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use std::sync::LazyLock;
use proc_macro::TokenStream;
use quote::quote;
use std::collections::HashMap;
use std::sync::Mutex;
use syn::{parse_macro_input, parse_quote, ImplItem, ItemFn, ItemImpl, ItemStruct};

static PLUGIN_METHODS: LazyLock<Mutex<HashMap<String, Vec<String>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));

#[proc_macro_attribute]
pub fn plugin_method(attr: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident;
let fn_inputs = &input_fn.sig.inputs;
let fn_output = &input_fn.sig.output;
let fn_body = &input_fn.block;

let struct_name = if attr.is_empty() {
"MyPlugin".to_string()
} else {
attr.to_string().trim().to_string()
};

let method = quote! {
#[allow(unused_mut)]
async fn #fn_name(#fn_inputs) #fn_output {
crate::GLOBAL_RUNTIME.block_on(async move {
#fn_body
})
}
}
.to_string();

PLUGIN_METHODS
.lock()
.unwrap()
.entry(struct_name)
.or_default()
.push(method);

TokenStream::new()
}

#[proc_macro_attribute]
pub fn plugin_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
// Parse the input struct
let input_struct = parse_macro_input!(item as ItemStruct);
let struct_ident = &input_struct.ident;

// Get the custom name from attribute or use the struct's name
let struct_name = if attr.is_empty() {
struct_ident.clone()
} else {
let attr_str = attr.to_string();
quote::format_ident!("{}", attr_str.trim())
};

let methods = PLUGIN_METHODS
.lock()
.unwrap()
.remove(&struct_name.to_string())
.unwrap_or_default();

let methods: Vec<proc_macro2::TokenStream> = methods
.iter()
.filter_map(|method_str| method_str.parse().ok())
.collect();

// Combine the original struct definition with the impl block and plugin() function
let expanded = quote! {
pub static GLOBAL_RUNTIME: std::sync::LazyLock<std::sync::Arc<tokio::runtime::Runtime>> =
std::sync::LazyLock::new(|| std::sync::Arc::new(tokio::runtime::Runtime::new().unwrap()));

#[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"),
};

#input_struct

#[async_trait::async_trait]
impl pumpkin::plugin::Plugin for #struct_ident {
#(#methods)*
}

#[no_mangle]
pub fn plugin() -> Box<dyn pumpkin::plugin::Plugin> {
Box::new(#struct_ident::new())
}
};

TokenStream::from(expanded)
}

#[proc_macro_attribute]
pub fn with_runtime(attr: TokenStream, item: TokenStream) -> TokenStream {
let mut input = parse_macro_input!(item as ItemImpl);

let use_global = attr.to_string() == "global";

for item in &mut input.items {
if let ImplItem::Fn(method) = item {
let original_body = &method.block;

method.block = if use_global {
parse_quote!({
GLOBAL_RUNTIME.block_on(async move {
#original_body
})
})
} else {
parse_quote!({
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
#original_body
})
})
};
}
}

TokenStream::from(quote!(#input))
}
8 changes: 8 additions & 0 deletions pumpkin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ FileDescription = "Pumpkin"
OriginalFilename = "pumpkin.exe"
LegalCopyright = "Copyright © 2025 Aleksander Medvedev"

# Required to expose pumpkin plugin API
[lib]
doctest = false

[dependencies]
# pumpkin
pumpkin-util = { path = "../pumpkin-util" }
Expand Down Expand Up @@ -69,6 +73,10 @@ time = "0.3.37"

# commands
async-trait = "0.1.83"

# plugins
libloading = "0.8.5"
oneshot = "0.1.8"
[build-dependencies]
git-version = "0.3.9"
# This makes it so the entire project doesn't recompile on each build on linux.
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use super::{
Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser,
};

pub(crate) struct BlockArgumentConsumer;
pub struct BlockArgumentConsumer;

impl GetClientSideArgParser for BlockArgumentConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_bool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use pumpkin_protocol::client::play::{
CommandSuggestion, ProtoCmdArgParser, ProtoCmdArgSuggestionType,
};

pub(crate) struct BoolArgConsumer;
pub struct BoolArgConsumer;

impl GetClientSideArgParser for BoolArgConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_bossbar_color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use pumpkin_protocol::client::play::{
CommandSuggestion, ProtoCmdArgParser, ProtoCmdArgSuggestionType,
};

pub(crate) struct BossbarColorArgumentConsumer;
pub struct BossbarColorArgumentConsumer;

impl GetClientSideArgParser for BossbarColorArgumentConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_bossbar_style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use pumpkin_protocol::client::play::{
CommandSuggestion, ProtoCmdArgParser, ProtoCmdArgSuggestionType,
};

pub(crate) struct BossbarStyleArgumentConsumer;
pub struct BossbarStyleArgumentConsumer;

impl GetClientSideArgParser for BossbarStyleArgumentConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
4 changes: 2 additions & 2 deletions pumpkin/src/command/args/arg_bounded_num.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ impl<T: ToFromNumber> FindArg<'_> for BoundedNumArgumentConsumer<T> {
}
}

pub(crate) type NotInBounds = ();
pub type NotInBounds = ();

#[derive(Clone, Copy)]
pub(crate) enum Number {
pub enum Number {
F64(f64),
F32(f32),
I32(i32),
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{

use super::{Arg, ArgumentConsumer, DefaultNameArgConsumer, FindArg, GetClientSideArgParser};

pub(crate) struct CommandTreeArgumentConsumer;
pub struct CommandTreeArgumentConsumer;

impl GetClientSideArgParser for CommandTreeArgumentConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_entities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser};
/// todo: implement (currently just calls [`super::arg_player::PlayerArgumentConsumer`])
///
/// For selecting zero, one or multiple entities, eg. using @s, a player name, @a or @e
pub(crate) struct EntitiesArgumentConsumer;
pub struct EntitiesArgumentConsumer;

impl GetClientSideArgParser for EntitiesArgumentConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser};
/// For selecting a single entity, eg. using @s, a player name or entity uuid.
///
/// Use [`super::arg_entities::EntitiesArgumentConsumer`] when there may be multiple targets.
pub(crate) struct EntityArgumentConsumer;
pub struct EntityArgumentConsumer;

impl GetClientSideArgParser for EntityArgumentConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_gamemode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{

use super::{Arg, ArgumentConsumer, DefaultNameArgConsumer, FindArg, GetClientSideArgParser};

pub(crate) struct GamemodeArgumentConsumer;
pub struct GamemodeArgumentConsumer;

impl GetClientSideArgParser for GamemodeArgumentConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use super::{
Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser,
};

pub(crate) struct ItemArgumentConsumer;
pub struct ItemArgumentConsumer;

impl GetClientSideArgParser for ItemArgumentConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use super::{
};

/// Consumes all remaining words/args. Does not consume if there is no word.
pub(crate) struct MsgArgConsumer;
pub struct MsgArgConsumer;

impl GetClientSideArgParser for MsgArgConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_players.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use super::super::args::ArgumentConsumer;
use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser};

/// Select zero, one or multiple players
pub(crate) struct PlayersArgumentConsumer;
pub struct PlayersArgumentConsumer;

impl GetClientSideArgParser for PlayersArgumentConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_position_2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser};
/// x and z coordinates only
///
/// todo: implememnt ~ ^ notations
pub(crate) struct Position2DArgumentConsumer;
pub struct Position2DArgumentConsumer;

impl GetClientSideArgParser for Position2DArgumentConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_position_3d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use super::coordinate::MaybeRelativeCoordinate;
use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser};

/// x, y and z coordinates
pub(crate) struct Position3DArgumentConsumer;
pub struct Position3DArgumentConsumer;

impl GetClientSideArgParser for Position3DArgumentConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_position_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use super::coordinate::MaybeRelativeBlockCoordinate;
use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser};

/// x, y and z coordinates
pub(crate) struct BlockPosArgumentConsumer;
pub struct BlockPosArgumentConsumer;

impl GetClientSideArgParser for BlockPosArgumentConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_rotation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use super::super::args::ArgumentConsumer;
use super::{Arg, DefaultNameArgConsumer, FindArg, GetClientSideArgParser};

/// yaw and pitch
pub(crate) struct RotationArgumentConsumer;
pub struct RotationArgumentConsumer;

impl GetClientSideArgParser for RotationArgumentConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/arg_simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use super::{

/// Should never be a permanent solution
#[allow(unused)]
pub(crate) struct SimpleArgConsumer;
pub struct SimpleArgConsumer;

impl GetClientSideArgParser for SimpleArgConsumer {
fn get_client_side_parser(&self) -> ProtoCmdArgParser {
Expand Down
Loading

0 comments on commit a0ae1ae

Please sign in to comment.