这里提供七个完整插件,每个都有 plugin.json 和 main.lua,配套框架可一次加载全部。代码只使用当前宿主 API。先读从零上手完成一个欢迎插件,再用这些例子扩展。
下载:完整示例包 ZIP · 框架源码 · 安装 README · 编辑器声明。ZIP 中没有账号、token、真实管理地址或实例配置。
先安装到正确的位置
将下载包中的框架与七个目录复制进顶层 runtime/plugins/,不要额外嵌套 examples/ 目录。已有框架则合并所需声明,保留原有插件;不必一次启用全部。
runtime/plugins/
framework.lua
welcome/ # 每个目录都含 plugin.json + main.lua
clear-announcement/
clear-experience/
item-policy/
shop-policy/
admission-policy/
chat-policy/
合并 plugins.enabled=true 与 plugins.framework="plugins/framework.lua" 到已有 runtime/config/instance.json,第一次通过原控制入口完整重启。以后修改框架、清单或已启用脚本,确认成功热重载即可。完整步骤及管理命令见上手教程。
默认行为:欢迎与自定义通关播报启用;经验倍率为 1;道具、商店、准入、聊天的限制集合为空。因此默认不会拦截任何真实资源。通关自定义 provider 会替代内置通关播报,仍可能受公告限流与队列影响。
| 目录 / ID | 清单与框架必须同时具备的权限 | 使用机制 |
|---|---|---|
welcome / docs-welcome |
notice.broadcast |
character.bound 通知 |
clear-announcement / docs-clear-announcement |
notice.broadcast |
dungeon.clear_announcement provider |
clear-experience / docs-clear-experience |
progression.clear_experience |
经验 transform |
item-policy / docs-item-policy |
inventory.item_use |
道具 before |
shop-policy / docs-shop-policy |
commerce.shop_purchase |
商店 before |
admission-policy / docs-admission-policy |
dungeon.admission |
准入 before |
chat-policy / docs-chat-policy |
chat.message |
聊天 transform |
配置集合的统一写法
示例使用“字符串键 → 布尔值”集合,符合框架配置的字符串键限制。空集合 {} 不增加限制,true 开启该条,false 保留但关闭该条。
-- 合法:聊天词语集合,不使用 Lua 正则/模式匹配。
config = { blocked_phrases = { ["测试屏蔽词"] = true } }
-- 合法:空编号集合;下载包的默认配置。
config = { blocked_item_ids = {} }
-- 错误:不能把配置值写成数字键数组。
-- config = { blocked_item_ids = { 123, 456 } }
资源编号从当前实例 PVF/商品定义核实,不从本文猜编号。把得到的十进制编号作为带引号的键,写为 blocked_item_ids = { ["真实十进制编号"] = true };这里的中文是说明占位,必须先替换为真实数字,原样使用会被配置校验拒绝。道具使用上下文本来就是字符串 ID;商店与副本编号是 uint32/整数,示例通过 string.format("%.0f", id) 得到集合键。角色 ID 则一直保留字符串,不能转换数字。
例 1:频道欢迎公告
源文件:plugin.json · main.lua
框架声明:
dnf.plugins.load("plugins/welcome", {
enabled = true,
permissions = { "notice.broadcast" },
config = { suffix = "祝冒险愉快!" }
})
关键回调:
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)
这里的 suffix 是完整脚本初始化时读取并验证的配置变量。完整脚本限制其 UTF-8 长度不超过 128 字节;公告 API 本身另有 511 UTF-16 码元限制,两者计量不同。
验收:重新选择测试角色,应向其当前频道广播;重连也会触发。检查插件 accepted、errors 和 actions,但计数不是送达凭据。不会自动补发已在线角色的欢迎,也没有每日去重或可靠在线统计能力。
例 2:接管通关播报
源文件:plugin.json · main.lua
框架启用自动归属,并保持此扩展点只有一个 provider:
dnf.plugins.clear_announcement("auto")
dnf.plugins.load("plugins/clear-announcement", {
enabled = true,
permissions = { "notice.broadcast" },
config = { scope = "channel", color = 0xFFFFD54F }
})
完整脚本提供缺失名称回退、用时格式化和频道检查。下面是最小实现结构:
dnf.providers.register("dungeon.clear_announcement", function(ctx, plan, event)
local data = event.data -- 这个 Hook 特别使用完整信封
return {
text = "[通关] 已完成「" .. data.dungeon_name .. "」。",
scope = "channel",
channel_id = data.character.channel_id
}
end)
provider 通过返回补丁交给宿主广播,不要在这个同步 Hook 内调用 ctx.notice.broadcast。不要再另订阅 dungeon.cleared 发送同样的公告,否则会产生额外消息。
验收:完成一次新的副本,检查文案与频道。dungeon.cleared 表示确认通关,后续奖励可能尚未结算。elapsed_ms 可为空,完整脚本会显示“未知”。错误或禁用取消本次插件播报,不补发内置消息。
auto 下停用此唯一 provider 并成功重载,恢复内置播报;builtin 强制使用内置,播报 Hook 不运行,但不阻止普通通知主动发公告;plugin 要求唯一 provider,缺失或冲突会使候选集合加载失败。
例 3:调整普通副本基础经验
源文件:plugin.json · main.lua
dnf.plugins.load("plugins/clear-experience", {
enabled = true,
permissions = { "progression.clear_experience" },
config = { multiplier = 2 }
})
下载框架默认是 multiplier=1,上面显式改为 2。完整脚本初始化校验倍率必须在 1..10,可为小数,结果向下取整并钳制正 uint32 上限:
dnf.hooks.transform("progression.clear_experience", function(ctx, plan, event)
-- event.character / event.dungeon_id,没有 event.data
return { amount = math.min(4294967295, math.floor(plan.amount * multiplier)) }
end)
它只调整普通副本基础通关经验,不是角色最终增加经验,也不影响击杀、任务经验。评分、契约等既有加成继续在核心执行,最终值不一定恰好翻倍;等级上限、SP/TP、资产事务及团本零经验规则保持。
验收必须使用尚未固定经验决策的新一次副本运行。首次计算结果(包含没有 Hook、故障回退)会固定;中途重载、事务重试、收据重放不会重新执行脚本。before=false、enabled=false 或报错都保留原基础值;不能返回 amount=0。这个历史入口仍在结算会话锁内,受 20ms 整链预算限制。
例 4:限制特定道具使用
源文件:plugin.json · main.lua
dnf.plugins.load("plugins/item-policy", {
enabled = true,
permissions = { "inventory.item_use" },
config = { blocked_item_ids = {} }
})
核实当前 PVF 的目标道具编号后,把它作为字符串键加入 blocked_item_ids。完整脚本在初始化验证整个集合,再在 before 阶段判断:
dnf.hooks.before("inventory.item_use", function(ctx, plan, event)
return blocked[event.item_id] ~= true
end)
event 是裸快照,item_id、count 为字符串,另外有 character/list_type/slot_index/in_dungeon。只在合法且实际存在源物品的 op27 UseStackable 接入,不覆盖 op160、op515、任意礼包或所有道具使用路径。
验收:在测试实例先保持空集合确认原流程,再配置一件通过该入口使用的真实测试道具,确认原生失败回包且没有扣除/发奖;未配置的道具仍服从核心规则。核心会在决策后重读源物品并完成原事务,Lua 不能自行发奖或扣费。
例 5:限制 NPC、秘密与 Cera 商店购买
源文件:plugin.json · main.lua
dnf.plugins.load("plugins/shop-policy", {
enabled = true,
permissions = { "commerce.shop_purchase" },
config = {
blocked_item_ids = {},
blocked_commodity_ids = {}
}
})
source 区分 npc、secret、cera。NPC/秘密商店按 blocked_item_ids 匹配请求行 item_id;Cera 按 blocked_commodity_ids 匹配 commodity_id,不能把最终奖励物品 ID 填到商品编号集合中。
dnf.hooks.before("commerce.shop_purchase", function(ctx, plan, event)
for _, item in ipairs(event.items) do
if event.source == "cera" then
local key = string.format("%.0f", item.commodity_id)
if blocked_commodities[key] == true then return false end
else
local key = string.format("%.0f", item.item_id)
if blocked_items[key] == true then return false end
end
end
return true
end)
上面两个集合由完整脚本初始化。items 是宿主传入的数组,可以 ipairs 读取;禁止配置数组不意味着宿主输入里没有数组。Cera Hook 行的 count=1 表示一行购物车请求,不是最终发货数量;此时没有最终商品/赠品发放列表。
验收:先空集合购买正常;再针对当前 PVF 中核实的一行配置限制,确认整笔请求走原生失败路径,未扣货币。true 仍要经过 PVF、价格、限购、余额、背包和归属检查,不能授权免费购买。这个 Hook 不接入拍卖或摆摊。
例 6:限制首次副本准入
源文件:plugin.json · main.lua
dnf.plugins.load("plugins/admission-policy", {
enabled = true,
permissions = { "dungeon.admission" },
config = { blocked_dungeon_ids = {} }
})
把当前 PVF 确认的副本 ID 作为十进制字符串键配置。裸输入包含 character、dungeon_id、difficulty 与 members(准备中的角色数组):
dnf.hooks.before("dungeon.admission", function(ctx, plan, event)
local key = string.format("%.0f", event.dungeon_id)
return blocked[key] ~= true
end)
验收:测试首次选图与训练直入,拒绝时不应进入后续门票/金币/次数扣费流程。这是准备好领队与跟随名单后的全队一次决策,不是每人独立回调;名单仍须由核心重新验证。不覆盖 op72 结算再次挑战,不能据此实现该路径的绝对禁入。
例 7:按文字子串拒绝聊天
源文件:plugin.json · main.lua
dnf.plugins.load("plugins/chat-policy", {
enabled = true,
permissions = { "chat.message" },
config = { blocked_phrases = { ["测试屏蔽词"] = true } }
})
下载框架默认空集合。完整脚本限制每个非空词语最多 128 UTF-8 字节,使用普通子串查找,1, true 表示不解释 Lua 模式:
dnf.hooks.transform("chat.message", function(ctx, plan, event)
for phrase, enabled in pairs(blocked) do
if enabled and string.find(event.text, phrase, 1, true) then
return { allowed = false }
end
end
return nil
end)
event.text 来自 GBK 解码后的快照;裸输入另有 character/mode/item_links/source/recipients。此扩展点只允许拒绝或保留原规则,不能改正文、频道或收件人。
验收:发送包含测试词的消息,确认不转发;发送不含词的消息,确认正常路由。喇叭路径在扣费前决策。匹配区分大小写,不处理全半角、Unicode 归一化、谐音或规避变体。转发通知的 recipients 是宿主接受量,不是客户端确认。
后四个策略示例共同的故障语义
这四项限制 Hook 在共享业务锁与资产事务外等待 Lua,整链 20ms、单回调最多 10ms;只允许补丁中的布尔 enabled/allowed。before=false、enabled=false 或 allowed=false 都会拒绝本次操作,后续脚本不能把已否决的请求重新放行。
任一脚本错误、非法补丁、超时或规则 VM 繁忙时,整条链放弃部分结果,保留原核心规则。因此它们适合增加玩法策略,不能当作故障时仍绝对生效的安全认证、封禁或持久账本。下载脚本把常见配置错误放在初始化校验,尽早拒绝候选版本。
同步规则不能广播;想观察结果,可以另订阅精确的稳定事件。但通知仅尽力交付,不能作为可靠发奖触发器,也不能在通知 VM 中改变量供规则 VM 使用:两者是独立初始化的 VM,状态不共享,热重载也不迁移。
自己写插件时的验收顺序
- 初始化:验证配置与所需
catalog项,清单与框架权限一致,启用集合无重复 ID/provider。 - 正常路径:空集合或正常参数保持核心行为;不因安装观察插件而多发重复公告。
- 限制路径:明确支持的入口走原生失败回包,资产没有提前扣除;其他入口按已说明覆盖范围处理。
- 故障路径:在独立测试实例制造可恢复的加载错误,确认旧集合仍服务,修复后
generation增加;测试回调错误时按该 Hook 的故障语义处理。 - 停用/回滚:框架改
enabled=false并确认成功切换;经验用新副本验证,不指望撤销历史决策。