Paper 1.21.x port of EaglerXServer — run Eaglercraft (browser) clients on modern Paper servers.
EaglerXPaper is a fork of lax1dude's EaglerXServer that extends Bukkit/Spigot/Paper support from 1.12.2–1.17 up to 1.21.x (Paper 26.x). It lets Eaglercraft browser clients connect to a modern Paper server alongside vanilla Java Edition players, using the same dual-stack architecture as the original plugin.
This is largely the same project as EaglerXServer — it only changes a few minor things to ensure 1.17+ compatibility, plus adds a couple of small features. All credit for the actual plugin goes to lax1dude.
Based on EaglerXServer v1.1.1 (includes the LimboAPI compression fix, reduced default WebSocket frame size, empty ByteBuf handshake fix, and RateLimiterLocking ternary fix from upstream).
| Platform | Version Range | Status |
|---|---|---|
| Paper | 1.12.2 – 1.21.11+ | ✅ Fully supported |
| Spigot | 1.12.2 – 1.21.x | |
| Folia | Any | ❌ Not supported |
| BungeeCord | 1.21+ | ✅ Use upstream EaglerXServer (already supported) |
| Velocity | 3.4+ | ✅ Use upstream EaglerXServer (already supported) |
Java requirement: Java 17+ for Paper 1.12–1.20, Java 21+ for Paper 1.21–1.21.4, Java 25+ for Paper 26.x (1.21.11+).
Tested and works on: Paper versions 1.12.2 to 1.17.1, and 1.21.11 to 26.2, all with Java 25.
Paper 1.17 switched the runtime NMS from CraftBukkit names (EntityPlayer, PlayerConnection, NetworkManager) to Mojang names (ServerPlayer, ServerGamePacketListenerImpl, Connection). EaglerXServer's Bukkit platform uses just reflection, but anchored every reflection on NMS types. Those names changed in 1.17, breaking every reflection site.
EaglerXPaper fixes this with a multi-version reflection name table (NmsNames.java) that maps each NMS symbol to the set of simple names it has been known by across all supported versions. It's not perfect or efficient, but it works.
// Before (broke on 1.17+): if (f.getType().getSimpleName().equals("PlayerConnection")) { ... } // After (works on all versions): if (NmsNames.matches(f.getType(), NmsNames.PLAYER_CONNECTION)) { ... }
where NmsNames.PLAYER_CONNECTION = Set.of("ServerGamePacketListenerImpl", "PlayerConnection").
- Config structure — identical to regular EaglerXServer. Existing
plugins/EaglercraftXServer/configs work without any changes. - Plugin name — still technically
"EaglercraftXServer"internally, mostly to maintain compatibility with the base EaglerXServer API. - BungeeCord/Velocity modules — untouched (it already supports 1.21 on those platforms, so no need to change any of that).
These are features added by EaglerXPaper that are not in upstream EaglerXServer:
On server start, EaglerXPaper reads usercache.json and asynchronously pre-downloads skins for recently-seen players from Mojang's sessionserver API. This means when a player joins for the first time, their skin is already cached and displays instantly — no 2-3 second stall on first connect.
The prewarmer is conservative about Mojang's API rate limits (max 2 concurrent requests, 500ms minimum between fetches) and runs on low-priority background threads so it won't slow down server startup. If Mojang's API is unreachable, it silently skips those players.
Config (settings.yml):
skin_cache_prewarm: enable: true # Set to false to disable max_players: 50 # Max players to pre-warm (limits API calls)
EaglerXPaper automatically batches outbound packets for Eaglercraft connections that are sending many packets rapidly (e.g. during chunk loading or heavy entity updates). This reduces the number of WebSocket frames sent, which cuts bandwidth usage and per-frame overhead — especially helpful for mobile/slow connections.
The batcher is self-adaptive:
- Idle connections (few packets per second) — packets pass through immediately with zero added latency
- Burst connections (20+ packets in 100ms) — packets are buffered for up to 20ms and flushed as a batch
- Sustained bursts — forced flush every 200ms to cap latency
It sits between the frame codec and the handshake handler in the Netty pipeline, so it batches raw ByteBufs before they get wrapped into WebSocket frames. This is what actually reduces frame count and saves bandwidth.
Config (settings.yml):
adaptive_packet_batching: enable: true # Set to false to disable
Both features are enabled by default and require no configuration.
- Download
EaglerXPaper.jar - Place in your Paper 1.21.x server's
plugins/folder - Start the server — config files generate in
plugins/EaglercraftXServer/ - OPTIONAL (only needed if you use BungeeCord or Velocity) — Configure your reverse proxy / tunnel. See the regular EaglerXServer setup guide for details.
- Connect with an Eaglercraft client to
ws://yourserver:25565/(orwss://if using a reverse proxy such as Caddy, Nginx, or EaglerXServer's built-in TLS) - That's it! You can configure extra options if needed, but you really don't have to if all you wanted to do was "just get it working".
Dual-stack mode is enabled by default — EaglerXPaper shares the main server port (25565) and auto-detects whether each connection is vanilla Minecraft TCP or an Eaglercraft WebSocket.
git clone https://github.com/PlanetDogeCodes/eaglerxpaper.git cd eaglerxpaper ./gradlew :core:shadowJarBukkit # Output: core/build/libs/EaglerXPaper.jar
Requires Java 17+ and Gradle 8.5+ (wrapper included). The build compiles with the Paper 1.12.2 stub; compatibility with 1.21.x is done via reflection, not compile-time stuff.
Eaglercraft Client (ws:// or wss://)
│
▼
[Reverse Proxy / Tunnel] ← TLS termination (Caddy, nginx, playit.gg, CloudFlare, etc.)
│
▼ regular unsecure WebSocket
Paper 1.12+
│
▼ ChannelInitializeListener injection
EaglerXPaper
│
├── Eaglercraft handshake → Eaglercraft protocol pipeline
└── Vanilla MC detection → passes through to Paper
EaglerXPaper injects into Paper's Netty channel pipeline via Paper's ChannelInitializeListener API (the supported, stable injection method). It inspects the first bytes of each connection to determine whether it's an HTTP/WebSocket upgrade request (Eaglercraft) or a raw Minecraft handshake (vanilla), and routes accordingly.
| File | Change |
|---|---|
core/core-platform-bukkit/.../bukkit/NmsNames.java |
NEW — multi-version reflection name table |
core/core-platform-bukkit/.../bukkit/BukkitUnsafe.java |
Ported all reflection anchors; added findGameProfileGetter, createOwnEventLoopGroup; synchronized PropertyInjector |
core/core-platform-bukkit/.../bukkit/async/PlayerPostLoginInjector.java |
Ported reflection anchors; 3-arg constructor support; findEnumValueByName; convertToComponent for disconnect; GameProfile sync; transferred flag passthrough |
core/core-platform-bukkit/.../bukkit/BukkitListener.java |
Clean up orphaned $eaglerMarker properties on player quit |
core/src/main/java/.../base/EaglerXServer.java |
Removed "modern server version" warning; added prewarmer lifecycle |
core/src/main/java/.../base/EaglerListener.java |
catch (Throwable) for icon loading; clean error messages |
core/src/main/java/.../base/ServerIconLoader.java |
Null-check ImageIO.read() |
core/src/main/java/.../base/skins/SkinImageLoaderImpl.java |
Null-check ImageIO.read() |
core/src/main/java/.../base/skins/SkinManagerHelper.java |
Null guard for getServer() |
core/src/main/java/.../base/skins/SkinCachePrewarmer.java |
NEW — skin cache pre-warming on server start |
core/src/main/java/.../base/query/MOTDConnectionWrapper.java |
Null-check MOTD list |
core/src/main/java/.../base/webview/WebViewManager.java |
Null-check config; bounds-check DataRunnable |
core/src/main/java/.../base/voice/VoiceManagerLocal.java |
Null-check ICE servers |
core/src/main/java/.../base/voice/VoiceManagerRemote.java |
Null-check handler |
core/src/main/java/.../base/handshake/HandshakerInstance.java |
Null-check UUID from auth events |
core/src/main/java/.../base/pipeline/HTTPInitialInboundHandler.java |
Proper error logging + channel close |
core/src/main/java/.../base/pipeline/AdaptivePacketBatcher.java |
NEW — adaptive outbound packet batching |
core/src/main/java/.../base/pipeline/WebSocketInitialHandler.java |
Insert AdaptivePacketBatcher into pipeline |
core/src/main/java/.../base/config/EaglerXPaperConfig.java |
NEW — config holder for EaglerXPaper features |
core/src/main/java/.../base/config/EaglerConfigLoader.java |
Added skin_cache_prewarm and adaptive_packet_batching config sections |
core/src/main/java/.../base/DeferredStartSkinCache.java |
Made service field volatile for thread safety |
core/core-platform-bukkit/.../bukkit/PlatformPluginBukkit.java |
Try/catch around updateRealAddress; EventLoopGroup ownership tracking + shutdown |
core/build.gradle |
JAR renamed to EaglerXPaper.jar |
core/core-platform-bukkit/build.gradle |
Added api-version: '1.21' merge task |
| Addon | Status |
|---|---|
| EaglerXRewind (1.5.2 client support) | |
| EaglerWeb (HTTP file hosting) | |
| EaglerMOTD |
The addon JARs from upstream EaglerXServer releases use the same reflection-based architecture. They may work as-is on 1.21, but if they throw reflection errors, the same NmsNames-style porting technique applies. The source for all addons is included in this repo under their respective directories.
- Original EaglerXServer: lax1dude — the entire plugin architecture, Eaglercraft protocol implementation, and dual-stack design.
EaglerXPaper is a derivative work of EaglerXServer. All credit for the plugin's core functionality goes to lax1dude. This fork only adds version compatibility for Paper 1.17+, and is not a substantial change or rewrite.
Same as EaglerXServer — see LICENSE.
If you find a bug on a specific Paper version, please open an issue and include:
- The Paper version (e.g.
paper-1.21.11-132) - The full stack trace from
logs/latest.log - The output of
java -version
The reflection-based architecture means most version-specific bugs are fixable by adding a new candidate name to NmsNames.java or a new fallback path in BukkitUnsafe.java / PlayerPostLoginInjector.java — no API changes needed.