This package provides Zig bindings for building Pumpkin plugins using WebAssembly (Wasm) components.
- Zig 0.16.0
wasm-toolsin yourPATH
Add the package to your project:
zig fetch --save git+https://github.com/Pumpkin-MC/pumpkin-api-zigThis pins the current commit in your build.zig.zon. Run it again to update. If you are working on the API itself, point the dependency at a local checkout instead:
.dependencies = .{
.pumpkin_api_zig = .{ .path = "../pumpkin-api-zig" },
},Then create a build.zig:
const std = @import("std");
const pumpkin_api = @import("pumpkin_api_zig");
pub fn build(b: *std.Build) void {
_ = pumpkin_api.addPlugin(b, b.dependency("pumpkin_api_zig", .{}), .{
.name = "my-plugin",
.root_source_file = b.path("src/main.zig"),
});
}zig build produces zig-out/my-plugin.wasm. Copy it into the server's plugins folder and restart the server.
const std = @import("std");
const pumpkin = @import("pumpkin");
const cmd = pumpkin.cmd;
const msg = pumpkin.msg;
pub const std_options: std.Options = .{ .logFn = pumpkin.logFn };
const MyPlugin = struct {
pub const metadata: pumpkin.Metadata = .{
.name = "my-plugin",
.version = "0.1.0",
.authors = &.{"You"},
.description = "Says hello.",
};
pub const events = .{
.player_join_event = onJoin,
};
pub const commands: []const cmd.Spec = &.{.{
.names = &.{ "hello", "hi" },
.description = "Says hello.",
.permission = "my-plugin:command.hello",
.run = hello,
}};
pub fn onLoad(_: pumpkin.Context) !void {
std.log.info("loaded", .{});
}
fn onJoin(_: pumpkin.Server, ev: *pumpkin.EventData(.player_join_event)) void {
ev.join_message = msg.build(&.{
.{ .text = "+ ", .color = .green, .bold = true },
.{ .text = ev.player.getName(), .color = .yellow },
});
}
fn hello(sender: cmd.Sender, _: pumpkin.Server, _: cmd.Args) cmd.Result {
sender.sendMessage(msg.fmt(.gold, "Hello, {s}!", .{sender.getName()}));
return cmd.ok(1);
}
};
comptime {
pumpkin.register(MyPlugin);
}For a complete plugin that uses typed commands, a menu and a config file, see example/src/main.zig.
Commands are a tree of literals and arguments, and any node with run set can be executed. cmd.typed hands the handler its arguments as a struct, with each field filled from the argument of the same name:
.{
.names = &.{"give"},
.permission = "my-plugin:command.give",
.then = &.{
.argument("targets", .players, .{
.run = cmd.typed(give),
.then = &.{.argument("amount", .{ .integer = .{ 1, 64 } }, .{ .run = cmd.typed(give) })},
}),
},
}
fn give(sender: cmd.Sender, _: pumpkin.Server, args: struct { targets: []const pumpkin.Player, amount: i32 = 1 }) cmd.Result {
// ...
return cmd.ok(@intCast(args.targets.len));
}Fields that are optional or have a default may be missing, which lets one handler serve both branches above. A field of type pumpkin.Player needs the argument to match exactly one player. If a required field is missing or invalid, the command fails with a message and the handler doesn't run. To read arguments by hand, take cmd.Args instead and use cmd.get(args, "targets", .players), cmd.int and cmd.float. A handler finishes with cmd.ok(n) or cmd.fail("...").
The permission node has to start with <plugin name>:. register registers it with the server for you, and .default sets who gets it when nobody has configured it.
pub fn onLoad(ctx: pumpkin.Context) !void {
pumpkin.menu.listen(ctx);
}
fn openShop(player: pumpkin.Player) void {
pumpkin.menu.open(player, .{
.title = &.{.{ .text = "Shop", .color = .gold }},
.buttons = &.{.{
.slot = 13,
.icon = .{ .id = "minecraft:diamond", .name = &.{.{ .text = "Buy a diamond", .color = .aqua }} },
.on_click = buy,
}},
.fill = .{ .id = "minecraft:gray_stained_glass_pane" },
});
}
fn buy(click: pumpkin.menu.Click) void {
click.player.sendSystemMessage(pumpkin.msg.plain("Bought!"), false);
}Items can't be taken out of a menu or put into it. Buttons that share a handler can be told apart with .data. on_close runs when the menu is closed, when another menu replaces it, or when the player leaves.
Plugins are built for wasm32-wasi, so the usual std.Io file APIs work. The server only grants what the plugin asks for in metadata.permissions:
| Permission | Grants |
|---|---|
fs.read.data |
Read access to plugins/data/<name> |
fs.write.data |
Read and write access to plugins/data/<name> |
network.dns, http.outbound, sys.env |
DNS lookups, outgoing HTTP, environment variables |
With a data permission, the data folder is the current directory:
const io = pumpkin.io();
const dir = std.Io.Dir.cwd();
try dir.writeFile(io, .{ .sub_path = "config.json", .data = "{}" });
const config = try dir.readFileAlloc(io, "config.json", pumpkin.abi.heap, .limited(64 * 1024));Server admins can still block permissions in the server config.
zig build test # unit tests for the ABI layer and the generator
zig build check # compiles every generated binding
zig build gen # regenerates src/wit/ and src/plugin.wit from the wit submoduleadapter/wasi_snapshot_preview1.reactor.wasm is the WASI preview 1 adapter from Wasmtime 48.0.2 (Apache-2.0 WITH LLVM-exception). Zig emits WASI preview 1, and the adapter maps it onto the preview 2 interfaces the server provides.