为 LuaInMinecraftBukkitII 实现 Lua 脚本的语法提示与补全
本帖最后由 hahahahahah 于 2025-10-31 09:39 编辑# 什么是 LuaInMinecraftBukkitII
是基于 Bukkit API 服务端的一个 引擎插件, 可以让基于 Bukkit API 的 Minecraft 服务器运行 脚本并与服务器进行交互.
可以把 想象成一个桥梁, 它在 Java 与 之间架起了一个通信桥梁, 能够让 Java 访问 中的内容, 并且 也能访问 Java 中的内容. 而这个通信的桥梁又基于 () 以及 Java 的动态反射技术. Java 控制 是通过 控制的; 而 操纵 Java 是通过动态反射技术操纵的.
对于 操纵 Java 来说, 假设我们有个Bukkit玩家类型变量 `Player player;`, 在 Java 中向该玩家发送消息需要调用 `player.sendMessage("信息")` 方法即可发送, 而在 中, 我们能保持相同的调用方法, 仅需要将 `.` 运算符置换为 `:` 运算符: `player:sendMessage("信息")`. 在 中调用上述语句时, 会自动的通过 Java 动态反射寻找 `Player` 类型的 `sendMessage` 方法, 并调用该方法以实现 操纵 Java.
# 目前编写 Lua 有什么问题?
目前为 插件编写 脚本时, 根据上述对插件运行机制的简单介绍可以了解到, 实际上还是对 Java 进行一个操纵. 所有在 中能够访问的 Java 对象都与在 Java 中编写调用的过程无异. 但是有一个非常致命的缺陷, 就是在编写 Java 时有语法提示, 能够提示该类型的方法和注释文档, 而在编写 时, 虽然同样是对 Java 进行操纵, 但是缺少相关语法提示和接口文档.
# 为 Lua 实现语法补全
针对上述问题, 我注意到有个专注于实现 语法补全的 LSP(语言服务器协议): (). 通过对 的了解, 我注意到可以通过文档注解为 代码中的变量标记类型以及为函数标记形参和用法文档. 并且可以通过在 源代码中声明类型以及它的字段和方法, 即可完成向 注册类型这一步骤.
例如假设 Bukkit 中的 `Player` 类中仅具有 `sendMessage(String)` 这一个方法, 那么可以编写一个名为 `org.bukkit.entity.Player.lua` 文件(在 中将其称为 **桩文件**), 在该文件中写入如下内容(和实际内容有所缩减), 之后将变量通过文档注解标记为`org.bukkit.entity.Player` 类型后, 就能享受到语法补全了.
```lua
--- org.bukkit.entity.Player.lua 桩文件
---@meta
---Represents a player, connected or not
---@class org.bukkit.entity.Player: java.lang.Object
local Player = {}
---Sends the component to the player
---@public
---@param component string the components to send
---@return nil
function Player:sendMessage(component) end
return Player
```
```lua
--- 使用 org.bukkit.entity.Player 语法补全
--- 方式 1, 使用 @Type 注解
---@type org.bukkit.entity.Player
local player = ...;
--- 方式 2, 使用 @as 注解
local player = xxx --[[@as org.bukkit.entity.Player]]
```
可是这会带来以下新的问题:
+ Java 中的类型有很多, Bukkit API 中的类型也有很多, 如果是手动编写如上桩文件, 那将是一个没完没了的任务.
+ 如果每次都要手动为变量标记类型才能使用语法补全的话, 那这个语法补全使用起来会很麻烦
下面的任务就是解决上述问题.
## 自动生成Lua的桩文件
我们希望语法补全覆盖面尽可能的广阔, 并且语法提示弹出来的方法的使用注释尽可能的与 Java 中编写的效果一样, 那么我们可以分析 Java 的源代码, 从 Java 的源代码中将定义的类型, 字段, 方法以及它们的注释全部提取出来, 再根据提取出来的东西自动生成 的桩文件.
这种生成方式能够保证类型以及其中的字段和方法, 包括它们的注释, 全部都原汁原味, 但是缺点也显而易见, 我们需要拿到 Java 标准 API 以及 Bukkit API 的源代码.
幸运的是, Java 标准 API 源代码在每个 JDK 中都附带, Bukkit API 服务端, 例如 Spigot 和 的 API 源代码也能够获取到. 所以我编写了一个新的程序 , 去自动分析 Java 源代码并生成 的桩文件.
此外 以 服务端的 Maven 仓库为基准, 编写了一套根据 Maven 依赖库获取到依赖树, 从 Maven 仓库中查询并下载依赖的源代码, 之后再进行生成 的桩文件的逻辑. 使得可以通过以下配置文件去直接生成 `paper-api` 及其依赖的 `adventure` 等库的 的桩文件.
```json
[
{
"model": "io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT",
"outputPath": "lua/paper-api",
"cachePath": "cache",
"repositories": [
"https://repo.papermc.io/repository/maven-public/"
],
"includeGroups": [
"io.papermc.paper",
"net.md-5",
"net.kyori"
],
"excludeArtifacts": [
"adventure-bom"
]
}
]
```
## 需要自动类型标记的目标
整体来说, 会涉及到标记类型的情况有如下几个情况:
+ 通过 `luajava.bindClass("Java 类名")` 绑定 Java 类到 Lua 变量中.
+ 通过 `luajava.newInstance("Java 类名", ...)` 创建 Java 类型实例到 Lua 变量中
+ 通过 `luajava.new(Java类型变量, ...)` 创建 Java 类型实例到 Lua 变量中
+ 通过 `luajava.createProxy("Java 类名", ...)` 创建 Java 接口代理实例到 Lua 变量中
+ 创建事件监听器时, 期望将事件类型自动标注到 Lua 中的事件处理器(函数, 闭包)的形参上
+ 注册指令时, 期望将 `CommandSender` 或者 `Player` 类型自动标注到 Lua 中的指令处理器(函数, 闭包)的形参上
也就是说, 我们希望执行 `local PlayerClass = luajava.bindClass("org.bukkit.entity.Player")` 时, 希望标记 `org.bukkit.entity.Player` 类型到 `PlayerClass` 变量上, 就像下面这样:
```lua
--- 方式 1
---@type org.bukkit.entity.Player
local PlayerClass = luajava.bindClass("org.bukkit.entity.Player")
--- 方式 2
local PlayerClass = luajava.bindClass("org.bukkit.entity.Player") --[[@as org.bukkit.entity.Player]]
```
当然无论是哪种形式, 我们最终目的只是需要为变量/方法形参标记类型就好了.
## 自动类型标记插件 I
在研究 的拓展文档过程中, 我注意到了 能够实现自动标记类型.
!
我遵循文档编辑了第一版自动类型标记插件: .
在初始版本的自动类型标记插件中, 基于 脚本文本操作, 主要依靠于正则表达式匹配语句并标记类型. 也就是基于 插件的 `OnSetText` 函数实现的自动类型标记:
`OnSetText` 函数实现的自动类型标记也是 官方文档所给出的唯一一个有详细文档和示例的一个方式. 但是我很快的就发现了正则表达式的局限性: 语句复杂的情况下效率低下, 难以匹配语句, 并且不利于拓展. 可以观察以下第一版的实现片段, 可以看见正则表达式非常复杂.
```lua
-- 实现片段
-- local var = luajava.bindClass
for localPos, varName, colonPos, typeName, finish in text:gmatch '()local%s+([%w_]+)()%s*=%s*luajava%.bindClass%s*%(%s*[\'"]([%w_.]+)[\'"]%s*%)()' do
annotationType(diffs, typeMap, localPos, varName, typeName)
placedLuajava = true
end
-- local var = luajava.newInstance
for localPos, varName, colonPos, typeName, finish in text:gmatch '()local%s+([%w_]+)()%s*=%s*luajava%.newInstance%s*%(%s*[\'"]([%w_.]+)[\'"]()' do
annotationType(diffs, typeMap, localPos, varName, typeName)
placedLuajava = true
end
-- local var = luajava.createProxy
for localPos, varName, colonPos, typeName, finish in text:gmatch '()local%s+([%w_]+)()%s*=%s*luajava%.createProxy%s*%(%s*[\'"]([%w_.]+)[\'"]()' do
annotationType(diffs, typeMap, localPos, varName, typeName)
placedLuajava = true
end
```
## 自动类型标记插件 II
在反思自己编写的第一代插件时, 我注意到了 页面中的 `OnTransformAst` 方法, 这个方法会传入 **ast**(语法树), 这样我就可以通过分析 脚本的语法树, 再对上述目标进行精准定位, 精准标注类型.
遗憾的是 页面中并未给出详细的文档, 并且在 wiki 中也没有对 **ast** 进行半点描述, 无奈我只能自己查阅 中的源代码, 最后研究出 中操纵语法树的API是如何运作的, 终于第二版修改完毕:
在这次实现中, 我并没有使用 `OnTransformAst` 方法, 而是依旧使用 `OnSetText` 对源代码进行标记. 因为在没有任何文档和方法注释情况下, 无法搞懂如何编辑语法树的. 所以我选了一个折中的方案: 在 `OnSetText` 方法中, 将传入的脚本文本编译成语法树, 再对语法树进行分析, 分析完成之后统一在源代码中插入类型标记注释. 此时第二版自动类型标记插件才能成功完成.
有了语法树的加持, 我才可以分析出在代码中的某个位置上, 哪些变量是可见的, 哪些变量是不可见的, 哪些变量是执行了赋值操作的. 有了这样的分析数据后, 我才可以在追踪在使用 `luajava.new(Java类型变量, ...)` 方法, 或注册监听器/指令时, 为变量/形参标记到真正的类型.
就以以下代码为例子, `aObj` 变量会被标记为 `java.lang.Object` 类型, `test1()` 函数内的 `player` 变量将会标记为 `org.bukkit.entity.Player` 类型, 倒数第二行的 `bObj` 变量将会被标记为 `java.lang.Object` 类型. 这么细致的变量类型追踪是正则表达式所无法做到的.
```lua
local Player = luajava.bindClass("org.bukkit.entity.Player")
local Object = luajava.bindClass("java.lang.Object")
local ObjectClass = Object
-- 将会被标记为 Object 类型
local aObj = luajava.new(ObjectClass)
local function test1()
ObjectClass = Player
-- 将会被标记为 Player 类型
local player = luajava.new(ObjectClass)
end
-- 将会被标记为 Object 类型
local bObj = luajava.new(ObjectClass)
test1()
```
!
!
!
需要注意的是, 因为我个人能力有限, 无法准确判断在 脚本运行情况下, 变量的真正的类型, 也就像上述示例代码那样, 将最后一行和倒数第二行执行顺序进行调换, 会导致类型标记错误. 这是我目前所编写的插件的一个问题. 希望之后能够实现准确预测变量类型好了.
# 结尾
之后只需要倒入 和桩文件, 就可以愉快的用 Lua 编写 Bukkit 插件了, 这样编写 Bukkit 插件既无需编译, 还能够体验上语法提示和补全.
: https://www.mcbbs.co/thread-3434-1-1.html
: https://www.lua.org/
: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/
: https://luals.github.io/
: https://luals.github.io/wiki/plugins/
: https://luals.github.io/videos/wiki/plugin-diff.webm
: https://github.com/SmileYik/LuaInMinecraftBukkitII-LLS-Generator
: https://papermc.io/
: https://github.com/SmileYik/LuaInMinecraftBukkitII-LLS-Addon
: https://github.com/SmileYik/LuaInMinecraftBukkitII-LLS-Addon/blob/master/plugin.lua
: https://github.com/SmileYik/LuaInMinecraftBukkitII-LLS-Addon/blob/08b4296b152f4cd73fe40e278056b187c26c4fdb/plugin.lua
: https://s21.ax1x.com/2025/10/31/pVzVwDI.png
: https://s21.ax1x.com/2025/10/31/pVzVdKA.png
: https://s21.ax1x.com/2025/10/31/pVzVUvd.png
https://luals.github.io/videos/wiki/plugin-diff.webm
页:
[1]