Paper API · Paper 網路
PacketEvents 封包過濾
不碰 NMS 的封包過濾:以 PacketEvents(主)或 ProtocolLib 軟依賴監聽封包,Hook + Bridge 隔離、Netty 執行緒只讀不可變快照、任何例外一律放行(fail-open)。
paper-packetevents-filter更新於 2026年10月9日觸發關鍵字
PacketEventspacketeventsProtocolLib封包過濾packet filterSYSTEM_CHAT_MESSAGEper-viewer lorePaper PacketEvents Filter
目的
在不使用 NMS、不注入 Netty pipeline 的前提下,過濾或改寫送給特定玩家的封包:隱藏/改寫系統聊天、每位觀看者的物品 lore 注入、對單一玩家隱藏粒子與音效。封包函式庫由伺服器上的 PacketEvents(主)或 ProtocolLib 提供,本插件只 compileOnly、不 shade,並以 softdepend 宣告。
需要 NMS 封包類別或自行改寫 Netty pipeline 時,請用 nms-packet-interceptor;軟依賴的通用做法見 paper-softdepend-hook。
平台需求
- Paper 1.21.11 / 26.2(兩版皆經編譯驗證,程式碼兩版相同)
- 純 Paper API,不需要 Paperweight
- 伺服器安裝 packetevents(編譯對象 2.13.0)或 ProtocolLib(5.3.0)
- Java 21(1.21.11)/25(26.2)
產生的代碼
PacketEventsHook.java(軟依賴接點,不含封包型別)
import java.util.logging.Level;
public boolean install(FilterState state) {
if (handles != null) return true;
if (!Bukkit.getPluginManager().isPluginEnabled("packetevents")) {
log.warning("packetevents is not installed: packet filters are disabled.");
return false;
}
try {
handles = PacketEventsBridge.register(state, log); // Bridge 才碰 PacketEvents 型別
return true;
} catch (LinkageError | RuntimeException e) {
log.log(Level.WARNING, "Could not register packet filters.", e);
return false;
}
}
ChatPacketFilter.java(Netty 執行緒、只讀快照、fail-open)
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSystemChatMessage;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
public final class ChatPacketFilter extends PacketListenerAbstract {
@Override
public void onPacketSend(PacketSendEvent event) {
if (!guard.active() || event.getPacketType() != PacketType.Play.Server.SYSTEM_CHAT_MESSAGE) return;
try {
UUID recipient = event.getUser() == null ? null : event.getUser().getUUID();
FilterSnapshot snapshot = state.current(); // 不可變快照
if (recipient == null || !snapshot.chatHidden().contains(recipient)) return;
WrapperPlayServerSystemChatMessage wrapper = new WrapperPlayServerSystemChatMessage(event);
if (wrapper.isOverlay()) return;
String plain = PlainTextComponentSerializer.plainText().serialize(wrapper.getMessage());
if (!plain.startsWith(snapshot.allowedPrefix())) event.setCancelled(true);
} catch (RuntimeException | LinkageError e) {
guard.trip(e); // 放行並停用,只記一次
}
}
}
生命週期
// onEnable
packetEvents = new PacketEventsHook(getLogger());
if (!packetEvents.install(state)) { // PacketEvents 優先,ProtocolLib 備援
protocolLib = new ProtocolLibHook(this);
protocolLib.install(state);
}
// onDisable
packetEvents.uninstall();
規則
- 封包函式庫型別只出現在 listener 與 Bridge;Hook、Plugin、Bukkit Listener 的欄位與方法簽名都不得出現
- Netty 執行緒只讀
FilterState#current(),不呼叫Bukkit.*/Player.*(UUID 例外),物品只改 clone - 任何例外一律 fail-open:放行原封包、
guard.trip(e)停用該過濾器並只記一次 setCancelled(true)/markForReEncode(true)放在 try 最後一行- 創造模式觀看者不注入 lore,並在
CREATIVE_INVENTORY_ACTION剝除標記行 - 不呼叫 PacketEvents 的
load()/init()/terminate();onDisable一定取消註冊
執行緒安全
- PacketEvents/ProtocolLib 的監聽器在 Netty 執行緒,不是主執行緒
- 設定、玩家狀態在主執行緒組成新的
FilterSnapshot後以FilterState#update發布 - 重送封包(
updateInventory()或PlayerManager#sendPacket)在主執行緒呼叫,且先改狀態再重送