A ProtocolLib-compatible ProtocolLib compatibility layer powered by PacketEvents.
P2P re-implements the high-value ProtocolLib API used by the server's installed plugins — same
com.comphenix.protocol.* package names, class names, and packet constants — on top of PacketEvents.
Plugins already compiled against the covered ProtocolLib surface link without recompiling, while the
actual packet interception is done by PacketEvents. It is intentionally not an NMS-compatible fork
of every internal ProtocolLib class.
Both projects name packets after the same wiki.vg/Mojang vocabulary, and PacketEvents already solves the hard part: version-independent packet identity and field decoding. What ProtocolLib adds on top is an API shape. P2P supplies that shape and delegates every protocol detail downwards, so it contains no version-specific packet logic of its own.
Whatever PacketEvents supports — verified from its ServerVersion enum, that is 1.7.2 through
the latest supported release (protocol 776 / "26.2" at the time of writing).
P2P does not narrow that range: all packet id and field mapping is delegated to PacketEvents at
runtime. plugin.yml deliberately omits api-version so the plugin is not pinned to one Bukkit
API bucket. The paper-api version in pom.xml is a compile-time surface only (JavaPlugin,
Player, Plugin, Logger); it is not a runtime restriction.
- Java 17+
- The PacketEvents plugin installed on the server (P2P
depends on it; it is not shaded)
mvn package
# -> target/ProtocolLib2PacketEvents-1.0.2.jarDrop the jar in plugins/ alongside PacketEvents. plugin.yml declares provides: [ProtocolLib],
so plugins whose plugin.yml lists depend: [ProtocolLib] resolve against P2P at the server
loader level, not just at the Java API level.
This is the part that makes real source compatibility possible.
ProtocolLib exposes packet types as compile-time constants (PacketType.Play.Server.ENTITY_ANIMATION),
and compiled plugins reference those fields directly. Java cannot add static fields to a class
reflectively, so a runtime-lookup registry would not link against existing plugin jars. P2P
therefore generates PacketType.java at build time:
maven-dependency-pluginresolves the compile classpath (where PacketEvents lives).P2PPacketTypeGenerator(run as a single-file source program) reflects over PacketEvents'PacketType, enumerating every per-phaseClient/Serverenum constant.- It emits one real
public static final PacketTypefield per constant intotarget/generated-sources/packettypes, whichbuild-helper-maven-pluginadds as a source root.
Bumping the PacketEvents dependency refreshes the constant set automatically. The current build generates 288 PacketEvents constants + 149 ProtocolLib-named aliases (aliases share the generated PacketEvents instances where the names are equivalent).
Most constants already agree between the two projects and are matched automatically (by exact
name, or by PacketEvents' wrapper class name matching one of ProtocolLib's declared packet class
aliases). The rest are pinned explicitly in src/generator/resources/protocollib-aliases.tsv, for
example:
| ProtocolLib | PacketEvents |
|---|---|
Play.Server.ANIMATION |
ENTITY_ANIMATION |
Play.Client.USE_ENTITY |
INTERACT_ENTITY |
Play.Server.MAP_CHUNK |
CHUNK_DATA |
Play.Server.ENTITY_DESTROY |
DESTROY_ENTITIES |
Play.Client.BLOCK_DIG |
PLAYER_DIGGING |
Fuzzy matching is deliberately not used for these. A wrong packet mapping fails silently and corrupts plugin behaviour, which is worse than not mapping at all, so each row was checked against ProtocolLib's own declarations and PacketEvents' wrapper classes. The generator fails the build if any alias points at a packet PacketEvents no longer defines, so an upstream rename surfaces as a build error instead of a silent behaviour change.
Both spellings resolve to the same instance:
PacketType.Play.Server.ANIMATION == PacketType.Play.Server.ENTITY_ANIMATION.
PacketType.Handshake (ProtocolLib's spelling) and PacketType.Handshaking (PacketEvents')
are both emitted.
The contract is pinned to ProtocolLib upstream SHA
67ce937109eb7bcc6e380ffe745d9b4a01ce1987 and PacketEvents 2.13.0. The latest local ABI
scan covers 428 upstream production classes and reports:
missing_classes=0missing_members=0descriptor_mismatches=0class_header_mismatches=37(inheritance/modifier differences still require behavior review)
This is an implementation/ABI result, not a version certification. Minecraft versions, pre-login
flows, and the Leaf/MCSV deployment remain live-test pending until a real server and client pass
the matrix in compatibility/packetevents-version-matrix.properties.
ProtocolManager— add/remove listeners,sendServerPacket,broadcastServerPacket,receiveClientPacketplus the historicalrecieveClientPacketoverloads, asynchronous-manager access,createPacket,getEntityFromID,getListeningTypes- Listener plumbing —
PacketAdapter,PacketListener,PacketEvent,ListeningWhitelist,ListenerPriority,ConnectionSide, with ProtocolLib's priority ordering (low first, MONITOR last) PacketContainerwith primitive, item, chat, profile, entity-type, chunk-coordinate, merchant/item-list, and 1.19.3+ DataValue/DataWatcher modifiers.getHandle()keeps the ProtocolLibObjectdescriptor;getPacketWrapper()is the PacketEvents-typed bridge method.- Wrappers —
WrappedChatComponent(Adventure-backed),WrappedGameProfile/signed properties,BlockPosition,ChunkCoordIntPair,WrappedDataWatcher,WrappedDataValue,WrappedWatchableObject, and serializer registry facades. - Reflection compatibility — the legacy
FuzzyReflection,FieldUtils,MethodUtils, accessor,MinecraftReflection, andStreamSerializermethods used by the installed plugins. AsynchronousManager— a real worker pool with per-player serial ordering, timeout/cancel handling, hold/release, and legacy boundedAsyncListenerHandler/AsyncMarkerqueues.- Hybrid transport — PacketEvents owns structured packets; a native Netty interceptor handles unmodelled raw frames, protocol transitions, temporary players, cancellation, byte edits, and output handlers without dispatching structured PacketEvents twice.
- Raw fallback — unmodelled packet types can be intercepted and cancelled with
getRawBuffer()byte-level access when the server exposes a compatible Netty channel. CheckhasStructuredAccess()and runtime capability detection first.
Dispatch indexes listeners by packet type, so a plugin listening for one packet does not add per-listener work to every other packet on the server.
These are real behavioural differences, not TODOs to gloss over:
- Field indices differ.
StructureModifierorders fields by declaration order on PacketEvents' wrapper classes, not on NMS packet classes.getIntegers().read(0)may therefore refer to a different field than under real ProtocolLib. Prefer the typed getters on PacketEvents' wrapper (packet.getHandle()) when you need certainty. - Direct Netty certification is pending. The fallback is implemented for native Netty channels and uses explicit capability detection; shaded/reflection-only channels fall back to PacketEvents' public channel API. The actual handshake/status/login/configuration/play matrix still needs a live server/client run.
WrappedDataWatcher.setObject(index, value)infers the entity data type only for unambiguous primitives. For a new index holding a component, optional, item stack or particle, usesetObject(int, EntityDataType, Object)— guessing would pick the wrong serializer.- Pre-login is capability-gated. The direct interceptor creates a temporary
Playerbefore Bukkit login and tracks handshake/status/login/configuration transitions. This path is not certified for every PacketEvents server version until the live matrix is run. - NMS types are never exposed. Anything expecting a literal NMS handle out of
getModifier().read(...)gets a PacketEvents object instead. - Behavior certification is narrower than class inventory. The class/member inventory is complete against the pinned production source, but native-only internals can still return an explicit unsupported-capability exception when the server has no equivalent PacketEvents/Bukkit representation. P2P does not silently report success for those paths.
- Packet allocation.
ProtocolManager.createPacketmust build a wrapper whose class has no no-arg constructor.ObjectAllocatorusessun.reflect.ReflectionFactory(exported viajdk.unsupported, verified working on JDK 25), falling back tosun.misc.Unsafeonly if that is unavailable —Unsafe.allocateInstanceis deprecated for removal, so it is not the primary path. The strategy in use is logged on enable. - Re-encoding. PacketEvents discards wrapper edits unless the event is marked for re-encode. P2P marks the event whenever a listener actually ran, so modifications reach the wire.
- Lazy holders. Generated holder classes initialize lazily, so registry-wide queries
(
PacketType.values(),fromKey) force all holders first.
Copyright (C) 2026 CyoriaSMP Team
Licensed under the GNU General Public License v3.0 or later — see LICENSE.
GPLv3 is required here rather than chosen freely: P2P links against PacketEvents, which is GPLv3, so any distributed build must be GPLv3-compatible. Using P2P privately on your own server is not distribution and carries no obligation.
Third-party components (see NOTICE for detail):
- PacketEvents (GPLv3) — not bundled; declared
providedand installed separately as its own plugin. - ProtocolLib (GPLv2) — no source code included. P2P independently re-implements ProtocolLib's
public API surface so existing plugins can link against it; the alias table in
src/generator/resources/protocollib-aliases.tsvis a packet-name mapping derived from ProtocolLib's public API declarations.