This commit is contained in:
boybook
2025-12-01 20:59:16 +08:00
parent 12738a142c
commit 760c2dd9ad
5535 changed files with 21070 additions and 2021 deletions

View File

@@ -0,0 +1,68 @@
---
front:
hard: 入门
time: 10分钟
---
# 调度器
## 传统写法
用Bukkit写调度器一般是通过下述代码完成
```java
Bukkit.getScheduler().runTask(BukkitPlugin.javaPlugin){
// TODO
}
new BukkitRunnable(){
@Override
public void run(){
// TODO
}
}
```
但是Taboolib创建调度器则非常方便代码也是非常简约
```kotlin
submit(period = 10, async = true, delay = 20) {
// TODO
}
```
这样就创建了一个 每10Tick 运行一次 异步的 第一次运行延迟20Tick的调度器
## 方法详解
```kotlin
fun submit(
now: Boolean = false, // 是否立即执行
async: Boolean = false, // 是否异步执行
delay: Long = 0, // 延迟执行时间
period: Long = 0, // 重复执行时间
comment: String? = null, // 注释(无用)
executor: PlatformExecutor.PlatformTask.() -> Unit, // 调度器具体行为
): PlatformExecutor.PlatformTask
```
> 如果now为true时这个task不会重复执行。
## 变体 - 简单的异步调度器
```kotlin
submitAsync{
// TODO
}
```
## 变体 - 通过注解的方式注册调度器
```kotlin
@Schedule(period = 20, async = true)
fun tick() {
Bukkit.getOnlinePlayers().forEach {
it.sendMessage("Hello super bee")
}
}
```

View File

@@ -0,0 +1,118 @@
---
front:
hard: 入门
time: 15分钟
---
# KetherScript
## 介绍
`Kether``TabooLib`框架中内置的脚本语言,由 `海螺先生` 创造。
可以轻松实现诸多功能(如:发送动作栏或标题信息、改变玩家游戏模式、获取变量等等),它还拥有良好的拓展 API能让其他开发者更加轻松地开发出自己的动作语句。
## 文档资源 (社区 · 新)
- 文档首页 [https://taboo.8aka.org](https://taboo.8aka.org)
- 动作语句大全 [https://taboo.8aka.org/kether-list](https://taboo.8aka.org/kether-list)
## 如何调用
```kotlin
fun runKether(script: List<String>, player: Player) {
KetherShell.eval(
script, options = ScriptOptions(
sender = adaptCommandSender(player)
)
)
}
```
## 获取返回值
```kotlin
fun runKether(script: List<String>, player: Player): CompletableFuture<Any> {
return KetherShell.eval(
script, options = ScriptOptions(
sender = adaptCommandSender(player)
)
).thenApply { it }
}
```
## 注册组件
提供了 AbolethPlus 的 Get组件作为参考
作者:鹰
```kotlin
class AboPlusGetActions {
class GetKeyValue(val key: ParsedAction<*>, val default: ParsedAction<*>? = null, val target: ParsedAction<*>? = null) : ScriptAction<Any?>() {
override fun run(frame: ScriptFrame): CompletableFuture<Any?> {
val future = CompletableFuture<Any?>()
val keyString = frame.newFrame(key).run<String>().get()
val defString = default?.let { frame.newFrame(it).run<String>().get() } ?: ""
val targetOrNull = target?.let { frame.newFrame(it).run<String>().get() }
val targetString = if (targetOrNull.isNullOrEmpty()) {
frame.player().uniqueId
} else {
AbolethPlusAPI.getUserUUID(targetOrNull)
}
val result = AbolethPlusAPI.getValue(targetString, keyString, defString).getValueData(defString)
future.complete(result)
return future
}
}
class GetKeyDefault(private val key: ParsedAction<*>) : ScriptAction<Any?>() {
override fun run(frame: ScriptFrame): CompletableFuture<Any?> {
val future = CompletableFuture<Any?>()
val keyString = frame.newFrame(key).run<String>().get()
val result = AbolethPlusAPI.getDefault(keyString)
future.complete(result)
return future
}
}
/**
* shared = true 公有语句
* abpg {action} [def [{action}]] [@ (server|ID)] -> 获取语句
*
* abpg key -> 获取 key 值, 默认值 ""
* abpg key def -> 获取 key 的默认值
* abpg key def "default" -> 获取 key 的值, 默认值 "default"
*
* @author 鹰
* @since 2023/9/6
*
*/
companion object {
@KetherParser(["abpg", "abolethplusget"], shared = true)
fun parser() = scriptParser {
val keyAction = it.nextParsedAction()
it.mark()
try {
it.expect("def")
it.mark()
val defaultAction = it.nextParsedAction()
if (it.hasNext()) {
val target = matchTarget(it)
GetKeyValue(keyAction, defaultAction, target)
} else {
it.reset()
GetKeyDefault(keyAction)
}
} catch (ex: Exception) {
it.reset()
val target = matchTarget(it)
GetKeyValue(keyAction, target = target)
}
}
private fun matchTarget(it: QuestReader) = try {
it.mark()
it.expect("@")
it.nextParsedAction()
} catch (ex: Throwable) {
it.reset()
literalAction("")
}
}
}
```

View File

@@ -0,0 +1,115 @@
---
front:
hard: 进阶
time: 25分钟
---
# 数据包基本运用
## 介绍
本章 也可以说是发包的介绍 如何拦截数据包进行篡改 来达到效果
或者是直接发送一个数据包 让客户端收到
![](../images/0_30.png)
## 篡改数据包
如上图所示 我们有两种类型的事件
1. PacketReceiveEvent 玩家发送给服务端的数据包
2. PacketSendEvent 服务端发送给客户端的数据包
## 例子
```kotlin
@SubscribeEvent
fun onPacketPlayOutEntityEquipment(event: PacketSendEvent) {
if (event.packet.name != "PacketPlayOutEntityEquipment") {
return
}
val item = event.packet.read<List<MoJangPair<EnumItemSlot, ItemStack>>>("slots") ?: return
val copy = mutableListOf<MoJangPair<EnumItemSlot, ItemStack>>()
item.forEach {
val events = PacketReadItemEvent(event.player, toBukkit(it.second))
events.call()
copy.add(MoJangPair(it.first, toNMSCopy(events.itemStack)))
}
event.packet.write("slots", copy)
}
```
## 发送数据包
我们需要构造一个数据包的对象 以发送一个虚拟头颅方块的数据给玩家为例子
```kotlin
val parse = MojangsonParser.parse("""{Owner:{Id:"014df015-7eba-4ad0-a0e0-83164b7a45f2",Properties:{textures:[{Value:"方块的贴图"}]},Name:"自定义方块"},Rot:${rot}b,x:${x},y:${y},z:${z},id:"minecraft:skull",SkullType:3b}""".trimIndent())
val packetPlayOutTileEntityData = PacketPlayOutTileEntityData(
BlockPosition(loc.blockX, loc.blockY, loc.blockZ), 4, parse
)
player.sendPacket(packetPlayOutTileEntityData)
```
## 复杂的例子
**发送 BossBar 实现**
注意:这里我们减少了一些对于数据包本身的叙述(例如如何寻找我要的包的类型,这些应该是必备技能),仅介绍 TabooLib 方法。
根据常识可知,我们应该发送 PacketPlayOutBoss 这个数据包。我们去寻找这两个包的构造参数。(这里我使用反编译的办法)
1.16 及以下的版本的参数如下:
![](../images/0_31.png)
1.17 及以上的版本的参数如下:
(这就是 TabooLib 带给我们的自信,我们可以直接使用反混淆版的服务端,由此我们可以很清楚的看到 id、operation 等字段)
可知 1.16 及以下版本有一个无参构造函数1.17+ 没有(有一个私有构造函数,不方便)。对于没有无参构造函数的类,我们可以通过 TabooLib Reflex 提供的函数 Class<T>#unsafeInstance() 来快速获得一个实例。
![](../images/0_32.png)
我们可以看到在 1.17+ 中我们使用的全是反混淆Mojang Mapping的字段名。不用担心TabooLib 会自动帮我们处理。
外部调用方法: NMS.INSTANCE.sendBossBar()
使用到的别名如下:
```kotlin
// 1.16
typealias NMS16PacketPlayOutBoss = net.minecraft.server.v1_16_R3.PacketPlayOutBoss
typealias NMS16PacketPlayOutBossAction = net.minecraft.server.v1_16_R3.PacketPlayOutBoss.Action
typealias NMS16BossBattleBarColor = net.minecraft.server.v1_16_R3.BossBattle.BarColor
typealias NMS16BossBattleBarStyle = net.minecraft.server.v1_16_R3.BossBattle.BarStyle
typealias CraftChatMessage16 = org.bukkit.craftbukkit.v1_16_R3.util.CraftChatMessage
// Universal
typealias NMSPacketPlayOutBoss = net.minecraft.network.protocol.game.PacketPlayOutBoss
typealias NMSBossBattleBarColor = net.minecraft.world.BossBattle.BarColor
typealias NMSBossBattleBarStyle = net.minecraft.world.BossBattle.BarStyle
typealias CraftChatMessage19 = org.bukkit.craftbukkit.v1_19_R3.util.CraftChatMessage
```
可以看到1.16 以下的版本中,我们只写了一套 1.16 的实现,实际上在 1.15、1.14 等版本也可运行,因为 TabooLib 会自动帮我们处理 nmsProxy 内的跨版本和混淆表。
**数据包监听器**
TabooLib 有数据包监听器,我们可以监听数据包发送和接收。
数据包不会自动处理跨版本和混淆表。
使用方法:
```kotlin
/**
* 数据包接收
*/
@SubscribeEvent
fun e(e: PacketReceiveEvent) {
// 随便拿一个数据包举例子
if (e.packet.name == "PacketPlayInSetCreativeSlot") {
// 读
val nmsItem = e.packet.read<Any>("b")!!
// 写
e.packet.write("b", nmsItem)
}
}
/**
* 数据包发送
*/
@SubscribeEvent
fun e(e: PacketSendEvent) {
// 随便拿一个数据包举例子
if (e.packet.name == "PacketPlayOutOpenWindowMerchant") {
// 读
val merchant = e.packet.read<Any>("b")!!
// 写
e.packet.write("b", NMS.INSTANCE.adaptMerchantRecipe(merchant, e.player))
}
}
```

View File

@@ -0,0 +1,138 @@
---
front:
hard: 入门
time: 12分钟
---
# 物品操作工具
## 介绍
TabooLib 内置了一套 关于 构建物品 编辑物品 修改/获取 NBT 的工具
本文就是着重介绍 涉及到的模块包括:
1. BUKKIT_ALL
2. NMS_UTIL
## 构建物品
ItemBuilder
```kotlin
buildItem(XMaterial.APPLE) {
name = "&d坏黑的大苹果"
lore.add("&7这是一个坏黑的大苹果")
colored()
}
```
然后就可以返回一个 ItemStack 供你使用
### 入口 - 以物造物
通过现有物品构建新的物品
```kotlin
fun buildItem(itemStack: ItemStack, builder: ItemBuilder.() -> Unit = {}): ItemStack
```
### 入口 - XMaterial 造物
XMaterial 是一个多版本兼容支持的类型
```kotlin
fun buildItem(material: XMaterial, builder: ItemBuilder.() -> Unit = {}): ItemStack
```
### 入口 - Material 造物
```kotlin
fun buildItem(material: Material, builder: ItemBuilder.() -> Unit = {}): ItemStack
```
### 可选参数
![](../images/0_33.png)
## 修改元数据
在Bukkit中把物品的职能通过ItemMeta进行拆分
比如:可掉耐久的 书本 他们都是ItemMeta的子类
但是如果使用getMeta的方法获得到的是个clone的副本还需要再设置回去
在TabooLib中可以通过一个DSL快速操作
> 以掉耐久的为例 这个得看自己的版本来决定写法
```kotlin
buildItem.modifyMeta<Damageable> {
damage = 10
}
```
## 修改 Lore
Lore是一个高频使用的功能 所以TabooLib把他从ItemMeta抽出来作为一个单独的方法使用
```kotlin
buildItem.modifyLore {
add("123")
clear()
add("&d新的一行")
colored()
}
```
## 修改NBT
TabooLib对NBT进行了封装 叫做 ItemTag 本文作为入门教学 只提供增删改查等基础教学
如果要深入操作 还请自行研究
### 入口
```kotlin
val itemTag = buildItem.getItemTag()
```
```kotlin
// 获取数据
itemTag.getDeep("自定义的节点.支持多节点")
// 获取数据 如果不存在则设置默认值
itemTag.getDeepOrElse("自定义的节点.支持多节点", ItemTagData(""))
// 设置数据
itemTag.putDeep("自定义的节点.支持多节点", ItemTagData("新的值"))
// 删除数据
itemTag.removeDeep("自定义的节点.支持多节点")
// 保存数据
itemTag.saveTo(buildItem)
```
操作起来还是略显繁琐 如果NBT节点数量多 那么操作的量是毁灭的
所以枫溪在TabooLib里内置了一个工具 ItemTagReader 让你像配置文件一样操作NBT
### ItemTagReader
```kotlin
buildItem.itemTagReader {
val value = getString("自定义的节点.支持多节点", "默认值")
set("自定义的节点.支持多节点", "新的值 + ${value}")
// 收尾方法 写了才算写入物品 不然不会写入 减少操作可能出现的失误
write(buildItem)
}
```
## 工具
TabooLib为了简化物品操作 内置了很多的工具函数方便你的使用
### 检查数量
检查玩家背包/容器中的特定物品是否达到特定数量
```kotlin
fun Player.checkItem(item: ItemStack, amount: Int = 1, remove: Boolean = false): Boolean
fun Inventory.checkItem(item: ItemStack, amount: Int = 1, remove: Boolean = false): Boolean
```
### 检查数量 - 过滤器
获取符合过滤器内函数的物品数量
```kotlin
fun Inventory.hasItem(amount: Int = 1, matcher: (itemStack: ItemStack) -> Boolean): Boolean
player.inventory.hasItem {
it.type == Material.APPLE
}
```
### 扣除物品
```kotlin
fun Inventory.takeItem(amount: Int = 1, matcher: (itemStack: ItemStack) -> Boolean): Boolean
player.inventory.takeItem(50) {
it.type == Material.APPLE
}
```
### 获取数量
```kotlin
fun Inventory.countItem(matcher: (itemStack: ItemStack) -> Boolean): Int
```

View File

@@ -0,0 +1,71 @@
---
front:
hard: 入门
time: 12分钟
---
# 输入捕获
## 介绍
有的时候需要玩家输入一些功能 比如想点击按钮后输入某个值
然后根据这个值进行操作
TabooLib基于Kotlin的函数式编程风格写了几个工具供你使用
## 基于告示牌捕获
```kotlin
player.inputSign(arrayOf("", "", "请在第一行输入内容")) { line ->
// line 是输入完成后的内容
val name = line.getOrNull(0)
println("输入的内容是 $name")
}
```
## 基于书本的内容捕获
```kotlin
/**
* 向玩家发送一本书
* 并捕获该书本的编辑动作
*
* @param display 展示名称
* @param disposable 编辑后销毁
* @param content 原始内容
* @param catcher 编辑动作
*/
fun Player.inputBook(display: String, disposable: Boolean = true, content: List<String> = emptyList(), catcher: (List<String>) -> Unit)
player.inputBook("书本名称", true, listOf("原始内容")) { book ->
//book 是编辑后的书本
println("输出第一页内容")
println(book[0])
}
```
## 基于聊天框捕获
```kotlin
player.nextChat {
// it 是玩家输入的内容
println("玩家输入了 $it")
}
player.nextChatInTick(20 * 5, {
// it 是玩家输入的内容
println("玩家输入了 $it")
}, {
// 超时回调
println("超时了")
}, {
// 取消回调
println("取消了")
})
```
## 拓展 - 铁砧捕获
利用了UI模块 具体实现还请自行编写
```kotlin
player.openMenu<Anvil>("输入内容"){
onRename { player, s, inventory ->
// s 是玩家输入的内容
println("玩家输入了 $s")
}
}
```

View File

@@ -0,0 +1,32 @@
---
front:
hard: 入门
time: 5分钟
---
# PAPI变量
## 介绍
是的 TabooLib 提供了一个非常快捷的Papi变量注册方法 你只需要简单的继承 实现方法
剩下的TabooLib都帮你完成
## 注册
```kotlin
object PapiHook : PlaceholderExpansion {
// 变量前缀
override val identifier: String = "index"
// 变量操作
override fun onPlaceholderRequest(player: Player?, args: String): String {
return "变量返回的文字"
}
}
```
## 调用
非常简单!
```kotlin
"字符串%player_name%".replacePlaceholder(player)
listOf("字符串%player_name%").replacePlaceholder(player)
```

View File

@@ -0,0 +1,202 @@
---
front:
hard: 进阶
time: 8分钟
---
# 便捷的反射工具
## 介绍
TabooLib 内置了专属的反射依赖库 [https://github.com/TabooLib/reflex](https://github.com/TabooLib/reflex)
Reflex 为基于 Kotlin 语言开发的反射工具,其与 Java 原生反射 API 及 kotlin-reflect 间最大区别在于其可无视软兼容反射目标类中的字段或方法。
你无需了解这么多 只需要看看有没有遇到这种情况:
1. 想获取一个private的变量
2. 想执行一个private的方法
## 用法
```kotlin
class TestNoEdit(){
private var a = 0
private fun say(){
println("say")
}
}
fun test(){
val test = TestNoEdit()
test.a = 1 // 并不可以这样
//
val property = test.getProperty<Int>("a") ?: 0
test.invokeMethod<Any>("say")
TestNoEdit::class.java.invokeConstructor()
}
```
至此 成为反射的神!
## 获取字段参数
```kotlin
test.getProperty<字段类型>("字段的名称")
// T:
val property = test.getProperty<Int>("a") ?: 0
```
那,老师 如果我不知道用什么字段类型应该怎么办?
可以使用 Any
```kotlin
test.getProperty<Any>("a")
```
## 设置字段内容
```kotlin
test.setProperty("字段的名称","要设置的值")
test.setProperty("a",10)
```
## 运行方法
```kotlin
invokeConstructor(构造函数的类型)
// 例如 构造函数需要两个字符串
invokeConstructor(String::class.java,String::class.java)
```
## 获取构造函数
```kotlin
invokeConstructor(构造函数的类型)
// 例如 构造函数需要两个字符串
invokeConstructor(String::class.java,String::class.java)
```
## 案例 - 扫包读取某class的注解进行注册 6.1
```kotlin
@Awake
object GermUIHook : ClassVisitor(5) {
// 在什么时机进行扫描
override fun getLifeCycle(): LifeCycle {
return LifeCycle.ENABLE
}
// 扫描类,同级的还有扫描方法 和 扫描常量的
override fun visitStart(clazz: Class<*>, instance: Supplier<*>?) {
// 判断是否实现了 IGermUI 接口
if (clazz.interfaces.contains(IGermUI::class.java)) {
// 判断是否这个类被 @HookGerm("value") 修饰
if (clazz.isAnnotationPresent(HookGerm::class.java)) {
// 获取注解 @HookGerm
val hook = clazz.getAnnotation(HookGerm::class.java)
// 取出内容并添加到 Map中
hookMap[hook.value] = clazz as Class<IGermUI>
} else {
val name = clazz.simpleName
hookMap[name] = clazz as Class<IGermUI>
}
}
}
}
```
```kotlin
/**
* 当类开始加载时
*
* @param clazz 类
* @param instance 实例
*/
public void visitStart(@NotNull Class<?> clazz, @Nullable Supplier<?> instance) {
}
/**
* 当类结束加载时
*
* @param clazz 类
* @param instance 实例
*/
public void visitEnd(@NotNull Class<?> clazz, @Nullable Supplier<?> instance) {
}
/**
* 当字段加载时
*
* @param field 字段
* @param clazz 类
* @param instance 实例
*/
public void visit(@NotNull ClassField field, @NotNull Class<?> clazz, @Nullable Supplier<?> instance) {
}
/**
* 当方法加载时
*
* @param method 方法
* @param clazz 类
* @param instance 实例
*/
public void visit(@NotNull ClassMethod method, @NotNull Class<?> clazz, @Nullable Supplier<?> instance) {
}
```
## 案例 - 扫包注册papi变量 6.2
```kotlin
@Awake
@Inject
class PlaceholderRegister : ClassVisitor(0) {
val hooked by unsafeLazy {
runCatching { Class.forName("me.clip.placeholderapi.expansion.PlaceholderExpansion") }.isSuccess
}
override fun visitStart(clazz: ReflexClass) {
if (hooked && clazz.structure.interfaces.any { it.name == PlaceholderExpansion::class.java.name }) {
val expansion = findInstance(clazz) as? PlaceholderExpansion ?: error("PlaceholderExpansion must have an instance")
if (!expansion.enabled) {
return
}
object : me.clip.placeholderapi.expansion.PlaceholderExpansion() {
override fun persist(): Boolean {
return true
}
override fun getIdentifier(): String {
return expansion.identifier
}
override fun getAuthor(): String {
return BukkitPlugin.getInstance().description.authors.toString()
}
override fun getVersion(): String {
return BukkitPlugin.getInstance().description.version
}
override fun onPlaceholderRequest(player: Player?, params: String): String {
return expansion.onPlaceholderRequest(player, params)
}
override fun onRequest(player: OfflinePlayer?, params: String): String {
return expansion.onPlaceholderRequest(player, params)
}
}.also { papiExpansion ->
// 自动重载
if (expansion.autoReload) {
registerBukkitListener(ExpansionUnregisterEvent::class.java) {
if (it.expansion == papiExpansion) {
submit { papiExpansion.register() }
}
}
}
}.register()
}
}
override fun getLifeCycle(): LifeCycle {
return LifeCycle.ENABLE
}
}
```

View File

@@ -0,0 +1,178 @@
---
front:
hard: 进阶
time: 8分钟
---
# 节流与防抖函数
> 本文部分来自于 [https://github.com/TabooLib/taboolib](https://github.com/TabooLib/taboolib) 源代码注释
## 节流函数
### 概念
节流函数(Throttle Function) 用于限制函数在指定时间间隔内的执行频率,避免函数在短时间内重复触发
### 基础节流(无对象绑定)
创建与特定对象无关的节流操作,在指定时间窗口内仅执行一次操作
#### 方法签名
`throttle(delay: Long, action: () -> Unit)`
#### 示例
```kotlin
// 创建一个 500ms 的节流函数
val throttledAction = throttle(500) {
println("节流后输出")
}
// 高频使用场景
throttledAction() // 执行
throttledAction() // 不会执行
throttledAction() // 不会执行
// 等待 600 毫秒后
Thread.sleep(600)
throttledAction() // 重新激活
// 最终输出:
// 节流后输出
// 节流后输出
```
### 对象绑定节流
针对特定类型对象(如 Player)的节流操作,不同对象独立计算时间窗口
#### 方法签名
`throttle<K : Any>(delay: Long, noinline action: (K) -> Unit)`
#### 示例
```kotlin
val playerThrottle = throttle<Player>(500) { player ->
println("${player.name} 触发操作")
}
// 高频使用场景
playerThrottle(player) // 执行
playerThrottle(player) // 不会执行
playerThrottle(player) // 不会执行
// 等待 600 毫秒后
Thread.sleep(600)
playerThrottle(player) // 重新激活
// 最终输出:
// player 触发操作
// player 触发操作
```
### 带参数节流
支持传递额外参数的节流实现,保留首次调用参数,忽略后续参数
#### 方法签名
`throttle<K: Any, T>(delay: Long, action: (K, T) -> Unit)`
#### 示例
```kotlin
val messageThrottle = throttle<Player, String>(500) { player, msg ->
println("${player.name}: $msg")
}
// 高频使用场景
messageThrottle(player, "我是坏黑") // 执行
messageThrottle(player, "我是奶龙") // 不会执行
messageThrottle(player, "我是Bkm016") // 不会执行
// 等待 600 毫秒后
Thread.sleep(600)
messageThrottle(player, "我是神秘人") // 重新激活
// 最终输出:
// player: 我是坏黑
// player: 我是神秘人
```
### 对比
| 特性 | 基础 | 对象绑定 | 带参数 |
| --- |----|--- |--- |
|对象关联|❌|✅|✅|
|参数传递|❌|❌|✅|
|独立时间窗口|全局|按对象|按对象|
|适用场景|全局状态操作|玩家行为限制|带参数的行为限制|
## 防抖函数
### 概念
防抖函数(Debounce Function) 用于延迟函数执行直到特定时间段内没有新触发,适用于处理高频事件中只需响应最后一次操作的场景
### 基础防抖
创建全局防抖操作,在最后一次调用后等待指定延迟执行动作,期间新调用会重置计时器
#### 方法签名
`debounce(delay: Long, async: Boolean, action: () -> Unit)`
#### 示例
```kotlin
val debouncedAction = debounce(500) {
println("防抖后输出")
}
// 连续调用
debouncedAction()
debouncedAction() // 重置计时
debouncedAction() // 取消前两次,延迟 500ms 后执行
// 等待 600ms
Thread.sleep(600)
// 最终输出:
// 防抖后输出
```
### 对象绑定防抖
针对特定对象(如玩家)使用。在指定时间内只执行一次函数,如果在这段时间内再次调用函数,则重新计时
#### 方法签名
`debounce<K: Any>(delay: Long, async: Boolean, action: (K) -> Unit)`
#### 示例
```kotlin
val debouncedAction = debounce<Player>(500) { player ->
println("玩家 ${player.name} 的防抖后输出")
}
// 连续调用
debouncedAction(player)
debouncedAction(player) // 重置计时
debouncedAction(player) // 取消前两次,延迟 500ms 后执行
// 等待 600 毫秒
Thread.sleep(600)
// 最终输出:
// 玩家 player 的防抖后输出
```
### 带参数防抖
支持传递额外参数的防抖实现,保留首次调用参数,忽略后续参数
#### 方法签名
`debounce<K: Any, T>(delay: Long, async: Boolean, action: (K, T) -> Unit)`
#### 示例
```kotlin
val debouncedAction = debounce<Player, String>(500) { player, message ->
println("玩家 ${player. name} 的防抖后输出:$message")
}
// 连续调用
debouncedAction(player, "消息1")
debouncedAction(player, "消息2") // 重置计时
debouncedAction(player, "消息3") // 取消前两次,延迟 500ms 后执行
// 等待 600 毫秒
Thread. sleep(600)
// 最终输出:
// 玩家 player 的防抖后输出消息3
```
### 对比
| 特性 | 基础 | 对象绑定 | 带参数 |
|------|----|--- |--- |
| 对象关联 |❌|✅|✅|
| 参数传递 |❌|❌|✅|
| 计时策略 |全局重置|按对象重置|按对象重置|

View File

@@ -0,0 +1,98 @@
---
front:
hard: 入门
time: 10分钟
---
# 动态依赖
一般在开发Bukkit插件时开发者想要引入第三方库基本需要将库打包在插件本体中
这样会使插件显得非常臃肿,所以一种十分“优雅”的加载方式就诞生了
> 该功能在后续添加的时候会因为cache导致无法运行,需要删除重新加载才能运行
## 单个依赖
```kotlin
@RuntimeDependency(value = "!com.google.code.gson:gson:2.10.1", relocate = ["!com.google.gson","!com.example.library.gson"])
object Example : Plugin()
```
这样在插件启动的时候就会载入Google的GSON到服务器中
## 方法详解
让我们来看看这个方法有什么参数
```kotlin
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(RuntimeDependencies.class)
public @interface RuntimeDependency {
/**
* 依赖地址,格式为:
* <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>
*/
String value();
/**
* 测试类
* <p>
* <code>
* test = "!org.bukkit.Bukkit" // 前面带个感叹号避免在编译时重定向
* </code>
*/
String test() default "";
/**
* 仓库地址,留空默认使用 <a href="https://maven.aliyun.com/repository/central">阿里云中央仓库</a>
*/
String repository() default "";
/**
* 是否进行依赖传递
*/
boolean transitive() default true;
/**
* 忽略可选依赖
*/
boolean ignoreOptional() default true;
/**
* 忽略加载异常
*/
boolean ignoreException() default false;
/**
* 依赖范围
*/
DependencyScope[] scopes() default {DependencyScope.RUNTIME, DependencyScope.COMPILE};
/**
* 依赖重定向
* <p>
* <code>
* relocate = ["!taboolib.", "!taboolib610."] // 同 test 参数
* </code>
*/
String[] relocate() default {};
/**
* 是否外部库(不会被扫到)
*/
boolean external() default true;
}
```
## 变体 - 多个依赖
```kotlin
@RuntimeDependencies(
RuntimeDependency(value = "!com.google.code.gson:gson:2.10.1", relocate = ["!com.google.gson","!com.example.library.gson"]),
RuntimeDependency(value = "!com.github.ben-manes.caffeine:caffeine:2.9.3", relocate = ["!com.github.benmanes.caffeine","!com.example.library.caffeine"])
)
object Example : Plugin()
```

View File

@@ -0,0 +1,154 @@
---
front:
hard: 入门
time: 10分钟
---
# 命令
## 了解命令
首先我们先对一个命令进行拆解 `/taboolib give <user>`
* `主节点: /taboolib`
* `子节点:/give`
* `参数层:<user>`
了解完以后我们创建命令
```kotlin
@CommandHeader("taboolib", ["tl"], permission = "taboolib.command")
object TestCommand {
// 子节点
@CommandBody
val give = subCommand {
// 参数 user
dynamic("user") {
execute<CommandSender> { sender, context, argument ->
// 获取参数的值
val user = context["user"]
sender.sendMessage("Hello, ${user}")
}
}
}
}
```
这样我们就创建了一个`/taboolib`的主命令,子命令是`give`,参数层是填写`玩家名`的命令了
## 参数类型
我们还可以根据实际情况对参数的类型进行限制和选择
1. String类型
```kotlin
dynamic("user") {
execute<CommandSender> { sender, context, argument ->
val user = context["user"]
sender.sendMessage("Hello, ${user}")
}
}
```
2. Int类型
```kotlin
int("amount") {
execute<CommandSender> { sender, context, argument ->
val amount = context.int("amount")
sender.sendMessage("Hello, ${amount}")
}
}
```
3. Double 类型
```kotlin
decimal("amount") {
execute<CommandSender> { sender, context, argument ->
val amount = context.double("amount")
sender.sendMessage("Hello, ${amount}")
}
}
```
4. Player类型
```kotlin
player("user") {
execute<CommandSender> { sender, context, argument ->
val user = context.player("user")
// 转化为Bukkit的Player
val bukkitPlayer = user.castSafely<Player>()
sender.sendMessage("Hello, ${user}")
}
}
```
5. Boolean类型
```kotlin
bool("选项"){
execute<Player> { sender, context, argument ->
UI.open(sender, xxx, context.bool("选项"))
}
}
```
## 节点忽略
面对命令 /taboolib give xxx 的时候 我想实现
1. 有xxx的时候给xxx发消息
2. 没有xxx的时候给指令执行者发消息
我应该如何写呢?
```kotlin
@CommandBody
val give = subCommand {
dynamic("user") {
execute<CommandSender> { sender, context, argument ->
val user = context["user"]
sender.sendMessage("Hello, ${user}")
}
}
execute<CommandSender> { sender, context, argument ->
sender.sendMessage("Hello, MySelf")
}
}
```
在没有接受到参数 user的时候就不再执行 user方法体内的函数了 执行下方的函数
## 参数补全
假如你有一个商店插件 你要对商店名进行补全 如何书写呢?
```kotlin
@CommandBody(permission = "shop.open")
val open = subCommand {
dynamic("商店名") {
suggestion<CommandSender>(uncheck = true) { sender, context ->
ShopManager.getShopNameList()
}
player("目标玩家") {
execute<CommandSender> { sender, context, argument ->
Bukkit.getPlayer(context.player("目标玩家").uniqueId)
?.let { UIShopInfo.open(it, context["商店名"]) }
}
}
execute<Player> { sender, context, argument ->
UIShopInfo.open(sender, context["商店名"])
}
}
}
```
我们注意到在参数节点下方的 `suggestion` 方法
```kotlin
suggestion<CommandSender>(uncheck = true) { sender, context ->
ShopManager.getShopNameList()
}
```
我们需要在这个环节 返回一个`List<String>` 然后就可以进行补全了
如果只想作为提示不想强行约束需要标记 (uncheck = true)
## 注册简单命令
如果想要注册一个 `/day` 的指令如何快速注册呢?
这里利用到了 [自唤醒](4-自唤醒.md) 功能
```kotlin
@Awake(LifeCycle.ENABLE)
fun test() {
simpleCommand("day") { sender, args ->
Bukkit.getWorld("world")?.time = 1000
}
}
```
我们就在服务器 Enable 环节进行注册命令

View File

@@ -0,0 +1,78 @@
---
front:
hard: 入门
time: 12分钟
---
# 自唤醒
在Bukkit中基本都要在主类的 `onEnable`执行自己想要注册的内容
但是Taboolib提供了一个注解器可以在任意地方进行注解从而简化代码
> TabooLib 最常用的功能必须掌握
## 入口 @Awake
在服务器的某个 生命周期 自动执行 object 内的 无参方法
```kotlin
@Awake(LifeCycle.ENABLE)
fun test() {
info("我运行了")
}
```
约等于
```java
public class SelfPlugin extends JavaPlugin{
//
@Override
public void onEnable(){
getLogger.info("我运行了");
}
}
```
而上面test方法你可以封装在任意object中
可以大大降低每个类对主类的耦合度
要注意的点:
1. 必须在 object类内 不然会无效/报错
2. 方法不可以含有参数 (某些工具会在构建时往函数内插入参数需要检查)
## 生命周期
```kotlin
public enum LifeCycle {
NONE, // 未启动
CONST, // 插件初始化(静态代码块被执行时)时
INIT, // 插件主类被实例化时
LOAD, // 插件加载时
ENABLE, // 插件启用时
ACTIVE, // 服务器完全启动(调度器启动)时
DISABLE; // 插件卸载时
}
```
![](../images/0_7.png)
## 另一种自唤醒
如果你想让一个类 继承某个接口/增加某个注解 然后初始化的时候自动执行些什么
应该这样写 你不应该自己随便写扫包模块 这是不理智的
> 如果你有这种需求 我觉得已经有一定的代码阅读能力了 应该可以看懂以下代码
```kotlin
@Awake
object ClassReader : ClassVisitor(0) {
override fun getLifeCycle(): LifeCycle {
return LifeCycle.ENABLE
}
override fun visitStart(clazz: Class<*>, instance: Supplier<*>?) {
if (clazz.interfaces.contains(AbstractSkill::class.java)) {
info("加载技能: ${clazz.simpleName}")
val newInstance = clazz.newInstance()
if (newInstance is AbstractSkill) {
SkillManager.skills[newInstance.id] = newInstance
}
}
}
}
```

View File

@@ -0,0 +1,203 @@
---
front:
hard: 入门
time: 20分钟
---
# 配置文件
## 介绍
EventBus 是 Bukkit开发核心内容之一
## 监听事件
通常情况下也就能使用到此功能
比如我们要监听玩家进入游戏的事件 应该如何书写?
```kotlin
@SubscribeEvent
fun hello(event: PlayerJoinEvent) {
event.player.sendMessage("Hello, ${event.player.name}")
}
```
> 和原版的监听器相似 都是在方法上 进行标记 但是我们不需要去主类注册
## 注册事件
```kotlin
@Config("lib/test.yml")
lateinit var config: ConfigFile
```
那设置配置文件了如何保存呢?
```kotlin
fun test() {
config.set("hello.hello", "value")
// 等价于
config["hello.hello"] = "value"
config.saveToFile()
}
```
## 本地数据文件
`createLocal( path, saveTime, type )`
比如某些数据,我想通过本地配置文件保存
方法参数:
1. path - 文件路径
2. saveTime - 自动保存时间
3. type - 文件类型
```kotlin
fun test() {
val test = createLocal("test.yml")
test[playerName, number]
// 不需要写保存 会自动保存 当然也可以手动保存
// createLocal.saveToFile()
}
```
## 创建配置文件
如果不想让TabooLib帮助你管理
比如你想自己在resources里手动创建好一个配置文件然后在里面预设好配置这个配置文件
应该怎么写?
```kotlin
fun test() {
val file = newFile(getDataFolder(), "path.yml", create = true)
val loadFromFile = Configuration.loadFromFile(file, Type.YAML)
}
```
## 读取某个文件
如果有一个 yaml 并不是你创建的或者是你创建了但是没有缓存应该如何读取呢?
和创建方法相似但是我们不需要 create = true
```kotlin
fun test() {
val file = newFile(getDataFolder(), "path.yml") ?: return
val loadFromFile = Configuration.loadFromFile(file, Type.YAML)
}
```
> 一个小 tip 给不会使用Kotlin的读者
> 当你在一个变量后面写了 ?: return 的时候
> 当这个变量为空的时候 就自动结束方法
## 创建并写入File文本
通常是在一些 序列化 的场景中使用
```kotlin
newFile(getDataFolder(), it.path, create = true).writeText(
Yaml.encodeToString(ShopGoodsBaseData.serializer(), it),
StandardCharsets.UTF_8
)
```
## 读取File文本
通常在一些 反序列化 的场景中使用
```kotlin
fun loadData(file: File) {
file.readText(StandardCharsets.UTF_8).let { text ->
ShopManager.goods.add(
Yaml.decodeFromString(ShopGoodsBaseData.serializer(), text)
)
}
}
```
## 根据File转化为配置文件
有些时候 我们是只能拿到对象 拿不到文件的路径的(文件在内存中) 所以我们可以这样写
```kotlin
Configuration.loadFromFile(file, Type.YAML)
```
还有其他类似方法
```kotlin
fun loadFromFile(file: File, type: Type? = null, concurrent: Boolean = true)
fun loadFromReader(reader: Reader, type: Type = Type.YAML, concurrent: Boolean = true)
fun loadFromString(contents: String, type: Type = Type.YAML, concurrent: Boolean = true)
fun loadFromInputStream(inputStream: InputStream, type: Type = Type.YAML, concurrent: Boolean = true)
```
## 从Bukkit平台加载配置文件
上文说到 不可以直接转换 需要通过一个方法转换
只要来源的那个配置文件类 包含 `saveToString` 方法 且是标准的 就可以进行读取
```kotlin
fun loadFromOther(otherConfig: Any, type: Type = Type.YAML, concurrent: Boolean = true)
```
## 实现文件更新监听
通过 FileWatcher 自动识别文件是否更新
如果有更新则自动重新获取File
```kotlin
object FileListener {
private val listening = mutableSetOf<File>()
val watcher = FileWatcher.INSTANCE
fun listener(file: File, runnable: File.() -> Unit) {
watcher.addSimpleListener(file, runnable)
listening.add(file)
}
fun clear() {
listening.removeIf {
val remove = !it.exists()
if (remove) {
watcher.removeListener(it)
}
remove
}
}
fun load() {
val file = File(getDataFolder(), "config.yml")
listener(file) {
info("监听文件重载了")
}
}
}
```
## 数据存储配置文件
如果想做本地数据存储 更快捷一点的 可以试试用这个方法
```kotlin
val database by lazy {
createLocal("data/storage/data.yml", type = Type.YAML)
}
```
这样使用的时候 就会创建这个配置文件 然后这个配置文件会自动保存
所以 不可以手动更改配置文件
## 拓展 - 读取文件夹里的所有Yaml并加载
```kotlin
val read = ArrayList<Configuration>()
@Awake(LifeCycle.ENABLE)
fun load() {
read.clear()
val file = newFolder(getDataFolder(), "marks", create = false)
// 文件不存在则释放jar内的文件
if (!file.exists()) {
file.mkdirs()
releaseResourceFile("marks/test.yml")
}
file.walk()
.filter { it.isFile }
.filter { it.extension == "yaml" || it.extension == "yml" }
.forEach {
read.add(Configuration.loadFromFile(it))
}
releaseResourceFolderAndRead("marks/"){
walk{
read.add(it)
}
}
}
```
> 注意,其中 **releaseResourceFolderAndRead** 这个方法并不是原生 `Taboolib` 自带的功能
> 而是拓展库 [Arim](https://github.com/FxRayHughes/Arim) 里的方法
> 这个是由社区开发者 枫溪、嘿鹰、 WhiteSoul、Saukiya、坏黑、Mical 编写的

View File

@@ -0,0 +1,46 @@
---
front:
hard: 入门
time: 8分钟
---
# 事件管理器
## 监听事件
通常情况下也就能使用到此功能
比如我们要监听玩家进入游戏的事件 应该如何书写?
```kotlin
@SubscribeEvent
fun hello(event: PlayerJoinEvent) {
event.player.sendMessage("Hello, ${event.player.name}")
}
```
和原版的监听器相似 都是在方法上 进行标记 但是我们不需要去主类注册
## 注册事件
在一些情况下 我们可以用事件来解决一些复杂问题 我这里仅进行举例
我要设计一个打招呼的事件 在玩家进入游戏时触发 然后传入的内容是打招呼的内容
1. 声明一个事件对象
```kotlin
data class HelloEvent(
val player: Player,
val message: String
) : BukkitProxyEvent()
```
2. 事件注册
```kotlin
@SubscribeEvent
fun hello(event: PlayerJoinEvent) {
val helloEvent = HelloEvent(event.player, "Hello, ${event.player.name}")
helloEvent.call()
if (!helloEvent.isCancelled) {
event.player.sendMessage(helloEvent.message)
}
}
```
> 上述两个东西你可以理解Bukkit当中 `Bukkit.callEvent` 的处理办法

View File

@@ -0,0 +1,268 @@
---
front:
hard: 入门
time: 30分钟
---
# 箱子页面与自定义GUI
## UI - 预声明UI框架
TabooLib 开发了以下几种类型的UI
1. Basic / Chest 基本箱子页面
2. Linked / PageableChest 可翻页的箱子页面
3. Stored / StorableChest 可储存箱子页面
4. Hopper 漏斗容器
5. Anvil 铁砧容器
## 快速上手
1. 引入模块
```kotlin
taboolib {
env {
install(BukkitUI)
}
}
```
2. 刷新Gradle
3. 编写测试类
```kotlin
object TestUI {
@Awake(LifeCycle.ENABLE)
fun init() {
simpleCommand("testui") { sender, args ->
sender.sendMessage("testui")
sender.castSafely<Player>()?.let {
openMenu(it)
}
}
}
fun openMenu(player: Player) {
...
}
}
```
4. 快速创建一个三行的箱子UI 里面包含一个按钮
```kotlin
fun openMenu(player: Player) {
player.openMenu<Chest>("箱子标题") {
map(
"#########",
"# A #",
"#########",
)
set('A', buildItem(XMaterial.APPLE) {
name = "&a苹果"
lore.add("&f这是一个苹果")
colored()
}) {
player.sendMessage("点击了苹果")
}
}
}
```
![](../images/0_9.png)
## Chest - 标准容器界面
就是普通的 箱子UI 支持最多 1~6 行
类似于Trmenu的布局模式
以下是主要的方法 (完整方法查询源码/GitHub)
```kotlin
interface Chest : Menu {
/**
* 行数
* 为 1 - 6 之间的整数,并非原版 9 的倍数
*/
fun rows(rows: Int)
/**
* 设置是否锁定玩家手部动作
* 设置为 true 则将阻止玩家在使用菜单时进行包括但不限于
* 丢弃物品,拿出菜单物品等行为
*/
fun handLocked(handLocked: Boolean)
/**
* 页面构建时触发回调
* 可选是否异步执行
*/
fun onBuild(async: Boolean = false, callback: (player: Player, inventory: Inventory) -> Unit)
/**
* 页面关闭时触发回调
* 只能触发一次(玩家客户端强制关闭时会触发两次原版 InventoryCloseEvent 事件)
*
* TODO 2023/10/09 若启用虚拟化菜单,则 player.closeInventory() 不会触发该回调函数
*/
fun onClose(once: Boolean = true, skipUpdateTitle: Boolean = true, callback: (event: InventoryCloseEvent) -> Unit)
/**
* 点击事件回调
* 仅在特定位置下触发
*/
fun onClick(bind: Char, callback: (event: ClickEvent) -> Unit = {})
/**
* 整页点击事件回调
* 可选是否自动锁定点击位置
*/
fun onClick(lock: Boolean = false, callback: (event: ClickEvent) -> Unit = {})
/**
* 使用抽象字符页面布局
*/
fun map(vararg slots: String)
/**
* 根据抽象符号设置物品
*/
fun set(slot: Char, itemStack: ItemStack)
/**
* 根据抽象符号设置物品
*/
fun set(slot: Char, itemStack: ItemStack, onClick: ClickEvent.() -> Unit = {})
/**
* 获取位置对应的抽象字符
*/
fun getSlot(slot: Int): Char
/**
* 获取抽象字符对应的位置
*/
fun getSlots(slot: Char): List<Int>
/**
* 获取抽象字符对应的首个位置
*/
fun getFirstSlot(slot: Char): Int
}
```
## PageableChest - 可翻页的容器界面
```kotlin
player.openMenu<PageableChest<Player>>("在线玩家列表") {
}
```
与 标准容器不一样的是 我们在构建的时候需要传入一个类型 代表这里面需要展示的数据类型
比如我们需要制作 玩家列表 那么应该传入 玩家对象
```kotlin
fun openLinked(player: Player) {
player.openMenu<PageableChest<Player>>("在线玩家列表") {
// 布局
map(
"########E",
"#@@@@@@@#",
"L#######N",
)
// 设置槽位映射
slotsBy('@')
// 设置边界
set('#', buildItem(XMaterial.BLACK_STAINED_GLASS_PANE) {
name = "§8⬛"
})
}
}
```
然后就是设置数据来源了
玩家列表的数据来源非常好获取
```kotlin
player.openMenu<PageableChest<Player>>("在线玩家列表") {
// 布局
map...
// 设置槽位映射
slotsBy('@')
// 设置边界
set...
// 数据来源
elements {
Bukkit.getOnlinePlayers().toList()
}
}
```
有数据来源了就要开始着手显示这个数据了 我们这里使用头颅
然后注册一个点击回调
```kotlin
fun openLinked(player: Player) {
player.openMenu<PageableChest<Player>>("在线玩家列表") {
map...
slotsBy('@')
set...
elements...
onGenerate { player, element, index, slot ->
buildItem(XMaterial.PLAYER_HEAD) {
name = "§f${element.name}"
skullOwner = element.name
}
}
onClick { event, element ->
player.sendMessage("点击了 ${element.name}")
element.sendMessage("你被 ${player.name} 点击了")
}
}
}
```
接下来我们需要一个翻页的按钮
```kotlin
player.openMenu<PageableChest<Player>>("在线玩家列表") {
map...
slotsBy('@')
set...
elements...
onGenerate...
onClick...
// 设置翻页按钮
setNextPage(getFirstSlot('N')) { page, hasNextPage ->
if (hasNextPage) {
buildItem(XMaterial.SPECTRAL_ARROW) {
name = "§f下一页"
}
} else {
buildItem(XMaterial.ARROW) {
name = "§7下一页"
}
}
}
setPreviousPage(getFirstSlot('L')) { page, hasPreviousPage ->
if (hasPreviousPage) {
buildItem(XMaterial.SPECTRAL_ARROW) {
name = "§f上一页"
}
} else {
buildItem(XMaterial.ARROW) {
name = "§7上一页"
}
}
}
}
```
至此 你已经掌握了翻页容器的基本使用
## 其他容器
其他容器都和Chest类似 查看其中的方法可以通过查看源码中封装好的方法去调用
## 布局槽位图
[官方维基百科](https://minecraft.wiki/w/Java_Edition_protocol/Inventory)

View File

@@ -0,0 +1,57 @@
---
front:
hard: 入门
time: 15分钟
---
# 数据库
## 介绍
Database模块主要就是简化 SQL操作 使用DSL生成SQL并且获取返回值
## 组成部分
1. Host 数据库连接配置
2. Datasourse 数据库连接对象
3. Table 表对象
4. Query 查询操作
## 首先创建Host
1. 直接读取配置文件创建Host
```kotlin
config.getHost("database")
```
2. 配置文件对应内容
```yaml
database:
host: 127.0.0.1
port: 3306
user: root
password: 123456
database: fengxi666
```
## 创建表对象
如果你还不怎么可以熟练的使用SQL 那么我们不妨就把数据库理解为 Excel表格
我们接下来就是要阐述这个表每列都是做什么的 用于创建表和管理表
> 代码来自 TabooLib [expansion-player-database](https://github.com/TabooLib/taboolib/tree/master/expansion/expansion-player-database)
```kotlin
val tableVar = Table("table_name", host) {
add { id() }
add("user") {
type(ColumnTypeSQL.VARCHAR, 36) {
options(ColumnOptionSQL.KEY)
}
}
add("key") {
type(ColumnTypeSQL.VARCHAR, 64) {
options(ColumnOptionSQL.KEY)
}
}
add("value") {
type(ColumnTypeSQL.VARCHAR, 128)
}
}
```

View File

@@ -0,0 +1,184 @@
---
front:
hard: 入门
time: 8分钟
---
# 语言文件
## 介绍
用于提高插件的灵活性增加插件的国际化应用
一般情况下语言文件是形如这样的
> lang/zh_CN.yml
```yaml
editor-input-enums: '请选择 {0}'
editor-input-chat:
- ==: JSON
text: ' &5&l &r &7请在聊天框中输入你要设置的值, 当前值为: &f&n[{0}]'
args:
- suggest: '{0}'
hover: '点击复制'
```
## 文件格式
通用语言文件使用 .yml 作为文件后缀,并以 zh_CN 等语言代码作为文件名。
- zh_CN.yml 为简体中文语言文件
- zh_TW.yml 为繁体中文语言文件
- en_US.yml 为英文语言文件
在文件中,每一行都是一个 键值对,键值对的格式为 键: 值 或 键: [值1, 值2, 值3]。
```yaml
example-language-list:
- '这是一个示例语言文件'
- '这是一个示例语言文件'
- '这是一个示例语言文件'
```
整个文件必须采用扁平化的结构(即不允许使用 键1: { 键2: 值 } 的格式)。
## 复合文本模式
通常一个节点可以有很多表现
```yaml
node:
- type: text
text: hello world!
- type: title
title: hello world!
subtitle: sub
fadein: 1
stay: 1
fadeout: 1
- type: sound
sound: block_stone_break
volume: 1
pitch: 1
- type: json
text:
- [hello] [world!]
args:
- hover: hello
command: say hello
- hover: world!
command: say world
```
我们举个例子 以Trmenu的 打开UI提示PAPI拓展不足举例
```yaml
Menu-Expansions-Header:
- '&8[&3Tr&bMenu&8] &7你必须安装 PAPI &f{0} &7个拓展以使用此菜单.'
- type: JSON
text: '&7请在安装后 [&3&n点击重载] &7拓展'
args:
- hover: '&7点击重载 PAPI 拓展'
command: '/papi reload'
Menu-Expansions-Format:
- type: JSON
text: '&8- [&a{0}]&r'
args:
- hover: '&7点击下载'
command: '/papi ecloud download {0}'
```
如果你觉得上文的复合文本模式比较复杂且难以编写,那么是时候了解一下 `TabooComponent` 了这是类似于 `MiniMessage` 的功能。
它允许你在单行内设置相应参数。需要操作的文本使用 `[]` 框住,在其后面加上 `()` 用以配置具体参数。参数之间使用 `;` 分隔。
如果我们想在语言文件中使用这个功能时,需要手动打开这个功能:
```kotlin
object ExamplePlugin : Plugin() {
override fun onEnable() {
//你的启动逻辑...
// 启用 TabooLibComponent
Language.enableSimpleComponent = true
}
}
```
```yaml
node: 'TabooLib真[强]b;u啊'
node1: '[点我]hover=快点我啊!;suggest=测试)送屠龙宝刀'
```
使用 sendLang 发送给玩家后得到的效果就是:
![](../images/0_10.png)
那么如果你需要在发送的文本中显示 `[]`,那么就需要进行转义。转义的方法很简单,只需要在对应文本前加上 `\` 符号即可,例如:
```yaml
node: '[\[点我\]]command=/stop关服'
# 复合文本要套在一个 [] 内
nodes: 这是一条[红色的[\[可点击\]]command=sb;hover=测试)的]测试信息。
# [红色的[可点击]command=test;hover=测试)的]
```
已知支持转义的字符有:`[ ]` `( )` `;` `=` `\\`
## 可选参数
`TabooComponent` 支持很多参数,如下表格为已知的可选参数:
| 名称 | 别名 | 功能 | 案例 | 效果图 |
|---------|-----|-----------------------|-----------------------------------------------------|-------------------------|
| s | | 删除下划线 | [删除线]s | ![](../images/0_11.png) |
| u | | 添加下划线 | [下划线]u | ![](../images/0_12.png) |
| italic | i | 添加斜体 | [斜体]i | ![](../images/0_13.png) |
| bold | b | 添加粗体 | [粗体]b | ![](../images/0_14.png) |
| obf | o | 添加模糊,也就是一直变化的代码 | [模糊]o | ![](../images/0_16.png) |
| reset | r | 移除所有装饰和颜色 | [&6移除]r | ![](../images/0_15.png) |
| newline | nl | 换行 | 坏黑,[]nl我爱你 | ![](../images/0_18.png) |
| font | f | 改变字体 | [TabooLib]f=uniform | ![](../images/0_19.png) |
| url | | 点击后打开链接 | [\[点击\]]url=https://tabooproject.org/打开TabooLib官网 | ![](../images/0_20.png) |
| command | cmd | 点击后执行对应指令 | 点击[\[关服\]]cmd=/stop | ![](../images/0_21.png) |
| hover | h | 鼠标悬停显示内容使用`<br>`,可以换行 | [悬浮]h=这是悬浮信息) | ![](../images/0_22.png) |
| suggest | | 点击后在输入框填入内容 | [建议]suggest=喵喵喵!) | ![](../images/0_23.png) |
| copy | | 点击后将内容复制到剪切板 | [点击以复制喵喵喵]copy=喵喵喵!) | ![](../images/0_24.png) |
| color | c | 设置颜色支持hex或原版颜色 | [我是粉色的]c=#e44b8d | ![](../images/0_25.png) |
| gradient | g | 设置渐变色 | []gradient=#f6d365,#fda085 | ![](../images/0_26.png) |
| insertion | insert | 按住shift点击后插入文本 | [&6按住shift点我]insert=坏黑爱我)告诉你一个秘密 | ![](../images/0_27.png) |
| keybind | key | 替换文本为键位 | 按下[key.jump]key以跳跃 | ![](../images/0_28.png) |
| translate | trans | 替换文本为当前语言下的译名 | 我叫[block.minecraft.diamond_block]trans | ![](../images/0_29.png) |
## 调用 - 直接发送
那么语言相关内容在代码里如何书写呢?
可以直接使用 `CommandSender#sendLang("路径节点",参数0,参数1,参数2)` 方法
```kotlin
if (expansions.isNotEmpty()) {
e.isCancelled = true
viewer.sendLang("Menu-Expansions-Header", expansions.size)
expansions.forEach { viewer.sendLang("Menu-Expansions-Format", it) }
}
```
## 调用 - 获取文本
结合菜单功能 我想给菜单也做 I18n 我应该如何写呢
```kotlin
fun openMenu(player: Player) {
player.openMenu<Chest>(player.asLangText("ui-title")) {
map(
"#########",
"# A #",
"#########",
)
set('A', buildItem(XMaterial.APPLE) {
name = player.asLangText("ui-apple-name")
lore.addAll(player.asLangTextList("ui-apple-lore"))
colored()
}) {
player.sendLang("ui-apple-click")
}
}
}
```
## 设置语言文件
```kotlin
@SubscribeEvent
fun lang(event: PlayerSelectLocaleEvent) {
event.locale = config.getString("Lang", "zh_CN")!!
}
@SubscribeEvent
fun lang(event: SystemSelectLocaleEvent) {
event.locale = config.getString("Lang", "zh_CN")!!
}
```

View File

@@ -0,0 +1,31 @@
---
front:
hard: 入门
time: 10分钟
---
# 署名
## 署名信息
本文作者: TabooLib社区.
TabooLib官网: [https://www.tabooproject.org/](https://www.tabooproject.org/).'
原文: [https://taboolib.feishu.cn/wiki/Lzf8wFEsfiHclskCuGkctoUNn9b](https://taboolib.feishu.cn/wiki/Lzf8wFEsfiHclskCuGkctoUNn9b).
特别鸣谢: 坏黑、枫溪以及TabooLib社区的贡献者们.
## 相关说明
中国版对文章部分内容有所改动.
本文主体内容以 `TabooLib 6.2.3` 为蓝本进行教学.
如果您有想完善本文的想法可以前往Github共创教程提出你的建议和新增内容
[https://github.com/MCNeteaseDevs/netease-bedrock-wiki](https://github.com/MCNeteaseDevs/netease-bedrock-wiki)
## 建议
虽然`Taboolib`能够使开发者能够更快速的开发Bukkit可运行的插件.
但是在使用之前需要确保你已经较为熟练的掌握Bukkit基本开发思路并且能够独立的开发一个中小型插件.
> 工具库始终只是为了你开发更灵活而建设的

View File

@@ -0,0 +1,22 @@
---
front:
hard: 入门
time: 10分钟
---
# TabooLib介绍
## 什么是TabooLib
TabooLib 正式创建于 2018/02/06, 为 MinecraftJava 版)提供一个跨平台的插件开发框架
旨在替代频繁的操作,以及解决一些令人头疼的问题。
+ 基于 Kotlin 独特的语法。
+ 仅占 30+ KB 插件体积。
+ 魔术般的工具。
## 使用TabooLib需要什么知识
1. 学会先开发Bukkit插件了解基本原理
2. 学会Kotlin基本语法

View File

@@ -0,0 +1,102 @@
---
front:
hard: 入门
time: 10分钟
---
# 快速上手
## 创建项目
打开 IDEA -> Plugins -> 搜索 Taboo Development -> 安装此插件
![](../images/0_0.png)
> https://plugins.jetbrains.com/plugin/25210-taboolib-development
新建项目中选择 Taboo Development .
选择项目名称和项目位置.
![](./images/0_1.png)
输入插件名、 主类、 版本等信息
选择需要用到的模块
![](./images/0_2.png)
> 模块化是`TabooLib`特点之一,开发者无需下载不需要的前置库,各取所需即可
> 如果还不确定自己会用到什么模块也不要着急,后续仍然可以在`Gradle`配置文件中配置Install
接下来输入插件基本信息
![](./images/0_3.png)
## 认识目录
![](./images/0_4.png)
### 修改项目名
`settings.gradle.kts`中.
你会看到 `rootProject.name = "TestProject"`.
修改后同步Gradle可以快速修改项目名.
### 修改基础信息
`gradle.properties`中.
您可以在这里面修改插件的基础信息
```java-properties
group=top.maplex.testproject
version=1.0.0
kotlin.incremental=true
kotlin.incremental.java=true
kotlin.caching.enabled=true
kotlin.parallel.tasks.in.project=true
kotlin.experimental.tryK2=true
kapt.use.k2=true
```
这其中包括了是否开启Kotlin2
> 为什么要用Kotlin2呢您可以看看这个图
![](./images/0_5.png)
**节省了将近一半的编译时间**
### 构建配置
在`build.gradle.kts`,这个是构建配置文件,非常重要
![](./images/0_6.png)
如果你需要使用Kotlin2
在构建配置中 id("org.jetbrains.kotlin.jvm") version "1.9.22" 版本设置成 1.9.22
```kotlin
plugins {
java
id("io.izzel.taboolib") version "2.0.6"
id("org.jetbrains.kotlin.jvm") version "1.9.22"
}
```
然后再代码块中新增一个信息
```kotlin
kotlin {
sourceSets.all {
languageSettings {
languageVersion = "2.0"
}
}
}
```