从一个“选择角色后向当前频道发送欢迎公告”的插件开始。按下面步骤完成后,你会得到可运行的三个文件,并学会启用、核验和热重载。全部代码面向 Go 服务端 dnf/v1 / Lua 5.1,与客户端 90CNLua.dll 的 API 独立。
先确认这四件事
- 使用包含服务端 Lua 插件能力的发行包;若开发 Hook,宿主还须具备所需 Hook。API 的自检方法见后文
catalog。 - 能编辑发行包顶层的
runtime/,并能通过实例原有控制入口完整重启服务端。 - 具备测试角色与本机管理接口的
platform权限。管理端口从实际实例管理配置取得;游戏端口和网关玩家 API 不能代替它。 - 用支持 UTF-8 的文本编辑器保存文件。只安装可信脚本:插件在服务端进程中执行,受限 Lua 库不是对恶意代码的 CPU/内存强隔离。
本教程不需要安装独立 Lua、LuaRocks 或第三方模块。Lua 解释器已经在服务端内;编辑器是可选工具。
第一步:建立目录
在顶层 runtime/ 下创建如下目录。框架中的所有路径也都相对这个 runtime/,使用正斜线:
runtime/
config/
instance.json # 已存在,稍后只合并 plugins 字段
plugins/
framework.lua # 文件 1:服主决定加载什么
welcome/
plugin.json # 文件 2:插件身份与权限申请
main.lua # 文件 3:插件代码
自定义内容放在 runtime/plugins/。官方 runtime/assets/plugins/ 中的框架与示例会随版本更新覆盖。插件不会靠扫描目录自动执行,必须在框架中显式启用。
已有框架时,应合并本教程的 load 声明,保留其他插件;不要把已有框架整体覆盖。同一清单 ID 不能出现两次。
第二步:写完整的三个文件
文件 1:plugins/framework.lua
框架负责启停、权限授权和配置,只提供框架管理 API。它不处理游戏事件。
local dnf = require("dnf")
dnf.plugins.clear_announcement("auto")
dnf.plugins.load("plugins/welcome", {
enabled = true,
permissions = { "notice.broadcast" },
config = { suffix = "祝冒险愉快!" }
})
enabled 必须显式写为布尔值。permissions 是服主授予的权限列表;config 是此插件收到的字符串键配置表。列表式 permissions 是这个 API 的专用格式,不能据此把 config 也写成数字键数组。
auto 让通关播报沿用当前自动归属:这个欢迎插件没有注册通关 provider,因此仍使用内置通关播报。
文件 2:plugins/welcome/plugin.json
{
"id": "docs-welcome",
"version": "1.0.0",
"api": "dnf/v1",
"entry": "main.lua",
"permissions": ["notice.broadcast"]
}
JSON 不能写注释或末尾多余逗号。id 以小写字母开头,只能使用小写字母、数字、短横线,最长 64 个字符;version 非空即可,修改代码时不必为了重载递增它。当前入口固定为 main.lua。
权限要满足两处:清单中申请,框架中授予。仅改其中一处不能获得公告能力。
文件 3:plugins/welcome/main.lua
local dnf = require("dnf")
assert(dnf.api_version == "dnf/v1", "requires dnf/v1")
assert(dnf.events.catalog()["character.bound"], "character.bound is unavailable")
local suffix = dnf.config.suffix or "祝冒险愉快!"
assert(type(suffix) == "string", "suffix must be a string")
assert(#suffix <= 128, "suffix must not exceed 128 UTF-8 bytes")
dnf.events.on("character.bound", function(ctx, event)
local character = event.data
if character.channel_id <= 0 then return end
ctx.notice.broadcast({
scope = "channel",
channel_id = character.channel_id,
text = "冒险家#" .. character.id .. "已选择角色," .. suffix,
color = 0xFFFFD54F
})
end)
这段代码在初始化阶段检查版本、事件能力和配置;配置错误会让新集合加载失败,避免坏配置等到玩家触发时才出错。随后只登记回调,初始化本身不发送公告。
character.bound 的 event.data 就是 { id, channel_id },没有 player_name 或嵌套的 character 字段。角色 ID 是字符串,直接拼接,不要 tonumber。该事件包括重连,不能理解为“每账号仅首次登录”,也不代表已经完成进城。
可以直接下载这三个源文件:最小框架(保存为 plugins/framework.lua)、欢迎插件清单、欢迎插件脚本。另有完整示例包,其总框架会加载七个示例。
第三步:合并实例配置并重启
打开已有的 runtime/config/instance.json,只添加或更新下面的 plugins 字段,保留其他全部实例配置:
{
"plugins": {
"enabled": true,
"framework": "plugins/framework.lua"
}
}
这是需要合并的配置片段,不是完整实例配置。不要拿它覆盖整个文件,也不要修改 runtime/configs/ 生成配置。曾使用旧 entries 配置时,先将旧插件迁入同一框架,不可同时设置 framework 和非空 entries。
通过实例现有控制入口完整重启一次。服务端默认关闭插件;修改总开关或框架路径必须重启。文件就位不代表已经启用,必须继续检查状态。首次加载失败会阻止游戏监听,应先修复启动日志中的加载错误。
第四步:通过管理接口确认加载
三个管理接口都挂在服务端现有本机管理监听上,沿用 X-Admin-Token 与 platform 管理权限。总开关关闭时不注册这些路由。
在安装目录运行以下 PowerShell 命令,从已有实例配置读取管理监听与令牌。令牌只用于内存中的请求头,不输出到终端,也不写进框架、清单或仓库。若使用额外 RBAC 或管理代理,按管理员分配的 platform 凭据调整连接方式,见管理接口连接方式。
$pluginInstance = Get-Content -LiteralPath './runtime/config/instance.json' -Raw | ConvertFrom-Json
$pluginAdminBase = 'http://' + $pluginInstance.server.adminListen
$pluginAdminHeaders = @{ 'X-Admin-Token' = $pluginInstance.server.adminToken }
$pluginStatus = Invoke-RestMethod -Method Get `
-Uri "$pluginAdminBase/local/plugins" -Headers $pluginAdminHeaders
$pluginStatus | ConvertTo-Json -Depth 8
$pluginCatalog = Invoke-RestMethod -Method Get `
-Uri "$pluginAdminBase/local/plugins/catalog" -Headers $pluginAdminHeaders
$pluginCatalog.events.'character.bound'
状态应满足:api 为 dnf/v1,顶层与 runtime 的 enabled、running 为 true,runtime.plugins 包含 id="docs-welcome"、version="1.0.0"。generation 是本次运行的插件集合代数,具体初值不作为固定约定;记录当前值用于比较重载结果。
目录接口成功返回 { api, events, hooks }。若管理请求失败,先核对实际服务端管理地址、认证与插件总开关,具体 HTTP 语义见运行与排错。
第五步:触发并观察欢迎公告
用测试角色重新选择角色,触发新的 character.bound。当前频道应出现“冒险家#…已选择角色,祝冒险愉快!”。这个插件发的是频道广播,同频道其他在线角色也可能看到,不是只给当前玩家的私信。
再次查询状态:正常情况下,docs-welcome 的 accepted 应随新通知增加,errors 应保持为 0。actions 表示宿主动作调用成功;公告仍可能因限流、队列、目标在线状态等被丢弃,不能把这些计数当作客户端送达确认。
没看到公告时,按顺序检查:插件是否列出 → 是否重新触发绑定 → errors 是否增加 → 频道是否为正数 → 是否有公告丢弃。重载不会补发旧登录事件,所以站在城里只改文件不能触发欢迎。
第六步:只改一句话,验证热重载
把框架中的 suffix 改成 "欢迎回来,祝今天掉落满满!" 后保存。通常下一个 2 秒检查周期会开始构建候选集合,通过后整体切换;不必增加清单 version,也不必重启。
也可以手动请求,无需 body:
$pluginReloaded = Invoke-RestMethod -Method Post `
-Uri "$pluginAdminBase/local/plugins/reload" -Headers $pluginAdminHeaders
$pluginReloaded | ConvertTo-Json -Depth 8
成功返回 HTTP 200,generation 增加;重新选择角色后应看到新文案。失败返回 HTTP 409,旧的完整集合继续工作;阅读响应文本和 last_reload_error,修正文件再保存或手动重载。一次成功的重载会重置全部插件 VM 状态和计数。
自动监测依据文件大小和修改时间。若部署工具保留了二者,即使内容改变也要手动重载。推荐原子保存;一组文件分批落盘时,期间尝试可能失败,全部文件写完再手动重载即可确认最终集合。
停用与回滚
临时停用欢迎插件:把框架里对应声明改为 enabled=false 并成功重载。启用状态列表将不再包含它;未启用目录不读取脚本,之后要重新启用仍改框架。
回滚代码:恢复自己保存的上一份框架/清单/脚本,确认下一代集合加载成功。已发出的公告不会撤回,已固定或提交的经验决策也不会因回滚改变。若要关闭整个插件系统,修改实例总开关后完整重启。
从这个例子改出自己的插件
- 复制
welcome/到新目录,改清单id,框架加入唯一的load声明。 - 先从事件目录选一个稳定事件,确认其
data字段和触发时机,替换事件名及字段读取。 - 若只观察,就使用
dnf.events.on;若要在核心业务前增加限制,选六项 Hook中的明确扩展点,并同时申请/授予对应权限。 - 在初始化验证配置、登记回调;回调内只调用现有 API。当前没有发物品、数据库、文件、网络、定时器或
dnf.logAPI。 - 在独立测试实例验证正常、拒绝、重载失败、修复及停用,再按目标负载验收。
继续看七个完整可运行示例,学习通关 provider、经验 transform、道具/商店/入场/聊天限制。编辑器可下载 dnf.lua 类型声明,加入 Lua Language Server 的 workspace library,并将语言版本设为 Lua 5.1;声明不复制到插件运行目录,也不通过 require 加载。