Skip to content

Native Go Plugins

el211 edited this page Sep 9, 2026 · 1 revision

Native Go Plugins

Native Go plugins are experimental. Each plugin runs as its own process and communicates with GoCraft through a versioned, protocol-neutral Plugin API. A panic or process crash therefore does not directly crash the server.

Install a plugin

Put its .gcpkg file in plugins/ and restart GoCraft. The server discovers bundles before opening its network listeners — a failed plugin prevents startup rather than leaving gameplay partly protected.

Configuration defaults stored under config/ inside the bundle are copied on first load to plugins/<plugin-id>/. Existing administrator files are never overwritten. Context.DataDirectory() returns that directory.

Lifecycle

Implement gocraft.Plugin:

import gocraft "github.com/GoCraft-MC/gocraft-api-go"

type Plugin struct{}

func (*Plugin) OnLoad(ctx gocraft.Context) error { return nil }
func (*Plugin) OnEnable() error                  { return nil }
func (*Plugin) OnDisable() error                 { return nil }

OnLoad receives the logger, event registry, command registry, scheduler, and data directory — register callbacks there. OnEnable runs after loading; OnDisable runs during orderly shutdown or failed-enable cleanup.

Listeners and command callbacks run synchronously and serially for one plugin — do not block them. Scheduler callbacks run asynchronously. When a plugin is disabled, GoCraft unregisters its listeners and commands and cancels its tasks. Panics at every callback boundary are recovered and logged with a stack trace.

Events

The shared schema exposes native events. The two core events:

Event Timing Cancellable
PlayerJoinEvent after the player is reachable no
BlockBreakEvent before the block mutation yes

Java and Bedrock actions produce the same event types. No packet or numeric protocol IDs are exposed. Cancelling BlockBreakEvent keeps the block intact for either edition.

ctx.Events().OnBlockBreak(func(event *gocraft.BlockBreakEvent, control gocraft.EventControl) {
    if event.Block.ID == "minecraft:diamond_block" {
        control.Cancel()
    }
})

Commands

Commands are declared in the bundle's generated commands.pb. Register a callback against the path through that tree during OnLoad:

ctx.Commands().Register("shop sell <price>", func(call *gocraft.CommandContext) error {
    call.Reply("Hello, " + call.SenderName)
    return nil
})

Register the path, not the executor ID. Literals appear as written; arguments are in angle brackets — the same spelling a Java plugin uses. A path that names nothing in the bundle is refused at load (listing the paths the bundle declares) rather than becoming a handler that silently never runs.

The host owns everything before the callback: it matches the line against the tree, resolves each argument to its declared type, and checks the permissions guarding the path. The plugin never sees the raw line — typed values arrive through CommandContext.Args, and call.Can answers from host-resolved permissions.

Replies are queued as effects and delivered on the next tick (the same path event-handler effects take), so they reach players on either edition and the console alike.

Both editions are told about the command — Java receives a Brigadier graph, Bedrock a flat signature per way of running it — because the host renders one neutral tree twice. Each player is sent only the branches their permissions allow, and the list is resent when a plugin loads or unloads while they are online.

Build a bundle

See gocraft-plugin-examples for a complete plugin. Build its executable for the server OS, place it at the manifest's entry, then package it:

go install github.com/GoCraft-MC/gocraft-cli@latest
gocraft-cli build -o my-plugin.gcpkg ./my-plugin

The build tool is its own module (a plugin author compiles; they never run a server). It reads the bundle format from the same code the server does.

Native binaries are OS- and architecture-specific. Hot reload and in-process unloading are not supported. Rebuild plugins after a Plugin API version change — the host rejects incompatible manifests before executing code.

Clone this wiki locally