制作模组与解析格式时的常见陷阱
这是一页速查表,汇集了散布在 kbot 各格式文档中、最常让人中招的坑。如果你遇到了一个莫名其妙的 bug,就从这里开始。每一条都链接到相应格式页面上更深入的解释。
Tip
在找“我的模组加载不了”的原因? TDF 的分号规则(#tdf--fbi--ota 的第一条)导致的“静默损坏”报告,比其他所有坑加起来还多。
HPI / UFO / CCX / GP3
- 安装根目录中的散文件会覆盖所有压缩包。 把
units/MYUNIT.fbi放到磁盘上,引擎就会优先使用它,而不是totala1.hpi中的任何内容。hpi.md 中的分层叠加示意图展示了完整的优先级栈。 - 压缩包之间,先挂载的胜出,而且只有十个
*.hpi会被挂载。 TA 3.1c 先挂载rev31.gp3,然后是*.ccx、*.ufo,以及最先能打开的十个*.hpi,每一组都按 ASCII 大写名称排序。以zz_mod.hpi发布的模组排在所有零售版压缩包之后;如果安装目录中已经有十个排在它前面的*.hpi,它就只能通过光盘扫描挂载,排在一切之后。请以.ufo发布模组。 - HPI v1 的指针是相对于文件开头的绝对偏移,而不是相对于目录开头。 不需要减去任何东西。标准解析器的诀窍是分配一个
DirectorySize字节的缓冲区,把目录解密到其中的Start偏移处,这样指针就能直接索引缓冲区。见目录树。 - HPI v1 的加密种子随文件位置推进。 你无法孤立地解密一个缓冲区——必须知道这个缓冲区来自文件中的哪个绝对偏移。见推导解密密钥。
- 如果
DecryptKey == 0,就完全不要应用 XOR 加密。 有些自制压缩包以明文发布;照样运行解密反而会把本来可读的内容打乱。 - SQSH 块的校验和是对加密后的字节计算的。 先校验,再解扰。见压缩块(SQSH)。
- 横扫千军:王国使用 HPI v2(
Version == 0x00020000),布局不同——没有 XOR,每个文件只有一个 SQSH 块。在 TAK 压缩包上尝试 v1 解析器之前,请先阅读横扫千军:王国 HPI v2 一节。 - 带
BANK标记的游戏存档完全是另一回事——它们沿用 HPI 的封装,但内容是序列化的游戏状态,而不是文件系统。kbot 不解码它们。 .gp3需要 rev3.1 补丁。 它内部仍然是 HPI,但未打补丁的 TA 拒绝挂载它。
TDF / FBI / OTA
- 每个值都必须以
;结尾。 漏掉分号会悄无声息地把多行的值合并在一起,产生错得离谱的属性。这是“我的单位生命值很怪 / 描述丢了 / 造价不对”的头号原因。 - 键不区分大小写,值区分。
Side=arm对大多数查找都有效,但任何对Side做字符串相等比较的模组代码都会出错。 - 列表值以空格分隔(
Category=COMMANDER MOBILE WEAPON)——而不是逗号。例外是 OTA 的numplayers=2,4,6,8,10,它用逗号。 - 布尔值是整数
1/0,而不是true/false。引擎只接受整数形式。 - 制作模组时,绝不要为了添加单位而修改
sidedata.tdf。 改用download/<UNITNAME>.tdf中的[MENUENTRY]注册——即 Core Contingency 所用的同一机制。见 TDF:[MENUENTRY]和模组教程。 Bmcode=0会把单位从建造菜单中隐藏,哪怕其他一切都有效。你想要的几乎总是Bmcode=1。- 没有列在任何建造者名下的单位无法被建造,即使它的 FBI 在其他方面都有效。这是最常见的“我的新单位存在但造不出来”的 bug。见模组教程第 5 步。
- 嵌套节的深度是惯例,不是规范。 有些工具假定最多 2 层;武器 TDF 有 3 层(
[WEAPON][DAMAGE][category])。
TNT / SCT——地图地形
- TNT 的
Width和Height以 16 像素的属性格为单位,既不是图块也不是像素。除以 2 得到图块数;乘以 16 得到像素尺寸。见 TNT 文件头。 SeaLevel == 0并不表示“没有水”——它表示最小的可能值恰好在阈值上,所以没有任何地方在水下。真正干燥的地图请使用SeaLevel = 1。- TNT 的地图物件字只有小于地图物件表条目数时才会放置地图物件。
0xFFFC是空白格;0xFFFE(Lava Run、AC02)、0xFFFD以及其他任何达到或超过表大小的字都不放置任何东西,该格子仍然可以建造。 - TNT 小地图的填充使用调色板索引
0x64,地图区域的大小由地图本身算出(较长的一边为 252,保持可见地图的宽高比),而不是通过扫描填充来确定。只有当文件头中0x2c处的字的第 0 位置位时,游戏才会读取存储的小地图。见 TNT。 - 横扫千军:王国的
.tnt使用IDVersion == 0x4000,而不是 TA 的0x2000(TA 也能读取较早的0x1020布局)。kbot tnt image对 TAK 的 TNT 只能输出乱码;图块解码器是 TA 专用的。见 TA:K 地图。 - SCT 区块的两个维度都必须是 4 的倍数,才能在 TAE 中加载。磁盘格式允许其他尺寸,但编辑器会报“section misaligned”并拒绝加载。
- SCT V2 的高度格是 8 字节,V3 是 4 字节。kbot 两者都接受;用编辑器制作的模组只会用到 V3。
3DO——模型
- Y 轴朝上,而不是 Z 轴。 把 3DO 导入 Z 轴朝上的引擎(Blender、现代的 Unreal)时如果不交换坐标轴,每个单位都会侧躺着。
- 无纹理的图元不可见,除非
IsColored != 0——伪代码visible = (texture != "") || (IsColored != 0)涵盖了这两种情况。这是最常见的 3DO 渲染 bug。 OffsetToSelectionPrim只在根对象上有意义; 子对象必须把它设为-1(0xFFFFFFFF)。- 定点数比例是
1 / 65536。 写作0x00010000的顶点坐标 = 1.0 个世界单位;低 16 位是小数部分。 - 顶点是对象的局部坐标,而不是全局坐标。两个对象可以有完全相同的顶点数组——那是镜像几何体,而不是共享数据。
COB / BOS——脚本
call-script的操作码是0x10062000,而不是0x10063000,后者是某些 3.1 之前的文档的说法。使用0x10063000的老反编译器会产出毫无意义的结果——见操作码参考。- 操作码中的部件编号是部件名表的索引,而不是字符串。COB 以字符串池的形式携带名称;运行时为了速度使用索引。
SET_SIGNAL_MASK只是登记关注——它不会终止任何东西。 真正让掩码中设有X的脚本停下来的,是发送signal X。lint 规则signal-never-signalled会捕捉设置了掩码却从来没有人发出相应信号的脚本。- Cavedog 的 Scriptor 把
CURRENT_SPEED内联为字面量29, 导致MoveWatcher一类的脚本在往返之后出错。kbot 的反编译器会恢复符号名;如果必须经过 Scriptor 往返,请加上括号:(get CURRENT_SPEED) > 5。 - 跳转偏移量是相对于
OffsetToScriptCode的字节偏移, 而不是相对于当前指令。JUMP 0x4C的意思是“跳到OffsetToScriptCode + 0x4C”。 GET_UNIT_VALUE的端口号在 TA 和 TA:K 之间有所不同。 TA 的端口目录(kbot 反编译器为端口 1–20 加了标签)记录在附录 B 中。
GAF——精灵图动画
- 只有当序列 +2 字的低字节被置位时,该序列才会循环播放;为 0 时只播放一次就停下。所有原版序列都存的是 1,所以写入零的工具会让动画地图物件定格不动。
textures/*.gaf和anims/vismasks.gaf中的帧必须是未压缩的。 游戏把它们当作普通像素数组读取。- 有些序列发布时帧时长为
0;游戏把这样的帧显示一个 tick。不要把它们“修正”成某个默认值。 - 调色板索引 0 本身并不透明。 只有未压缩帧的透明色键和压缩帧的跳过游程才是透明的。
- 压缩的像素行可能短于
Width。 游戏会继续读进后面的字节;kbot 用透明像素补齐该行并给出警告。没有任何原版文件存在这样的行。 - 图层原点就是热点:合成帧的每个图层的放置方式,是让它的原点落在该帧的原点上。
anims/terrain.gaf和anims/vismasks.gaf的文件头中Version == 0。 游戏忽略版本字。- 横扫千军:王国的
.taf有两种。 带调色板的.taf就是 GAF——磁盘格式相同,只是 TAK 从按阵营区分的.pcx中取调色板,而不是用 TA 的全局调色板。真彩色的.taf沿用同一容器,但存储 16 位 ARGB 像素,不需要调色板。起决定作用的是帧格式字节(0/1= 带调色板,4/5= 真彩色),而不是扩展名——见 TAF / TSF。
TAF / TSF——真彩色动画
.taf有多重含义——带调色板的(按 GAF 读取)与真彩色的(用kbot taf读取)。一定要检查帧格式字节,绝不要看文件名,尽管大多数 TAK 真彩色文件按惯例带有_4444/_1555后缀。- GIF 导出有损。 GIF 只能容纳 1 位镂空和 255 种颜色,因此 ARGB4444 的柔和 alpha 会变成硬边。要保留真正的 alpha 通道请用 APNG(
kbot taf export --format apng)。 - 帧名称字段和
0x0B标志字节可能含有垃圾数据。 要实现逐字节一致的往返,就原样保留它们;不要规范化。 Delay/Duration为0是合法的——与 GAF 一样,零 tick 的帧由事件驱动(静态的菜单背景就这样用)。不要“修正”它。
PCX——图像
- TA 忽略
BytesPerLine。 当BytesPerLine > Width时,标准读取器会丢掉填充;TA 3.1c 每行恰好解码Width个字节,所以带填充的文件在游戏中会错位。请写入BytesPerLine == Width,并用kbot pcx describe检查。 - TA 只加载版本 5,并把每个文件都当作 8 位单平面数据解码。
- 最高两位被置位的字面字节必须经过 RLE 转义。 值为
0xC0–0xFF的裸字节不能作为字面量出现;要把它写成长度为 1 的游程(单个0xFF写作0xC1, 0xFF)。处理不当会使解码在一行之后失去同步。 - 无论有没有
0x0C标记,游戏都把末尾的 768 字节当作调色板;标准读取器则需要这个标记。
PAL / ALP / LHT / SHD——调色板与查找表
- 每个条目的第 4 个字节不是 alpha。 它是一个未使用的填充字节,Cavedog 总是把它设为
0x00。把它当作 alpha 的开源调色板工具会拒绝渲染这个调色板(alpha=0 → 全部透明)。 - 索引 0 是黑色,不是透明。 地形、小地图和背景都把它画成不透明;精灵图各自选择自己的透明色键。
palette.pal为空或缺失时,会由palette.pcx(它的最后 768 字节)代替;长于 1,024 字节的.pal只读到第 1,024 字节为止。.alp/.lht/.shd不是 RGB 调色板——.alp是 256×256 的调色板索引表,.shd/.lht是 32×256 的调色板索引表,游戏会忽略(并重建)任何其他大小的表。用kbot pal describe渲染它们只会得到乱码;请改用kbot pal lookup。- TA 调色板中有 13 组重复的 RGB 三元组。 如果你的编辑器在导入时去重(有些会这样做),就会丢失条目,调色板会悄无声息地变成 243 个条目。
FNT——位图字体
Offsets[c] == 0表示“没有字形”,而不是“字形位于偏移 0”。 偏移 0 落在文件头中间——Cavedog 把零用作哨兵值。- 每个字节内的位序是高位在前(MSB-first)。 现代位图字体更常见低位在前(LSB-first);不要想当然。
- 位流在行与行之间是连续的——每条扫描线的末尾没有填充。
AI 配置文件
- 横扫千军:王国的 AI 文件没有
plan指令——只有光秃秃的weight/limit行。kbot 的解析器会隐式打开一个defaultplan;读取器应当容忍缺失的块。见 TA:K——没有 plan 的配置文件。 - 任何地方都没有校验。 拼错的单位名、损坏的权重值、写在
plan之外的行——全都被悄无声息地忽略。配置文件损坏的第一个迹象是“AI 行为古怪”,而且没有任何日志消息。 - 类别别名(
PLANT、LEVEL3)只有在至少一个单位在Category=中声明了它们时才“存在”。 把它拼错成LEVL3,这条指令就会悄无声息地什么也不做。
音效
- 缺失的 WAV 只会无声,不会报错。 把
select1=cmsdel1拼错(字母颠倒),引擎就什么也不播放——没有日志,也没有后备。 - TDF 值中隐含了
.wav扩展名——绝不要写出来。select1=cmdsel1.wav会变成sounds/cmdsel1.wav.wav,什么也找不到。
横扫千军:王国的特有问题
- TAK 需要命令行参数
-disablecavedogverification才能加载大多数改过的文件。没有它,引擎会拒绝任何哈希值不在其允许列表中的文件。 - TAK 的数据中有 8 个阵营代码,而不是 4 个:四个可玩阵营 ARA / TAR / VER / ZON,加上 CRE(Creon,Iron Plague)、MON(怪物)、LIF(野生动物)、NPC(战役角色)。单位列表的筛选需要处理所有这些代码。
- TAK 的武器以内联方式写在每个 FBI 中,即
[WEAPON1]–[WEAPON3]子节,而不是放在单独的weapons/*.tdf目录里。 - TAK 的建造菜单是线性的,而不是 2×3。每个建造者有一个
canbuild/<builder>/<unit>.tdf目录,每个文件只有一个[Menu] { Priority = N; }块。