Skip to content

Networking ​

Plugins can make web requests, run TCP servers, connect to other services and send UDP datagrams. pumpkin.http covers web requests, pumpkin.net covers sockets that are handled once per tick without blocking the game, and the standard std.Io.net functions work as well.

Permissions ​

PermissionGrants
network.dnsLooking up host names
network.tcp.connectConnecting to TCP servers, which web requests need too
network.tcp.bindListening for TCP connections
network.udpUDP sockets

A request to a host name needs both network.dns and network.tcp.connect. Server admins can also restrict plugins to the loopback interface with loopback_only in the [plugins] section of pumpkin.toml.

Web requests ​

zig
const http = pumpkin.http;

const Status = struct { online: bool, players: u32 };

fn checkStatus(sender: cmd.Sender, _: pumpkin.Server, _: cmd.Args) cmd.Result {
    const res = http.get("https://api.example.com/status", .{}) catch |err| return cmd.fail(@errorName(err));
    if (!res.ok()) return cmd.fail("the API answered with an error");
    const status = res.json(Status) catch return cmd.fail("the API sent something unexpected");
    sender.sendMessage(msg.fmt(.green, "{d} players online", .{status.players}));
    return cmd.ok(1);
}

http.get, head and delete take a URL, and post, put and patch a URL and a body. http.sendJson(method, url, value, options) sends any value as JSON. The options set headers, content_type, authorization, user_agent, and max_body, the longest response body accepted, which is 8 MiB unless you set it.

A response has a status, a body and two helpers: ok() tells whether the status is in the 2xx range, and json(T) parses the body. The body and the parsed value are only valid until the callback returns.

A request blocks until the response arrives, so send requests from commands or rare events, not from events that fire every tick.

HTTPS needs certificates to check the server against. Copy the system's certificate bundle into the plugin's data folder, for example from /etc/ssl/certs/ca-certificates.crt on Linux, and add the fs.read.data permission. Without it, HTTPS requests fail with error.NoCertificates and a message in the log.

A TCP server ​

net.listen starts a TCP server that hands each line it receives to a callback:

zig
const net = pumpkin.net;

var server: *net.Listener = undefined;
var game: pumpkin.Server = undefined;

pub fn onLoad(ctx: pumpkin.Context) !void {
    game = ctx.getServer();
    game.keep();
    server = try net.listen(.{
        .port = 25580,
        .on_connect = onConnect,
        .callbacks = .{ .on_line = onLine },
    });
}

fn onConnect(conn: *net.Connection) void {
    conn.send("connected to the Minecraft chat\n") catch {};
}

fn onLine(_: *net.Connection, line: []const u8) void {
    game.broadcast(std.fmt.allocPrint(pumpkin.abi.arena(), "[tcp] {s}", .{line}) catch return);
}

fn onChat(_: pumpkin.Server, ev: *pumpkin.EventData(.player_chat_event)) void {
    server.broadcast(std.fmt.allocPrint(pumpkin.abi.arena(), "<{s}> {s}\n", .{ ev.player.getName(), ev.message }) catch return);
}

onChat is registered for player_chat_event, so chat messages go out to every connected client. The server listens on 127.0.0.1 unless host says otherwise, so only programs on the same machine can connect. Set .host = "0.0.0.0" to accept connections from other machines.

Sockets are checked once per tick, and nothing runs for a socket until something happens on it, so a quiet socket costs almost nothing and never holds up the game. on_connect runs when the server accepts a connection, and each connection has these callbacks:

  • on_line, with one line at a time without its line ending, when one arrives. This is the default framing.
  • on_data, with the bytes in the chunks they arrive in, when framing is .raw.
  • on_close, when the connection closes from either side.

On these connections, conn.send and conn.print never block. Data the other side isn't ready for is sent on later ticks, and a connection with more than max_outgoing bytes waiting, 1 MiB by default, is closed for reading too slowly. listener.broadcast sends to every connection, and close ends a connection or the whole server. conn.data is a number the plugin can use to tell connections apart.

Sockets still open when the plugin unloads are closed automatically.

Connecting to a server ​

zig
const conn = try net.connect("chat.example.com", 4000, .{ .callbacks = .{ .on_line = onReply } });
try conn.send("hello\n");

net.connect takes a host name or an IP address and tries its addresses in order until one accepts. It blocks while it looks up the host and waits for the connection, for up to timeout_ms, which is 10 seconds unless you set it. A host that doesn't answer holds up the command or event that called connect for that long, so keep the timeout short where it matters.

Once connected, a connection with callbacks is checked every tick like the ones of a server. Without callbacks, the plugin reads it itself with conn.readLine(), which waits up to read_timeout_ms for a line, and conn.send waits until the other side has taken the data.

UDP ​

zig
pub fn onLoad(_: pumpkin.Context) !void {
    _ = try net.udp(.{ .port = 25581, .on_datagram = onDatagram });
}

fn onDatagram(socket: *net.Udp, from: net.Address, data: []const u8) void {
    std.log.info("{f} sent {d} bytes", .{ from, data.len });
    socket.sendTo(from, "got it\n") catch {};
}

Using std.Io.net directly ​

pumpkin.io() returns an std.Io with networking, so std.Io.net works as it does in any Zig program, for example to look up a host name or to make a connection with a timeout:

zig
const io = pumpkin.io();
const host: std.Io.net.HostName = try .init("example.com");
const stream = try host.connect(io, 80, .{
    .mode = .stream,
    .timeout = .{ .duration = .{ .raw = .fromSeconds(5), .clock = .awake } },
});
defer stream.close(io);

These calls block until they finish, so the game waits for them. net.ready(handle) tells whether a raw socket has a connection, data or a datagram waiting, which lets a plugin check its own sockets from a task without blocking.

The examples cover each of these: https for web requests, tcp-server for a TCP server, udp for UDP, dns and http for std.Io.net, and nettest runs every networking path with /nettest.

Released under the MIT License.