2.6
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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("")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
front:
|
||||
hard: 进阶
|
||||
time: 25分钟
|
||||
---
|
||||
|
||||
|
||||
# 数据包基本运用
|
||||
## 介绍
|
||||
本章 也可以说是发包的介绍 如何拦截数据包进行篡改 来达到效果
|
||||
或者是直接发送一个数据包 让客户端收到
|
||||
|
||||

|
||||
|
||||
## 篡改数据包
|
||||
如上图所示 我们有两种类型的事件
|
||||
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 及以下的版本的参数如下:
|
||||

|
||||
|
||||
1.17 及以上的版本的参数如下:
|
||||
(这就是 TabooLib 带给我们的自信,我们可以直接使用反混淆版的服务端,由此我们可以很清楚的看到 id、operation 等字段)
|
||||
可知 1.16 及以下版本有一个无参构造函数,1.17+ 没有(有一个私有构造函数,不方便)。对于没有无参构造函数的类,我们可以通过 TabooLib Reflex 提供的函数 Class<T>#unsafeInstance() 来快速获得一个实例。
|
||||
|
||||

|
||||
|
||||
我们可以看到在 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))
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
|
||||
### 可选参数
|
||||
|
||||

|
||||
|
||||
## 修改元数据
|
||||
在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
|
||||
```
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
### 对比
|
||||
|
||||
| 特性 | 基础 | 对象绑定 | 带参数 |
|
||||
|------|----|--- |--- |
|
||||
| 对象关联 |❌|✅|✅|
|
||||
| 参数传递 |❌|❌|✅|
|
||||
| 计时策略 |全局重置|按对象重置|按对象重置|
|
||||
@@ -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()
|
||||
```
|
||||
154
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/1-基础技术/3-命令.md
Normal file
154
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/1-基础技术/3-命令.md
Normal 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 环节进行注册命令
|
||||
@@ -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; // 插件卸载时
|
||||
}
|
||||
```
|
||||

|
||||
|
||||
|
||||
## 另一种自唤醒
|
||||
如果你想让一个类 继承某个接口/增加某个注解 然后初始化的时候自动执行些什么
|
||||
应该这样写 你不应该自己随便写扫包模块 这是不理智的
|
||||
> 如果你有这种需求 我觉得已经有一定的代码阅读能力了 应该可以看懂以下代码
|
||||
```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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
203
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/1-基础技术/5-配置文件.md
Normal file
203
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/1-基础技术/5-配置文件.md
Normal 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 编写的
|
||||
|
||||
@@ -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` 的处理办法
|
||||
@@ -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("点击了苹果")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||

|
||||
|
||||
|
||||
## 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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
```
|
||||
184
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/1-基础技术/9-语言文件.md
Normal file
184
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/1-基础技术/9-语言文件.md
Normal 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 发送给玩家后得到的效果就是:
|
||||

|
||||
|
||||
那么如果你需要在发送的文本中显示 `[]`,那么就需要进行转义。转义的方法很简单,只需要在对应文本前加上 `\` 符号即可,例如:
|
||||
```yaml
|
||||
node: '[\[点我\]](command=/stop)关服'
|
||||
# 复合文本要套在一个 [] 内
|
||||
nodes: 这是一条[红色的[\[可点击\]](command=sb;hover=测试)的]测试信息。
|
||||
# [红色的[可点击](command=test;hover=测试)的]
|
||||
```
|
||||
|
||||
已知支持转义的字符有:`[ ]` `( )` `;` `=` `\\`
|
||||
|
||||
## 可选参数
|
||||
`TabooComponent` 支持很多参数,如下表格为已知的可选参数:
|
||||
|
||||
| 名称 | 别名 | 功能 | 案例 | 效果图 |
|
||||
|---------|-----|-----------------------|-----------------------------------------------------|-------------------------|
|
||||
| s | | 删除下划线 | [删除线](s) |  |
|
||||
| u | | 添加下划线 | [下划线](u) |  |
|
||||
| italic | i | 添加斜体 | [斜体](i) |  |
|
||||
| bold | b | 添加粗体 | [粗体](b) |  |
|
||||
| obf | o | 添加模糊,也就是一直变化的代码 | [模糊](o) |  |
|
||||
| reset | r | 移除所有装饰和颜色 | [&6移除](r) |  |
|
||||
| newline | nl | 换行 | 坏黑,[](nl)我爱你! |  |
|
||||
| font | f | 改变字体 | [TabooLib](f=uniform) |  |
|
||||
| url | | 点击后打开链接 | [\[点击\]](url=https://tabooproject.org/)打开TabooLib官网 |  |
|
||||
| command | cmd | 点击后执行对应指令 | 点击[\[关服\]](cmd=/stop) |  |
|
||||
| hover | h | 鼠标悬停显示内容使用`<br>`,可以换行 | [悬浮](h=这是悬浮信息) |  |
|
||||
| suggest | | 点击后在输入框填入内容 | [建议](suggest=喵喵喵!) |  |
|
||||
| copy | | 点击后将内容复制到剪切板 | [点击以复制喵喵喵](copy=喵喵喵!) |  |
|
||||
| color | c | 设置颜色,支持hex或原版颜色 | [我是粉色的](c=#e44b8d) |  |
|
||||
| gradient | g | 设置渐变色 | [](gradient=#f6d365,#fda085) |  |
|
||||
| insertion | insert | 按住shift点击后插入文本 | [&6按住shift点我](insert=坏黑爱我)告诉你一个秘密 |  |
|
||||
| keybind | key | 替换文本为键位 | 按下[key.jump](key)以跳跃 |  |
|
||||
| translate | trans | 替换文本为当前语言下的译名 | 我叫[block.minecraft.diamond_block](trans) |  |
|
||||
|
||||
## 调用 - 直接发送
|
||||
|
||||
那么语言相关内容在代码里如何书写呢?
|
||||
可以直接使用 `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")!!
|
||||
}
|
||||
```
|
||||
31
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/1-署名.md
Normal file
31
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/1-署名.md
Normal 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基本开发思路,并且能够独立的开发一个中小型插件.
|
||||
|
||||
> 工具库始终只是为了你开发更灵活而建设的
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
front:
|
||||
hard: 入门
|
||||
time: 10分钟
|
||||
---
|
||||
|
||||
|
||||
# TabooLib介绍
|
||||
|
||||
## 什么是TabooLib
|
||||
|
||||
TabooLib 正式创建于 2018/02/06, 为 Minecraft(Java 版)提供一个跨平台的插件开发框架
|
||||
旨在替代频繁的操作,以及解决一些令人头疼的问题。
|
||||
|
||||
+ 基于 Kotlin 独特的语法。
|
||||
+ 仅占 30+ KB 插件体积。
|
||||
+ 魔术般的工具。
|
||||
|
||||
## 使用TabooLib需要什么知识?
|
||||
|
||||
1. 学会先开发Bukkit插件,了解基本原理
|
||||
2. 学会Kotlin基本语法
|
||||
102
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/3-快速上手.md
Normal file
102
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/3-快速上手.md
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
front:
|
||||
hard: 入门
|
||||
time: 10分钟
|
||||
---
|
||||
|
||||
|
||||
# 快速上手
|
||||
|
||||
## 创建项目
|
||||
|
||||
打开 IDEA -> Plugins -> 搜索 Taboo Development -> 安装此插件
|
||||
|
||||

|
||||
|
||||
> https://plugins.jetbrains.com/plugin/25210-taboolib-development
|
||||
|
||||
新建项目中选择 Taboo Development .
|
||||
选择项目名称和项目位置.
|
||||
|
||||

|
||||
|
||||
输入插件名、 主类、 版本等信息
|
||||
选择需要用到的模块
|
||||
|
||||

|
||||
|
||||
> 模块化是`TabooLib`特点之一,开发者无需下载不需要的前置库,各取所需即可
|
||||
> 如果还不确定自己会用到什么模块也不要着急,后续仍然可以在`Gradle`配置文件中配置Install
|
||||
|
||||
接下来输入插件基本信息
|
||||
|
||||

|
||||
|
||||
|
||||
## 认识目录
|
||||
|
||||

|
||||
|
||||
### 修改项目名
|
||||
|
||||
在`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呢,您可以看看这个图
|
||||
|
||||

|
||||
|
||||
**节省了将近一半的编译时间**
|
||||
|
||||
### 构建配置
|
||||
|
||||
在`build.gradle.kts`,这个是构建配置文件,非常重要
|
||||
|
||||

|
||||
|
||||
如果你需要使用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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_0.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_0.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_1.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_1.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_10.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_10.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_11.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_11.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_12.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_12.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_13.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_13.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_14.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_14.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_15.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_15.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_16.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_16.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_18.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_18.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_19.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_19.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_2.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_2.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_20.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_20.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_21.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_21.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_22.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_22.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_23.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_23.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_24.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_24.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_25.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_25.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_26.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_26.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_27.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_27.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_28.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_28.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_29.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_29.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_3.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_3.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_30.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_30.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_31.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_31.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_32.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_32.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_33.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_33.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_4.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_4.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_5.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_5.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_6.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_6.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_7.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_7.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_8.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_8.png
LFS
Normal file
Binary file not shown.
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_9.png
LFS
Normal file
BIN
docs/mcguide/28-电脑网络游戏/课程4:常见第三方库教程/31-TabooLib/images/0_9.png
LFS
Normal file
Binary file not shown.
Reference in New Issue
Block a user