帖子

Memorial Edition

查看: 30|回复: 0

[插件开发讨论] 为 LuaInMinecraftBukkitII 实现 Lua 脚本的语法提示与补全

[复制链接]

Lv.9 牧场主

人气
611 点
金粒
23628 粒
宝石
17 颗
爱心
3 颗
钻石
2402 颗
贡献
11 点

论坛元老勋章骨灰勋章Java正版勋章Windows 10正版勋章金锭勋章开发人员勋章石镐矿工勋章铁镐矿工勋章钻镐矿工勋章小麦种勋章苹果树勋章猪灵勋章

发表于 昨天 00:48 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 hahahahahah 于 2025-10-31 09:39 编辑

什么是 LuaInMinecraftBukkitII

LuaInMinecraftBukkitII 是基于 Bukkit API 服务端的一个 Lua 引擎插件, 可以让基于 Bukkit API 的 Minecraft 服务器运行 Lua 脚本并与服务器进行交互.

可以把 LuaInMinecraftBukkitII 想象成一个桥梁, 它在 Java 与 Lua 之间架起了一个通信桥梁, 能够让 Java 访问 Lua 中的内容, 并且 Lua 也能访问 Java 中的内容. 而这个通信的桥梁又基于 JNI(Java Native Interface) 以及 Java 的动态反射技术. Java 控制 Lua 是通过 JNI 控制的; 而 Lua 操纵 Java 是通过动态反射技术操纵的.

对于 Lua 操纵 Java 来说, 假设我们有个Bukkit玩家类型变量 Player player;, 在 Java 中向该玩家发送消息需要调用 player.sendMessage("信息") 方法即可发送, 而在 Lua 中, 我们能保持相同的调用方法, 仅需要将 . 运算符置换为 : 运算符: player:sendMessage("信息"). 在 Lua 中调用上述语句时, LuaInMinecraftBukkitII 会自动的通过 Java 动态反射寻找 Player 类型的 sendMessage 方法, 并调用该方法以实现 Lua 操纵 Java.

目前编写 Lua 有什么问题?

目前为 LuaInMinecraftBukkitII 插件编写 Lua 脚本时, 根据上述对插件运行机制的简单介绍可以了解到, 实际上还是对 Java 进行一个操纵. 所有在 Lua 中能够访问的 Java 对象都与在 Java 中编写调用的过程无异. 但是有一个非常致命的缺陷, 就是在编写 Java 时有语法提示, 能够提示该类型的方法和注释文档, 而在编写 Lua 时, 虽然同样是对 Java 进行操纵, 但是缺少相关语法提示和接口文档.

为 Lua 实现语法补全

针对上述问题, 我注意到有个专注于实现 Lua 语法补全的 LSP(语言服务器协议): Lua Language Server(LuaLS). 通过对 LuaLS 的了解, 我注意到可以通过文档注解为 Lua 代码中的变量标记类型以及为函数标记形参和用法文档. 并且可以通过在 Lua 源代码中声明类型以及它的字段和方法, 即可完成向 LuaLS 注册类型这一步骤.

例如假设 Bukkit 中的 Player 类中仅具有 sendMessage(String) 这一个方法, 那么可以编写一个名为 org.bukkit.entity.Player.lua 文件(在 LuaLS 中将其称为 桩文件), 在该文件中写入如下内容(和实际内容有所缩减), 之后将变量通过文档注解标记为org.bukkit.entity.Player 类型后, 就能享受到语法补全了.

--- 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
--- 使用 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 的源代码中将定义的类型, 字段, 方法以及它们的注释全部提取出来, 再根据提取出来的东西自动生成 LuaLS 的桩文件.

这种生成方式能够保证类型以及其中的字段和方法, 包括它们的注释, 全部都原汁原味, 但是缺点也显而易见, 我们需要拿到 Java 标准 API 以及 Bukkit API 的源代码.

幸运的是, Java 标准 API 源代码在每个 JDK 中都附带, Bukkit API 服务端, 例如 Spigot 和 Paper 的 API 源代码也能够获取到. 所以我编写了一个新的程序 LuaInMinecraftBukkitII-LLS-Generator, 去自动分析 Java 源代码并生成 LuaLS 的桩文件.

此外 LuaInMinecraftBukkitII-LLS-GeneratorPaper 服务端的 Maven 仓库为基准, 编写了一套根据 Maven 依赖库获取到依赖树, 从 Maven 仓库中查询并下载依赖的源代码, 之后再进行生成 LuaLS 的桩文件的逻辑. 使得可以通过以下配置文件去直接生成 paper-api 及其依赖的 adventure 等库的 LuaLS 的桩文件.

[
  {
    "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 变量上, 就像下面这样:

--- 方式 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

在研究 LuaLS 的拓展文档过程中, 我注意到了 LuaLS Plugin 能够实现自动标记类型.

LuaLS-Plugin-Demo

我遵循文档编辑了第一版自动类型标记插件: LuaInMinecraftBukkitII-LLS-Addon.

在初始版本的自动类型标记插件中, 基于 Lua 脚本文本操作, 主要依靠于正则表达式匹配语句并标记类型. 也就是基于 LuaLS Plugin 插件的 OnSetText 函数实现的自动类型标记: plugin.lua

OnSetText 函数实现的自动类型标记也是 LuaLS 官方文档所给出的唯一一个有详细文档和示例的一个方式. 但是我很快的就发现了正则表达式的局限性: 语句复杂的情况下效率低下, 难以匹配语句, 并且不利于拓展. 可以观察以下第一版的实现片段, 可以看见正则表达式非常复杂.

-- 实现片段
-- 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[colonPos] = 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[colonPos] = 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[colonPos] = true
end

自动类型标记插件 II

在反思自己编写的第一代插件时, 我注意到了 LuaLS Plugin 页面中的 OnTransformAst 方法, 这个方法会传入 ast(语法树), 这样我就可以通过分析 Lua 脚本的语法树, 再对上述目标进行精准定位, 精准标注类型.

遗憾的是 LuaLS Plugin 页面中并未给出详细的文档, 并且在 LuaLS wiki 中也没有对 ast 进行半点描述, 无奈我只能自己查阅 LuaLS 中的源代码, 最后研究出 LuaLS 中操纵语法树的API是如何运作的, 终于第二版修改完毕: plugin.lua

在这次实现中, 我并没有使用 OnTransformAst 方法, 而是依旧使用 OnSetText 对源代码进行标记. 因为在没有任何文档和方法注释情况下, 无法搞懂如何编辑语法树的. 所以我选了一个折中的方案: 在 OnSetText 方法中, 将传入的脚本文本编译成语法树, 再对语法树进行分析, 分析完成之后统一在源代码中插入类型标记注释. 此时第二版自动类型标记插件才能成功完成.

有了语法树的加持, 我才可以分析出在代码中的某个位置上, 哪些变量是可见的, 哪些变量是不可见的, 哪些变量是执行了赋值操作的. 有了这样的分析数据后, 我才可以在追踪在使用 luajava.new(Java类型变量, ...) 方法, 或注册监听器/指令时, 为变量/形参标记到真正的类型.

就以以下代码为例子, aObj 变量会被标记为 java.lang.Object 类型, test1() 函数内的 player 变量将会标记为 org.bukkit.entity.Player 类型, 倒数第二行的 bObj 变量将会被标记为 java.lang.Object 类型. 这么细致的变量类型追踪是正则表达式所无法做到的.

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()

P1
P2
P3

需要注意的是, 因为我个人能力有限, 无法准确判断在 Lua 脚本运行情况下, 变量的真正的类型, 也就像上述示例代码那样, 将最后一行和倒数第二行执行顺序进行调换, 会导致类型标记错误. 这是我目前所编写的插件的一个问题. 希望之后能够实现准确预测变量类型好了.

结尾

之后只需要倒入 LuaLS 和桩文件, 就可以愉快的用 Lua 编写 Bukkit 插件了, 这样编写 Bukkit 插件既无需编译, 还能够体验上语法提示和补全.



您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|小黑屋| MCBBS纪念版 ( 新ICP备2024014954号|兵公网安备66010002000149号 )|隐私政策| 手机版

GMT+8, 2025-11-1 05:15 , Processed in 0.102862 second(s), 19 queries , Redis On.

"Minecraft"以及"我的世界"为美国微软公司的商标 本站与微软公司没有从属关系

© 2010-2025 MCBBS纪念版 版权所有 本站内原创内容版权属于其原创作者,除作者或版规特别声明外未经许可不得转载

返回顶部