dnf/v1
开始使用 / dnf/v1

写出第一个插件

创建三个文件,启用插件,并在游戏内验证第一条欢迎公告。

从一个“选择角色后向当前频道发送欢迎公告”的插件开始。按下面步骤完成后,你会得到可运行的三个文件,并学会启用、核验和热重载。全部代码面向 Go 服务端 dnf/v1 / Lua 5.1,与客户端 90CNLua.dll 的 API 独立。

先确认这四件事

  1. 使用包含服务端 Lua 插件能力的发行包;若开发 Hook,宿主还须具备所需 Hook。API 的自检方法见后文 catalog。
  2. 能编辑发行包顶层的 runtime/,并能通过实例原有控制入口完整重启服务端。
  3. 具备测试角色与本机管理接口的 platform 权限。管理端口从实际实例管理配置取得;游戏端口和网关玩家 API 不能代替它。
  4. 用支持 UTF-8 的文本编辑器保存文件。只安装可信脚本:插件在服务端进程中执行,受限 Lua 库不是对恶意代码的 CPU/内存强隔离。

本教程不需要安装独立 Lua、LuaRocks 或第三方模块。Lua 解释器已经在服务端内;编辑器是可选工具。

第一步:建立目录

在顶层 runtime/ 下创建如下目录。框架中的所有路径也都相对这个 runtime/,使用正斜线:

text
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。它不处理游戏事件。

lua
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

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

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 字段,保留其他全部实例配置:

json
{
  "plugins": {
    "enabled": true,
    "framework": "plugins/framework.lua"
  }
}

这是需要合并的配置片段,不是完整实例配置。不要拿它覆盖整个文件,也不要修改 runtime/configs/ 生成配置。曾使用旧 entries 配置时,先将旧插件迁入同一框架,不可同时设置 framework 和非空 entries。

通过实例现有控制入口完整重启一次。服务端默认关闭插件;修改总开关或框架路径必须重启。文件就位不代表已经启用,必须继续检查状态。首次加载失败会阻止游戏监听,应先修复启动日志中的加载错误。

第四步:通过管理接口确认加载

三个管理接口都挂在服务端现有本机管理监听上,沿用 X-Admin-Token 与 platform 管理权限。总开关关闭时不注册这些路由。

在安装目录运行以下 PowerShell 命令,从已有实例配置读取管理监听与令牌。令牌只用于内存中的请求头,不输出到终端,也不写进框架、清单或仓库。若使用额外 RBAC 或管理代理,按管理员分配的 platform 凭据调整连接方式,见管理接口连接方式。

powershell
$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:

powershell
$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 并成功重载。启用状态列表将不再包含它;未启用目录不读取脚本,之后要重新启用仍改框架。

回滚代码:恢复自己保存的上一份框架/清单/脚本,确认下一代集合加载成功。已发出的公告不会撤回,已固定或提交的经验决策也不会因回滚改变。若要关闭整个插件系统,修改实例总开关后完整重启。

从这个例子改出自己的插件

  1. 复制 welcome/ 到新目录,改清单 id,框架加入唯一的 load 声明。
  2. 先从事件目录选一个稳定事件,确认其 data 字段和触发时机,替换事件名及字段读取。
  3. 若只观察,就使用 dnf.events.on;若要在核心业务前增加限制,选六项 Hook中的明确扩展点,并同时申请/授予对应权限。
  4. 在初始化验证配置、登记回调;回调内只调用现有 API。当前没有发物品、数据库、文件、网络、定时器或 dnf.log API。
  5. 在独立测试实例验证正常、拒绝、重载失败、修复及停用,再按目标负载验收。

继续看七个完整可运行示例,学习通关 provider、经验 transform、道具/商店/入场/聊天限制。编辑器可下载 dnf.lua 类型声明,加入 Lua Language Server 的 workspace library,并将语言版本设为 Lua 5.1;声明不复制到插件运行目录,也不通过 require 加载。

DNF90 · 服务端 Lua 插件核对版本 cda08ef · 2026-10-26

输入关键词,查找指南和全部事件。

点击结果跳转 · Esc 关闭