docs/waterfall/messaging/SKILL.md
# Plugin Messaging Skill — Waterfall ## Purpose 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
npx skillsauth add MrPippi/MPS docs/waterfall/messagingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
BungeeCord channel messages at the proxy| 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 |
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();
}
}
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.
event.getData() returns a byte array. Guava's ByteStreams (bundled with BungeeCord) handles serialization.development
透過 NMS Scoreboard/Objective/Team API 操作 sidebar、tablist 顯示名稱與計分板(Paper NMS + Mojang-mapped)/ Operate sidebar, tablist, and scoreboard via NMS Scoreboard/Objective/Team API
research
操作 GameProfile 進行 skin 注入,用於 NPC 外觀設定與假玩家實體(Paper NMS + Mojang-mapped)/ Manipulate GameProfile for skin injection used in NPC appearance and fake player entities
tools
透過 ClientboundLevelParticlesPacket 實現進階 NMS 粒子效果:客戶端專屬、大量粒子、自定義參數(Paper NMS + Mojang-mapped)/ Advanced NMS particle effects via ClientboundLevelParticlesPacket with per-client and bulk support
documentation
直接操作 CompoundTag 讀寫物品、實體、方塊實體的 NBT 資料(Paper NMS + Mojang-mapped)/ Read and write NBT data on items, entities, and block entities via CompoundTag