Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin Development Tutorial / API Docs #5

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,37 @@ export default defineConfig({
{ text: "World", link: "developer/world" },
],
},
{
text: "Plugin Development",
items: [
{
text: "Introduction",
link: "/plugin-dev/introduction",
},
{
text: "Example Plugin",
link: "/plugin-dev/example-plugin/introduction",
items: [
{
text: "Environment Setup",
link: "/plugin-dev/example-plugin/environment",
},
{
text: "Creating Project",
link: "/plugin-dev/example-plugin/creating-project",
},
{
text: "Basic Logic",
link: "/plugin-dev/example-plugin/basic-logic",
},
{
text: "Join Event",
link: "/plugin-dev/example-plugin/join-event",
},
],
},
],
},
{
text: "Troubleshooting",
items: [
Expand Down
4 changes: 2 additions & 2 deletions docs/developer/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ Welcome to the Pumpkin Documentation!

Whether you're an internal Pumpkin developer or working on a Pumpkin plugin, this documentation is your resource for everything Pumpkin.

> [!IMPORTANT]
> While Pumpkin currently doesn't have plugin support yet, this documentation provides valuable insights into the platform's architecture and functionality, which can be helpful for understanding how to create potential future plugins.
#### Plugin Development
For a better documentation structure, plugin development docs have been moved to a [separate category](/plugin-dev/introduction)
142 changes: 142 additions & 0 deletions docs/plugin-dev/example-plugin/basic-logic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Writing the basic logic
## Plugin base
There is a lot going on under the hood of even a basic plugin, so to greatly simplify plugin development we will use the `pumpkin-api-macros` crate to create a basic empty plugin.

Open the `src/lib.rs` file and replace its contents with this:
```rs:line-numbers
use pumpkin_api_macros::plugin_impl;

#[plugin_impl]
pub struct MyPlugin {}

impl MyPlugin {
pub fn new() -> Self {
MyPlugin {}
}
}

impl Default for MyPlugin {
fn default() -> Self {
Self::new()
}
}
Comment on lines +10 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pub struct MyPlugin; will be enough, no need for the other implementations

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mhh i get an error when having non default, is there a way to prevent that ?

rustc: no function or associated item named `new` found for struct `MyPlugin` in the current scope
items from traits can only be used if the trait is implemented and in scope
the following traits define an item `new`, perhaps you need to implement one of them:

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The #[plugin_impl] proc macro uses the new() on the struct to construct it so that you can have your own fields in it

```
This will create a empty plugin and implement all the necessary methods for it to be loaded by pumpkin.

We can now try to compile our plugin for the first time, to do so, run this command in your project folder:
```bash
cargo build --release
```
::: tip NOTICE
If you are using Windows, you **must** use the `--release` flag, or you will run into issues. If you are on another platform, you don't have to use it if you want to speed up compile time
:::
The initial compilation will take a bit, but don't worry, later compilations will be faster.

If all went well, you should be left with a message like this:
```log
╰─ cargo build --release
Compiling hello-pumpkin v0.1.0 (/home/vypal/Dokumenty/GitHub/hello-pumpkin)
Finished `release` profile [optimized] target(s) in 0.68s
```

Now you can go to the `./target/release` folder (or `./target/debug` if you didn't use `--release`) and locate your plugin binary

Depending on your operating system, the file will have one of three possible names:
- For Windows: `hello-pumpkin.dll`
- For MacOS: `libhello-pumpkin.dylib`
- For Linux: `libhello-pumpkin.so`

::: info NOTE
If you used a different project name in the `Cargo.toml` file, look for a file which contains your project name
:::

You can rename this file to whatever you like, however you must keep the file extension (`.dll`, `.dylib`, or `.so`) the same.

## Testing the plugin
Now that we have our plugin binary, we can go ahead and test it on the Pumpkin server. Installing a plugin is as simple as putting the plugin binary that we just built into the `plugins/` folder of your Pumpkin server!

Thanks to the `#[plugin_impl]` macro, the plugin will have it's details (like the name, authors, version, and description) built into the binary so that the server can read them.

When you start up the server and run the `/plugins` command, you should see an output like this:
```
There is 1 plugin loaded:
hello-pumpkin
```

## Basic methods
The Pumpkin server currently uses two "methods" to tell the plugin about it's state. These methods are `on_load` and `on_unload`.

These methods don't have to be implemented, but you will usually implement at least the `on_load` method. In this method you get access to a `Context` object which can give the plugin access to information about the server, but also allows the plugin to register command handlers or events.

To make implementing these methods easier, there is another macro provided by the `pumpkin-api-macros` crate. Add these lines to your `src/lib.rs` file:
```rs
use pumpkin_api_macros::{plugin_impl, plugin_method}; // [!code ++:2]
use pumpkin::plugin::Context;
use pumpkin_api_macros::plugin_impl; // [!code --]

#[plugin_method] // [!code ++:4]
async fn on_load(&mut self, server: &Context) -> Result<(), String> {
Ok(())
}

#[plugin_impl]
pub struct MyPlugin {}

impl MyPlugin {
pub fn new() -> Self {
MyPlugin {}
}
}

impl Default for MyPlugin {
fn default() -> Self {
Self::new()
}
}
Comment on lines +83 to +95
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here aswell

```

::: warning IMPORTANT
It is important that you define any plugin methods before the `#[plugin_impl]` block
:::

This method gets a mutable reference to itself (in this case the `MyPlugin` struct) which it can use to initialize any data stored in the plugin's main struct, and a reference to a `Context` object. This object is specifically constructed for this plugin, based on the plugin's metadata.

### Methods implemented on the `Context` object:
```rs
fn get_data_folder() -> String
```
Returns the path to a folder dedicated to this plugin, which should be used for persistant data storage
```rs
async fn get_player_by_name(player_name: String) -> Option<Arc<Player>>
```
If a player by the name `player_name` is found (has to be currently online), this function will return a reference to him.
```rs
async fn register_command(tree: CommandTree, permission: PermissionLvl)
```
Registers a new command handler, with a minimum required permission level.
```rs
async fn register_event(handler: H, priority: EventPriority, blocking: bool)
```
Registers a new event handler with a set priority and if it is blocking or not.

## Basic on-load method
For now we will only implement a very basic `on_load` method to be able to see that the plugin is running.

Here we will setup the env_logger and setup a "Hello, Pumpkin!", so that we can see out plugin in action.

Add this to the `on_load` method:
```rs
#[plugin_method]
async fn on_load(&mut self, server: &Context) -> Result<(), String> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); // [!code ++:3]

log::info!("Hello, Pumpkin!");

Ok(())
}
```

If we build the plugin again and start up the server, you should now see this somewhere in the log:
```log
[2025-01-18T09:36:16Z INFO hello_pumpkin] Hello, Pumpkin!
```
68 changes: 68 additions & 0 deletions docs/plugin-dev/example-plugin/creating-project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Creating a new project
Pumpkin Plugins use the [Cargo](https://doc.rust-lang.org/book/ch01-03-hello-cargo.html) build system.

The complete code for this plugin can be found as a [template on GitHub](https://github.com/vyPal/Hello-Pumpkin).

## Initializing a new crate
Before we can get started, we need to have a folder to house our plugin's source code. Either create one using the file explorer, or from the command line using:
```bash
mkdir hello-pumpkin && cd hello-pumpkin
```

Next we need to initialize our crate, do so by running this command in the folder you created:
```bash
cargo init --lib
```
Comment on lines +7 to +15
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would simpler to replace this with cargo new <name> --lib.

This will create a couple files in our folder, which should now look like this:
```
├── Cargo.toml
└── src
└── lib.rs
```

## Configuring the crate
Since Pumpkin Plugins are loaded at runtime as dynamic libraries, we need to tell Cargo to build this crate as one. Open the `Cargo.toml` file and add these lines:
```toml
[package]
name = "hello-pumpkin"
version = "0.1.0"
edition = "2021"

[lib] // [!code ++:3]
crate-type = ["cdylib"]

[dependencies]
```

Next we need to add some basic dependencies. Since Pumpkin is still in early development, the internal crates aren't published to crates.io, so we need to tell Cargo to download the dependencies directly from GitHub. Add this to `Crago.toml`:
```toml
[package]
name = "hello-pumpkin"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
pumpkin = { git = "https://github.com/Pumpkin-MC/Pumpkin.git", branch = "master", package = "pumpkin" } // [!code ++:9]
pumpkin-util = { git = "https://github.com/Pumpkin-MC/Pumpkin.git", branch = "master", package = "pumpkin-util" }
pumpkin-api-macros = { git = "https://github.com/Pumpkin-MC/Pumpkin.git", branch = "master", package = "pumpkin-api-macros" }

async-trait = "0.1.83"
tokio = { version = "1.42", features = [ "full" ] }

env_logger = "0.11.6"
log = "0.4.22"
```

This adds three dependencies from Pumpkin:
- `pumpkin` - This is the base crate with most high-level type definitions
- `pumpkin-util` - Other utilities used by Pumpkin (like TextComponent)
- `pumpkin-api-macros` - Macros for easier plugin development

as well as these other dependencies:
- `async-trait` - A utility allowing plugins to work asynchronously
- `tokio` - A rust asynchronous runtime
- `log` - For logging
- `env_logger` - Configure logger using environment variables
5 changes: 5 additions & 0 deletions docs/plugin-dev/example-plugin/environment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Setting up a development environment
## Installing rust
To build a plugin for pumpkin, you will need to have the rust toolchain installed on your system. We recommend you follow the [official installation instructions](https://www.rust-lang.org/tools/install) for this.
## Building Pumpkin
To be able to test the plugin during development, you will need a build of the Pumpkin server software to run the plugins. You can follow the [quickstart guide](about/quick-start) if you don't yet have a build.
9 changes: 9 additions & 0 deletions docs/plugin-dev/example-plugin/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Writing an Example Plugin
This tutorial will guide you through creating a new plugin.

This tutorial expects that you have some experience with rust and with using a command line.

Topics described in this tutorial:
- Modifying the join message
- Creating a rock-paper-scissors command
- Saving plugin data in the plugin's directory
102 changes: 102 additions & 0 deletions docs/plugin-dev/example-plugin/join-event.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Writing a Event Handler
Event handlers are one of the main functions of plugins, they allow a plugin to tap into the internal workings of the server, and alter it's behavior to perform some other action. For a simple example, we will implement a handler for the `player_join` event.

The Pumpkin Plugin Event System tries to copy the Bukkit/Spigot Event system, so that developers coming from there will have a easier time learning Pumpkin.

The Event System also uses inheritance to make it easy to expect the data that you will get from an event.
## Join Event
To further explain the inheritance system, let's demonstrate it on the Player Join event:
```
Event
├── get_name()
├── CancellableEvent
│ ├── is_cancelled()
│ └── set_cancelled()
│ │
│ └── PlayerEvent
│ ├── get_player()
│ │
│ └── PlayerJoinEvent
│ ├── get_join_message()
│ └── set_join_message()
```
As you can see, the `PlayerJoinEvent` exposes two methods. But since it inherits the `PlayerEvent` type, it will also expose the `get_player()` method. This continues up the tree structure, so in the end, all the methods you see here will be available on the `PlayerJoinEvent`

## Implementing the Join Event
Individual event handlers are just structs which implement the `EventHandler<T>` trait (where T is a specific event implementation).

### What are blocking events?
The Pumpkin Plugin Event System differentiates between two types of evetns: blocking and non-blocking. Each have their benefits:
#### Blocking events
```diff
+ Can modify the event (like editing the join message)
+ Can cancel the event
+ Have a priority system
- Are executed in sequence
- Can slow down the server if not implemented well
```
#### Non-blocking events
```diff
+ Are executed in parallel
+ Are executed after all blocking events finish
+ Can still do some modifications (anything that is behin a Mutex or RwLock)
- Can not cancel the event
- No priority system
- Allow for less control over the event
```

### Writing a handler
Since our main aim here is to change the welcome message that the player sees when they join a server, we will be choosing the blocking event type with a low priority.

Add this code above the `on_load` method:
```rs
use async_trait::async_trait; // [!code ++:4]
use pumpkin_api_macros::{plugin_impl, plugin_method, with_runtime};
use pumpkin::plugin::{player::{join::PlayerJoinEventImpl, PlayerEvent, PlayerJoinEvent}, Context, EventHandler};
use pumpkin_util::text::{color::NamedColor, TextComponent};
use pumpkin_api_macros::{plugin_impl, plugin_method}; // [!code --:2]
use pumpkin::plugin::Context;

struct MyJoinHandler; // [!code ++:12]

#[with_runtime(global)]
#[async_trait]
impl EventHandler<PlayerJoinEventImpl> for MyJoinHandler {
async fn handle_blocking(&self, event: &mut PlayerJoinEventImpl) {
event.set_join_message(
TextComponent::text(format!("Welcome, {}!", event.get_player().gameprofile.name))
.color_named(NamedColor::Green),
);
}
}
```

**Explanation**:
- `struct MyJoinHandler;`: The struct for our event handler
- `#[with_runtime(global)]`: Pumpkin uses the tokio async runtime, which acts in wierd ways across the plugin boundary. Even though it is not necessary in this specific example, it is a good practise to wrap all async `impl`s that interact with async code with this macro.
- `#[async_trait]`: Rust doesn't have native support for traits with async methods. So we use the `async_trait` crate to allow this.
- `async fn handle_blocking()`: Since we chose for this event to be blocking, it is necessary to implement the `handle_blocking()` method instead of the `handle()` method.

::: warning IMPORTANT
It is important that the `#[with_runtime(global)]` macro is **above** the **`#[async_trait]`** macro. If they are in the opposite order, the `#[with_runtime(global)]` macro might not work
:::

### Registering the handler
Now that we have written the event handler, we need to tell the plugin to use it. We can do that by adding a single line to the `on_load` method:
```rs
use pumpkin::plugin::{player::{join::PlayerJoinEventImpl, PlayerEvent, PlayerJoinEvent}, Context, EventHandler, EventPriority}; // [!code ++]
use pumpkin::plugin::{player::{join::PlayerJoinEventImpl, PlayerEvent, PlayerJoinEvent}, Context, EventHandler}; // [!code --]

#[plugin_method]
async fn on_load(&mut self, server: &Context) -> Result<(), String> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();

log::info!("Hello, Pumpkin!");

server.register_event(MyJoinHandler, EventPriority::Lowest, true).await; // [!code ++]

Ok(())
}
```
Now if we build the plugin and join the server, we should see a green "Welcome !" message with our username!
10 changes: 10 additions & 0 deletions docs/plugin-dev/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Pumpkin Plugin Development
::: warning
The Pumpkin Plugin API is still in a very early stage of development and may change at any time.
If you run into any issues please reach out on [our discord](https://discord.gg/aaNuD6rFEe)
:::

Pumpkin Plugins integrate with the server software on a very deep level allowing for many things that would not be possible on other server software.

The Pumpkin Plugin API takes inspiration from the Spigot/Bukkit plugin API in many places, so if you have previous experience with these and have experience with rust development, you should have a pretty easy time writing plugins for Pumpkin :smile: