dnf/v1
实践与交付 / dnf/v1

完整插件示例

下载七个完整插件,按照实际需求修改配置和策略。

这里提供七个完整插件,每个都有 plugin.json 和 main.lua,配套框架可一次加载全部。代码只使用当前宿主 API。先读从零上手完成一个欢迎插件,再用这些例子扩展。

下载:完整示例包 ZIP · 框架源码 · 安装 README · 编辑器声明。ZIP 中没有账号、token、真实管理地址或实例配置。

先安装到正确的位置

将下载包中的框架与七个目录复制进顶层 runtime/plugins/,不要额外嵌套 examples/ 目录。已有框架则合并所需声明,保留原有插件;不必一次启用全部。

text
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
-- 合法:聊天词语集合,不使用 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

框架声明:

lua
dnf.plugins.load("plugins/welcome", {
    enabled = true,
    permissions = { "notice.broadcast" },
    config = { suffix = "祝冒险愉快!" }
})

关键回调:

lua
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:

lua
dnf.plugins.clear_announcement("auto")
dnf.plugins.load("plugins/clear-announcement", {
    enabled = true,
    permissions = { "notice.broadcast" },
    config = { scope = "channel", color = 0xFFFFD54F }
})

完整脚本提供缺失名称回退、用时格式化和频道检查。下面是最小实现结构:

lua
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

lua
dnf.plugins.load("plugins/clear-experience", {
    enabled = true,
    permissions = { "progression.clear_experience" },
    config = { multiplier = 2 }
})

下载框架默认是 multiplier=1,上面显式改为 2。完整脚本初始化校验倍率必须在 1..10,可为小数,结果向下取整并钳制正 uint32 上限:

lua
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

lua
dnf.plugins.load("plugins/item-policy", {
    enabled = true,
    permissions = { "inventory.item_use" },
    config = { blocked_item_ids = {} }
})

核实当前 PVF 的目标道具编号后,把它作为字符串键加入 blocked_item_ids。完整脚本在初始化验证整个集合,再在 before 阶段判断:

lua
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

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 填到商品编号集合中。

lua
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

lua
dnf.plugins.load("plugins/admission-policy", {
    enabled = true,
    permissions = { "dungeon.admission" },
    config = { blocked_dungeon_ids = {} }
})

把当前 PVF 确认的副本 ID 作为十进制字符串键配置。裸输入包含 character、dungeon_id、difficulty 与 members(准备中的角色数组):

lua
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

lua
dnf.plugins.load("plugins/chat-policy", {
    enabled = true,
    permissions = { "chat.message" },
    config = { blocked_phrases = { ["测试屏蔽词"] = true } }
})

下载框架默认空集合。完整脚本限制每个非空词语最多 128 UTF-8 字节,使用普通子串查找,1, true 表示不解释 Lua 模式:

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,状态不共享,热重载也不迁移。

自己写插件时的验收顺序

  1. 初始化:验证配置与所需 catalog 项,清单与框架权限一致,启用集合无重复 ID/provider。
  2. 正常路径:空集合或正常参数保持核心行为;不因安装观察插件而多发重复公告。
  3. 限制路径:明确支持的入口走原生失败回包,资产没有提前扣除;其他入口按已说明覆盖范围处理。
  4. 故障路径:在独立测试实例制造可恢复的加载错误,确认旧集合仍服务,修复后 generation 增加;测试回调错误时按该 Hook 的故障语义处理。
  5. 停用/回滚:框架改 enabled=false 并确认成功切换;经验用新副本验证,不指望撤销历史决策。

更多字段、事务边界与限制分别见API 参考、完整事件目录、Hook 参考和运行与排错。

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

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

点击结果跳转 · Esc 关闭