Adapters
An adapter is one implementation of the NmsAdapter contract for a given version (or range). Exactly one is selected at startup by NmsAdapters, published by NmsModule, and resolved everywhere else with service(NmsAdapter.class). Feature code depends only on the interface โ adding a server version never touches it.
The contract
Section titled โThe contractโpublic interface NmsAdapter extends Service { MinecraftVersion version(); boolean isSupported(); // false for the no-op fallback boolean supports(NmsFeature feature); // gate version-sensitive calls with this
Object nmsHandle(Object craftBukkitObject); // CraftPlayer โ ServerPlayer/EntityPlayer void sendPacket(Player player, Object packet); int getPing(Player player);}NmsFeature is a small starter set of capability flags (NMS_HANDLE, PACKET_SENDING, PLAYER_PING). Always gate calls โ on an unmapped version the active adapter may be the no-op, whose capability methods throw:
NmsAdapter nms = service(NmsAdapter.class);if (nms.supports(NmsFeature.PLAYER_PING)) { player.sendMessage("Ping: " + nms.getPing(player) + "ms");}Built-in adapters
Section titled โBuilt-in adaptersโReflectiveNmsAdapter
Section titled โReflectiveNmsAdapterโShips in core; selected for any 1.16+ server. Reflection-only โ no server jar, no BuildTools โ so the template works everywhere out of the box. It implements the primitives reflection can do reliably across the breakpoints:
| Feature | Supported | How |
|---|---|---|
NMS_HANDLE |
โ | getHandle() exists on every CraftBukkit wrapper |
PLAYER_PING |
โ | Bukkit-native Player#getPing() from 1.17; the NMS EntityPlayer.ping field before |
PACKET_SENDING |
โ | NMS connection names are obfuscated per version โ needs a mapped adapter (see Extending) |
PACKET_SENDING is deliberately left off: reliable packet work past 1.17 needs version-specific mapped names, which is exactly the reason to add a fork adapter rather than fight reflection.
NoOpNmsAdapter
Section titled โNoOpNmsAdapterโThe fallback when nothing matches (e.g. a server older than 1.16 in lenient mode). isSupported() and every supports(...) return false, and capability methods throw a clear UnsupportedOperationException naming the feature and version โ so a missing supports(...) gate fails loudly with a useful message instead of mysteriously.
Selection โ NmsAdapters
Section titled โSelection โ NmsAdaptersโNmsAdapters.defaults() // reflective adapter for 1.16+, NoOp below .register(predicate, factory) // add/override (most recent match wins) .strict() // throw UnsupportedVersionException instead of NoOp .select(VersionDetector.detect());The factory is invoked only when its predicate matches the selected version โ so a version-specific adapter class (and the classes it imports) is never linked by the JVM on a different server. This is the same lazy-loading guarantee HookManager gives for hooks.
The Reflection toolkit
Section titled โThe Reflection toolkitโme.dzusill.core.nms.reflect.Reflection is the shared helper adapters use to resolve CraftBukkit/NMS classes and members across versions, with caching. It generalises the getHandle() + private-field style already used by util.SkullTextures:
Object handle = Reflection.getHandle(player); // EntityPlayer / ServerPlayerClass<?> craftPlayer = Reflection.craftBukkitClass("entity.CraftPlayer");Field ping = Reflection.field(handle.getClass(), "ping");The CraftBukkit base package is resolved lazily on first use, so the toolkit loads cleanly under MockBukkit.
Manual verification matrix
Section titled โManual verification matrixโNMS calls canโt execute under MockBukkit (no real server), so the unit tests cover version parsing and adapter selection only. Verify real behaviour by dropping the built JAR on real Paper servers at the range endpoints and checking the startup log line (NMS adapter: โฆ for <version>) plus any capability you rely on:
| Version | Why it matters |
|---|---|
| 1.16.5 | Legacy relocated NMS package; craftBukkitTag present; SkullTextures uses GameProfile field reflection |
| 1.18.2 | Post-1.17 net.minecraft.* layout; SkullTextures switches to PlayerProfile API (setOwnerProfile) |
| 1.20.4 | Last version with the relocated CraftBukkit package |
| 1.21.1 | Mojang-mapped, no relocation (craftBukkitTag empty) |
| 1.21.4 | Paper tightened module access on CraftMetaSkull โ SkullTextures must use SkullMeta interface handle, not CraftMetaSkull directly |
SkullTextures โ what to check on each version
Section titled โSkullTextures โ what to check on each versionโRun ItemBuilder.head("<some base64>") (e.g. via a command that places a custom head) on each version above. The head should display the correct skin texture. If it shows a default Steve/Alex skin, SkullTextures strategy selection failed for that version.