C.C.Nemo
C.C.Nemo
发布于 2026-10-27 / 37 阅读
0
1

把「危险指示灯」做成可发布的盒子插件:从 88 条目返工到 611 KB

起初我以为这只是"把几个文件塞进 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

两个坑:

  1. ZipArchive + CompressionLevel::Optimal → DEFLATE,不认;
  2. 换 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 实测位置)。

九、复盘

  1. "不报错"不等于"没问题"。 打包器对 79 条错误路径的处理是:全部跳过,一声不吭。不做对照实验根本发现不了。
  2. 对照实验要隔离变量。 我做了一个包含 6 个变体的测试包(嵌套/扁平 × EscapePath/FileList × txt/json/pyc),一次装完就能读出规则。
  3. 语义要靠证据,不能靠推理自洽。 KeepPath 那次我的推理很顺,但没验证过;管理员的实测经验直接推翻了整个结论。
  4. 官方包是最好的文档。 五条规则全部能从官方 .wbp 的结构里读出来。黑盒没有文档时,去看它的正常样本。
  5. 先排除打包,再怀疑插件。 五个历史版本本体哈希相同 + 日志无异常,这两条一出来,问题必然在配置或客户端。
  6. 不要用"覆盖"解决别人的包。 路径重叠 = 整包被丢弃,这个代价比想象中大得多。
  7. 砍依赖要看历史版本。 最老的包没带的东西,往往是后来被塞进去的;实测能去掉,就是净收益。

附:文件清单

文件 大小 说明
危险指示灯_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


评论