Skip to main content
Glama
covalschi

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.pbobuild.sources 可以将模组的源重定向到其他地方(例如,对于 config.cpp 位于仓库根目录本身的模组,使用 ".")。

可移植部分 — dayz-mcp.toml

含义

project.name

这个项目叫什么

build.mods

每个模组一个条目,按名称;源、pbo 和 @folder 都由此得出

build.sources

模组源实际所在位置,相对于配置文件("." = 仓库根目录本身)

build.exclude

绝不能打包的内容。列出它会完全替换默认值;默认值是 .git*.blend*.blend1.gitignore.gitattributesREADME.md*.ps1

build.stage

当存在被排除的内容时,打包一个过滤后的副本,而不是拒绝——这是根目录布局模组所需的布局

build.project_root

所有模型路径解析所依据的目录,相对于此文件。它必须包含模组的前缀文件夹。仅模型工具需要,其他都不需要——参见“资源管线”

build.pre_script

打包前运行的 PowerShell 脚本,用于先生成代码的项目

expect.ready_line

模组完成加载时打印的行——见下文

expect.counters

该行携带的 key = value 对,按数值比较

expect.max_warnings

警告预算;省略该键以禁用检查

expect.forbid

无论其他情况如何,只要出现这些子串就使运行失败

expect.error_regex

标记属于你的脚本错误的正则表达式,用于客户端编译检查

expect.noise

除了内置列表之外,要忽略的额外引擎噪音

机器部分 — dayz-mcp.local.toml,绝不提交

含义

machine.game

游戏安装;如果缺失则自动发现

machine.tools

DayZ 工具;如果缺失则自动发现

machine.blender

Blender 可执行文件,仅用于 asset_export;如果缺失则自动发现,其他都不需要

machine.stand_root

准备好的测试台。服务器针对它启动,其日志从 <stand_root>/profiles 读取。默认为 <root>/testenv

machine.config

测试台内的服务器配置文件名称(默认 serverDZ.cfg)。这是一个设置,因为测试台可能包含一个在世界编译后永远挂起的配置,以及另一个名称下可用的配置;它必须在 stand_root 内解析

machine.port

server_start 传递给服务器的端口(默认 2302

mods.required

模组文件夹名称,在游戏自己的 !Workshop 文件夹下解析——required = ["@CF"] 表示 <game>/!Workshop/@CF

mods.extra

要加载的其他任何内容的完整路径,用于不在 !Workshop 中的模组

mods.server_only

要路由到 -serverMod 而不是 -mod 的文件夹名称。按文件夹名称与所有正在加载的模组匹配,无论其来源如何——mods.requiredmods.extra 或项目自己的 @Name 文件夹;未列出的所有内容都进入 -mod。诊断客户端从不加载仅服务器模组,因此客户端编译检查会丢弃它们

就绪行

server_start 在启动开始后写入的日志中出现 expect.ready_line 时完成。这是服务器无法自行推断的唯一一件事,没有它,两件事会改变:无法等待任何内容,因此启动作业会启动服务器,确认它片刻后仍然存活,然后完成并说明这一点;并且 log_verdict 没有行可读取计数器,因此 expect.counters 永远不会匹配。错误、崩溃和警告预算仍然会被判断。没有就绪行的配置文件是受支持的,而不是损坏的——project_open 会在其注释中说明这一点。

Related MCP server: DayZ API MCP Server

工具

工具

功能

project_open(path)

读取配置文件,发现游戏和工具,报告缺失项

project_status()

当前项目、运行中的服务器、最近的作业

mod_build()

打包并签名所有声明的模组;返回作业 ID。同一项目已有构建在运行时拒绝第二次构建

server_start(timeout)

启动测试服务器,就绪后完成。如果配置指定的任务不在 <game>/mpmissions 下则拒绝 — 引擎会在正在运行的可执行文件旁边查找任务,而不是在 -config 旁边。立即返回 pid — 进程在调用返回前就已生成,因此下一个工具立即就能看到运行中的服务器。如果游戏端口已被他人占用则拒绝,如果镜像无法启动则当场拒绝。就绪状态来自 expect.ready_line(若声明),否则来自两个引擎信号同时满足 — 端口已绑定 AND 任务模块已编译。端口比脚本早约 17 秒绑定,在此期间被标记为就绪的启动正在监听但没有任务:它响应查询并拒绝所有玩家。作业摘要指明哪个

server_status(pulse_seconds)

pid、进程是否存活、日志是否在增长(间隔 pulse_seconds 采样)、以及已停滞多久

server_stop(pid)

停止本会话启动的服务器(孤儿服务器的可选 pid)

server_signatures(value)

读取 — 或有意更改 — 测试台的 verifySignatures。无参数时仅报告。它编辑配置文件指定的该测试台的配置,拒绝解析到 machine.stand_root 之外的配置,拒绝在服务器正在运行时编辑,保留文件的注释和行尾,并在之后从文件中读回该值

client_compile_check(extra_mods, wait_seconds)

运行诊断客户端并读取其日志

log_verdict(source, since)

通过/失败及原因:计数器、禁止字符串、警告预算。source"server"(测试台中最新的日志)或 "client"(最近一次 client_compile_check 产生的日志)。since(纪元时间戳,例如 server_start 返回的 since)拒绝在被评判的运行之前写入的日志,因此早期启动的过期日志不会被误认为本次运行的结果

log_tail(source, pattern, n)

最后几行,可选过滤;同上述两个来源

job_status(job_id)

长时间运行作业的状态

job_wait(job_id, timeout)

等待作业完成

job_artifacts(job_id)

从已完成的作业中检索输出

bridge_build()

打包桥接模组,其源码随此服务器(bridge/)一起提供,而非随你的项目;返回作业 ID。构建为未签名 — 见下文

bridge_status(window)

运行中的游戏内的桥接是否仍在跳动:报告跳动编号以及是否在 window 秒内前进。仅对实际移动过的跳动成功,或对采样中途重启的测试台成功

bridge_clear(force, probe_window)

丢弃卡在邮箱中的命令,并说明丢弃了什么。除非 force=True,否则在桥接看起来存活时拒绝

world_ready(timeout)

等待游戏内的桥接实际认领命令。在启动作业完成后、第一个世界命令之前调用一次 — 见下文"服务器就绪不等于桥接就绪"

world_state(class_name, radius, pos)

来自桥接每秒一次发布的世界的快照:玩家、位置、生命值、双手。无参数时免费;带 class_name 时还会统计附近该类的对象数量(一次命令往返)

world_spawn(class_name, where, pos, quantity)

在地面(无生命周期,因此不会在检查中途消失)、玩家手中或玩家背包中创建物品

world_teleport(pos)

将玩家移动到 "x y z" — 与 world_state 报告的格式相同,因此读取的位置可以直接传回

world_set(what, value, target)

设置 health(玩家或手持物品)或 quantity(手持物品)

world_delete(class_name, radius, pos)

删除附近某一类的对象。需要类名;绝不删除真实玩家

world_entities(class_name, radius, pos, limit)

附近哪些对象,而非多少:每个对象的类、位置、距离和生命值。一页,并且会说明 — 真实总数随列表一起返回

world_time_set(hour, minute, day, month, year)

移动世界时钟。每个保持 -1 的字段保留其当前值,先从引擎读回,因为引擎将日期一次设置为五个数字

world_weather_set(what, value, seconds, duration)

移动阴天、雨、雾、雪或风。轻推而非锁定:引擎之后会继续模拟天气,工具和模组都会说明这一点

world_action(action_class, target_class, subject, radius, pos)

通过引擎的门运行模组自己的动作 — 见下文

world_exec(verb, args)

逃生舱口:通过同一传输通道的任意动词,在每个回答中标记为非标准

client_start(timeout, extra_args)

启动游戏客户端并将其连接到测试台;返回作业 ID。始终窗口化。当桥接报告 players >= 1 时完成 — 是计数,不是计时器

| 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 工具施加的三个限制,均未绕过: 普通 TextWidgetenwidgets.c 中有 SetText没有 GetText,因此标签的字符串完全无法读取——mod 界面的含义只能留给服务端桥接器来回答,那里的数据才是真实的。脚本级点击只能到达已打开的脚本化菜单,因为 WidgetSetHandler 而没有 GetHandlervia="cursor" 用于其他一切情况。此外,客户端必须加载桥接器:一个 pbo 同时携带两半,因此配置文件将其列在 mods.server_only 下会使其远离客户端的 -mod 行——这种情况会按名称被拒绝,而不是返回空树。

job_wait 是用于等待的工具,其 timeout 上限为 600 秒,无论传入多大的值。另外两个工具会休眠:server_status 采样日志两次,间隔 pulse_seconds,上限 10 秒——这个停顿就是它区分慢启动与挂起的方式——bridge_status 采样桥接器的 tick 两次,间隔 window,上限同为 10 秒,原因相同。其他所有工具立即返回;需要数分钟的工作在任务 id 背后进行。

桥接器 mod

bridge_buildbridge/ 从本服务器自身的仓库打包到其旁边的 @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_clearserver_start 的启动前清理会清空它。因此,在 stand 关闭时或桥接器附加之前发送的命令不会被丢弃,也不会自行过期——它会持续阻塞后续的每次发送,而通过这些工具之外启动的 stand 会拾取它。server_start 在每次启动前都会清空两个传输文件,因此通过此工具启动的服务器永远不会运行来自先前会话的命令;这是卫生习惯,而非了解命令存在的替代品。该状态以 stale_command 返回,而 bridge_clear() 是摆脱它的方法。清空是有意作为独立工具的:丢弃排队的命令是一个决定,而不是状态检查应该在背后替你做的事情。当桥接器看起来还活着时它会拒绝,除非你传入 force=True,无论哪种方式它都会报告它丢弃的命令 id。

bridge_status 能区分什么

仅凭 tick 不足以判断桥接器,因为它每次启动都会从 0 重新开始,而状态文件在配置文件目录中持续存在。每个答案都在 heartbeat 中携带通道自身的判定,而这四种是真正不同的事实:

state

heartbeat

含义

alive

growing

tick 在同一会话内移动——唯一 ok: true 的存活答案

restarted

restarted

两次采样之间出现了新世界:存活,冻结,且发送给旧会话的任何内容都已消失

frozen

stalled

同一世界被看到两次且未移动——脚本侧问题,因此 log_verdict 是下一步

unknown

unmeasurable

无法读取采样(或 window=0):未进行比较。不是诊断——请再次调用

每个读取过采样的答案还携带 session_id——当前存活世界的 id——而 restarted 答案还携带 previous_session_id,因此调用方可以说出哪个世界消失了。

no_serverstale_commandno_state_fileinvalid_stateunreadable_stateoutdated_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_MissionKnownVerbs() 上方的注释精确指出了位置);故意不设注册机制——一个由本服务器输入并验证的动词,就是一个由本服务器负责的动词。

客户端:三个输入层,以及为什么是三个

桥接层到达服务器。它做不到的是查看客户端的屏幕或通过客户端行动——让角色走过地面、打开菜单、填写模组绘制的字段。client_* 工具就是干这个的,它们使用三条不同的通道,因为没有哪一条能完成另外两条的工作。下面每一行都是对照真实客户端的测量结果,而不是设计意图。

通道

它做什么

需要前台

桥接层(world_*client_chat

世界,以及向聊天发送文本

虚拟手柄,ViGEmBus(client_move / look / press

移动、镜头,以及部分界面

真实按键,SendInputclient_type

向仅存在于客户端上的字段输入文本

是,而且会占用它

键盘模拟不会移动角色,窗口消息则完全无效。 已验证前台的 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_startclient_status读取该设置并发出警告;它们从不写入它,因为它属于机器的所有者。

client_type 是唯一会占用屏幕的工具,而且它对此很诚实:答案带有 foreground_taken 和一句话,说明坐在机器前的人在它运行期间无法在自己的窗口里输入。它在请求前台后用 GetForegroundWindow 验证前台,因为当 Windows 拒绝时,SetForegroundWindow 会返回成功但实际上什么都没做——而盲打会把按键发送到那个人实际正在使用的任何窗口。当无法获得前台时,什么都不会输入,拒绝信息会指出持有它的进程。

ViGEm 是对真实设备的模拟,而这是一个测试台。 驱动已签名,无需重启即可安装,手柄是一个新的设备,而不是对机器自身键盘和鼠标的过滤——这里曾尝试过一次过滤驱动,结果让机器所有者失去了所有键盘和鼠标输入,直到手动解除。这些都不是对真实服务器上反作弊的承诺,本阶段也不会做出任何承诺。

知识索引

编写模组的智能体不断向游戏提出同样的问题:是否有这样的 API,它叫什么,在哪里声明,谁覆盖了它。回答这些问题意味着解包 scripts.pbo 并扫描文本——而每次会话都要重新付出代价。knowledge_* 把这项工作变成了一个问题。

它是项目自己的 .dayz-mcp/ 目录中的一个普通 SQLite 文件,由本服务器从游戏、项目声明的模组以及项目自身的源码构建。没有嵌入、没有外部服务、没有密钥。

三层,以及为什么它们的节奏不同

来源

何时过期

core

游戏:API 用 dta/scripts.pbo,物品类用 Addons/*.pbo

游戏更新

deps

配置文件声明的模组的存档,不解包直接读取

依赖更新,或声明的集合改变

project

模组自身的源码,在其所在位置读取

每次编辑

一次性构建的索引会在正确后一分钟内就出错:游戏一年动几次,依赖一个月动几次,而项目在智能体的一次回合和下一次之间就会变。因此每一层都独立构建、老化、测量,每次构建都是增量的——未变化的源码按大小和修改时间跳过,only=[path] 连发现它们的遍历都跳过。

答案携带其来源层的年龄

陈旧度是测量出来的,不是猜出来的:一层记录它读取的每个源码的大小和修改时间,然后与文件当前的状态进行比较。

  • 每个答案都指明它使用了哪些层以及每层有多旧。没有结果的答案指明它搜索过的每一层——“未找到”的价值恰好等于其背后各层的时效性。

  • 项目层的时效性在每次搜索时都会测量,无论它是否贡献了结果。那是危险的情况:智能体添加了一个类,询问它,而一分钟前构建的层说“未找到”——这是对已存在代码的一个自信陈述。

  • 对从未构建过的层的搜索会被拒绝,拒绝信息会指明构建它的调用。“未找到”和“未查找”是不同的事实,其中只有一个可以安全地据此行动。

  • 收窄范围在下一层带有同样的陷阱,因此空的收窄答案会报告该名称确实存在的位置:对游戏仅在配置中声明的名称询问 kind='class',会得到一个真实的“否”,读起来像是“游戏没有这样的类”。

配置类位于 kind='config' 下,而不是 kind='class' 下。在本机器自己的游戏索引中统计:88 102 个配置类,对比 43 595 个各种脚本声明加在一起。如果混入同一个种类,它们会淹没所有脚本答案。分开后,“游戏是否有名为 X 的物品类”就是一个可以精确提出的问题。

它不回答什么

索引回答存在什么:类、方法、签名、在哪里声明、谁覆盖。它不回答什么是对的——modded class X extends X 能编译但静默地不生效,_co 会消耗 alpha 通道,binarize 接受目录而不是文件。这些都不是能从源码推导出来的;它们是靠惨痛教训学到的,存在于模组编写技能和模组本身中。索引不试图取代这两者,也不试图理解字段的含义或类存在的原因。

语义搜索刻意不在这里

这个决定是通过测量而非谨慎做出的:塑造本服务器早期阶段的每一次查找都是按名称的查找。而嵌入索引会打破本服务器其余部分遵守的规则——安装即可用,没有外部服务、没有密钥。本项目刻意不基于其构建的前身项目,将其知识层描述为本地且免费,而其代码却导入付费的嵌入客户端,没有密钥就失败,并带有硬编码的价格。它的两个搜索工具还会永远挂起,因为背后的客户端创建时没有超时;因此这里的每次搜索都在一个上限下运行。如果精确搜索被证明不够用,语义搜索是单独的一个阶段,有一个条件:模型随交付物一起内置。

实测数字

在这台机器上——游戏有 2810 个脚本文件、35 个已安装模组、一个 41 个源文件的真实项目——通过工具而非其内部机制测量:

构建

结果

时间

core

2927 个源文件,131 697 个声明(41 个无产出)

70.2 秒

deps,四个声明的模组

8 个存档,10 925 个声明

0.9 秒

deps,此处安装的所有模组

523 个存档,204 768 个声明,3 个存档不可读且已点名

139–147 秒

project

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 秒。这就是能够回答"谁调用了这个"所付出的代价,在这里明确写出来,而不是等到磁盘满了之后才发现。

答案

时间

knowledge_find,精确名称

端到端 4.2 ms,其中 3.0 ms 是项目遍历

knowledge_find,前缀,限制 500

查询 3.2 ms

knowledge_overrides

4.2 ms

knowledge_callers,113 703 个调用点中的 23 个

查询 0.38 ms

mod_lint,针对一个 76 文件的 mod

文本检查 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——而水平测量正是引擎自身半径测试所采用的方式。

knowledge_show,一个含 400 个成员及其祖先链的类

6.8 ms

knowledge_status,三层全部测量

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 需要十个步骤,而在这个阶段之前,所有这些步骤都是手工完成的。价值不在于启动这些工具,而在于这条链中的每个工具在结构上都无法报告失败,而每一次这种沉默都已经付出了数天的代价。

在真实二进制上测量,而非假设:

发生了什么

工具返回了什么

binarize 收到一个文件,而它期望的是一个目录

0,一个空的输出目录,没有任何一行文本

binarize 遇到一个加载失败的材料

0,一个 46,190 字节的 ODOL,而正确构建应为 58,644

binarize 收到一个已经二进制的模型

0xC0000005 以及输出目录中一个零长度文件,覆盖在原有内容之上

Blender 导出器使用其自身的默认参数

FINISHED,退出码 0,一个有效的 MLOD,只携带了模型 5 个 LOD 中的 2 个,169 行日志中对此只字未提

因此,整个命名空间所依据的规则是:结论从产物本身读取,绝不从工具的报告读取。 退出码会被记录,但两个方向都不予采信。

根目录是声明的,而非假设的

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 数量及其名称集合。在那七次导出中,这个指纹始终是同一个值

实测数据

一个小模型,在这台机器上,经过这些工具:

步骤

结果

时间

asset_export

MLOD,334,032 B,5 个 LOD,干净

2.1 秒(冷启动约 8 秒)

asset_build

ODOL v55,58,646 B,4 个 LOD,全部五个 C4 标记

43.8 秒(此前四次运行测得 75.6–78.7 秒)

asset_check

1 个模型和 10 对纹理被判定

毫秒级

asset_convert

一个 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.pypack_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 → 警告,因此包含 ERRORFATAL(或你的 forbid 字符串之一)的行在咨询 noise 之前就已经被决定了。这个顺序是刻意的——如果 noise 先匹配,一个无害的子串可能会吞掉致命行——但这意味着 noise 只能抑制警告和普通行,而不能降级错误级别的行。

  • client_verdict 是错误和崩溃判定,而非就绪判定。 [expect] 描述的是服务器的日志:其就绪行和计数器由模组的服务器端初始化打印,max_warnings 是在同一日志上计算的预算。客户端 .RPT 不包含这些内容,因此这三个键在此处刻意不适用,答案在 not_applied 中列出。forbiderror_regexnoise 与日志行的文本相关,仍然适用。没有客户端就绪行可声明;客户端是否进入由 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

Install Server
A
license - permissive license
B
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Related MCP Servers

View all related MCP servers

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

View all MCP Connectors

Latest Blog Posts

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