dnf/v1
接口手册 / dnf/v1

业务 Hook

用明确的输入和返回方案调整播报、经验与四类业务限制。

先选对扩展点

Hook 在核心明确预留的位置参与规则计算。它不会把数据库事务、原始会话或协议写入权交给脚本。当前共 6 项:

扩展点 所需权限 可改变的内容 出错、超时或繁忙
dungeon.clear_announcement notice.broadcast 通关公告文案、颜色、范围、启停 丢弃本次插件播报,不补发内置公告
progression.clear_experience 同名权限 普通副本基础通关经验 保留原始基础经验
inventory.item_use 同名权限 允许或拒绝已接入道具使用 保留原核心规则
commerce.shop_purchase 同名权限 允许或拒绝商店购买 保留原核心规则
dungeon.admission 同名权限 允许或拒绝已接入首次入场 保留原核心规则
chat.message 同名权限 允许或拒绝聊天 保留原核心规则

以下 event 和 plan 代码块是数据结构示意,ID 和数字仅用于说明类型,不代表已存在的 PVF 对象。可运行安装目录、清单和框架文件见完整示例。把本页 main.lua 片段装入插件前,清单与框架都必须填写该点要求的权限。

所有 Hook 共用的执行规则

lua
local dnf = require("dnf")

dnf.hooks.before(point, function(ctx, plan, event)
    return true -- 必须 boolean;false 停止后续阶段
end)

dnf.providers.register(point, function(ctx, plan, event)
    return {} -- 必须字符串键补丁表,省略字段保留当前值
end)

dnf.hooks.transform(point, function(ctx, plan, event)
    return nil -- 字符串键补丁表,或 nil 保留方案
end)

point 在实际脚本中必须替换为下文某个扩展点字符串。只在初始化注册。执行顺序为所有 before → 唯一 provider → 所有 transform;同阶段按框架 load 顺序。每插件每点每阶段最多一个回调,整个集合每点最多一个 provider;冲突会拒绝整个候选集合。经验和四项业务限制可只注册 before 或 transform,无需 provider。通关播报需要唯一 provider 才能由插件接管。

每阶段都收到当前 plan 和原 event 的独立快照,直接修改参数没有效果,必须返回补丁。未知字段、类型错误、整数越界、非法返回值会让整条链失败,丢弃先前部分结果。所有规则回调都不能调用 notice.broadcast。

before=false 会设置 enabled=false,此后不执行剩余回调。对四项限制 Hook,allowed=false 也会停止剩余回调,后面的插件不能重新放行;请按每点的取消语义使用。

整条链预算 20ms,单阶段最多 10ms,规则等待队列每插件 1 项,繁忙会快速失败。不要执行高开销计算,也不要依赖通知 VM 的变量;它们的内存互不共享。

dungeon.clear_announcement:通关播报

所需权限是 notice.broadcast。框架使用 auto 且存在唯一 provider,或使用 plugin 且存在唯一 provider 时运行;builtin 模式跳过此点全部阶段。它只决定展示,不影响通关和奖励。

输入

本点的 event 是完整 dungeon.cleared 信封,字段必须从 event.data 读取。

lua
plan = {
    enabled = true,
    text = "",              -- provider 需要给出最终非空文案
    color = 0xFFFFD54F,
    scope = "instance",
    channel_id = 1          -- 通关角色所在频道
}

event = {
    id = "event-123",
    name = "dungeon.cleared",
    version = 1,
    occurred_at_ms = 1700000000000,
    data = {
        character = { id = "123", channel_id = 1 },
        dungeon_id = 1,
        dungeon_name = "示例副本",
        player_name = "示例角色",
        party = false,
        elapsed_ms = 45000,  -- 未知时为 nil
        source = "example"  -- 实际来源由宿主提供
    }
}

仅允许返回 enabled、text、color、scope、channel_id。text 最终必须非空白且最多 511 个 UTF-16 码元;color 为 uint32 ARGB;scope 为 instance 或 channel;频道范围要求正 channel_id。展示面固定 native_chat,不能返回 surface。

main.lua 示例

lua
local dnf = require("dnf")

dnf.providers.register("dungeon.clear_announcement", function(ctx, plan, event)
    local data = event.data
    local suffix = ""
    if data.elapsed_ms ~= nil then
        suffix = string.format(",用时 %.1f 秒", data.elapsed_ms / 1000)
    end
    return {
        text = string.format("%s 通关 %s%s", data.player_name, data.dungeon_name, suffix),
        scope = "channel",
        channel_id = data.character.channel_id,
        color = 0xFFFFD54F
    }
end)

before=false、最终 enabled=false 或任意错误均取消这次插件播报,不补发内置公告。最终动作按 provider 所属插件计入公告限流和队列。普通 dungeon.cleared 事件订阅仍是另一条通知路径,同时写两条播报逻辑会导致你自己重复发公告。

progression.clear_experience:基础通关经验

需要 progression.clear_experience 权限。输入是裸上下文,没有 event.data。

输入与返回字段

lua
plan = { enabled = true, amount = 100 }
event = {
    character = { id = "123", channel_id = 1 },
    dungeon_id = 1
}

仅允许返回 enabled:boolean、amount:integer。amount 始终必须为正 uint32,即 1..4294967295,禁用时也不能返回 0。它是普通副本基础通关经验;原有加成、等级上限、SP/TP、归属和资产事务继续由核心处理。不改变击杀经验、任务经验,也不把团本原有零经验路径改成普通副本经验。

main.lua 示例

框架可设置 config={multiplier=2},本脚本也默认 2 倍。此示例将最终基础值钳在合法区间。

lua
local dnf = require("dnf")
local multiplier = dnf.config.multiplier or 2
assert(type(multiplier) == "number" and multiplier >= 1 and multiplier <= 10,
    "multiplier must be between 1 and 10")

dnf.hooks.transform("progression.clear_experience", function(ctx, plan, event)
    local amount = math.floor(plan.amount * multiplier)
    return { amount = math.max(1, math.min(4294967295, amount)) }
end)

before=false、enabled=false、错误、超时、繁忙均使用原基础经验,不是“不给经验”。不要用 amount=0 禁止奖励;该返回会失败并回到原规则。

首次计算后,本次运行固定经验决策,包含无 Hook 和失败回退的情况。后续重载、事务失败重试或收据重放不会再次计算或改变已确定的基础值。测试修改后应开始一轮新副本。

此历史入口仍在角色副本结算锁内执行同步规则,不能将后文四项 Hook 的“业务锁外执行”承诺套到这里。

四项业务限制的共同保障

后面四项输入都是裸上下文,初始计划相同:

lua
plan = { enabled = true, allowed = true }

只有 enabled、allowed 两个 boolean 字段可返回。成功计算后 before=false、enabled=false 或 allowed=false 走适配器已有原生拒绝 ACK / 取消路径。不能自定义错误码、错误文案、价格、奖励或正文。

它们在共享业务锁和写事务外等待 Lua;通过后核心重新读取并验证当前状态。allowed=true 只表示插件没有额外拒绝,原归属、PVF、价格、余额、容量、装备锁、次数和门票等检查继续执行,脚本无法绕过。

任意脚本错误、非法返回、超时或繁忙都会保留原核心规则,整链部分修改不生效。这些 Hook 不适合作为必须在插件故障时仍保持的唯一安全认证或持久限购账本。

inventory.item_use:道具使用限制

需要同名权限。当前仅接入 op27 UseStackable 的合法、真实存在源物品;不包含 op160、op515 或所有礼包与其他使用路径。

输入

lua
event = {
    character = { id = "123", channel_id = 1 },
    item_id = "10001",       -- string
    list_type = 0,           -- integer,合法源类型为 0 或 7
    slot_index = 0,          -- integer,源槽位
    count = "1",             -- string,当前真实源堆叠总量,不是本次消耗数量
    in_dungeon = true        -- boolean
}

main.lua 示例

按框架配置中的字符串键映射,在副本内限制指定道具。默认空映射不限制任何物品;要测试,配置中填入当前 PVF 的真实道具 ID。

lua
local dnf = require("dnf")
local blocked = dnf.config.blocked_items or {}
assert(type(blocked) == "table", "blocked_items must be a table")

dnf.hooks.before("inventory.item_use", function(ctx, plan, event)
    return not (event.in_dungeon and blocked[event.item_id] == true)
end)

配置形如 blocked_items={ ["10001"]=true },数字是示例。Hook 不自行扣物、扣费或发奖;核心 owner 之后重新校验源仓储,在原事务中处理消耗和奖励。一次请求通过不代表后续一定提交成功。

commerce.shop_purchase:购买限制

需要同名权限,覆盖 NPC、秘密商店和 Cera 商城。输入是请求快照,不能据此推断实际发货和额外赠品。

输入

lua
-- NPC / 秘密商店:source 分别为 "npc" / "secret"。
event = {
    character = { id = "123", channel_id = 1 },
    source = "npc",
    items = {
        { item_id = 10001, count = 2 }
    }
}

-- Cera:按请求购物车商品 ID 判断,count=1 是请求行,非发货量。
event = {
    character = { id = "123", channel_id = 1 },
    source = "cera",
    items = {
        { item_id = 0, commodity_id = 20001, count = 1 }
    }
}

这里的 item_id、commodity_id、count 都是 integer。Cera Hook 不提供解析后的最终物品,当前共享结构会带 item_id=0 占位,应按 commodity_id 判断。NPC/secret 不适用的 commodity_id 通常缺省为 nil。

main.lua 示例

lua
local dnf = require("dnf")
local blocked_items = dnf.config.blocked_items or {}
local blocked_commodities = dnf.config.blocked_commodities or {}
assert(type(blocked_items) == "table", "blocked_items must be a table")
assert(type(blocked_commodities) == "table", "blocked_commodities must be a table")

dnf.hooks.transform("commerce.shop_purchase", function(ctx, plan, event)
    for _, row in ipairs(event.items) do
        if event.source == "cera" then
            if blocked_commodities[tostring(row.commodity_id)] == true then
                return { allowed = false }
            end
        elseif blocked_items[tostring(row.item_id)] == true then
            return { allowed = false }
        end
    end
    return nil
end)

两张配置表使用字符串 ID 键。此例只增加黑名单,不改数量、价格、限购或奖品。真实 PVF、余额、每日限购等仍由原 owner 检查;错误时恢复原规则。若需要观察真正购买提交,请订阅 commerce.shop_purchased,其 Cera 行与请求 Hook 的含义不同。

dungeon.admission:首次入场限制

需要同名权限。当前接入首次副本选择器入场及训练直入;领队和跟随者准备完成后、扣金币/门票/次数之前,全队进行一次决策。不包含 op72 结算后的再次挑战。

输入

lua
event = {
    character = { id = "123", channel_id = 1 },
    dungeon_id = 1,
    difficulty = 0,
    members = {
        { id = "123", channel_id = 1 },
        { id = "456", channel_id = 1 }
    }
}

character 是发起侧角色,members 是准备中的角色名单。数字均为 integer;不能把这个快照作为扣费时的最终权威状态。

main.lua 示例

lua
local dnf = require("dnf")
local blocked = dnf.config.blocked_dungeons or {}
assert(type(blocked) == "table", "blocked_dungeons must be a table")

dnf.hooks.before("dungeon.admission", function(ctx, plan, event)
    return blocked[tostring(event.dungeon_id)] ~= true
end)

配置形如 blocked_dungeons={ ["1"]=true },应使用真实目标副本 ID。拒绝会回到原生入场拒绝/取消路径;不自行扣门票。通过后原门禁、成员验证和事务扣费仍执行。若要统一限制再次挑战,当前此点覆盖不足,应先扩展宿主接入,不能用此文档示例宣称全路径封禁。

chat.message:聊天限制

需要同名权限。在合法请求进入聊天策略之后、转发和喇叭扣费之前运行,只允许/拒绝,不能改正文、频道和收件人。

输入

lua
event = {
    character = { id = "123", channel_id = 1 },
    mode = 0,               -- integer,宿主解码的聊天模式值
    text = "这是一条示例消息", -- string,客户端 GBK 解码后的文本快照
    item_links = 0,         -- integer,附带的道具链接数量
    source = "example",    -- string,实际来源由宿主填入
    recipients = 0          -- integer,决策时尚不是最终转发确认
}

不要猜测 mode 的语义或把示例 source 当作固定枚举;按实际宿主事件和已核验客户端路由制定规则。通知中的同结构 chat.message.forwarded.recipients 指宿主受理数,仍不是客户端确认。

main.lua 示例

以下按一个可配置的字面关键词限制消息。plain=true 避免把配置解释为 Lua 模式。

lua
local dnf = require("dnf")
local blocked_text = dnf.config.blocked_text or ""
assert(type(blocked_text) == "string", "blocked_text must be a string")

dnf.hooks.before("chat.message", function(ctx, plan, event)
    if blocked_text == "" then
        return true
    end
    return string.find(event.text, blocked_text, 1, true) == nil
end)

空关键词默认不限制消息。拒绝时不进行转发与喇叭消耗;正常路由、编码和原生反馈仍由核心负责。策略出错时保留原聊天规则,因此这不是故障情况下仍能保证执行的唯一内容管控系统。可订阅 chat.message.received、blocked、forwarded 观察各自阶段,不能把 received 视为已发送。

验证一个限制 Hook

先在测试实例安装完整示例,查当前版本 generation 和权限是否正确。选择一条明确在接入范围内的请求验证允许,再填入真实对象 ID 或关键词验证拒绝,观察原生反馈与资产保持。移除限制后再次请求,确认原核心规则继续工作。

经验修改用新副本验证;播报用新的通关事件验证。通知计数和 handler_failed=false 都不是业务成功证据。状态查询和加载错误定位见管理与排错。

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

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

点击结果跳转 · Esc 关闭