横扫千军:王国的 .gui——以空白分隔的控件树
《横扫千军:王国》的主菜单、大厅、游戏内 HUD 和对话框都是根据
.gui文件绘制的——这是一种古怪的、以空白分隔的控件描述格式。尽管它看上去与 TA 自己的界面文件(同样是.gui)相似,但 TA:K 的格式在结构上完全不同,两者不能互换。
Note
kbot 目前没有提供 TA:K .gui 子命令——这里记录这种格式,是为了内容完整,也为将来的工具提供参考。你仍然可以手工查看这些文件;它们是纯 ASCII 文本。
你看到的是什么
.gui 文件是一串扁平排列的控件。文件的首个控件(文件头控件)声明控件总数和全局元数据(背景图像、快捷键);其后的每个控件各描述一个界面元素。
一个典型的控件如下:
4 1 ← widget type (4 = button), unused flag (always 1)
1 ← unused (always 1 except in headers)
2 196 79 95 20 1 0 0 0 ← bounds: type, X, Y, W, H, flags…
3 0 0 0 0 ← colour: type, R, G, B, A
1 0 0 ← unused (always 1 0 0)
1 14 hattfont11.gaf ← font: type, len, name
12 PlayerColor 2 12 ← control name + state count
3 ← number of gaf frames that follow
1 17 singlemachine.gaf 14 SingleMachine0 0 18 ← disabled frame
1 17 singlemachine.gaf 14 SingleMachine0 1 18 ← pressed frame
1 17 singlemachine.gaf 14 SingleMachine0 2 18 ← released frame
3 ← number of strings that follow
2 0 0 2 0 4 Exit 2 0 4 Exit 3 ← state strings (disabled, pressed, up)
1 7 Default
1 12 skirmish.wav ← sound played when activated
1 7 Default
2 0 16 Play the Machine ← tooltip / mouseover help
0 ← number of child widgets
记号以空白分隔。每个控件的第一个记号是控件类型 ID;之后的一切都遵循各类型自己的结构。
带长度前缀的字符串
这种格式中最重要的技巧:每个字符串前面都有一个整数记号,给出它的字符长度。这意味着字符串可以包含空格——但解析器必须先读取长度。
1 14 MainScreen.gaf
^ ^^ ^^^^^^^^^^^^^^
│ │ │
│ │ └─ the string itself (14 characters)
│ └─ length
└─ string-flavour prefix (1 = literal string)
空字符串写作 1 0 0(前缀、长度为零,空的载荷即“空字符串”)。反复出现的 1/0 前缀非常容易解析错:如果你只是按空白切分记号再开始计数,就会出错——必须按控件顺序依次读取。
Important
你无法有效地在 .gui 文件中 grep 出“第三个按钮”——这种结构是顺序的,不可寻址。针对这种格式编写的编辑器都是顺序解析器。
控件类型
观察到的控件类型 ID:
| ID | 控件 | 说明 |
|---|---|---|
2 |
文件头 / 窗体 | 文件中的第一个控件。携带背景、快捷键和子控件数。 |
4 |
按钮 | 3 种状态(禁用 / 按下 / 弹起)。 |
5 |
复选框 | 5 种状态的矩阵(禁用、未选中按下、选中按下、未选中弹起、选中弹起)。 |
9 |
框架 | 不可见的容器;只在视觉上把控件分组。 |
12 |
滚动条容器 | 框定滑块及其增/减按钮。 |
14 |
滚动条按钮(增 / 减 / 滑块) | 每个 12 之后跟着三个这样的控件。 |
15 |
列表框 | 运行时填充;模板几乎是空的。 |
17 |
单选按钮 | 3 种状态。 |
18 |
多状态按钮 | N 种状态循环切换的按钮(例如“AI 难度”选择器)。 |
19 |
图像 + 标签 | TA:K 把静态美术与说明文字结合在一起的控件。 |
每种类型在类型 ID 之后的记号都有固定的结构。由 Dark Rain 逆向得出的标准结构保存在本仓库随附的源资料中。
控件的一般结构
[TYPE] 1
1
2 [X] [Y] [W] [H] 1 0 0 0
3 [colour: A R G B]
1 0 0
1 [font: length name]
[control-name length] [name] 2 3
[N]
1 [len] [gaf] [len] [seq] [frame] 9 × N
[N]
2 0 0 2 [string set] × N entries inline
1 7 Default
1 [len] [activate.wav]
1 7 Default
2 0 [help length] [help text]
[child-count]
对于每种控件类型,[N] 的大小和字符串集的结构各不相同;源资料中的结构说明逐一记录了它们。
对齐代码
在字符串集中:
| 代码 | 对齐方式 |
|---|---|
0 |
居中 |
1 |
左对齐 |
2 |
右对齐 |
单状态控件中你会看到例如 2 0 6 Player;3 状态按钮则会把三种状态连在一起,例如 2 0 0 2 0 6 Player 2 3 6 Play2r 3。
实例解析——主菜单
2 1
2 0 0 640 480 1 1 0 0
3 255 255 255 255
1 0 0
1 0
8 MainMenu 1 2
2
1 15 TitleScreen.tsf 7 JPGTest 0 9
1 15 TitleScreen.tsf 7 JPGTest 1 9
2
2 0 0 2 0 0 2
1 0
1 0
2 0 24 #Enter#Play#Esc#Previous
6
从上往下读:
2 1——控件类型 2(文件头),标志 1。2 0 0 640 480 1 1 0 0——位于原点的边界,640×480,加上四个标志记号。3 255 255 255 255——不透明的白色颜色条目。8 MainMenu 1 2——控件名“MainMenu”(8 个字符),后面跟着1 2。- 两个
TitleScreen.tsf JPGTest条目是背景图像(重复引用了两次——引擎要求两个都有)。 2 0 24 #Enter#Play#Esc#Previous——按键绑定:Enter触发名为“Play”的控件,Esc触发“Previous”。6——这个窗体包含 6 个子控件,紧随其后。
TSF 背景与 GAF 背景
文件头控件支持两种背景来源:
.tsf——TA:K 真彩色动画的文本形式,界面加载器会直接读取它。对于静态菜单,它只是一个很薄的 JPG 载体(TitleScreen.tsf示例包装的是TitleScreen.jpg),但同样的格式也能描述完整的多帧动画。二进制和文本布局见 TAF / TSF,kbot 对两者都能解析并往返。.gaf——标准的横扫千军:王国动画。文件头引用 GAF 中的一个序列;引擎把它作为背景播放。
无论哪种,这种格式都要求图像条目连续列出两次;真正渲染的只有第二份。这是一个已知的怪癖。
坑点
Warning
.gui 文件没有校验和,也没有恢复标记。 只要有一个子控件数记号数错了,后面的每个控件都会悄无声息地发生偏移。引擎会加载这个文件(它不校验结构),但你会看到控件出现在错误的位置、找不到控件名,或者冒出幽灵按钮。
- 空白除了分隔记号之外没有意义。 Cavedog 发布的文件既用空格也用制表符;把任何一串连续的空白都当作一个分隔符。
- 子控件数必须准确。 一个以
4结尾的框架控件(类型 9)会把接下来的四个顶层控件认作它的子控件。数字弄错了,窗体的其余部分就会支离破碎。 - 列表框和滚动条控件在发行文件中基本上是空模板——引擎在运行时根据脚本状态填充它们。试图静态地填充它们毫无意义。
- TA:K 需要命令行参数
-disablecavedogverification才能加载大多数改过的.gui文件。没有它,引擎会拒绝任何哈希值与内嵌列表不符的文件。 - 有些文件解包后会崩溃,另一些则是打包后会崩溃。 格式本身是稳定的;问题在于引擎的加载器在虚拟文件系统各层与直接访问目录之间行为不一致。制作模组时,两种状态都要测试。