-
Notifications
You must be signed in to change notification settings - Fork 1
Messaging
PowerLib ships a small, typed publish/subscribe layer for talking between a proxy (BungeeCord / Velocity) and its backend Spigot servers. You declare a Channel<T>, hand it a payload type, and publish / subscribe to that type. PowerLib serializes the payload, multiplexes it over a single Minecraft plugin-messaging channel (powerlib:main), and hands the other end the decoded object.
No DataOutputStream boilerplate per message, no juggling raw byte[] at the call site, and no shared password travelling on the wire to "secure" an ad-hoc protocol: both ends import the same Channel declaration and exchange real DTOs.
This page covers the common API (Channel, Codec, Messenger, Messengers), how framing multiplexes logical channels, and the per-platform transport that actually moves the bytes.
All messaging types live in
it.mycraft.powerlib.common.messaging, shipped inpowerlib-common. The platform transports live in thepowerlib-bukkit,powerlib-bungee, andpowerlib-velocitymodules and are picked up automatically — see Transports.
- The model
- Declaring a channel
- Publishing and subscribing
- How it travels: framing and transports
- End-to-end example
- Gotchas
- Reference
Four common types, each doing exactly one thing:
| Type | Role |
|---|---|
Channel<T> |
A named, typed channel. The same declaration is shared by both ends so they agree on the name and the payload type. |
Codec<T> |
Serializes a payload T to and from byte[]. Codec.string() is built in; write your own for DTOs. |
Messenger |
Typed pub/sub. publish(channel, payload) encodes and sends; subscribe(channel, handler) decodes inbound frames and dispatches them. |
Messengers |
Resolves the platform MessageTransport via ServiceLoader and exposes a ready singleton Messenger. |
Underneath sits a MessageTransport (the raw, channel-tagged byte[] wire) which you normally never touch — each platform module supplies its own.
publish(channel, payload)
│ channel.encode(payload) ── Codec<T> T → byte[]
▼
MessageTransport.send(name, bytes)
│ Framing.frame(name, bytes) ── multiplex onto one MC channel
▼
powerlib:main ──────────────────────► (other servers)
│ Framing.parse(...) ── recover (name, bytes)
▼
Messenger.receive(name, bytes)
│ channel.decode(bytes) ── Codec<T> byte[] → T
▼
subscriber.accept(payload)
A Channel<T> is just a name plus a Codec<T>. Declare it once, in code both the proxy and the backend can see (a shared module, or copied identically), and reference that single declaration from both sides:
import it.mycraft.powerlib.common.messaging.Channel;
import it.mycraft.powerlib.common.messaging.Codec;
public final class Channels {
private Channels() {}
// Plain-text channel using the built-in string codec.
public static final Channel<String> PING =
Channel.of("ping", Codec.string());
}The channel name ("ping" here) is the logical address inside the powerlib:main link — it is not a Minecraft plugin-messaging channel of its own. Many Channels coexist over the one MC channel; the name is what keeps them apart (see Framing). Names are matched verbatim, so both ends must use the same string.
Codec.string() encodes a String as UTF-8 bytes and back. Good for pings, simple commands, or a payload you have already serialized yourself:
Codec<String> codec = Codec.string();
byte[] wire = codec.encode("hello"); // UTF-8 bytes
String back = codec.decode(wire); // "hello"For anything structured, give the channel a codec that serializes your DTO. Codec<T> is a two-method interface:
public interface Codec<T> {
byte[] encode(T value);
T decode(byte[] data);
}powerlib-common already depends on the javax.json API, so you can write a JSON codec with no extra dependency. Given a DTO:
public record Announcement(String server, String message, int priority) {}a codec for it:
import it.mycraft.powerlib.common.messaging.Codec;
import javax.json.Json;
import javax.json.JsonObject;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
public final class AnnouncementCodec implements Codec<Announcement> {
@Override
public byte[] encode(Announcement a) {
String json = Json.createObjectBuilder()
.add("server", a.server())
.add("message", a.message())
.add("priority", a.priority())
.build()
.toString();
return json.getBytes(StandardCharsets.UTF_8);
}
@Override
public Announcement decode(byte[] data) {
try (var reader = Json.createReader(new ByteArrayInputStream(data))) {
JsonObject o = reader.readObject();
return new Announcement(
o.getString("server"),
o.getString("message"),
o.getInt("priority"));
}
}
}and the channel that uses it:
public static final Channel<Announcement> ANNOUNCEMENTS =
Channel.of("announcements", new AnnouncementCodec());Any serializer works — Gson, Jackson, a hand-rolled DataOutputStream — as long as both ends use the same codec for the channel. JSON is the lazy, debuggable default; reach for a binary codec only if you have measured that you need it.
Get the shared Messenger from Messengers.get(). It is built once per server from whatever transport that platform registered.
Subscribe (typically on startup) — the handler receives the already-decoded payload:
import it.mycraft.powerlib.common.messaging.Messenger;
import it.mycraft.powerlib.common.messaging.Messengers;
Messenger messenger = Messengers.get();
if (messenger != null) {
messenger.subscribe(Channels.ANNOUNCEMENTS, announcement -> {
Bukkit.broadcastMessage(
"[" + announcement.server() + "] " + announcement.message());
});
}Publish — pass a real object, not bytes:
messenger.publish(Channels.ANNOUNCEMENTS,
new Announcement("lobby", "Restart in 5 minutes", 1));Notes:
-
Messengers.get()returnsnullwhen no transport is registered (for example a unit-test classpath, or a platform with messaging unavailable). Guard it — do not assume non-null. - You may register more than one handler for the same channel; every subscriber is invoked for each inbound payload.
- A
Channelwith no subscriber on the receiving end is simply ignored there — its inbound frames are dropped, decoded by nobody. Publishing to a channel nobody listens on is harmless. - Subscription and dispatch are by channel name. Keep your
Channeldeclarations in one shared place so a typo can't silently split a channel in two.
Minecraft gives a plugin one named plugin-messaging channel at a time, but PowerLib wants many logical channels. Framing bridges the gap: it packs the logical channel name in front of the payload so a single MC channel can carry all of them.
public record Frame(String channel, byte[] data) {}
byte[] frame = Framing.frame("announcements", payloadBytes); // name + length + bytes
Framing.Frame parsed = Framing.parse(frame); // → ("announcements", payloadBytes)On the wire a frame is writeUTF(name) + writeInt(length) + the raw payload. Every transport send wraps its bytes with Framing.frame(...), and every transport onPluginMessage/listener unwraps with Framing.parse(...) before handing (name, data) to the Messenger. You never call Framing yourself — it is the glue between MessageTransport and the single MC channel powerlib:main.
A MessageTransport is the raw byte wire under the Messenger. Each platform module provides one and registers it through the Java ServiceLoader mechanism (a META-INF/services/it.mycraft.powerlib.common.messaging.MessageTransport file naming the class). Messengers loads the first one it finds:
MessageTransport transport =
ServiceLoader.load(MessageTransport.class, ...).findFirst().orElse(null);That is why Messengers.get() "just works" on each platform and returns null where no module contributed a transport. All three transports ride the same MC channel, powerlib:main, so a proxy and its backends interoperate out of the box.
| Platform | Transport | MC channel | Inbound | Outbound send semantics |
|---|---|---|---|---|
| Spigot / Paper | BukkitMessageTransport |
powerlib:main |
PluginMessageListener#onPluginMessageReceived |
Sent through the first online player as carrier. No players online ⇒ the frame is dropped. |
| BungeeCord | BungeeMessageTransport |
powerlib:main |
@EventHandler PluginMessageEvent (cancelled) |
Broadcast to every backend via ServerInfo#sendData(...). |
| Velocity | VelocityMessageTransport |
powerlib:main |
@Subscribe PluginMessageEvent (marked handled) |
Broadcast to every registered server via RegisteredServer#sendPluginMessage(...). |
The asymmetry is inherent to Minecraft plugin messaging, not a PowerLib choice:
-
Backends need a player. A Spigot server can only emit a plugin message through a connected player's connection — that is the only pipe to the proxy. With nobody online there is no pipe, so
BukkitMessageTransport.sendfinds no carrier and the frame is silently dropped. -
Proxies broadcast. Bungee and Velocity hold the connections to every backend, so their
sendfans the frame out to all servers. There is no per-server addressing in this API — if you only want one backend to act, include a target in the payload and let receivers filter (e.g. theserverfield inAnnouncementabove).
A backend announces something; the proxy relays it to the other backends.
Shared declaration (visible to both sides — same code, same names):
public final class Channels {
private Channels() {}
public static final Channel<Announcement> ANNOUNCEMENTS =
Channel.of("announcements", new AnnouncementCodec());
}Backend (Spigot) — publish on a command:
public void announce(String message) {
Messenger messenger = Messengers.get();
if (messenger == null) return; // messaging unavailable
messenger.publish(Channels.ANNOUNCEMENTS,
new Announcement(serverName, message, 1));
}Proxy (Velocity / Bungee) — subscribe and re-broadcast:
Messenger messenger = Messengers.get();
if (messenger != null) {
messenger.subscribe(Channels.ANNOUNCEMENTS, a ->
// proxy publish() fans this out to every backend
messenger.publish(Channels.ANNOUNCEMENTS, a));
}Every other backend — subscribe and act:
Messenger messenger = Messengers.get();
if (messenger != null) {
messenger.subscribe(Channels.ANNOUNCEMENTS, a -> {
if (!a.server().equals(serverName)) { // ignore our own echo
new it.mycraft.powerlib.chat.Message("&e" + a.message())
.broadcast(); // see the Messages page
}
});
}Channel names and codecs must be identical on both ends. The cleanest way to guarantee that is a shared module both the proxy plugin and the backend plugin depend on; copy-pasting the
Channels/ codec classes works too, as long as they stay in sync.
-
Bukkit needs an online player.
BukkitMessageTransport.sendcarries the frame on the first online player. With zero players online the frame is dropped — there is no buffering and no retry. Don't rely on a backend's startuppublishreaching the proxy before anyone joins; send it in response to player activity, or re-send on the first join. -
Messengers.get()can benull. No registered transport ⇒null(e.g. tests, or a platform without messaging). Always null-check beforepublish/subscribe. - No delivery guarantees. Plugin messaging is fire-and-forget over the player connection. There is no ack, no redelivery, and a frame can be lost if the carrier disconnects mid-send. Treat messages as best-effort; if a backend must converge on state, make the payload idempotent or carry a version/sequence in the DTO and reconcile on receipt.
-
Ordering is per-link, not global. Frames over one connection arrive in order, but a proxy
sendfans out to many servers independently and different backends publish through different players — so there is no global total order across the cluster. Never assume message A from server X was processed before message B from server Y. -
Broadcast, not unicast. A proxy
publishreaches every backend, including, after a relay, the original sender. Filter on a payload field (server id, target) when only some receivers should act, and guard against processing your own echo (see the example). -
Names are matched verbatim. A mismatched channel name doesn't error — it silently never delivers. Centralize
Channeldeclarations. -
One codec per channel, both ends. If the proxy encodes JSON and a backend decodes with a different codec,
decodethrows inside dispatch. Keep theCodecpaired with theChanneldeclaration and shared. -
powerlib:mainis reserved. All PowerLib messaging shares this single MC channel; don't register or hijack it from other plugin-messaging code. Add logical channels via newChannelnames, never new MC channels.
| Member | Signature | Description |
|---|---|---|
of |
static <T> Channel<T> of(String name, Codec<T> codec) |
Declares a channel with the given logical name and payload codec. |
name |
String name() |
The logical channel name shared by both ends. |
| Member | Signature | Description |
|---|---|---|
encode |
byte[] encode(T value) |
Serialize a payload to bytes. |
decode |
T decode(byte[] data) |
Deserialize bytes back to a payload. |
string |
static Codec<String> string() |
Built-in UTF-8 string codec. |
| Member | Signature | Description |
|---|---|---|
publish |
<T> void publish(Channel<T> channel, T payload) |
Encode payload with the channel's codec and send it. |
subscribe |
<T> void subscribe(Channel<T> channel, Consumer<T> handler) |
Register a handler invoked with every decoded payload received on the channel. Multiple handlers per channel are allowed. |
| Member | Signature | Description |
|---|---|---|
get |
static Messenger get() |
The shared messenger over the platform transport, or null if no transport is registered. |
| Member | Signature | Description |
|---|---|---|
Frame |
record Frame(String channel, byte[] data) |
A decoded frame: logical channel name + raw payload. |
frame |
static byte[] frame(String channel, byte[] data) |
Pack a logical channel name and payload into one frame. |
parse |
static Frame parse(byte[] frame) |
Decode a frame produced by frame(...). |
| Member | Signature | Description |
|---|---|---|
send |
void send(String channel, byte[] data) |
Send a raw frame on the given logical channel. |
listen |
void listen(BiConsumer<String, byte[]> handler) |
Register the single handler for inbound frames (called once by Messenger). |
Built-in implementations, registered via META-INF/services: BukkitMessageTransport, BungeeMessageTransport, VelocityMessageTransport — all on MC channel powerlib:main.
See also: Configuration for loading channel names and other settings from YAML · Scheduler for running subscribe handlers' work back on the main thread · Messages for formatting the text a received payload becomes.
Core (all platforms)
Bukkit / Paper
Commands