Reference this skill when a Waterfall proxy plugin needs to send or receive plugin messages to/from backend Paper/Purpur servers via the BungeeCord channel or custom plugin channels.
When to Use This Skill
Intercepting BungeeCord channel messages at the proxy
Building custom backend ↔ proxy communication on Waterfall
Forwarding messages between backend servers via the proxy
API Quick Reference
Class / Method
Purpose
Notes
ProxyServer#registerChannel(String)
Register a channel for listening
Must be called before handling messages
ProxyServer#unregisterChannel(String)
Unregister a channel
Call in onDisable
PluginMessageEvent
Fires when a plugin message arrives at the proxy
net.md_5.bungee.api.event.PluginMessageEvent
event.getSender()
Connection
The backend server or player who sent it
event.getReceiver()
Connection
The destination
event.getData()
byte[]
Raw payload
event.setCancelled(boolean)
Prevent forwarding
Use when handling internally
event.getTag()
String
The channel name
ProxiedPlayer#sendData(String, byte[])
Send plugin message to player's backend
Proxy → Backend
Code Pattern
package com.yourorg.waterfallplugin.messaging;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.event.EventHandler;
import java.util.logging.Logger;
public class WaterfallMessagingHandler implements Listener {
private static final String CUSTOM_CHANNEL = "myplugin:network";
private static final String BUNGEE_CHANNEL = "BungeeCord";
private final Plugin plugin;
private final Logger logger;
public WaterfallMessagingHandler(Plugin plugin) {
this.plugin = plugin;
this.logger = plugin.getLogger();
}
public void register() {
ProxyServer proxy = plugin.getProxy();
proxy.registerChannel(CUSTOM_CHANNEL);
// BungeeCord channel is always registered by Waterfall itself
proxy.getPluginManager().registerListener(plugin, this);
}
public void unregister() {
plugin.getProxy().unregisterChannel(CUSTOM_CHANNEL);
}
@EventHandler
public void onPluginMessage(PluginMessageEvent event) {
String tag = event.getTag();
if (CUSTOM_CHANNEL.equals(tag)) {
handleCustomMessage(event);
} else if (BUNGEE_CHANNEL.equals(tag)) {
handleBungeeCordMessage(event);
}
}
private void handleCustomMessage(PluginMessageEvent event) {
// Only handle messages from backend servers
if (!(event.getSender() instanceof Server backendServer)) return;
ByteArrayDataInput in = ByteStreams.newDataInput(event.getData());
String action = in.readUTF();
switch (action) {
case "BROADCAST" -> {
String message = in.readUTF();
plugin.getProxy().broadcast(new TextComponent("§6[Network] §f" + message));
event.setCancelled(true); // Don't forward to player
}
case "MOVE_PLAYER" -> {
String playerName = in.readUTF();
String targetServer = in.readUTF();
movePlayer(playerName, targetServer);
event.setCancelled(true);
}
case "GET_PLAYER_COUNT" -> {
int count = plugin.getProxy().getOnlineCount();
respondToBackend(event, "PLAYER_COUNT_RESPONSE", out -> out.writeInt(count));
event.setCancelled(true);
}
default -> logger.warning("Unknown action: " + action);
}
}
private void handleBungeeCordMessage(PluginMessageEvent event) {
// Intercept specific BungeeCord sub-channels for logging/auditing
ByteArrayDataInput in = ByteStreams.newDataInput(event.getData());
String subChannel = in.readUTF();
if ("Connect".equals(subChannel)) {
String targetServer = in.readUTF();
logger.info("BungeeCord Connect request → " + targetServer);
// Allow Waterfall to process it normally
}
// Most BungeeCord sub-channels should be forwarded — don't cancel them
}
private void movePlayer(String playerName, String targetServer) {
ProxiedPlayer player = plugin.getProxy().getPlayer(playerName);
if (player == null) return;
ServerInfo server = plugin.getProxy().getServerInfo(targetServer);
if (server == null) return;
player.connect(server);
}
private void respondToBackend(PluginMessageEvent event,
String responseAction,
java.util.function.Consumer<ByteArrayDataOutput> writer) {
if (!(event.getReceiver() instanceof ProxiedPlayer player)) return;
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF(responseAction);
writer.accept(out);
player.sendData(CUSTOM_CHANNEL, out.toByteArray());
}
// Send from proxy → specific backend server
public void sendToServer(String serverName, byte[] data) {
// Must route via an online player on that server
plugin.getProxy().getPlayers().stream()
.filter(p -> p.getServer() != null
&& serverName.equals(p.getServer().getInfo().getName()))
.findFirst()
.ifPresent(p -> p.sendData(CUSTOM_CHANNEL, data));
}
}
Register in main class:
private WaterfallMessagingHandler messagingHandler;
@Override
public void onEnable() {
messagingHandler = new WaterfallMessagingHandler(this);
messagingHandler.register();
}
@Override
public void onDisable() {
if (messagingHandler != null) {
messagingHandler.unregister();
}
}
Common Pitfalls
Using MinecraftChannelIdentifier on Waterfall: This is a Velocity class. On Waterfall, channel names are plain strings ("myplugin:network").
Not cancelling PluginMessageEvent when handled: If you handle a message internally but don't cancel the event, Waterfall forwards it to the player. Call event.setCancelled(true) for messages you handle yourself.
Cancelling BungeeCord sub-channels: Cancelling built-in BungeeCord channel messages (like Connect, GetServer) prevents Waterfall from processing them. Only cancel if you're fully replacing Waterfall's handling.
Sending without a player on that server: To send data from the proxy to a specific backend, you need a player on that backend to route the message. If no player is on the target server, queue the message.
Version Notes
Waterfall 1.21: Plugin messaging API unchanged from older BungeeCord.
event.getData() returns a byte array. Guava's ByteStreams (bundled with BungeeCord) handles serialization.
Related Skills
bungeecord-channels.md — Full BungeeCord channel sub-channel reference
1---2name: messaging-33description: Plugin Messaging Skill — Waterfall4---5# Plugin Messaging Skill — Waterfall67## Purpose8Reference this skill when a Waterfall proxy plugin needs to send or receive plugin messages to/from backend Paper/Purpur servers via the `BungeeCord` channel or custom plugin channels.910## When to Use This Skill11- Intercepting `BungeeCord` channel messages at the proxy12- Building custom backend ↔ proxy communication on Waterfall13- Forwarding messages between backend servers via the proxy1415## API Quick Reference1617| Class / Method | Purpose | Notes |18|---------------|---------|-------|19| `ProxyServer#registerChannel(String)` | Register a channel for listening | Must be called before handling messages |20| `ProxyServer#unregisterChannel(String)` | Unregister a channel | Call in `onDisable` |21| `PluginMessageEvent` | Fires when a plugin message arrives at the proxy | `net.md_5.bungee.api.event.PluginMessageEvent` |22| `event.getSender()` | `Connection` | The backend server or player who sent it |23| `event.getReceiver()` | `Connection` | The destination |24| `event.getData()` | `byte[]` | Raw payload |25| `event.setCancelled(boolean)` | Prevent forwarding | Use when handling internally |26| `event.getTag()` | `String` | The channel name |27| `ProxiedPlayer#sendData(String, byte[])` | Send plugin message to player's backend | Proxy → Backend |2829## Code Pattern3031```java32package com.yourorg.waterfallplugin.messaging;3334import com.google.common.io.ByteArrayDataInput;35import com.google.common.io.ByteArrayDataOutput;36import com.google.common.io.ByteStreams;37import net.md_5.bungee.api.ProxyServer;38import net.md_5.bungee.api.chat.TextComponent;39import net.md_5.bungee.api.config.ServerInfo;40import net.md_5.bungee.api.connection.ProxiedPlayer;41import net.md_5.bungee.api.connection.Server;42import net.md_5.bungee.api.event.PluginMessageEvent;43import net.md_5.bungee.api.plugin.Listener;44import net.md_5.bungee.api.plugin.Plugin;45import net.md_5.bungee.event.EventHandler;4647import java.util.logging.Logger;4849public class WaterfallMessagingHandler implements Listener {5051 private static final String CUSTOM_CHANNEL = "myplugin:network";52 private static final String BUNGEE_CHANNEL = "BungeeCord";5354 private final Plugin plugin;55 private final Logger logger;5657 public WaterfallMessagingHandler(Plugin plugin) {58 this.plugin = plugin;59 this.logger = plugin.getLogger();60 }6162 public void register() {63 ProxyServer proxy = plugin.getProxy();64 proxy.registerChannel(CUSTOM_CHANNEL);65 // BungeeCord channel is always registered by Waterfall itself66 proxy.getPluginManager().registerListener(plugin, this);67 }6869 public void unregister() {70 plugin.getProxy().unregisterChannel(CUSTOM_CHANNEL);71 }7273 @EventHandler74 public void onPluginMessage(PluginMessageEvent event) {75 String tag = event.getTag();7677 if (CUSTOM_CHANNEL.equals(tag)) {78 handleCustomMessage(event);79 } else if (BUNGEE_CHANNEL.equals(tag)) {80 handleBungeeCordMessage(event);81 }82 }8384 private void handleCustomMessage(PluginMessageEvent event) {85 // Only handle messages from backend servers86 if (!(event.getSender() instanceof Server backendServer)) return;8788 ByteArrayDataInput in = ByteStreams.newDataInput(event.getData());89 String action = in.readUTF();9091 switch (action) {92 case "BROADCAST" -> {93 String message = in.readUTF();94 plugin.getProxy().broadcast(new TextComponent("§6[Network] §f" + message));95 event.setCancelled(true); // Don't forward to player96 }97 case "MOVE_PLAYER" -> {98 String playerName = in.readUTF();99 String targetServer = in.readUTF();100 movePlayer(playerName, targetServer);101 event.setCancelled(true);102 }103 case "GET_PLAYER_COUNT" -> {104 int count = plugin.getProxy().getOnlineCount();105 respondToBackend(event, "PLAYER_COUNT_RESPONSE", out -> out.writeInt(count));106 event.setCancelled(true);107 }108 default -> logger.warning("Unknown action: " + action);109 }110 }111112 private void handleBungeeCordMessage(PluginMessageEvent event) {113 // Intercept specific BungeeCord sub-channels for logging/auditing114 ByteArrayDataInput in = ByteStreams.newDataInput(event.getData());115 String subChannel = in.readUTF();116117 if ("Connect".equals(subChannel)) {118 String targetServer = in.readUTF();119 logger.info("BungeeCord Connect request → " + targetServer);120 // Allow Waterfall to process it normally121 }122 // Most BungeeCord sub-channels should be forwarded — don't cancel them123 }124125 private void movePlayer(String playerName, String targetServer) {126 ProxiedPlayer player = plugin.getProxy().getPlayer(playerName);127 if (player == null) return;128129 ServerInfo server = plugin.getProxy().getServerInfo(targetServer);130 if (server == null) return;131132 player.connect(server);133 }134135 private void respondToBackend(PluginMessageEvent event,136 String responseAction,137 java.util.function.Consumer<ByteArrayDataOutput> writer) {138 if (!(event.getReceiver() instanceof ProxiedPlayer player)) return;139140 ByteArrayDataOutput out = ByteStreams.newDataOutput();141 out.writeUTF(responseAction);142 writer.accept(out);143 player.sendData(CUSTOM_CHANNEL, out.toByteArray());144 }145146 // Send from proxy → specific backend server147 public void sendToServer(String serverName, byte[] data) {148 // Must route via an online player on that server149 plugin.getProxy().getPlayers().stream()150 .filter(p -> p.getServer() != null151 && serverName.equals(p.getServer().getInfo().getName()))152 .findFirst()153 .ifPresent(p -> p.sendData(CUSTOM_CHANNEL, data));154 }155}156```157158**Register in main class:**159```java160private WaterfallMessagingHandler messagingHandler;161162@Override163public void onEnable() {164 messagingHandler = new WaterfallMessagingHandler(this);165 messagingHandler.register();166}167168@Override169public void onDisable() {170 if (messagingHandler != null) {171 messagingHandler.unregister();172 }173}174```175176## Common Pitfalls177178- **Using `MinecraftChannelIdentifier` on Waterfall**: This is a Velocity class. On Waterfall, channel names are plain strings (`"myplugin:network"`).179180- **Not cancelling `PluginMessageEvent` when handled**: If you handle a message internally but don't cancel the event, Waterfall forwards it to the player. Call `event.setCancelled(true)` for messages you handle yourself.181182- **Cancelling BungeeCord sub-channels**: Cancelling built-in `BungeeCord` channel messages (like `Connect`, `GetServer`) prevents Waterfall from processing them. Only cancel if you're fully replacing Waterfall's handling.183184- **Sending without a player on that server**: To send data from the proxy to a specific backend, you need a player on that backend to route the message. If no player is on the target server, queue the message.185186## Version Notes187188- **Waterfall 1.21**: Plugin messaging API unchanged from older BungeeCord.189- `event.getData()` returns a byte array. Guava's `ByteStreams` (bundled with BungeeCord) handles serialization.190191## Related Skills192193- [bungeecord-channels.md](bungeecord-channels.md) — Full BungeeCord channel sub-channel reference194- [../events/SKILL.md](../events/SKILL.md) — Waterfall event system195- [../../paper/messaging/plugin-channels.md](../../paper/messaging/plugin-channels.md) — Paper backend messaging196- [../../velocity/messaging/plugin-messages.md](../../velocity/messaging/plugin-messages.md) — Equivalent Velocity messaging
Run npx skillmds@latest add mrpippi/messaging-3 in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Plugin Messaging Skill — Waterfall It is listed under Coding & Dev Tools on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
MrPippi (@mrpippi) published this skill. Their other Agent Skills are listed on their SkillMD profile.