起初我以为这只是"把几个文件塞进 zip"。结果一路踩了 KeepPath 语义、绿灯配置、打包器五条硬规则、OpenWG 运行时、wotmod 冲突,以及一个加密 pyc 的坑。 这篇把全过程、每条规则的实测依据、以及最终成品完整写清楚,包括我判断错的地方。
TL;DR
- KeepPath 不是"只检查不安装",而是"安装,但卸载时不删除"。它保护多插件共享的库;把共享库塞进 EscapePath,用户卸载一个插件就会连带删掉别人的依赖。
- 绿灯不亮的真凶是配置里
logicSightedDisplay/logicSightedSound被关成false。这两个开关是"被点亮目标"的总闸,而绿灯判定恰好是"敌人在视线外但已被点亮"。 - 盒子打包器有五条硬规则,违反时静默跳过、不报错:正斜杠、res_mods 只能走 FileList、FileList 只能覆盖已有文件、wotmod 条目必须 STORED、wotmod 之间不能路径重叠。
- 最终包 611,613 B / 8 个条目,比初版小 10 倍(初版 88 条目 / 6.26 MB,其中 79 条注定装不进去)。
下载
| 文件 | 大小 | 用途 |
|---|---|---|
| 危险指示灯 2.4.1.1 安装包(.wbp) | 611 KB | 盒子里「安装本地插件」选它 |
| 完整发布包(.zip) | 630 KB | 安装包 + 发布说明 + 构建脚本 + 本文 Markdown |
SHA256(.wbp):
4AEBBFC36E8E12BEFC177A658F9B763A7473284D44E739CC22F7D706FCED6B7F
一、成品速览
| 项 | 值 |
|---|---|
| 文件名 | 危险指示灯_2.4.1.1.wbp |
| 大小 | 611,613 B |
| SHA256 | 4AEBBFC36E8E12BEFC177A658F9B763A7473284D44E739CC22F7D706FCED6B7F |
| 条目 | 8(EscapePath 2 / KeepPath 4 / FileList 0) |
| 适用 | WOT 国服 2.4.1.1 |
| 作者 | C.C.Nemo(原作 protanki / tv.protanki.enemyannouncer 3.4.08) |
包内结构:
wbpinfo
wbpimg.jpg
Files/tv.protanki.enemyannouncer_3.4.08.wotmod 345,972 EscapePath
Files/configs/protanki/enemyAnnouncer.json 478 EscapePath
Files/poliroid.modslistapi_1.8.0.wotmod 167,079 KeepPath
Files/izeberg.modssettingsapi_1.7.1.wotmod 162,958 KeepPath(已汉化)
Files/net.openwg.gameface_1.2.2.wotmod 48,445 KeepPath
Files/zzz.ccnemo.scaleform.compat_1.1.0.wotmod 50,117 KeepPath
效果:战斗界面右上角显示三色指示灯 —— 红(50 米内有敌人)、黄(视野外但能造成伤害)、绿(视线外但已被点亮),红灯附带距离倒计时。安装即用,不需另装任何依赖。
二、先修功能:绿灯为什么不亮
2.1 三种灯走两条不同的代码路径
官方说明写得很清楚:
红色 - 敌人在 50 米内。黄色 - 敌人不在视野内,但可以造成伤害。绿色 - 敌人在视线外被点亮。
红、黄走距离判定,绿走点亮(spotting)判定。这解释了为什么"黄红都正常,绿灯死活不亮"——不是显示问题,是判定条件永远不成立。
2.2 先把"包"排除掉
WOTBox\WBP 缓存里躺着五个历史版本(2.3.1.3 / 2.4.0.0 / 2.4.0.1 / 2.4.0.2 / 2.4.1.0),我把里面的 tv.protanki.enemyannouncer_3.4.08.wotmod 全部算了一遍哈希:
AD055A8EC1EB46BE6D0690A82FCB1D12 345,972 B ← 五份完全相同
插件本体自 2.3.1.3 起就没更新过,game.log 里也只有 Mod package ... loaded。所以问题不可能在包结构,只可能在配置或客户端行为。
2.3 配置在 2.4.1.0 那一版被改坏了
| 包标签 | 配置大小 | 特点 |
|---|---|---|
| 2.3.1.3 / 2.4.0.0 / 2.4.0.1 / 2.4.0.2 | 479 B | 旧结构,logicSightedDisplay/Sound = true |
| 2.4.1.0 | 702 B | 多出 viewMode、dock*、free* 九个键,且 logicSighted* = false |
把 479 B 那套配置换回去,绿灯立刻恢复。
再补一个观察:插件启动后会自己重写配置,写出来只有旧结构那 10 个键,viewMode / dockIdleLight / dockSize / dockSkin / freeFlow / freeIdleLight / dockMapPath / dockPosition 全部被丢弃——3.4.08 根本不认识它们。
所以新旧配置真正生效的差别只有两行:
- "logicSightedDisplay": false
- "logicSightedSound": false
+ "logicSightedDisplay": true
+ "logicSightedSound": true
2.4 结论
logicSightedDisplay / logicSightedSound 是"被点亮目标"的总开关。关掉后插件不再维护被点亮状态,绿灯条件永远不成立;红黄走距离判定,不受影响。
顺带一提:玩家在设置里关掉"点亮提示",绿灯同样会失效——插件设计如此,不是包的锅。
三、KeepPath 到底是什么
3.1 我一开始判断错了
.wbp 的 wbpinfo 分三段:EscapePath、KeepPath、FileList。我最初认为 KeepPath 只是"声明依赖、只检查存在",于是把 84 条依赖全搬进了 EscapePath——理由是"这样纯净客户端也能装上"。
这个结论是错的。
3.2 证据一:盒子自己的线上包怎么写
| 包 | EscapePath | KeepPath |
|---|---|---|
| 最小+实时落弹环 v3.1.13 | 本体 | modslistapi、modssettingsapi、gameface |
| 伤害统计 | 20 条(本体 + 配置 + 资源) | mod_mods_gui、gambiter.guiflash |
| 点亮协助提醒 | 本体 + 配置 | 三个共享库 |
共享库只出现在 KeepPath,从不在 EscapePath 里重复。如果 KeepPath 不安装,这些百万下载量的插件在干净客户端上全都该是坏的。
3.3 证据二:res_wbp 的双写与登记表
res_wbp\<版本>\ 里躺着一批带包 ID 前缀的文件(3A9C9625....zz4b.txt),同名真名文件同时存在于 res_mods\<版本>\——说明 FileList 是"写一份真名 + 留一份带 ID 的备份",备份用于卸载时还原被其他包覆盖过的公共文件。
res_wbp\list.xml 是安装登记表,res_wbp\filemap.xml 记录 SourceFile → <包ID>.文件名 的映射。
3.4 准确语义
| 段 | 安装 | 卸载 | 用途 |
|---|---|---|---|
| EscapePath | ✅ | 删除 | 插件独有文件 |
| KeepPath | ✅ | 保留 | 多插件共享的依赖 |
| FileList | ✅ | 删除,但备份可还原 | 覆盖客户端已有的 res 文件 |
我这次踩的反面案例:最初把 res_mods\... 的运行时全写进 KeepPath,一条都没装进去;后来又把共享库挪进 EscapePath,虽然能装,但用户卸载时会连带删掉 OpenWG 运行时、modslistapi、gameface,伤害统计 / 落弹环 / 点亮提醒随即全废。
这就是 KeepPath 存在的理由,也是为什么共享库不能重复塞进多个包——重复塞会触发下一节的冲突规则。
四、打包器的五条硬规则(全部实测)
黑盒最麻烦的地方:规则错了不报错,文件就是不出现。以下每条都是对照实验得出的。
4.1 声明路径必须用正斜杠
我第一版写 mods\configs\protanki\enemyAnnouncer.json(Windows 习惯),装完配置文件不见了;而同包的 mods\2.4.1.1\xxx.wotmod 却装进去了——因为扁平路径剥完前缀只剩文件名,两种斜杠都能对上。
官方包全部用正斜杠:
<File Name="mods/2.3.1.3/pruszko.dispersion_reticle_3.1.9.wotmod" />
<File Name="mods/configs/DMod/mod_MultiHitLog.json" />
对应 payload:
Files/pruszko.dispersion_reticle_3.1.9.wotmod
Files/configs/DMod/mod_MultiHitLog.json
改成正斜杠后,嵌套路径立刻装上了。
4.2 res_mods 内容不能进 EscapePath / KeepPath
我最初把 79 条 res_mods\... 路径写进 KeepPath,一条都没写进去,且无任何报错。
Escape/Keep 只认 mods/ 开头的路径;res_mods 内容只能走 FileList,且声明路径是相对 res_mods\<版本>\ 的:
<FileList>
<File Name="gui/flash/debugPanel.swf" />
</FileList>
落地到 res_mods\<版本>\gui\flash\debugPanel.swf。这也意味着 res_mods\mods\openwg_packages 这种非版本路径无法用 .wbp 表达——"把散装运行时打包进插件"这条路从一开始就走不通。
4.3 FileList 只能覆盖已有文件,不能新建
g_entitiesFactories.pyc 在 2.4.1.1 里已经不存在(2.4.1.0 起官方删除)。用 FileList 声明它会怎样?
实测:盒子把内容写进了 res_wbp\2.4.1.1\<包ID>.g_entitiesFactories.pyc,真名路径没有写。
对照官方包:debugPanel.swf、damageIndicator.dds、environment.xml、GUI.bnk 这些被覆盖的文件,客户端 res 里本来就有,所以真名 + 备份都有。
结论:FileList 是"覆盖"语义,不是"新建"语义。 新建文件只能靠 wotmod。
4.4 wotmod 包内条目必须是 STORED(zip method 0)
g_entitiesFactories.pyc 得走 wotmod,于是我打了个 822 B 的小包。启动报错:
Mod package '...zzz.ccnemo.scaleform.compat_1.0.0.wotmod' load error: compression not supported
两个坑:
ZipArchive+CompressionLevel::Optimal→ DEFLATE,不认;- 换
CompressionLevel::NoCompression→ 还是 DEFLATE(方法字段仍是 8,只是流里存未压缩块),照样不认。
必须生成真正的 STORED(method 0)。.NET 的 ZipArchive 做不到,最后用 Python:
import zipfile
with zipfile.ZipFile(out, 'w', zipfile.ZIP_STORED) as z:
z.writestr('meta.xml', meta)
z.writestr('res/scripts/client/gui/Scaleform/framework/g_entitiesFactories.pyc', pyc)
官方所有 wotmod 的条目都是 STORED——这也是 wotmod 体积偏大的原因。
4.5 wotmod 之间不允许路径重叠(最贵的一课)
我曾想通过"覆盖"来汉化 modsSettingsAPI:在 compat 包里放一份 res/mods/izeberg.modssettingsapi/text/ru.yml。
结果启动后插件列表整个空了,日志里两行:
Mod package 'zzz...compat_1.2.0.wotmod' conflicting with 'izeberg.modssettingsapi_1.7.1.wotmod'
Package 'zzz...compat_1.2.0.wotmod' ignored due to conflict with package 'izeberg...'
只要你的包里有任何一个路径已被别的包占用,整个包会被丢弃,连带里面的 g_entitiesFactories 一起没了,所有老插件 import 失败。
正确做法:要么不碰别人的路径,要么直接提供一个改好的同 id 同版本包(我们最终就是这么做的:把汉化语言包做进 modssettingsapi 本体)。
五、依赖瘦身:删掉 8 MB 的 openwg.common
5.1 现象:玩家目录里多出 mods\temp 一堆文件
openwg_loader 里写着 PATH_TEMP = "mods/temp",openwg_native 会把 wotmod 里的原生库(.pyd/.dll)解包到那里再加载。net.openwg.common_2.8.4.0000.wotmod(8 MB)里就塞了 17 个原生库:openwg_gpu.pyd、sl.common.dll、sl.interposer.dll、sl.pcl.dll、sl.reflex.dll 等等。
这些文件不是盒子安装器建的(WBPInstaller.exe 里没有 mods\temp 字面量),是运行时行为,无害,删了下次启动按需重建。
5.2 但插件根本不需要它
关键线索:最老的 [2.3.1.3] 包只有四个文件(本体 + modslistapi + modssettingsapi + gameface),从来没有 openwg.common。它是 [2.4.1.0] 那一版才加进来的。
实测把它移走:
- 无弹窗
- 无
ImportError - 无 OpenWG 报错
- 车库设置正常
- 进战斗三色灯与倒计时全部正常
5.3 收益
| 指标 | 含 common | 删掉 common |
|---|---|---|
| 包体 | 3,659,404 B | 611,613 B |
mods\temp |
可能产生 17 个原生库 | 不产生 |
| 干净客户端多装 | 8 MB GPU/音频组件 | 无 |
六、兼容层:为什么必须自己补两个模块
6.1 g_entitiesFactories
2.4.1.0 起官方删除了这个 Scaleform 模块。而 mods_gui、guiflash、modssettingsapi、quests、DModButton、危险指示灯都依赖它。
实测证据:izeberg.modssettingsapi_1.7.1 的 pyc 里能直接搜到 g_entitiesFactories;缺了它,日志报:
ImportError: cannot import name g_entitiesFactories
车库里的设置面板直接打不开。
6.2 openwg_loader
用 wotmod 提供 pyc 之后,弹窗没了、灯也亮了,但日志开始刷:
[OpenWG/Entrypoint_mods] fini
ImportError: No module named openwg_loader
net.openwg.common.wotmod 里确实带着 openwg_loader,但它在 res/mods/openwg_libraries/ 下;而入口 mod_openwg.pyc 用的是 import openwg_loader 这种顶层导入,必须在 scripts/client/ 路径下能找到。
所以把 5 个引导模块一起塞进同一个 wotmod(res/scripts/client/openwg_loader/... 等),通过 VFS 提供 import 路径。最终 compat 包 50,117 B,6 个条目全是 STORED:
meta.xml
res/scripts/client/gui/Scaleform/framework/g_entitiesFactories.pyc 1,415 B
res/scripts/client/openwg_loader/__init__.pyc 15,600 B
res/scripts/client/openwg_loader/dag.pyc 11,023 B
res/scripts/client/openwg_loader/ordered_dict.pyc 7,898 B
res/scripts/client/openwg_vfs/__init__.pyc 11,340 B
res/scripts/client/openwg_scaleform/__init__.pyc 1,457 B
七、汉化:能做到哪一步
7.1 设置窗口是俄文,因为 modsSettingsAPI 没有中文
izeberg.modssettingsapi 自带语言包只有七种:
be.yml de.yml en.yml hu.yml pl.yml ru.yml uk.yml
没有 zh_cn,中文客户端回退到俄语,于是窗口标题、按钮、提示全是俄文。
7.2 我们做的:把语言包改成中文
思路:直接提供一个已汉化的 modssettingsapi(保留原 id 与 1.7.1 版本号),把 ru.yml 换成中文并新增 zh_cn.yml。这样无论客户端解析成 zh_cn 还是回退 ru,都命中中文。
结果:窗口标题"模组设置"、按钮"确定 / 取消 / 应用 / 关闭 / 清空 / 默认值"、提示"绿色指示灯 - 模组已开启"全部中文。
7.3 但窗口内的"选项文字"改不了
那些标签由危险指示灯插件本体提供,固化在 mod_enemyannouncer.pyc 里,而这个 pyc 被作者加了壳:
| 指标 | 本插件 pyc | 普通 pyc 对照 |
|---|---|---|
| 字节码段熵 | 7.95 | 4.77 |
srcsize 字段 |
99 | 正常体积 |
| 可读字符串 | 全是乱码 | 正常 |
熵 7.95 意味着字节码被整体加密(接近随机),文件里只剩一个 99 字节的 stub 外壳。反编译工具读不出内容,也无法还原出可编译的源码——除非拿到作者的加密工具。
7.4 一个意外收获:客户端自带 python27.dll
发现 win64\python27.dll 可以被外部调用,配合 Py_NoSiteFlag(否则会因缺 stdlib 报 No module named site)就能编译 Python 2.7 的 pyc:
# 伪代码:LoadLibrary → Py_NoSiteFlag=1 → Py_Initialize
# → Py_CompileString → PyMarshal_WriteObjectToString
实测成功。也就是说,做一个自己的中文设置页在技术上完全可行——只是它会在设置列表里多出一个入口(俄文原页 + 中文新页),最终因为体验取舍没有做。
八、最终自测清单
环境:把所有散装运行时(openwg_packages、openwg_loader、g_entitiesFactories.pyc)全部移走,模拟干净客户端,只用盒子安装本包。
| 检查项 | 结果 |
|---|---|
| 6 个文件全部落地 | ✅ 6/6 |
| compat 包内部 6 条目 | ✅ 全部 STORED |
| 启动日志 | ✅ zzz.ccnemo.scaleform.compat ... loaded |
No module named openwg_loader |
✅ 无 |
compression not supported |
✅ 无 |
| 绿灯 / 黄灯 / 红灯 / 倒计时 | ✅ 全部正常 |
| 车库设置面板 | ✅ 可打开,外壳中文 |
mods\temp / mods\configs\net.openwg.common |
✅ 不产生 |
另外做了一次对照实验证明"效果与老包一致":
- 六个包(2.3.1.3 到我们的)插件本体哈希完全相同 → 图形、颜色、判定、音效同一份二进制;
- 我们的配置与
2.3.1.3/2.4.0.2逐字节相同,只改了offset(老包-74,109 / -74,151 / -74,151,本包-74,53 / -46,249 / -46,229,1920×1080 实测位置)。
九、复盘
- "不报错"不等于"没问题"。 打包器对 79 条错误路径的处理是:全部跳过,一声不吭。不做对照实验根本发现不了。
- 对照实验要隔离变量。 我做了一个包含 6 个变体的测试包(嵌套/扁平 × EscapePath/FileList × txt/json/pyc),一次装完就能读出规则。
- 语义要靠证据,不能靠推理自洽。 KeepPath 那次我的推理很顺,但没验证过;管理员的实测经验直接推翻了整个结论。
- 官方包是最好的文档。 五条规则全部能从官方
.wbp的结构里读出来。黑盒没有文档时,去看它的正常样本。 - 先排除打包,再怀疑插件。 五个历史版本本体哈希相同 + 日志无异常,这两条一出来,问题必然在配置或客户端。
- 不要用"覆盖"解决别人的包。 路径重叠 = 整包被丢弃,这个代价比想象中大得多。
- 砍依赖要看历史版本。 最老的包没带的东西,往往是后来被塞进去的;实测能去掉,就是净收益。
附:文件清单
| 文件 | 大小 | 说明 |
|---|---|---|
危险指示灯_2.4.1.1.wbp |
611,613 B | 盒子安装包,可直接上传 |
发布说明.txt |
10,676 B | 结构、规则、自测、兼容性备注 |
博客_危险指示灯插件打包与排错.md |
本文 | 全流程记录 |
build_危险指示灯_wbp.ps1 |
5,812 B | 构建脚本,读 version.xml 自动出包 |
旧文更正提示.md |
855 B | 贴在本系列旧文顶部的更正声明 |
SHA256(安装包):
4AEBBFC36E8E12BEFC177A658F9B763A7473284D44E739CC22F7D706FCED6B7F