WASM plugin runtime for Dragonfly servers. Plugins run in sandboxed WebAssembly via Extism, so a buggy plugin won't take down your server.
go build -o server ./cmd/server
./serverPlugins live in plugins/<name>/. Each plugin folder needs:
plugin.toml- manifest with metadata and event subscriptionsplugin.wasm- compiled WebAssembly binary
The server scans the plugin directory on startup, validates manifests, resolves dependencies, and loads everything in the right order.
Plugins can be written in any language that compiles to WASM. Rust with extism-pdk works best.
[package]
name = "my-plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
extism-pdk = "1.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"use extism_pdk::*;
use serde::Deserialize;
#[derive(Deserialize)]
struct Player {
uuid: String,
name: String,
}
#[derive(Deserialize)]
struct Position {
x: i32,
y: i32,
z: i32,
}
#[derive(Deserialize)]
struct Block {
block_type: String,
position: Position,
}
#[derive(Deserialize)]
struct BlockBreakEvent {
player: Player,
block: Block,
}
#[host_fn]
extern "ExtismHost" {
fn host_log(data: &[u8]);
fn host_send_message(data: &[u8]) -> i64;
}
// called once when plugin loads
#[plugin_fn]
pub fn plugin_init() -> FnResult<()> {
Ok(())
}
// called for every subscribed event
#[plugin_fn]
pub fn handle_event(input: Vec<u8>) -> FnResult<Vec<u8>> {
// input format: "event_type\0{json_payload}"
let sep = input.iter().position(|&b| b == 0).unwrap();
let event_type = std::str::from_utf8(&input[..sep]).unwrap();
let payload = &input[sep + 1..];
let cancelled = match event_type {
"block_break" => {
let ev: BlockBreakEvent = serde_json::from_slice(payload)?;
// example: prevent breaking diamond ore
if ev.block.block_type.contains("diamond_ore") {
send_message(&ev.player.uuid, "§cYou can't break that!");
true // cancel the event
} else {
false
}
}
_ => false,
};
// first byte: 0 = allow, 1 = cancel
Ok(vec![cancelled as u8])
}
fn send_message(uuid: &str, msg: &str) {
#[derive(serde::Serialize)]
struct Req { player_uuid: String, message: String }
let req = Req { player_uuid: uuid.into(), message: msg.into() };
if let Ok(data) = serde_json::to_vec(&req) {
unsafe { host_send_message(&data) }.ok();
}
}cargo build --release --target wasm32-unknown-unknown
cp target/wasm32-unknown-unknown/release/my_plugin.wasm plugin.wasmid = "com.yourname.pluginid"
name = "Human Readable Name"
description = "What it does"
entry_point = "plugin.wasm"
license = "MIT"
authors = ["Your Name"]
[version]
major = 1
minor = 0
patch = 0
[api_version]
major = 1
minor = 0
patch = 0
# subscribe to events - can have multiple [[events]] blocks
[[events]]
event = "block_break"
priority = 0 # -200 to 300, lower runs first
ignore_cancelled = false # skip if already cancelled by another plugin?
[[events]]
event = "player_join"
priority = 100
# resource limits (optional, has defaults)
[limits]
max_memory_mb = 32
max_execution_ms = 50
max_fuel = 500000Lower runs first: -200 (lowest) → 0 (normal) → 300 (monitor)
Player: player_join player_quit player_chat player_move player_teleport player_jump player_sprint player_sneak player_death player_respawn player_hurt player_heal player_attack_entity
Block: block_break block_place block_interact
Item: item_use item_use_on_block item_use_on_entity item_consume item_drop item_pickup
Other: entity_spawn entity_despawn command sign_edit server_transfer
Call these from your plugin to interact with the server. All functions take JSON-encoded bytes and return a status code or JSON response.
host_log({"level": "info"|"warn"|"error"|"debug", "message": "..."})
host_get_player({"uuid": "..."}) -> Player
host_get_online_players() -> [Player]
host_send_message({"player_uuid": "...", "message": "..."})
host_broadcast({"message": "..."})
host_kick_player({"uuid": "...", "reason": "..."})
host_teleport_player({"uuid": "...", "x": 0, "y": 64, "z": 0})
host_set_player_health({"uuid": "...", "health": 20})
host_set_player_gamemode({"uuid": "...", "gamemode": "survival"|"creative"|"adventure"|"spectator"})
host_give_item({"uuid": "...", "item_type": "minecraft:diamond", "count": 1})
host_get_block({"x": 0, "y": 64, "z": 0}) -> Block
host_set_block({"x": 0, "y": 64, "z": 0, "block_type": "minecraft:stone"})
Persistent key-value storage per plugin. Data survives server restarts.
host_storage_set({"key": "...", "value": "..."})
host_storage_get({"key": "..."}) -> {"value": "..."}
host_storage_delete({"key": "..."})
host_schedule_task({"delay_ms": 1000, "task_id": "my-task"})
host_cancel_task({"task_id": "my-task"})
plugins.toml in server root:
plugin_dir = "plugins"
data_dir = "plugin_data"
[default_limits]
max_memory_mb = 64
max_execution_ms = 100
[logging]
level = "info" # debug, info, warn, errorCheck out examples/plugins/block-logger/ for a complete example that:
- Protects specific blocks from being broken
- Tracks player statistics (blocks broken/placed)
- Shows periodic notifications
- Welcomes returning players with their stats
MIT