Skip to content

About

Pumpkin plugin API for Zig

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Repository files navigation

Pumpkin API for Zig

This package provides Zig bindings for building Pumpkin plugins using WebAssembly (Wasm) components.

Requirements

Setup

Add the package to your project:

zig fetch --save git+https://github.com/Pumpkin-MC/pumpkin-api-zig

This 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.

A plugin

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

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.

Menus

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.

Files and other system access

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.

Working on the bindings

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 submodule

adapter/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.

About

Pumpkin plugin API for Zig

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages