name: nms-reflection-bridge description: "反射式 NMS 存取橋接:避開 CraftBukkit v1_21_R1 版本編譯依賴,透過 reflection 快取取得跨版本相容性 / Reflection-based NMS bridge for cross-version compatibility without Paperweight compile dependency"
NMS Reflection Bridge / NMS 反射橋接器
技能名稱 / Skill Name
nms-reflection-bridge
目的 / Purpose
提供不依賴 Paperweight userdev 的 NMS 存取方式,透過 Java reflection + 快取 Method/Field handle,使同一 JAR 可在多個 NMS 版本執行(例如 1.21、1.21.1、1.21.3)。
若專案只需單一版本,請使用 Paperweight 的原生 API(
nms-packet-sender等)更簡潔。本技能適用於跨版本分發場景。
NMS 版本需求 / NMS Version Requirements
- Paper 1.20.5+(原生使用 Mojang mappings)
- 不需 Paperweight 編譯依賴
- 僅需
org.spigotmc:spigot-api或io.papermc.paper:paper-api
觸發條件 / Triggers
- 「reflection bridge」「反射橋接」「NMS reflection」
- 「跨版本 NMS」「cross-version」「版本無關」
- 「避開 Paperweight」「no paperweight」
輸入參數 / Inputs
| 參數 | 範例 | 說明 |
|---|---|---|
package_name |
com.example.nms |
產出類別所在 package |
bridge_class_name |
NmsBridge |
核心反射類 |
cache_enabled |
true |
是否快取 Method handle |
輸出產物 / Outputs
NmsBridge.java— 反射工具核心(含 Method 快取)NmsClasses.java— NMS 類別名稱常數MethodHandleCache.java—java.lang.invoke.MethodHandle快取機制
代碼範本 / Code Template
NmsClasses.java
package com.example.nms;
/**
* Mojang-mapped NMS 類別全名常數。
* Paper 1.20.5+ runtime 原生使用這些名稱,無需 remap。
*/
public final class NmsClasses {
private NmsClasses() {}
public static final String SERVER_PLAYER = "net.minecraft.server.level.ServerPlayer";
public static final String SERVER_LEVEL = "net.minecraft.server.level.ServerLevel";
public static final String SERVER_GAME_PACKET_LISTENER =
"net.minecraft.server.network.ServerGamePacketListenerImpl";
public static final String PACKET = "net.minecraft.network.protocol.Packet";
public static final String CONNECTION = "net.minecraft.network.Connection";
public static final String MINECRAFT_SERVER = "net.minecraft.server.MinecraftServer";
/** CraftBukkit 套件名隨版本變動,需動態取得。 */
public static String craftBukkitPackage() {
String serverClassName = org.bukkit.Bukkit.getServer().getClass().getName();
// e.g. "org.bukkit.craftbukkit.v1_21_R1.CraftServer"
int lastDot = serverClassName.lastIndexOf('.');
return serverClassName.substring(0, lastDot);
}
}
MethodHandleCache.java
package com.example.nms;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class MethodHandleCache {
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
private static final Map<String, MethodHandle> METHOD_CACHE = new ConcurrentHashMap<>();
private static final Map<String, MethodHandle> FIELD_GETTER_CACHE = new ConcurrentHashMap<>();
private static final Map<String, MethodHandle> FIELD_SETTER_CACHE = new ConcurrentHashMap<>();
private MethodHandleCache() {}
public static MethodHandle method(Class<?> owner, String name, Class<?>... params) {
String key = owner.getName() + "#" + name + "#" + paramKey(params);
return METHOD_CACHE.computeIfAbsent(key, k -> {
try {
Method m = owner.getDeclaredMethod(name, params);
m.setAccessible(true);
return LOOKUP.unreflect(m);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Method not found: " + key, e);
}
});
}
public static MethodHandle fieldGetter(Class<?> owner, String name) {
String key = owner.getName() + "#get#" + name;
return FIELD_GETTER_CACHE.computeIfAbsent(key, k -> {
try {
Field f = owner.getDeclaredField(name);
f.setAccessible(true);
return LOOKUP.unreflectGetter(f);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Field not found: " + key, e);
}
});
}
public static MethodHandle fieldSetter(Class<?> owner, String name) {
String key = owner.getName() + "#set#" + name;
return FIELD_SETTER_CACHE.computeIfAbsent(key, k -> {
try {
Field f = owner.getDeclaredField(name);
f.setAccessible(true);
return LOOKUP.unreflectSetter(f);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Field not found: " + key, e);
}
});
}
private static String paramKey(Class<?>... params) {
StringBuilder sb = new StringBuilder();
for (Class<?> p : params) sb.append(p.getName()).append(',');
return sb.toString();
}
}
NmsBridge.java
package com.example.nms;
import java.lang.invoke.MethodHandle;
import org.bukkit.entity.Player;
public final class NmsBridge {
private NmsBridge() {}
private static final Class<?> SERVER_PLAYER_CLASS = loadClass(NmsClasses.SERVER_PLAYER);
private static final Class<?> PACKET_CLASS = loadClass(NmsClasses.PACKET);
private static final Class<?> GAME_LISTENER_CLASS = loadClass(NmsClasses.SERVER_GAME_PACKET_LISTENER);
private static final Class<?> CRAFT_PLAYER_CLASS =
loadClass(NmsClasses.craftBukkitPackage() + ".entity.CraftPlayer");
private static Class<?> loadClass(String name) {
try {
return Class.forName(name);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("NMS class not found: " + name, e);
}
}
/** 取得 Bukkit Player 對應的 NMS ServerPlayer 物件。 */
public static Object getHandle(Player player) {
try {
MethodHandle handle = MethodHandleCache.method(CRAFT_PLAYER_CLASS, "getHandle");
return handle.invoke(player);
} catch (Throwable t) {
throw new IllegalStateException("getHandle failed", t);
}
}
/** 透過 ServerPlayer 發送 NMS 封包。 */
public static void sendPacket(Player player, Object packet) {
try {
Object serverPlayer = getHandle(player);
MethodHandle connectionGetter = MethodHandleCache.fieldGetter(SERVER_PLAYER_CLASS, "connection");
Object connection = connectionGetter.invoke(serverPlayer);
if (connection == null) return; // 已離線
MethodHandle sendMethod = MethodHandleCache.method(GAME_LISTENER_CLASS, "send", PACKET_CLASS);
sendMethod.invoke(connection, packet);
} catch (Throwable t) {
throw new IllegalStateException("sendPacket failed", t);
}
}
/** 建立指定類名的 NMS 物件(使用 default constructor)。 */
public static Object newInstance(String className) {
try {
return loadClass(className).getDeclaredConstructor().newInstance();
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("newInstance failed: " + className, e);
}
}
}
推薦目錄結構 / Recommended Directory Structure
src/main/java/com/example/
├── MyNmsPlugin.java
└── nms/
├── NmsBridge.java
├── NmsClasses.java
└── MethodHandleCache.java
執行緒安全注意事項 / Thread Safety
- ✅
MethodHandleCache使用ConcurrentHashMap,多執行緒安全 - ✅
MethodHandle本身執行緒安全,可跨執行緒重用 - ⚠️ 反射呼叫的方法若非執行緒安全(如 NMS 世界存取),仍需遵守主執行緒規則
- ⚠️
getHandle()應在玩家存活時呼叫,否則可能取到 stale reference
失敗回退 / Fallback
| 錯誤 | 原因 | 解法 |
|---|---|---|
ClassNotFoundException: net.minecraft.server.level.ServerPlayer |
Paper < 1.20.5(runtime 仍用 Spigot mappings) | 升級至 Paper 1.20.5+ 或改用 Paperweight |
NoSuchMethodException: send |
方法簽名變更(如新增 optional 參數) | 改用 getDeclaredMethods() 迴圈比對 |
IllegalAccessException |
JVM module system 阻擋反射 | 在 build.gradle 加 --add-opens java.base/java.lang=ALL-UNNAMED |
| 效能問題(反射呼叫慢) | 未使用 MethodHandle 快取 |
確認所有呼叫走 MethodHandleCache |
| CraftBukkit package 版本錯誤 | hardcode v1_21_R1 |
永遠用 NmsClasses.craftBukkitPackage() 動態取得 |