Repository navigation
Compat
查询“当前服务端属于哪个已验证版本基准、支持哪些能力”,而不是到处判断版本号;另外提供按玩家发送的数据包侧边栏 Sidebars。
| 模块名 | 制品 | 自动带入 |
|---|---|---|
compat |
me.kzheart.klib:klib-compat |
无 |
compat-v1_12 |
me.kzheart.klib:klib-compat-v1_12 |
compat |
compat-v1_20 |
me.kzheart.klib:klib-compat-v1_20 |
compat |
compat-v1_21 |
me.kzheart.klib:klib-compat-v1_21 |
compat |
compat-v26 |
me.kzheart.klib:klib-compat-v26 |
compat |
只用契约和自定义提供者时,选基础模块:
klib {
modules {
compat()
}
}需要仓库内全部版本实现时,显式选择四个实现:
klib {
modules {
compatV1_12()
compatV1_20()
compatV1_21()
compatV26()
}
}依赖、直接引用制品和打包方式见 构建与打包。
- Gradle 插件按模块图自动补入
compat。compat 系列不依赖core,不会因此引入core。 - 发布依赖里没有 Bukkit、NMS 或其他第三方运行时库。
- 侧边栏运行时通过反射访问服务端内部类;
Sidebars编译期使用 Spigot API 与klib-core,用它的插件还要选core()。
不用 Gradle 插件时直接依赖发布物,内嵌与重定位由自己的打包流程处理:
dependencies {
implementation("me.kzheart.klib:klib-compat:<klib-version>")
implementation("me.kzheart.klib:klib-compat-v1_12:<klib-version>")
implementation("me.kzheart.klib:klib-compat-v1_20:<klib-version>")
implementation("me.kzheart.klib:klib-compat-v1_21:<klib-version>")
implementation("me.kzheart.klib:klib-compat-v26:<klib-version>")
}最小示例:解析当前服务端并查询能力。
import me.kzheart.klib.compat.CompatProvider;
import me.kzheart.klib.compat.CompatProviders;
import me.kzheart.klib.compat.CompatResolver;
CompatResolver resolver = CompatProviders.resolver();
CompatProvider provider = resolver.resolve(getServer().getBukkitVersion());
logger().info("兼容实现:" + provider.id() + " / " + provider.version());boolean hexColors = provider.capability(Capabilities.TEXT)
.map(TextBridge::supportsHexColors)
.orElse(false);
boolean legacyMaterialNames = provider.capability(Capabilities.MATERIAL)
.map(MaterialBridge::usesLegacyNames)
.orElseThrow(() -> new IllegalStateException("缺少材质能力"));klib-compat 是能力声明注册表与版本选择工具:把“哪个版本区间由哪个适配器负责、它声明了哪些能力”整理成可查询、可自检的数据结构。业务代码面向 TextBridge、NbtBridge、MaterialBridge、InventoryBridge 和 SidebarBridge。
- 解析、选择和能力查询都不依赖 Bukkit 类型。四个实现模块只描述已经验证的基准差异。
- 除侧边栏外,它不会替你执行 Bukkit/NMS 适配。
TextBridge、NbtBridge、MaterialBridge、InventoryBridge只回答布尔形式的能力问题,不封装物品、文本或容器操作。 -
SidebarBridge是唯一真正发送数据包的能力,由 数据包侧边栏 中的Sidebars使用。 - Klib 其他模块的运行时版本适配(如
klib-item的物品标签桥接、klib-lang的文本渲染差异)在各自模块内部完成,不经过CompatResolver。打不打包compat-v*都不改变它们的行为。
| 适合 | 不适合 |
|---|---|
| 插件自身要按版本分支,用统一口径查询所属基准和能力 | 期待引入本模块后 Bukkit API 的版本差异被自动抹平 |
| 启动自检或排障时,输出所有已打包适配器的能力矩阵 | |
| 按同样的模型登记自定义的版本相关能力 | |
| 按玩家显示、不替换玩家 Bukkit 记分板的侧边栏 | |
| 新增有真实服务器回归证据的版本适配器 |
| 入口 | 行为 |
|---|---|
CompatProviders.resolver() |
装配已打包的内置实现,返回 CompatResolver(推荐) |
CompatProviders.resolve(String) |
只解析一次时一步到位 |
CompatProviders.detectServerVersion() |
反射读 org.bukkit.Bukkit.getBukkitVersion();不在 Bukkit 运行时返回 Optional.empty()
|
CompatProviders.resolveCurrent() |
探测 + 解析一步完成;探测不到时抛 IllegalStateException
|
CompatProviders.discover() |
已打包的内置实现列表;只打包基础模块时为空 |
new CompatResolver(List) |
手动指定参与解析的集合 |
CompatProvider provider = CompatProviders.resolve(getServer().getBukkitVersion());// 反射读取 org.bukkit.Bukkit.getBukkitVersion();不在 Bukkit 运行时返回 Optional.empty()
Optional<String> detected = CompatProviders.detectServerVersion();
// 探测 + 解析一步完成;探测不到时抛 IllegalStateException
CompatProvider current = CompatProviders.resolveCurrent();本模块不依赖 Bukkit,所以版本字符串默认由调用方传入;运行在 Bukkit 上时才可以用上面的自动探测。
CompatProviders 的发现方式:
- 按固定的实现类名列表用
Class.forName探测,没打包的实现直接跳过。 - 不用
ServiceLoader,也不需要META-INF/services资源。 - 类名字符串常量会被 Klib Gradle 插件的类文件重定位一并改写,shade + relocate 产物里同样有效。
- 发现结果不缓存。建议启用时装配一次,自己持有
CompatResolver。 - 一个内置实现都没打包时,
resolver()/resolve(String)抛IllegalStateException,消息里列出期望的实现类名。
需要混入自己的提供者,或刻意排除某个实现时,手动构造:
CompatResolver resolver = new CompatResolver(Arrays.asList(
new V1_12CompatImplementation(),
new V1_20CompatImplementation(),
new V1_21CompatImplementation(),
new V26CompatImplementation()));-
ServerVersion.parse在字符串中查找第一个主版本.次版本[.补丁],可以直接解析常见 Bukkit 版本文本;省略补丁时按0处理。 - 无法解析或没有适配器覆盖时抛
IllegalArgumentException。不要静默假定最新版能力。 - 无匹配时的异常消息列出每个已注册提供者的基准版本和落选原因(基准高于服务端 / 未声明支持该版本),据此区分是漏打包实现模块,还是版本落在覆盖空隙。
- 解析器选择“基准版本不高于当前服务器、且声明支持当前服务器”的最新实现。
构造期校验以下约束,任一不满足都抛 IllegalArgumentException:
- 提供者集合非空;
- 每个提供者的 ID 唯一;
- 每个提供者的基准版本唯一;
- 每个提供者必须
supports自己的基准版本(否则该实现永远无法被选中)。
下表是当前四个实现通过 CompatResolver 的实际解析结果。区间同时受 supports 和“基准版本不得高于当前版本”约束。
| 实现 | 适配器基准版本(SERVER_VERSION) |
实测联机版本 |
supports 声明范围 |
实际可解析范围 |
|---|---|---|---|---|
compat-v1_12 |
1.12.2 |
1.12.2 | [1.12.2, 1.20.0) |
[1.12.2, 1.20.0) |
compat-v1_20 |
1.20.4 |
1.20.4 | [1.20.0, 1.21.0) |
[1.20.4, 1.21.0) |
compat-v1_21 |
1.21(即 1.21.0) |
1.21.4 | [1.21.0, 26.2.0) |
[1.21.0, 26.2.0) |
compat-v26 |
26.2(即 26.2.0) |
26.2 | 同为 26.x 且不低于 26.2 | [26.2.0, 27.0.0) |
-
适配器基准版本:实现类中
SERVER_VERSION常量声明的支持起点,决定区间下界。ServerVersion省略补丁号时按0补齐,"1.21"与1.21.0等价。 -
实测联机版本:根
README.md里该版本线真实跑过回归的服务端版本(如 1.21 线是 1.21.4)。基准版本可以低于实测版本,解析行为以表中的代码常量为准。 - 适配器只按版本字符串选择实现。表中是解析器的选择范围,不等于区间内每个服务器版本都经过实际运行验证。
覆盖空隙与回退:
- 当前没有实现覆盖低于 1.12.2、1.20.0–1.20.3、27.x 及更高版本。
- 26.0/26.1 会落到
compat-v1_21,这是源码中明确保留的最近实现回退。 - 新增覆盖范围前应先有真实服务器回归证据,不能仅靠版本号猜测行为。
四个实现都公开完整的五项能力,但返回值或实现方式不同:
| 实现 | 十六进制颜色 | PersistentDataContainer | 旧材质名 | 运行时更新容器标题 | 侧边栏 |
|---|---|---|---|---|---|
compat-v1_12 |
否 | 否 | 是 | 否 | 队伍前缀,显示红色分数 |
compat-v1_20 |
是 | 是 | 否 | 是 | 分数显示文本,隐藏分数 |
compat-v1_21 |
是 | 是 | 否 | 是 | 分数显示文本,隐藏分数 |
compat-v26 |
是 | 是 | 否 | 是 | 分数显示文本,隐藏分数 |
- 通过类型化能力键访问(见快速开始),不要强制转换到版本实现。
- 能力缺失用
Optional.empty()表达。是否允许降级由业务决定,不要无条件调用get()。
启动时审计所有实现,可读取能力快照:
CompatCapabilityMatrix matrix = resolver.capabilityMatrix();
for (CompatCapabilityMatrix.Row row : matrix.rows()) {
logger().info(row.providerId() + " text=" + row.has(Capabilities.TEXT));
}me.kzheart.klib.compat.sidebar.Sidebars 按玩家维护侧边栏:
- 标题和行只以数据包形式发给目标玩家的客户端。
-
不读取、不替换、不修改玩家的 Bukkit
Scoreboard,原有队伍、计分项和其他插件的记分板保持不变。 - 更新时只发送与上一次不同的标题和行。
需要 core(),以及覆盖目标服务端版本的 compat-v*(Gradle 插件自动补入 compat):
klib {
modules {
core()
compatV1_21()
}
}import me.kzheart.klib.KPlugin;
import me.kzheart.klib.compat.sidebar.Sidebars;
import me.kzheart.klib.scheduler.Ticks;
import org.bukkit.entity.Player;
public final class QuestPlugin extends KPlugin {
@Override
protected void setup() {
Sidebars sidebars = Sidebars.install(context().scope());
tasks().every(Ticks.seconds(1), () -> {
for (Player player : getServer().getOnlinePlayers()) {
sidebars.show(player, "§6任务追踪",
"§f击杀僵尸 §a" + kills(player) + "/10",
"§f收集木头 §a5/5");
}
});
}
private int kills(Player player) {
return 3;
}
}| 方法 | 行为 |
|---|---|
Sidebars.install(Scope) |
用 CompatProviders.resolveCurrent() 选出当前版本的实现并要求其公开 Capabilities.SIDEBAR;没打包对应实现或版本不受支持时抛 IllegalStateException / IllegalArgumentException
|
Sidebars.install(Scope, SidebarBridge) |
使用指定实现,例如已经自行解析的 provider.capability(Capabilities.SIDEBAR)
|
show(Player, String title, String... lines) / show(Player, String, List<String>)
|
首次调用创建侧边栏,之后只发送差异;lines 自上而下排列,可以为空;离线玩家被忽略 |
hide(Player) |
移除侧边栏;之前未显示时返回 false
|
isShown(Player) |
当前登录会话是否显示着本实例的侧边栏 |
maxLines() |
实现可完整显示的最大行数,当前为 15;超过时 show 抛 IllegalArgumentException 且不发送数据包 |
在不支持的服务端上需要降级时,先查询能力再安装:
CompatProviders.resolveCurrent().capability(Capabilities.SIDEBAR)
.map(bridge -> Sidebars.install(context().scope(), bridge))
.orElse(null);| 规则 | 说明 |
|---|---|
| 颜色 | 只用 § 颜色代码,§x 十六进制颜色从 1.16 起生效 |
| 不解析 |
&、MiniMessage、JSON 文本都不解析,需要时先自行转换 |
| 换行 | 行文本不应包含换行 |
1.12.2 至 1.19.x(compat-v1_12) |
每行用一个队伍前缀显示文本,右侧显示客户端默认的红色分数 |
| 1.12 长度 | 行文本超过 16 个字符时按颜色延续拆到后缀,前缀加后缀最多 32 个字符;标题最多 32 个字符;超出部分截断。1.13 起没有这些限制 |
| 1.20.4 起 | 每行是带自定义显示文本的分数,计分项使用空白数字格式,不显示右侧分数 |
- 玩家自己的 Bukkit 记分板之后把计分项放到侧边栏槽位时,会覆盖本侧边栏。
-
hide只清空客户端的侧边栏槽位,不会替其他记分板恢复显示。 - 每个
Sidebars实例使用随机的计分项名,多个插件或实例互不覆盖内部状态。但客户端同一时间只显示一个侧边栏,后发送显示请求的一方生效。
展开:各实现已在哪些服务端版本上实服验证
验证环境为 Paper 服务端 + 真实客户端,覆盖创建、差异更新、增删行、15 行上限、移除与重新登录。
| 实现 | 已验证服务端 | 说明 |
|---|---|---|
compat-v1_12 |
1.12.2、1.16.5、1.18.2 | 1.16.5 只验证了数据包往返和客户端日志,未截图核对显示 |
compat-v1_20 |
1.20.4 | 1.20.5、1.20.6 按与 1.21 相同的数据包结构实现,未实服验证 |
compat-v1_21 |
1.21.1、1.21.4、1.21.11、26.1.2 | |
compat-v26 |
26.2 |
- 1.13 至 1.15、1.17、1.19、1.20.5 至 1.20.6,以及非 Paper 的 Spigot 服务端没有实服验证。
- 1.20.0 至 1.20.3 没有任何实现覆盖(见 解析区间),
resolveCurrent()会直接失败。
实现 CompatProvider,或继承 AbstractCompatProvider 并传入不可变能力映射。
- 能力键由 ID 和 Java 类型共同确定。
- 自定义键应保存为共享常量,避免不同模块创建外观相同但实际不相等的键。
public final class CustomCompat extends AbstractCompatProvider {
public CustomCompat() {
super("custom-1_20", "1.20.6", capabilities());
}
@Override
public boolean supports(ServerVersion version) {
return version.compareTo(ServerVersion.of(1, 20, 6)) >= 0
&& version.compareTo(ServerVersion.of(1, 21, 0)) < 0;
}
private static Map<Capability<?>, Object> capabilities() {
Map<Capability<?>, Object> values = new HashMap<Capability<?>, Object>();
values.put(Capabilities.TEXT, (TextBridge) () -> true);
return values;
}
}解析、能力发现与侧边栏实现使用 Kotlin,Java 仍通过 CompatProviders、Capabilities 与 Sidebars 静态入口调用。业务查询能力并提交标题/行,不需要维护 NMS 名称、包映射或数据包构造器。
现代侧边栏公共逻辑集中在 Compat 内部实现,版本模块只保留版本选择入口。相同 packet shape 不复制整套反射、差异更新或文本逻辑;旧版文本拆分仍由旧版适配器处理。
CompatProvider、SidebarBridge 保留 Java 平台类型契约;仅返回 primitive boolean 的四个能力 SPI 使用等价 Kotlin fun interface,继续支持 Java/Kotlin lambda。NbtBridge 只描述 PDC 能力,实际原生标签读写使用 Item 的 NativeItemTags,不把版本解析当作 NBT 布局探测。
版本解析成功只代表选择到适配器,不等于每个服务端补丁版本均经过实服验证。验证范围以 验证记录 为准。
- 解析器、版本对象、提供者和能力矩阵是普通 JVM 对象,不创建线程,也不需要释放。
- 除
SidebarBridge外,桥接能力只是能力描述,不替调用方执行 Bukkit/NMS 操作;业务模块仍需遵守相关 API 的线程约束。 -
Sidebars由作用域管理释放:-
install把实例安装到作用域,并通过作用域事件能力监听PlayerQuitEvent。玩家退出时直接丢弃其状态,不发送数据包;同一 UUID 重新登录后的首次show会重新创建侧边栏。 - 作用域关闭时向仍在线的玩家移除侧边栏,之后再调用
show抛IllegalStateException。 - 方法可从任意线程调用,同一玩家的更新按调用顺序串行发送。
-
-
compat基础模块不会自动带入任何版本实现。只打包基础模块时CompatProviders.discover()返回空列表,CompatProviders.resolver()抛IllegalStateException,直接构造实现类则会在运行时缺类。 - 能力缺失时不要无条件调用
Optional.get()。 - 版本匹配不替代目标服务端上的功能验证。版本范围是经过测试的支持边界,不是对未来版本的兼容承诺。
- 侧边栏反射在首次发送时解析。遇到无法识别的服务端结构会抛出带服务端版本的
IllegalStateException,不会静默降级。 - 1.20.0–1.20.3 没有实现覆盖,
resolveCurrent()和Sidebars.install(Scope)都会失败。