先认识运行位置
本文档对应 Go 服务端的 dnf/v1 插件宿主,源码核对基线为 cda08ef(2026-10-26)。它与客户端 90CNLua.dll 是两套独立系统;客户端窗口、按键或 DLL API 不能直接搬到这里使用。
一个插件由 plugin.json 和 main.lua 组成,框架文件负责声明哪些插件被加载、获得什么权限和配置。先完成快速上手,再查函数参考、事件目录或 Hook。
Lua 5.1:会写基本脚本即可开始
解释器为 gopher-lua v1.1.1,采用 Lua 5.1 语法。保存脚本为 UTF-8 文本。常用的局部变量、函数、表、if、for、pairs、ipairs、assert、string、table 和 math 均可用于日常插件逻辑。
local dnf = require("dnf")
assert(dnf.api_version == "dnf/v1")
local function is_enabled(config)
-- Lua 里只有 false 和 nil 为假;0 和空字符串都为真。
return config.enabled == true
end
dnf.events.on("character.bound", function(ctx, event)
local id = event.data.id -- 角色 ID 是字符串
if is_enabled(ctx.config) and id ~= "" then
-- 在这里处理当前事件,具体可用动作见 API 页。
end
end)
需要特别注意:
- 没有 Lua 5.3 的整数类型、
//整除和&、|、<<等位运算语法。ARGB 颜色可以直接写0xFFFFD54F。 - Lua 数值采用浮点表示。角色、拍卖单、运行等契约明确标为字符串的 ID 应始终按字符串比较,不能
tonumber(id)后作为身份键。 - 数组从 1 开始,事件中的
items、members等数组用ipairs遍历;插件配置和返回补丁则使用字符串键表。 #text、string.sub等常见字符串操作按字节处理,不能拿字节长度当作中文字符数。截断中文或表情前应理解 UTF-8 边界;公告长度的最终校验由宿主按 UTF-16 码元完成。- 事件中的可选字段可能为
nil。先判空再运算,不能把缺失值或 0 编造为真实业务对象。
可用库与宿主边界
宿主开放受限的 base、table、string、math。require 只接受 "dnf",没有通用模块加载器;需要的辅助函数放在同一个 main.lua 中。
| 能力 | 当前行为 |
|---|---|
| 文件、网络、数据库、系统命令 | 没有 io、os、socket、FFI,也没有业务仓储句柄 |
| 包系统和反射 | 没有 package、debug、coroutine |
| 动态加载与环境修改 | 移除 dofile、loadfile、load、loadstring、getfenv、setfenv、newproxy |
| 输出和垃圾回收控制 | 移除 print、collectgarbage |
| 日志、计时器、直接发物、修改数据库 | 当前没有对应插件 API;不能调用 dnf.log、setTimeout 或假想的发奖函数 |
| 错误诊断 | 宿主记录带插件 ID、阶段和错误次数的错误,管理端提供状态计数 |
受限库和时间预算用于控制日常插件影响,不能当作运行恶意脚本的强隔离环境。安装前应审阅脚本,只运行可信插件。
四个阶段各做什么
| 阶段 | 应做的事 | 可使用的接口 |
|---|---|---|
| 框架初始化 | 选择插件、授予权限、填写配置、选择通关播报策略 | dnf.api_version、dnf.plugins |
| 插件初始化 | 校验配置、声明事件与 Hook 回调 | dnf.config、events、hooks、providers;此时不能发公告 |
| 通知回调 | 读取已发生事件的独立快照,按需暂存公告 | ctx.config、ctx.notice.broadcast,或等价的 dnf.notice.broadcast |
| 规则回调 | 读取输入,返回允许字段组成的计划补丁 | ctx.config;此时不能调用广播动作 |
注册操作只能在 main.lua 顶层初始化时完成。不要在通知或 Hook 内再次 events.on、hooks.before、providers.register 或 hooks.transform。
通知与 Hook 的区别
通知是“发生了什么”的异步观察。核心发布时不执行 Lua,插件稍后消费快照。事件可能因队列满、停机、版本切换而丢失,没有持久化、消费确认和自动补发。即使名字带 committed,也只说明该事件描述的事务已经提交,不能推断后续回包或独立奖励事务完成。
Hook 是核心在明确扩展点请求一个规则结果。它有总时间预算、返回值校验和对应业务的失败语义。四项业务限制 Hook 只增加限制,资产变更、归属检查和原生回包仍由服务端核心处理;失败时保留原核心规则。经验和播报的失败语义不同,见 6 项 Hook 详解。
不要用异步通知计数实现“每日限购”“只发一次奖励”这样的持久业务保证。当前 API 没有持久存储和可靠发奖接口。内存计数适合非关键的展示节流,且必须接受重载或重启归零、事件丢失和重复请求。
一个插件可能有两个 VM
每个插件先初始化通知 VM;含 Hook 的插件还会用相同 main.lua 和配置单独初始化规则 VM。两个 VM 的全局变量、闭包、配置表互不共享,也可能并行执行。
local dnf = require("dnf")
local observed = 0
dnf.events.on("character.bound", function(ctx, event)
observed = observed + 1 -- 只改变通知 VM
end)
-- 如果此文件还声明 Hook,该 Hook 看到的 observed 属于规则 VM,
-- 不会得到上面通知回调累加的值。
同一 VM 内回调串行执行;不同插件的通知互不等待。同一插件中,dnf.config 与 ctx.config 指向该 VM 的同一配置表。修改这张表只影响当前 VM,不写回框架,也不传到另一个 VM。
因此注册应保持确定性:不要依赖随机分支决定是否注册回调,不要让规则依赖通知 VM 更新的变量。配置变更应修改框架,再让整个插件集合成功重载。
资源预算
以下是当前实现的固定限制,设计插件时预留余量。
| 资源 | 当前上限 |
|---|---|
| 框架声明 / 启用插件 | 128 次声明 / 16 个启用插件 |
框架文件 / 每个 main.lua |
各 256 KiB,必须是普通文件 |
每个 plugin.json / 插件配置 JSON |
各 16 KiB |
| Lua 输出值 | 单字符串 4096 字节、每表 128 项、嵌套深度 16;字符串键、有限数字、布尔、字符串、表、nil |
| 框架初始化 / 单插件每个 VM 初始化 | 各 100ms 上下文预算 |
| 通知回调注册 | 每插件最多 64 个 |
| 通知队列与预算 | 每插件队列 64;同一插件同一事件的全部匹配回调共享 10ms、最多 4 条公告 |
| 规则预算 | 整条链 20ms;每个阶段回调最多 10ms;每插件规则等待队列 1 |
| 公告动作 | 每插件队列 64,20 次/秒,突发 8 次;通关播报协调队列 64 |
事件 data |
最多 64 KiB |
| 退休版本 | 最多一个;未退出前拒绝下一次重载 |
Lua 输出限制用于配置序列化、返回补丁和公告等 Lua 向宿主交付的数据;不能因此假设收到的事件表只有 128 项。输入数组是宿主快照,输出补丁不接受数字键数组。
时间预算依赖上下文协作,不能保证立即中断原生库的长时间计算或提供内存硬隔离。回调应只做有界遍历、简单判断和格式化。四项新增业务限制 Hook 在共享业务锁和写事务外等待;既有经验 Hook 仍在副本结算锁内,详见 Hook 语义。
重载意味着重新开始
成功重载会重建完整插件集合的所有 VM,内存变量与计数归零,不调用卸载函数,不补发 server.ready 或历史事件。只想验证加载成功,应查 generation 与插件清单,再触发一个新的可观察事件。
热重载失败会保留当前版本。实际运维步骤、管理接口和故障定位见部署与排错。