横扫千军:王国的地图——.tnt + .ota + .crt + .kmp
《横扫千军:王国》的地图是一种 TNT 变体(IDVersion 字不同),外加一到两个元数据文件;单人任务则另用一种 HPI v2 战役压缩包格式。这与 TA 的地图模型远比早先那份“BTM/高度图 BMP”研究笔记所设想的接近——查看实际的 GOG 版 TA:K 安装目录就会发现,里面根本没有
.btm或附属 BMP 文件。kbot 现有的 HPI v2 读取器可以透明地处理
.kmp压缩包。kbot 的 TNT 读取器能识别 TA:K 的版本字,并解码文件头、DataUnit 高度图、纹理映射网格(地形名表 + U/V 偏移表)、地图物件放置网格 + 名称表,以及内嵌的小地图——足以把外部地形 JPG 和地图物件的 GAF 精灵图合成起来,以完整分辨率渲染出整张地图。本页记述能从发行版安装文件中验证的内容,以及尚存的空白。
Important
状态更新(相对于本页最初的“研究笔记”版本)。 本页过去讨论的那些文件类型——.btm、附属 BMP、按阵营区分的调色板 PCX——在发行的 GOG 版横扫千军:王国安装目录中实际上并不存在。真正的文件类型记述如下。若要与另一个开源 TA:K 项目对照,可参阅 iiCompleteDestruction;对于某些 TA:K 专有的结构,它的解析器实现比 kbot 更完整。
组成一张 TA:K 地图的文件
对 GOG 版安装目录中 maps 目录的统计:
| 扩展名 | 数量 | 用途 |
|---|---|---|
.kmp |
181 | HPI v2 压缩包,打包一个战役任务的地图 + 脚本 + 资源 |
.tnt |
55 | 地图地形数据(TNT 结构的二进制文件,带 TA:K 专有的 IDVersion 字) |
.ota |
55 | 地图元数据——TDF 格式,结构与 TA 的 .ota 相同,另有 TAK 专有字段 |
.txt |
55 | 自由文本的地图描述(寥寥数词,显示在大厅中) |
.crt |
33 | 场景附属文件:预置单位、按玩家划分的规则引擎、命名的触发区域 |
.tdf |
6 | 按地图划分的玩法调整文件(战役调校、AI 引导) |
典型的多人游戏地图以 .tnt + .ota + .txt 三件套发布,有时附带一个 .crt 附属文件。战役任务则打包在 .kmp 压缩包里,其中包含同样一组文件和任务脚本。
Tip
不解包,直接列出一个 TA:K 地图包的内容:
# The HPI v2 reader handles .kmp transparently
kbot hpi list /path/to/tak/maps/adamantine\ gate.kmp -v
kbot hpi info /path/to/tak/maps/adamantine\ gate.kmp
.tnt——TA:K 地形(TNT v2)
TA:K 沿用了 TA 的 TNT 结构,但改动了 IDVersion 字。abnar's terrace.tnt 的十六进制转储:
00000000: 00 40 00 00 20 01 00 00 e0 00 00 00 3a 00 00 00 .@.. .......:...
00000010: 34 00 00 00 34 fc 00 00 34 f4 02 00 16 00 00 00 4...4...4.......
第一个 uint32 是 0x00004000——而 TA 的 TNT 用的是 0x00002000。容器仍然是文件头加上一组按指针寻址的区段,但 TA:K 根本不是图块拼接格式。 TA 存储的是图块索引网格 + 一组 32×32 的图块图形,而 TA:K 把地形存储为引用外部 JPG 纹理的纹理映射网格,另加独立的高度图和地图物件放置网格。以下内容均已在全部 55 张发行地图上验证:
Width/Height是以 16 像素的 DataUnit(数据单元)计的数量,而不是像素数。 像athri cay这样的“15 × 15”地图存储的是Width = Height = 480个 DataUnit → 每边480 × 16 = 7680像素。高度图和地图物件网格按 DataUnit 采样;地形则在更粗的 32 像素 Graphic Unit(图形单元)网格上采样,每个轴上恰好是 DataUnit 网格的一半(guW = Width/2)。- 文件头由十二个连续的
uint32组成,内容是偏移量和计数;TA 的字段名已不再能描述其内容。下表给出它们在 TA:K 中的含义。 - 区段指针是绝对偏移,因此每个区段都按它自己的指针 + 算出的长度读取。
| 偏移量 | TA:K 字段 | 区段大小 | 说明 |
|---|---|---|---|
0x00 |
Version |
— | 0x4000(TA 写的是 0x2000) |
0x04 |
Width |
— | 以 DataUnit 计;×16 → 像素,÷2 → Graphic Unit |
0x08 |
Height |
— | 以 DataUnit 计 |
0x0C |
SeaLevel |
— | 水面的高度阈值 |
0x10 |
HeightMapOffset |
Width × Height 字节 |
每个 DataUnit 一个高度字节 |
0x14 |
AttributesOffset |
Width × Height × 2 |
地图物件网格:每个 DataUnit 一个 uint16 索引(≥0xFF00 = 无) |
0x18 |
FeatureNamesOffset |
Count × 132 |
地图物件名称表(4 字节索引 + 128 字节名称) |
0x1C |
FeatureCount |
— | 地图物件类型的数量 |
0x20 |
TerrainNamesOffset |
guW × guH × 4 |
每个 GU 的纹理名 → terrain/%08x.jpg |
0x24 |
UMappingOffset |
guW × guH 字节 |
每个 GU 在其纹理中的列偏移(×32 像素) |
0x28 |
VMappingOffset |
guW × guH 字节 |
每个 GU 在其纹理中的行偏移(×32 像素) |
0x2C |
MiniMapOffset |
8 + 126×126 |
uint32 w、uint32 h,然后是索引 |
高度图(0x10)每个 DataUnit 一个字节——是一个灰度海拔场,不是彩色位图。地形通过纹理映射重建:对每个 Graphic Unit,地形名表(0x20)给出一个纹理文件(terrain/<name>.jpg,名称格式化为小写的 8 位十六进制数),U/V 表(0x24/0x28)给出要从该 JPG 中 (u×32, v×32) 处复制的 32×32 子图块。地图物件网格(0x14)是放置层:一个 Width × Height 的 uint16 数组,每个格子要么是指向名称表的地图物件索引,要么是一个高位哨兵值(≥0xFF00),表示“此处没有地图物件”;放置位置所在的 DataUnit 格子 (x, y) 对应地形像素 (x×16, y×16)。名称表(0x18)的 132 字节条目布局与 TA 的 TileAnim 表相同。
在全部发行地图中,小地图块一律是 126 × 126,并且与 TA 一样,用同一个 0x64 空白字节填充非正方形地图之外的区域。
Note
目前在 TA:K TNT 上能做到的。 kbot tnt describe 报告文件头、所属王国、高度图/地形尺寸以及地图物件的分类统计。kbot tnt preview --vfs <root> 渲染完整分辨率的纹理映射地形,并把每个地图物件的 GAF 精灵图按其热点锚定合成在上面——也就是游戏所画出的完整地图。kbot tnt image 渲染自包含的灰度高度图(不需要外部纹理;--features 会叠加按类别着色的标记)。kbot tnt minimap 导出内嵌的 126 × 126 预览图。
小地图调色板——每个王国一个
TA:K 不用单一的全局调色板为小地图着色。每张地图的 126 × 126 预览图都是按其所属王国的纹理调色板烘焙的——即同目录 .ota 中的 kingdom= 字段(aramon / taros / veruna / zhon / creon)。用错误王国的调色板(或 TA 的 PALETTE.PAL)渲染同样的索引,会得到满是斑点的噪声;用正确的调色板渲染,则能得到自然的水面、海滩和地形。
kbot tnt minimap 会从同目录的 .ota 自动解析出王国。没有 .ota 时(例如单独的一个 .tnt 散文件),请显式传入 --kingdom <name>:
kbot tnt minimap "athri cay.tnt" -t mini.png # auto: veruna
kbot tnt minimap loose.tnt --kingdom aramon -t mini.png
.ota——地图元数据(TDF,大体熟悉)
TA:K 的 .ota 与 TA 的一样是 TDF 结构的格式,另有几个 TAK 专有字段。以下是 abnar's terrace.ota 的完整内容:
[GlobalHeader]
{
Copyright=Copyright 1998 Cavedog Entertainment. All rights reserved.;
missionname=Abnar's Terrace;
missiondescription=9 x 7 4 Player 32MB;
kingdom=veruna; // ← TA:K-specific: the side the map "belongs" to
numplayers=4;
size=9 x 7; // ← TA:K-specific: free-text size for the lobby
memory=32 MB; // ← TA:K-specific: ~RAM requirement for the lobby
hasscenario=0;
[Map Data]
{
Type=Network 1;
aiprofile=DEFAULT;
[specials]
{
[special0] { specialwhat=StartPos1; XPos=37; ZPos=190; }
[special1] { specialwhat=StartPos2; XPos=215; ZPos=38; }
…
}
}
}
| 字段 | TA 中的对应 | 说明 |
|---|---|---|
kingdom= |
无 | 阵营归属(aramon / taros / veruna / zhon / creon)。大厅按它筛选;它还选定了 .tnt 烘焙预览图所用的地形/小地图调色板。 |
size= |
无 | 给大厅看的、便于阅读的地图尺寸。 |
memory= |
无 | 建议内存。仅作显示。 |
hasscenario= |
无 | 战役任务为 1,遭遇战地图为 0。 |
[Map Data] |
[Schema N] |
把方案子节映射为单一一个块(TA:K 的每张地图只发布一个方案)。 |
其余一切(StartPos1..N、aiprofile= 等)的用法都与 TA 完全相同——kbot mount 的 OTA 查看器无需修改即可处理两者。
.kmp——战役任务的 HPI v2 压缩包
.kmp 是一个改了扩展名的 HPI v2 压缩包。adamantine gate.kmp 开头的十六进制转储:
00000000: 48 41 50 49 00 00 02 00 57 32 02 00 77 00 00 00 HAPI....W2..w...
00000010: 15 32 02 00 42 00 00 00 20 00 00 00 00 00 00 00 .2..B... .......
00000020: 53 51 53 48 … SQSH…
完全是 v2 的文件头结构(HAPI 标记 → 0x00020000 版本号 → 24 字节的 v2 文件头 → SQSH 块)。kbot hpi list、info、extract 都能直接使用。每个 .kmp 通常打包了:
- 该任务的
.tnt和.ota - 少量过场 / 任务简报 GAF
- 任务脚本 COB 文件——TA:K 沿用 COB 格式(
VersionSignature == 6)编写任务脚本,而不是另用一种脚本语言。零售版 TAK 安装中 3,885 处MISSION_COMMAND(0x10073000)操作码都在这里。其线上格式(wire format),以及任务命令所索引的、每个 COB 各自的音效名字符串池,见 cob.md 中的横扫千军:王国——COB v6 差异和附录 C。 - 有时还有针对单个任务的单位覆盖
.crt——场景脚本(单位、规则、触发器)
.crt 承载地图的脚本层:开局前就已放置好的单位、驱动遭遇战 AI 和战役目标的按玩家划分的规则引擎,以及这些规则所引用的命名矩形触发区域。多人游戏地图发布的是一个空壳文件(stub):没有单位,九个空的玩家槽,没有触发器,共 56 字节;战役地图和特殊地图则填满每个区段,最大约 120 KB。
整个文件都是小端序,是引擎内存中结构体的直接映像,因此单位记录中有几个未初始化的填充字段。
文件头
| 偏移量 | 类型 | 字段 | 说明 |
|---|---|---|---|
0x00 |
uint32 |
Signature | 0x3F800000——IEEE-754 1.0f 标记 |
0x04 |
uint32 |
Unknown1 | 除一张以外的所有发行地图上都是 0 |
0x08 |
uint32 |
UnitCount | 随后的预置单位记录数 |
abnar's terrace.crt(一个空壳文件)的开头:
00000000: 00 00 80 3f 00 00 00 00 00 00 00 00 09 00 00 00
1.0f、Unknown1 = 0、UnitCount = 0,然后是玩家数 9。
单位记录(每条 568 字节)
| 偏移量 | 类型 | 字段 |
|---|---|---|
+0 |
char[256] |
UnitType(以 NUL 结尾) |
+256 |
char[256] |
UniqueName(通常为空) |
+512 |
uint32 |
X(世界坐标) |
+516 |
uint32 |
Y(高度轴) |
+520 |
uint32 |
Z(世界坐标) |
+524 |
uint32 |
PlayerId(从 0 开始) |
+528 |
uint32 |
HealthPercent(通常为 100) |
+532 |
uint32 |
ArmorPercent(通常为 100) |
+536 |
uint32 |
WeaponPercent(通常为 100) |
+540 |
uint32 |
Angle(通常为 180) |
+544 |
uint32 |
Veteran |
+548 |
uint32 |
未知 |
+552 |
uint32 |
未知 |
+556 |
uint32 |
FootprintX |
+560 |
uint32 |
FootprintZ |
+564 |
uint32 |
未知 |
玩家、规则、条件、动作
单位之后是一个 uint32 玩家数(总是 9),其后跟着同样数量的玩家块。每个玩家块是一个 uint32 规则数,后面跟着同样数量的规则。一条规则的结构是:
uint32条件数,随后每个条件是一个uint32操作码加五个 64 字节的参数槽;uint32动作数,随后每个动作是一个uint32操作码加五个 64 字节的参数槽。
触发器
文件末尾的一个 uint32 触发器数之后,跟着同样数量的触发器记录。每条记录是一个 256 字节、以 NUL 结尾的名称,后跟四个 uint32 边界——Left、Top、Right、Bottom——以地图格子为单位。名称是便于阅读的文字(The Hill、Player 1)。
工具
kbot crt describe <map.crt> 打印单位类型的分类统计、规则数和触发区域;加上 --verbose 可列出完整的放置清单。MCP 的 crt_describe 工具以 JSON 形式返回同样的数据。
有一个文件——cairbray coast landing.crt——的文件头经过手工编辑、发生了错位,与这个布局不符;解析器会以明确的错误拒绝它,而不是错误地解码。
.txt——自由文本描述
单行 ASCII 字符串,用于大厅。例如 abnar's terrace.txt:9 x 7 4 Player 32MB(与 .ota 的 missiondescription 中的字符串相同)。制作工具大概是在导出时把两者一起写出的;游戏显示的似乎是 .ota 中的值。
目前如何处理 TA:K 地图
可行的做法,按工作量从小到大排列:
- 随意查看——对
.kmp文件使用kbot hpi list/info/extract开箱即用。.ota和.txt是纯文本。 - 描述一张地图——
kbot tnt describe <map.tnt>报告 TA:K 文件头、所属王国、高度图/地形尺寸以及地图物件的分类统计。 - 渲染小地图——
kbot tnt minimap <map.tnt> -t mini.png以真彩色导出内嵌的 126 × 126 预览图,并根据同目录的.ota自动选择王国调色板(可用--kingdom覆盖;见上文关于调色板的说明)。 - 渲染带纹理的完整地图——
kbot tnt preview <map.tnt> --vfs <flattened-tak-root> -t map.png用安装目录中的terrain/*.jpg对地形做纹理映射,并合成每个地图物件的 GAF 精灵图,得到与游戏渲染一致的地图(athri cay → 7680 × 7680)。地图物件精灵图的调色板读自palettes/<kingdom>_features.pcx。 - 渲染高度图——
kbot tnt image <map.tnt> -t height.png不需要任何外部资源;它输出灰度的 DataUnit 高度图(--features会叠加按类别着色的放置标记)。 - 重新打包战役压缩包——
kbot hpi pack目前只能生成 v1,所以重新打包的.kmp会无法加载。在 kbot 支持写出 v2 之前,要手工修改.kmp,只能先解包、修改散文件,再用支持 v2 的第三方打包工具打包。
Warning
横扫千军:王国需要 -disablecavedogverification 才能加载大多数修改过的文件,包括修改过的地图。没有它,引擎会拒绝任何哈希值不在其允许列表中的文件。请在所有模组的安装说明中写明这一点。
kbot 目前对 TA:K 地图能做什么、不能做什么
| 操作 | 状态 |
|---|---|
读取 .kmp(HPI v2)文件头 + 文件列表 |
✅ |
从 .kmp 中提取文件 |
✅ |
解析 .ota(TDF 格式) |
✅ |
读取 TA:K .tnt 文件头(kbot tnt describe) |
✅ |
导出 TA:K .tnt 小地图(kbot tnt minimap) |
✅(真彩色——按王国的调色板取自 .ota) |
渲染带纹理的完整 TA:K 地图(kbot tnt preview --vfs) |
✅(纹理映射地形 + 地图物件 GAF 精灵图) |
渲染 TA:K 高度图(kbot tnt image) |
✅(自包含的灰度图;--features 叠加标记) |
写出 TA:K .tnt(kbot tnt pack) |
❌ |
写出 .kmp(HPI v2 打包) |
❌ |
欢迎针对任何 ❌ 项提交拉取请求。高度图、纹理映射网格、地图物件网格和地图物件表都已解码,地形和地图物件都能以完整分辨率渲染;剩下的空白是让修改过的 TA:K 地图能够往返的写出路径,以及 v2 .kmp 的打包。