dayz-agentic-modding-mcp
dayz-agentic-modding-mcp
一个 MCP 服务器,让代理能够构建 DayZ 模组、检查客户端能否编译、运行测试服务器,并获取结构化结论而不是日志。
服务器自己完成这些工作:它直接调用 FileBank、签名工具和诊断可执行文件。项目不需要自己的构建脚本。
为什么需要配置文件
服务器对任何特定模组一无所知。所有具体内容都存在于两部分配置文件中:
dayz-mcp.toml— 可移植,提交到模组仓库:要打包哪些模组,健康启动是什么样的。dayz-mcp.local.toml— 特定于机器,绝不提交:游戏、工具和测试台在哪里。
加载时如果混合了两部分,会被拒绝:这就是仓库不会在别人机器上构建的原因。从 dayz-mcp.example.toml 开始。
模组按名称声明一次:默认源在 <root>/Name,输出为 @Name/addons/Name.pbo。build.sources 可以将模组的源重定向到其他地方(例如,对于 config.cpp 位于仓库根目录本身的模组,使用 ".")。
可移植部分 — dayz-mcp.toml
键 | 含义 |
| 这个项目叫什么 |
| 每个模组一个条目,按名称;源、pbo 和 |
| 模组源实际所在位置,相对于配置文件( |
| 绝不能打包的内容。列出它会完全替换默认值;默认值是 |
| 当存在被排除的内容时,打包一个过滤后的副本,而不是拒绝——这是根目录布局模组所需的布局 |
| 所有模型路径解析所依据的目录,相对于此文件。它必须包含模组的前缀文件夹。仅模型工具需要,其他都不需要——参见“资源管线” |
| 打包前运行的 PowerShell 脚本,用于先生成代码的项目 |
| 模组完成加载时打印的行——见下文 |
| 该行携带的 |
| 警告预算;省略该键以禁用检查 |
| 无论其他情况如何,只要出现这些子串就使运行失败 |
| 标记属于你的脚本错误的正则表达式,用于客户端编译检查 |
| 除了内置列表之外,要忽略的额外引擎噪音 |
机器部分 — dayz-mcp.local.toml,绝不提交
键 | 含义 |
| 游戏安装;如果缺失则自动发现 |
| DayZ 工具;如果缺失则自动发现 |
| Blender 可执行文件,仅用于 |
| 准备好的测试台。服务器针对它启动,其日志从 |
| 测试台内的服务器配置文件名称(默认 |
|
|
| 模组文件夹名称,在游戏自己的 |
| 要加载的其他任何内容的完整路径,用于不在 |
| 要路由到 |
就绪行
server_start 在启动开始后写入的日志中出现 expect.ready_line 时完成。这是服务器无法自行推断的唯一一件事,没有它,两件事会改变:无法等待任何内容,因此启动作业会启动服务器,确认它片刻后仍然存活,然后完成并说明这一点;并且 log_verdict 没有行可读取计数器,因此 expect.counters 永远不会匹配。错误、崩溃和警告预算仍然会被判断。没有就绪行的配置文件是受支持的,而不是损坏的——project_open 会在其注释中说明这一点。
Related MCP server: DayZ API MCP Server
工具
工具 | 功能 |
| 读取配置文件,发现游戏和工具,报告缺失项 |
| 当前项目、运行中的服务器、最近的作业 |
| 打包并签名所有声明的模组;返回作业 ID。同一项目已有构建在运行时拒绝第二次构建 |
| 启动测试服务器,就绪后完成。如果配置指定的任务不在 |
| pid、进程是否存活、日志是否在增长(间隔 |
| 停止本会话启动的服务器(孤儿服务器的可选 pid) |
| 读取 — 或有意更改 — 测试台的 |
| 运行诊断客户端并读取其日志 |
| 通过/失败及原因:计数器、禁止字符串、警告预算。 |
| 最后几行,可选过滤;同上述两个来源 |
| 长时间运行作业的状态 |
| 等待作业完成 |
| 从已完成的作业中检索输出 |
| 打包桥接模组,其源码随此服务器( |
| 运行中的游戏内的桥接是否仍在跳动:报告跳动编号以及是否在 |
| 丢弃卡在邮箱中的命令,并说明丢弃了什么。除非 |
| 等待游戏内的桥接实际认领命令。在启动作业完成后、第一个世界命令之前调用一次 — 见下文"服务器就绪不等于桥接就绪" |
| 来自桥接每秒一次发布的世界的快照:玩家、位置、生命值、双手。无参数时免费;带 |
| 在地面(无生命周期,因此不会在检查中途消失)、玩家手中或玩家背包中创建物品 |
| 将玩家移动到 |
| 设置 |
| 删除附近某一类的对象。需要类名;绝不删除真实玩家 |
| 附近哪些对象,而非多少:每个对象的类、位置、距离和生命值。一页,并且会说明 — 真实总数随列表一起返回 |
| 移动世界时钟。每个保持 -1 的字段保留其当前值,先从引擎读回,因为引擎将日期一次设置为五个数字 |
| 移动阴天、雨、雾、雪或风。轻推而非锁定:引擎之后会继续模拟天气,工具和模组都会说明这一点 |
| 通过引擎的门运行模组自己的动作 — 见下文 |
| 逃生舱口:通过同一传输通道的任意动词,在每个回答中标记为非标准 |
| 启动游戏客户端并将其连接到测试台;返回作业 ID。始终窗口化。当桥接报告 |
| client_status() | pid、窗口几何信息、窗口是否最小化或在前台、背景设置、玩家数量,以及是否连接了虚拟控制器 |
| client_stop() | 停止本会话启动的客户端,并拔出虚拟控制器 |
| client_shot(path) | 将客户端窗口捕获为 PNG,附带 lit_fraction —— 用于区分真实帧与全黑帧的数字。无需焦点 |
| client_move(x, y, seconds) | 用左摇杆移动角色。模拟量,也是唯一能移动角色的通道。无需焦点 |
| client_look(x, y, seconds) | 用右摇杆转动镜头。无需焦点 |
| client_press(button, seconds) | 按下一个手柄按键,来自十四个名称的封闭表。无需焦点 |
| client_chat(text, color) | 在聊天中发送一行——由桥接器在服务端投递,因此无需键盘、无需窗口、无需焦点 |
| client_type(text, submit) | 用真实按键输入到客户端侧输入框。这是此处唯一需要抢占前台的工具,其返回值中也会说明这一点 |
| client_verdict(since) | 根据客户端自身的 .RPT 判断其状态——一个错误与崩溃判定;见下文 |
| ui_menu() | 客户端界面当前在做什么:打开的菜单类、光标、对话框。免费——每 tick 重新发布 |
| ui_tree(root, depth, limit) | 客户端的控件树:路径、类、名称、可见性、屏幕矩形、深度和文本。是一页数据,返回值中也会说明 |
| ui_find(name, class_name, text, root) | 同样的遍历,但在客户端内过滤,因此整棵树无需传输 |
| ui_click(path, expect_name, expect_class, via) | 按下一个控件。via="script" 通过已打开菜单的处理器执行,无需焦点;via="cursor" 将真实鼠标放到其矩形上 |
| ui_text(path, text, expect_name) | 写入编辑框,并从控件中读回该值 |
| mod_lint(mod, strict) | 在不打包或不启动任何东西的情况下检查 Enforce Script。mod_build 会先运行它,并在其拒绝的内容上拒绝 |
| knowledge_build(layer, full, only) | 构建或刷新 API 索引的某一层;返回一个任务 id。only=[path] 只重新读取你指定的文件 |
| knowledge_status() | 每一层包含什么、有多旧,以及是否仍与磁盘上的内容匹配 |
| knowledge_find(name, kind, owner, layer, prefix, limit) | 按名称查找类、方法、常量、枚举或配置类 |
| knowledge_show(name, ..., body) | 完整展示一个声明:签名、成员、继承链,以及源码本身——如果它存在于归档中,则直接从归档中读取 |
| knowledge_overrides(name, owner, layer) | 谁覆写了此类或方法 |
| knowledge_callers(name, kind, owner, layer) | 谁调用此方法或构建此类——每个调用点,连同发起调用的类和方法 |
| asset_export(blend, mod, source, name) | 从 .blend 中导出模型到 build.project_root,无头模式;返回一个任务 id。可选的第一步——见下文 |
| asset_build(mod, source, deploy) | 将 mod 的模型从其 MLOD 源二进制化,检查产出,然后才放入 mod;返回一个任务 id |
| asset_check(mod, model) | 检查 mod 已随附的模型和纹理。不构建任何东西,不需要 DayZ Tools,毫秒级响应 |
| asset_convert(source, output) | 在 .png 和 .paa 之间转换一个纹理,并检查结果 |
签名,以及为什么引擎自身的消息会把你引向错误方向。 在 verifySignatures = 2 下,stand 会以代码 118 和 "缺少 dta.pbo" 拒绝每个客户端——这是一个原版文件名,完全未提及签名。原因通常是密钥环:此工具从 CLIENT 安装目录启动诊断可执行文件,因此引擎读取的是该可执行文件旁边的 keys,而 dayz.bikey——即签署游戏自身 pbo 的密钥——随独立的 DayZServer 安装一起提供。keys 文件夹缺失,或只包含 mod 自身的密钥,会导致服务器无法验证任何内容,包括原版文件。client_start 会拒绝并指出是三种情况中的哪一种;server_start 只发出警告,因为无客户端的无头启动仍然有用。客户端 -mod 行上的未签名 pbo 也会以同样方式被点名——包括本服务器自身的桥接器,它是有意以未签名方式打包的。
引擎对 UI 工具施加的三个限制,均未绕过: 普通 TextWidget 在 enwidgets.c 中有 SetText 而没有 GetText,因此标签的字符串完全无法读取——mod 界面的含义只能留给服务端桥接器来回答,那里的数据才是真实的。脚本级点击只能到达已打开的脚本化菜单,因为 Widget 有 SetHandler 而没有 GetHandler;via="cursor" 用于其他一切情况。此外,客户端必须加载桥接器:一个 pbo 同时携带两半,因此配置文件将其列在 mods.server_only 下会使其远离客户端的 -mod 行——这种情况会按名称被拒绝,而不是返回空树。
job_wait 是用于等待的工具,其 timeout 上限为 600 秒,无论传入多大的值。另外两个工具会休眠:server_status 采样日志两次,间隔 pulse_seconds,上限 10 秒——这个停顿就是它区分慢启动与挂起的方式——bridge_status 采样桥接器的 tick 两次,间隔 window,上限同为 10 秒,原因相同。其他所有工具立即返回;需要数分钟的工作在任务 id 背后进行。
桥接器 mod
bridge_build 将 bridge/ 从本服务器自身的仓库打包到其旁边的 @DZMCP_Bridge 中。它是服务器的 mod,不是你的:一份副本服务于所有项目,除了任务记录外不会向你的仓库写入任何内容,也不使用任何项目的签名密钥——-serverMod pbo 永远不会交给客户端去验证,因此它以未签名方式构建,其输出文件夹保持无签名和密钥。
构建它并不会加载它。这仍然是你配置文件的决定,因为桥接器是 stand 中的一个额外 pbo,没有它的运行必须仍然可行。要附加它,请在 dayz-mcp.local.toml 中添加两行(bridge_build 的任务摘要也会打印同样的两行):
[mods]
extra = ["<path printed by bridge_build>/@DZMCP_Bridge"]
server_only = ["@DZMCP_Bridge"]server_only 是将其路由到 -serverMod 而非 -mod 的原因。没有它,stand 也能正常启动,而 bridge_status 会报告桥接器从未写入任何状态——这是事实,但容易被误认为是桥接器损坏。
bridge_status 还会报告命令邮箱。在游戏内部只有 mod 通过认领命令来清空它;在这一侧,bridge_clear 和 server_start 的启动前清理会清空它。因此,在 stand 关闭时或桥接器附加之前发送的命令不会被丢弃,也不会自行过期——它会持续阻塞后续的每次发送,而通过这些工具之外启动的 stand 会拾取它。server_start 在每次启动前都会清空两个传输文件,因此通过此工具启动的服务器永远不会运行来自先前会话的命令;这是卫生习惯,而非了解命令存在的替代品。该状态以 stale_command 返回,而 bridge_clear() 是摆脱它的方法。清空是有意作为独立工具的:丢弃排队的命令是一个决定,而不是状态检查应该在背后替你做的事情。当桥接器看起来还活着时它会拒绝,除非你传入 force=True,无论哪种方式它都会报告它丢弃的命令 id。
bridge_status 能区分什么
仅凭 tick 不足以判断桥接器,因为它每次启动都会从 0 重新开始,而状态文件在配置文件目录中持续存在。每个答案都在 heartbeat 中携带通道自身的判定,而这四种是真正不同的事实:
|
| 含义 |
|
| tick 在同一会话内移动——唯一 |
|
| 两次采样之间出现了新世界:存活,未冻结,且发送给旧会话的任何内容都已消失 |
|
| 同一世界被看到两次且未移动——脚本侧问题,因此 |
|
| 无法读取采样(或 |
每个读取过采样的答案还携带 session_id——当前存活世界的 id——而 restarted 答案还携带 previous_session_id,因此调用方可以说出哪个世界消失了。
no_server、stale_command、no_state_file、invalid_state、unreadable_state 和 outdated_bridge 都先于上述任何情况出现:没有东西在运行、命令被卡住、mod 未加载、状态文档是合法 JSON 但某个命名字段错误(它会指出是哪个字段,并在断言之前检查两次)、文件完全无法解析,或者它能解析但早于本服务器的协议(用 bridge_build 重建它)。
世界命令
世界工具通过服务器 -profiles 目录中的两个 JSON 文件与桥接器通信:一个命令邮箱(从这一侧原子写入,由 mod 作为认领删除)和一个 mod 每秒覆写一次的状态文件。Enforce Script 没有重命名操作,因此 mod 无法原子写入——读取器改为容忍撕裂写入,一次失败的读取永远不算新闻。在活 stand 上测量的四个事实决定了如何使用它们:
服务器就绪不等于桥接器就绪。 桥接器在服务器报告就绪之后数十秒才开始认领命令——目前观察到的散布范围是 18–38 秒,且每次启动都不同。在该窗口内发送的命令不会被拒绝——它会被延迟认领,并在调用方放弃之后才完成。因此:server_start,等待启动任务,然后 world_ready(),再发命令。如果 tick 未移动,每个世界工具也会预先拒绝,并点名 world_ready 作为补救措施。
每个参数值都以字符串形式过线。 mod 的解析器很严格:args 中任何位置的 JSON 数字都会拒绝整个 args 块。工具自行将数字和布尔值字符串化,并拒绝没有忠实字符串形式的值(列表、字典、None)。位置以单个字符串 "x y z" 传输。
拒绝就是结果。 mod 自身的句子会逐字作为错误返回:"服务器上没有玩家"、"该类不存在"、"动作自身的 Can() 拒绝了"。无人连接是无头 stand 的正常状态,每个需要玩家的动词都会明确说明,而不是静默地什么都不做。
会话 id 防止昨天的命令。 每个命令都携带桥接器最近发布的会话;mod 拒绝任何发往其他会话(或无会话)的命令而不执行它。因此,在 stand 关闭时写入的命令永远不会射入新启动的世界。工具会自动盖上会话戳——只有当你手写邮箱时才需要关心。
动作,以及为什么没有动词字典
像“上交样本”这样的语义动词具有欺骗性:在真实的模组中,同样的词语会根据附近是哪个设备、玩家的阵营以及已经解锁了什么而表示不同的含义。这种上下文无法枚举,因此桥接层不做这种尝试。world_action 接受一个动作的类名、一个目标和手持物品,并请求引擎通过它自己的门控来运行——与按键所经过的同一个门控。适用性由动作自身的 Can() 决定,其拒绝是一个有意义的测试结果,而不是工具故障。可区分的答案有:管理器忙、玩家已在行动中、玩家在冲刺、未知的动作类,以及“动作自身的 Can() 拒绝了”。“已接受”也不等于成功——命令会一直运行,直到引擎真正释放该动作,而每一条失败路径都会释放管理器,使玩家之后仍能行动。
world_exec 是逃生舱
模组暴露的任何不是动作的东西——“阵营池里有多少点数”——都通过 world_exec(verb, args) 处理:在同一个传输通道上执行任意动词。每个答案都标记为 non_standard:此服务器不知道这个动词,不验证它,也不为模组用它做了什么而负责。桥接层构建时不知道的动词,会返回它所知道的动词列表。需要自己动词的项目,编辑它自己的副本中的桥接层分发器(bridge/scripts/5_Mission 中 KnownVerbs() 上方的注释精确指出了位置);故意不设注册机制——一个由本服务器输入并验证的动词,就是一个由本服务器负责的动词。
客户端:三个输入层,以及为什么是三个
桥接层到达服务器。它做不到的是查看客户端的屏幕或通过客户端行动——让角色走过地面、打开菜单、填写模组绘制的字段。client_* 工具就是干这个的,它们使用三条不同的通道,因为没有哪一条能完成另外两条的工作。下面每一行都是对照真实客户端的测量结果,而不是设计意图。
通道 | 它做什么 | 需要前台 |
桥接层( | 世界,以及向聊天发送文本 | 否 |
虚拟手柄,ViGEmBus( | 移动、镜头,以及部分界面 | 否 |
真实按键, | 向仅存在于客户端上的字段输入文本 | 是,而且会占用它 |
键盘模拟不会移动角色,窗口消息则完全无效。 已验证前台的 SendInput 扫描码:向前 25 秒,0 米。向主窗口及其子窗口发送 PostMessage/SendMessage WM_KEYDOWN:0 米,菜单也没有任何反应。引擎从原始输入读取移动,忽略模拟按键,这就是为什么这里没有任何工具提供窗口消息。
虚拟手柄确实能移动它,无需聚焦,而且是模拟量——这就是即使按键也能做到时仍保留它的原因。 在一次运行中测量,第三方应用全程持有前台:
stick fully forward, 10.0 s -> 38.40 m (3.84 m/s)
stick at 0.3 forward, 8.0 s -> 11.34 m (1.42 m/s)同一条通道,同一个角色,仅靠摇杆偏转就达到 2.7 倍速度。“角色在走而不是在跑”无法用按键表达,因为按键只知道开和关。在同一次运行中,角色自行走了约 141 米,模组自身统计的 10 米内物体数量从 1 → 0 → 1,因为它离开该位置又回来——这是由在场状态引起的状态变化,传送无法产生这种变化。
部分界面响应手柄,部分不响应。 实测,游戏窗口全程位于另一个应用后面:back 打开和关闭物品栏,start 打开暂停菜单,b 关闭它——全部在默认的 0.1 秒点按下完成,所以一次点按足以让引擎锁定。但 a 什么都没移动,无论是 0.1 秒还是 0.5 秒,方向键在这些界面内也一样:客户端没有切换到控制器导航模式,所以没有聚焦元素可供确认键作用于其上。将菜单关闭视为手柄的工作,将菜单确认视为未经证实。
眼睛也不需要聚焦。 捕获是实时帧,窗口位于 z 序最底部(同一会话中未聚焦时 lit_fraction 为 0.9997,聚焦时也为 0.9997)。唯一能击败它们的状态是最小化的窗口,其客户区塌缩为 0×0——会附上原因被拒绝,而不是保存为一张看起来有效的空白图片。
所有这些后台行为都取决于一个客户端设置,pauseMode(GAME → UPDATE IN BACKGROUND)。在此处测量的值下,客户端在未聚焦时仍持续绘制和模拟,这就是帧是实时的、摇杆仍能移动角色的原因。在“无图形”模式下,两者都会静默停止——冻结的帧看起来和实时帧一模一样。因此 client_start 和 client_status 会读取该设置并发出警告;它们从不写入它,因为它属于机器的所有者。
client_type 是唯一会占用屏幕的工具,而且它对此很诚实:答案带有 foreground_taken 和一句话,说明坐在机器前的人在它运行期间无法在自己的窗口里输入。它在请求前台后用 GetForegroundWindow 验证前台,因为当 Windows 拒绝时,SetForegroundWindow 会返回成功但实际上什么都没做——而盲打会把按键发送到那个人实际正在使用的任何窗口。当无法获得前台时,什么都不会输入,拒绝信息会指出持有它的进程。
ViGEm 是对真实设备的模拟,而这是一个测试台。 驱动已签名,无需重启即可安装,手柄是一个新的设备,而不是对机器自身键盘和鼠标的过滤——这里曾尝试过一次过滤驱动,结果让机器所有者失去了所有键盘和鼠标输入,直到手动解除。这些都不是对真实服务器上反作弊的承诺,本阶段也不会做出任何承诺。
知识索引
编写模组的智能体不断向游戏提出同样的问题:是否有这样的 API,它叫什么,在哪里声明,谁覆盖了它。回答这些问题意味着解包 scripts.pbo 并扫描文本——而每次会话都要重新付出代价。knowledge_* 把这项工作变成了一个问题。
它是项目自己的 .dayz-mcp/ 目录中的一个普通 SQLite 文件,由本服务器从游戏、项目声明的模组以及项目自身的源码构建。没有嵌入、没有外部服务、没有密钥。
三层,以及为什么它们的节奏不同
层 | 来源 | 何时过期 |
| 游戏:API 用 | 游戏更新 |
| 配置文件声明的模组的存档,不解包直接读取 | 依赖更新,或声明的集合改变 |
| 模组自身的源码,在其所在位置读取 | 每次编辑 |
一次性构建的索引会在正确后一分钟内就出错:游戏一年动几次,依赖一个月动几次,而项目在智能体的一次回合和下一次之间就会变。因此每一层都独立构建、老化、测量,每次构建都是增量的——未变化的源码按大小和修改时间跳过,only=[path] 连发现它们的遍历都跳过。
答案携带其来源层的年龄
陈旧度是测量出来的,不是猜出来的:一层记录它读取的每个源码的大小和修改时间,然后与文件当前的状态进行比较。
每个答案都指明它使用了哪些层以及每层有多旧。没有结果的答案指明它搜索过的每一层——“未找到”的价值恰好等于其背后各层的时效性。
项目层的时效性在每次搜索时都会测量,无论它是否贡献了结果。那是危险的情况:智能体添加了一个类,询问它,而一分钟前构建的层说“未找到”——这是对已存在代码的一个自信陈述。
对从未构建过的层的搜索会被拒绝,拒绝信息会指明构建它的调用。“未找到”和“未查找”是不同的事实,其中只有一个可以安全地据此行动。
收窄范围在下一层带有同样的陷阱,因此空的收窄答案会报告该名称确实存在的位置:对游戏仅在配置中声明的名称询问
kind='class',会得到一个真实的“否”,读起来像是“游戏没有这样的类”。
配置类位于 kind='config' 下,而不是 kind='class' 下。在本机器自己的游戏索引中统计:88 102 个配置类,对比 43 595 个各种脚本声明加在一起。如果混入同一个种类,它们会淹没所有脚本答案。分开后,“游戏是否有名为 X 的物品类”就是一个可以精确提出的问题。
它不回答什么
索引回答存在什么:类、方法、签名、在哪里声明、谁覆盖。它不回答什么是对的——modded class X extends X 能编译但静默地不生效,_co 会消耗 alpha 通道,binarize 接受目录而不是文件。这些都不是能从源码推导出来的;它们是靠惨痛教训学到的,存在于模组编写技能和模组本身中。索引不试图取代这两者,也不试图理解字段的含义或类存在的原因。
语义搜索刻意不在这里
这个决定是通过测量而非谨慎做出的:塑造本服务器早期阶段的每一次查找都是按名称的查找。而嵌入索引会打破本服务器其余部分遵守的规则——安装即可用,没有外部服务、没有密钥。本项目刻意不基于其构建的前身项目,将其知识层描述为本地且免费,而其代码却导入付费的嵌入客户端,没有密钥就失败,并带有硬编码的价格。它的两个搜索工具还会永远挂起,因为背后的客户端创建时没有超时;因此这里的每次搜索都在一个上限下运行。如果精确搜索被证明不够用,语义搜索是单独的一个阶段,有一个条件:模型随交付物一起内置。
实测数字
在这台机器上——游戏有 2810 个脚本文件、35 个已安装模组、一个 41 个源文件的真实项目——通过工具而非其内部机制测量:
构建 | 结果 | 时间 |
| 2927 个源文件,131 697 个声明(41 个无产出) | 70.2 秒 |
| 8 个存档,10 925 个声明 | 0.9 秒 |
| 523 个存档,204 768 个声明,3 个存档不可读且已点名 | 139–147 秒 |
| 41 个源文件,1196 个声明 | 0.12 秒 |
磁盘上的索引:真实项目三层的 74.7 MB;仅 523 个依赖存档就 110 MB。这些存档共 92 GB,没有一个被解包。
调用点(call site)才是索引真正花钱的地方。 游戏自身的脚本包含 43 579 个声明和 113 703 个调用点,记录第二组数据大约使索引体积翻倍:仅就游戏层本身测量,不记录调用点时构建需要 23.8 MB 和 3.7 秒,记录后则需要 49.6 MB 和 4.5 秒。这就是能够回答"谁调用了这个"所付出的代价,在这里明确写出来,而不是等到磁盘满了之后才发现。
答案 | 时间 |
| 端到端 4.2 ms,其中 3.0 ms 是项目遍历 |
| 查询 3.2 ms |
| 4.2 ms |
| 查询 0.38 ms |
| 文本检查 277 ms,索引检查 7 ms |
在实机上测量,三次启动:world_time_set(hour=3, minute=7) 将时钟拨到 2026-09-20 03:07 并保持日期不变;world_weather_set("fog", 0.9, seconds=2) 将已发布的雾浓度从 0.085 提升到 0.900 并保持;world_entities(pos="7500 0 7500", radius=150, limit=5) 列出了 171 个对象中的 5 个,并带有 truncated: true。距离在改为水平测量之前,150 m 半径返回的是 320 m——而水平测量正是引擎自身半径测试所采用的方式。
| 6.8 ms |
| 41 ms(构建后首次调用为 110 ms) |
在真实项目上的增量性能:完整重建 136 ms;遍历发现一个被编辑的文件 8.8 ms(15×);同一个文件通过 only= 指定 5.8 ms(23×)。在 2810 个文件的树上,遍历占主导地位,only= 的价值远不止于此——但在这种规模的项目上,15× 才是普通重建实际能获得的提升。
上限确实会生效:一个测得 77 ms 的查询,在 19.3 ms 的上限下运行,在 19.9 ms 时被停止,连接继续正常应答。
资源管线
把模型从 Blender 弄进一个 mod 需要十个步骤,而在这个阶段之前,所有这些步骤都是手工完成的。价值不在于启动这些工具,而在于这条链中的每个工具在结构上都无法报告失败,而每一次这种沉默都已经付出了数天的代价。
在真实二进制上测量,而非假设:
发生了什么 | 工具返回了什么 |
| 0,一个空的输出目录,没有任何一行文本 |
| 0,一个 46,190 字节的 ODOL,而正确构建应为 58,644 |
|
|
Blender 导出器使用其自身的默认参数 |
|
因此,整个命名空间所依据的规则是:结论从产物本身读取,绝不从工具的报告读取。 退出码会被记录,但两个方向都不予采信。
根目录是声明的,而非假设的
binarize 根本没有项目根目录选项——完整的开关列表是针对真实二进制逐一枚举出来的。根目录就是进程的工作目录。同样的命令、同样的输入、不同的目录,输出的是一个有效的 ODOL,带有看似合理的纹理路径,但引擎渲染出来却没有纹理,而且返回成功码、毫无怨言。导出的 Blender 插件在自己的偏好设置里也有同样的根目录,记住的是上次打开的项目:在开发这台机器时,它指向的是某个无关会话的目录,而面对错误的根目录,插件同样不会失败——它会去掉盘符,保留其余部分,写出看起来像路径的路径。
build.project_root 就是那个目录,在配置文件的便携半部分声明一次。服务器将其设置为二进制化器的工作目录,并在运行期间推送给插件,因此插件存储的内容不决定任何事(它会被报告出来,这样你可以去修复)。这就是让错误根目录不可能发生而非"可检测"的原因,也是为什么在运行任何与模型相关的东西之前,这个键是必需的。
它产生的拒绝发生在进程存在之前——测得 0.0003 秒——而被拒绝的构建会让 mod 已经随附的模型逐字节保持原样不动。
对产物进行十二项检查,其中四项会拒绝
asset_check 在不构建任何东西、也不需要 DayZ Tools 的情况下运行这些检查,因为一个全新的克隆必须能够询问它所发布的内容是否健康。四项会拒绝:构建的模型存在且是 ODOL(C1)、没有引用逃出 mod(C3)、材料确实被内联了(C4)、没有把已经二进制化的东西再交回给 binarize(C10)。其余的是警告:悬空引用、指向另一个 mod 的 rvmat、因 DXT1 而丢失的透明度(C7)、从未到达产物的动画、不是产物构建所依据的那个 model.cfg、与上次构建部署的结构指纹不再匹配。每条发现都会说明该怎么做。
C4 是最值得了解的。当 binarize 解析 rvmat 时,它会将该材料自身的阶段纹理复制到模型中——fresnel、#(argb,8,8,3)、env_land_co.paa、_nohq、_smdi——这些字符串是任何 MLOD 都不包含的。六个产物中有六个被这一项测试正确区分开来,而且它在这台机器上发现了一个没人知道的损坏模型。
Blender 步骤是可选的
asset_export 是这里唯一需要 Blender 的工具,而下游的一切都适用于来自任何地方的 .p3d——手工导出、合作伙伴的文件、多年前提交的模型。一台没有 Blender 的机器也能完美地构建和发布 mod;拒绝会明确说明这一点,而不是把它呈现为安装损坏。它确实需要在其找到的 Blender 中启用导出插件,而且它从不写回 Blender 的用户偏好设置(已验证:每次运行后偏好设置文件都逐字节相同)。
导出和构建是两个调用而非一个,因为每一半都有自己的结论,而一个被一方拒绝、被另一方允许的构建不是一个决定。
从不承诺字节相等
这条管线的两半都不可复现,设计上明确说明了这一点,而不是假装可以:
导出。 对一个未更改的源文件进行七次导出——三次来自同一会话,三次来自另一会话,一次是几个月前在 GUI 中手工完成的——在恒定的 334,032 字节下给出了七个不同的 SHA-256。差异在于一个内部块的顺序。
binarize。 对一个未更改的输入运行四次,得到三个不同的结果:大小移动了 5 字节,两个 8 字节片段从压缩区域泄漏出来。
因此,模型永远不会按内容哈希缓存或比较。被比较的是结构指纹——文件的种类、LOD 数量及其名称集合。在那七次导出中,这个指纹始终是同一个值。
实测数据
一个小模型,在这台机器上,经过这些工具:
步骤 | 结果 | 时间 |
| MLOD,334,032 B,5 个 LOD,干净 | 2.1 秒(冷启动约 8 秒) |
| ODOL v55,58,646 B,4 个 LOD,全部五个 C4 标记 | 43.8 秒(此前四次运行测得 75.6–78.7 秒) |
| 1 个模型和 10 对纹理被判定 | 毫秒级 |
| 一个 PNG 转 DXT1,50,764 B | 0.52 秒 |
错误根目录上的拒绝 | 在任何进程启动之前 | 0.0003 秒 |
两份日志几乎全是套话,被静默的内容是被计数而非丢弃:Blender 的 169 行缩减为 4 行,binarize 的 91 行缩减为 6 行——其中一行是模型唯一真正的抱怨。
端到端串联起来,导出和构建复现了一个几个月前手工制作的模型:同样的种类、同样的 4 个 LOD、同样的 50 个字符串,大小只差一个字节。
这些都无法回答的问题
模型看起来是否正确、缩放是否正确、绕序是否正确、是否有碰撞。游戏之外没有任何东西能回答这些。C1–C12 缩短了通往答案的路;它们不能替代答案。
已知限制
过时 pbo 检测基于修改时间,而非内容。
mod_build会拒绝打包比其源文件更新的 pbo——通常原因是服务器仍在运行并持有旧文件,导致打包静默地没有产生任何结果。但git checkout会改变文件的修改时间而不改变内容,因此在切换分支后立即构建的 pbo 即使完全正常也可能触发此检查。如果mod_build在切换分支后立即报告"过时 pbo",很可能就是这个原因,而不是真正的打包失败——重新构建即可通过。该领域的成熟工具正是因为这个原因改用了内容哈希;这是未来的工作,本阶段未完成(参见packer.py、pack_one)。模组源文件夹是整体打包的。 如果模组的源目录包含任何匹配
build.exclude的内容(默认的七模式列表见上文),mod_build会拒绝打包,而不是将其静默地包含在发布的 pbo 中。当源目录包含该服务器自身的产物时——签名密钥、配置文件的任一半、任务存储、模组之前的构建——无论build.exclude如何设置,它都会拒绝,因为打包这些内容会发布私有的签名密钥,任何项目都不应该需要配置来排除这些。默认情况下,它不会先暂存过滤后的副本:副本总是比源文件新,如果检查的是副本,这将永久禁用上述的过时 pbo 检查。build.stage = true选择无论如何都进行复制——之所以安全,仅仅是因为过时 pbo 比较总是测量原始源树,而不是副本。这是源目录为仓库根目录的模组所需的布局(它总是至少包含.git)。判定会检查整个日志,而不仅仅是模组的行。
log_verdict读取其指向的服务器实例的日志,因此与其他模组共享的服务器实例会将其警告计入你的expect.max_warnings预算,并将其错误视为原因。两个项目共享一个machine.stand_root会看到彼此的基线。要么给每个项目分配自己的服务器实例,要么在知道还加载了什么的情况下设置预算。与expect.error_regex对称的项目级过滤器是明显的改进方向,但尚未实现。expect.noise无法挽救已经算作错误的行。 分类顺序为forbid→ 崩溃 → 错误 → noise → 警告,因此包含ERROR或FATAL(或你的forbid字符串之一)的行在咨询 noise 之前就已经被决定了。这个顺序是刻意的——如果 noise 先匹配,一个无害的子串可能会吞掉致命行——但这意味着noise只能抑制警告和普通行,而不能降级错误级别的行。client_verdict是错误和崩溃判定,而非就绪判定。[expect]描述的是服务器的日志:其就绪行和计数器由模组的服务器端初始化打印,max_warnings是在同一日志上计算的预算。客户端.RPT不包含这些内容,因此这三个键在此处刻意不适用,答案在not_applied中列出。forbid、error_regex和noise与日志行的文本相关,仍然适用。没有客户端就绪行可声明;客户端是否进入由client_start等待的玩家数量来回答,而不是其日志。客户端工具加入的是本会话未启动的服务器实例;
client_chat不能。client_start会愉快地连接到端口上已有的任何内容,并说明其归属。但聊天是通过与world_*工具相同的通道在服务器端传递的,而该通道仅作用于本会话启动的服务器——因此,在借用的服务器实例上,除client_chat之外的所有功能都可用。拒绝时会指出占用端口的进程 ID,而不是建议一个会拒绝它们的server_start。聊天无法从游戏手柄访问,确认菜单也不行。 游戏将聊天行绑定到回车键,没有其他方式,也没有屏幕键盘,因此文本要么是桥接消息(
client_chat,免费),要么是真实的按键(client_type,需要前台)。client_type("", submit=True)单独发送回车键,这就是打开聊天行的方式——根据上述证据,这也是该工具集唯一的确认方式。每次知识搜索都会遍历项目树。 每次回答时,项目层的过期检测都是通过这种遍历来衡量的,这是索引存在的唯一目的。在一个真实的 41 源文件模组(其树约有 1800 个条目)上,这需要 3.0 毫秒;在 2810 个文件的树上需要 21 毫秒。将其缓存一两秒将消除成本,并恢复设计所拒绝的静默窗口;如果它变得过于昂贵,这个权衡必须是有意做出的,而不是偶然的。
构建总是通过任务进行,而任务的开销大于小型构建本身。 实测周转时间为 70-95 毫秒,而项目重建仅需 6 毫秒:
job_wait以 100 毫秒轮询。单一形态是刻意的——调用者不必知道哪些构建块——而且没有什么强制你等待,因为下一次搜索会自己测量该层。依赖层是针对当前状态的配置文件进行测量的。 向
mods.required添加模组,其存档会以added形式出现;移除模组,其存档会显示为missing。这是符合预期的(声明的集合是构建该层的一部分),但这看起来像是索引过期了,而实际改变的是配置文件。core始终包含游戏的配置文件,这是其大部分成本所在。 包含配置文件需要 70 秒,而仅脚本需要约 4 秒。没有开关:没有配置文件,"是否存在名为 X 的物品类"就无法回答,而第二个轴会使过期检测变得模糊——遍历将不知道是否期望Addons存档。knowledge_show优先显示最近的层。 对于依赖项使用modded class重新打开的类,模组的声明在游戏的声明之前。这是正确的顺序,但也是令人惊讶的;传递layer='core'以获取游戏自身的声明。条件编译被索引,但不被解析。 游戏中 4.9% 的脚本行位于
#ifdef内,包括大约一百个类声明。该服务器驱动服务器、客户端和诊断构建,因此不存在单一正确的宏定义集:所有内容都被索引,守卫记录在声明上。因此,可能会报告特定构建排除的名称——另一种选择是根据对宏定义的猜测进行过滤,这会否认正在运行的构建中存在的方法。C12 的指纹包含文件大小,而
binarize的大小不稳定。 重新构建一个没有人编辑过的模型,生成的产物比发布的版本大一个字节,具有相同的类型、相同的 LOD 数量和相同的五十个字符串——但摘要不同,因为大小是其中的一部分。因此,C12 可能会对没有改变任何内容的重新构建发出警告。正是因为这个原因,它只是警告而不是拒绝,并且其构建部分会显示在旁边,以便手动进行比较。将摘要拆分为稳定部分和大小是明显的改进方向,但尚未完成。部分导出会警告,但不会拒绝。 使用导出器自身的默认参数,模型导出了 5 个 LOD 中的 2 个,并通过了所有其他检查。该服务器不传递这些参数,因此不应该发生这种情况——但标记为 LOD 且未链接到场景中的对象只计算在比较的一侧,这是数量差异的合理原因,因此拒绝会产生误报。请阅读 E3。
包含规则无法发现所有错误的根目录。 它拒绝不包含模组前缀文件夹的根目录,这是已测量的失败。一个确实包含该名称文件夹的根目录——例如,其自身模组目录拼写与前缀相同的仓库——会通过检查,而捕获这种情况的是下一层的 C10 或 C3/C4。已测量:指向这样的根目录时,构建拒绝,不部署任何内容,并保持已发布的产物不变,但拒绝来自任务而不是调用。
asset_export需要在 Blender 中启用导出插件,并且无法自行安装。 Blender 以机器所有者的真实首选项启动,因为使用--factory-startup启动会完全移除插件。他们的其他插件在运行时被排除在搜索路径之外(此处安装的两个插件在启动时访问网络并被归咎于崩溃),Blender 在日志中报告为"插件未加载"——该行是此服务器自身的行为,不是故障。二进制的配置文件没有可显示的正文。
knowledge_show(body=True)从索引时所在的文件或存档中读回声明,但config.bin保存的是二进制形式,而索引保存的是CfgConvert处理后的结果。答案会说明这一点,而不是返回空值。
安装
python -m pip install -e ".[dev]"
python -m pytest在您的 MCP 客户端中注册:
{ "mcpServers": { "dayz": { "command": "dayz-mcp" } } }许可证
GPL-3.0-or-later。参见 NOTICE.md。
Maintenance
Related MCP Servers
- AlicenseNot gradedqualityNot gradedmaintenanceAn MCP server for Arma Reforger and Enfusion engine modding that enables users to create mods, search API classes, and generate scripts through natural language. It provides a comprehensive suite of tools for scaffolding addons, generating prefabs, and building projects using the Workbench CLI.1,74214
- AlicenseAqualityCmaintenanceMCP server for DayZ Enforce Script that gives AI coding assistants deep knowledge of the DayZ scripting API with semantic search, code validation, class hierarchy, and reverse call graphs.81MIT
- AlicenseAqualityAmaintenanceAn MCP server that empowers AI coding agents to work effectively with Minecraft mod development, providing static analysis of decompiled source code and runtime interaction with a running Minecraft instance.313913MIT
- FlicenseNot gradedqualityCmaintenanceMCP server that helps AI agents inspect Minecraft project evidence (crash logs, mod files, datapacks) before writing development code.3
Related MCP Connectors
An MCP server that gives your AI access to the source code and docs of all public github repos
A MCP server built for developers enabling Git based project management with project and personal…
Augments MCP Server - A comprehensive framework documentation provider for Claude Code
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/covalschi/dayz-agentic-modding-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server