TAF / TSF——横扫千军:王国的真彩色动画
TAF 存储《横扫千军:王国》的真彩色精灵图动画:法术特效、爆炸、火球、魔法光环、水面精灵图以及动态菜单背景。它沿用《横扫千军》的 GAF 容器布局,但把 8 位调色板索引像素换成了 16 位 ARGB,因此 TAF 自带颜色和真正的 alpha 通道——不需要外部调色板。TSF 是同一动画便于阅读的文本形式:一种以花括号分隔的脚本,界面加载器会直接读取它。
Tip
自己试试。
kbot taf list anims/manabomb_1555.taf # sequence + frame count
kbot taf export anims/fireballa_1555.taf --format gif -o fireball.gif # animated preview
kbot taf sheet anims/manabomb_1555.taf --cols 5 -o manabomb-sheet.png # contact sheet
kbot taf decompile anims/manabomb_1555.taf --target ./manabomb # → TSF + PNG layers
所有参数见 CLI 的 kbot taf / kbot tsf 参考。
在 Go 中调用。 使用 formats/tsf:
import "github.com/coreprime/kbot/formats/tsf"
data, _ := os.ReadFile("anims/manabomb_1555.taf")
taf, _ := tsf.ParseTAF(data)
fmt.Println(taf.Name, len(taf.Frames))
img, _ := taf.FrameImage(0) // *image.NRGBA, full alpha preserved
概览
┌─ Header (12B) ─┐
│ ver 1 pad │ ver = 0x00010100, exactly one sequence
└────────┬───────┘
▼
┌─ Entry pointer ─┐ 1 × uint32 absolute offset → SequenceHeader
└───┬─────────────┘
▼
┌─ SequenceHeader (40B) ─┐
│ frameCount, name[32] │
└─────────┬──────────────┘
▼
┌─ FrameListItem (8B) × frameCount ─┐ ptr + tick duration
└───┬───────────────────────────────┘
▼
┌─ FrameInfo (24B) ─┐
│ size, origin, │
│ format(4/5), │ ← the GAF "compressed" byte, repurposed
│ layers=0, flag, │
│ ptrPixelData │
└─────┬─────────────┘
▼
┌─ Pixel data: Width×Height × uint16 ─┐ raw ARGB4444 or ARGB1555
└──────────────────────────────────────┘
一个 TAF 就是一个动画序列——一组帧,每帧是一个宽 × 高的矩形,由小端序的 16 位真彩色像素组成,带一个原点和一个以 tick 计的时长。在二进制层面上没有压缩,也没有分层:每一帧都是一张扁平的、未压缩的位图。
Note
TAF 就是换了一种像素载荷的 GAF 容器。 读过 GAF 的话,文件头、条目表、序列头和帧列表都会逐字节地眼熟。只有 FrameInfo 的解读方式和像素编码不同。两者共用 .taf 扩展名:横扫千军:王国发布的既有带调色板的 .taf 文件(按 GAF 读取),也有 真彩色的 .taf 文件(本页)。kbot 通过每帧中的格式字节区分两者——见区分两者。
文件头(12 字节)
typedef struct {
uint32 Version; // 0x00010100 — identical to the GAF version word
uint32 SequenceCount; // Always 1 for a truecolor TAF
uint32 Unknown1; // Always 0
} TAFHeader;
| 字段 | 说明 |
|---|---|
Version |
0x00010100,与 TA 的 GAF 所带的字相同。在信任任何偏移量之前先校验它。 |
SequenceCount |
总是 1。紧随其后的是一个 uint32 绝对偏移量,指向 SequenceHeader。含多个序列的文件不是真彩色 TAF。 |
Unknown1 |
总是零;写入时也写零。 |
序列(40 字节)
typedef struct {
uint16 FrameCount;
uint16 Unknown1; // Always 1
uint32 Unknown2; // Always 0
char Name[32]; // NUL-padded, case-preserved
} TAFSequence;
其后是 FrameCount × FrameListItem:
typedef struct {
uint32 PtrFrameInfo; // → FrameInfo
uint32 Duration; // Game ticks (30 ticks = 1 second)
} FrameListItem;
Warning
32 字节的名称字段在结束符之后可能带有垃圾数据。 有几个零售版 TAF 在 NUL 之后留有未初始化的缓冲区字节——kbot 原样保留这 32 个字节,使解析 → 序列化的往返逐字节一致,但只有第一个 NUL 之前的文字才是真正的名称。
帧(24 字节信息 + 载荷)
typedef struct {
uint16 Width;
uint16 Height;
int16 OriginX; // Render anchor, may be negative
int16 OriginY;
uint8 Transparency; // 0x08: always 0 (alpha lives in the pixels)
uint8 Format; // 0x09: 4 = ARGB4444, 5 = ARGB1555
uint8 LayerCount; // 0x0A: always 0 (no sub-frames)
uint8 Flag; // 0x0B: 0x00 or 0xFF, meaning unconfirmed
uint32 Unknown2; // 0x0C: always 0
uint32 PtrPixelData; // 0x10: → raw pixel bytes
uint32 Unknown3; // 0x14: always 0
} FrameInfo;
TAF 与 GAF 的分歧就在这里。在 GAF 的 FrameInfo 中,偏移量 0x09 是 Compressed 标志,偏移量 0x0A–0x0B 是一个 16 位的 LayerCount。真彩色 TAF 沿用这几个字节,但含义变为:
| 偏移量 | GAF 中的含义 | TAF 中的含义 |
|---|---|---|
0x08 |
TransparencyIndex |
Transparency——未使用,总是 0 |
0x09 |
Compressed(0/1) |
Format——4(ARGB4444)或 5(ARGB1555) |
0x0A |
LayerCount 低字节 |
LayerCount——总是 0 |
0x0B |
LayerCount 高字节 |
Flag——0x00 或 0xFF,用途未知 |
OriginX / OriginY 是绘制(blit)该帧时所用的锚点——通常是特效的中心,所以一个 50×48 的爆炸的原点是 25,24。尽管 Flag 字节的含义在全部零售版资源中都未得到确认,kbot 仍原样保留它,以实现逐字节一致的往返。
像素数据
PtrPixelData 指向恰好 Width × Height 个小端序 uint16 值——每像素 2 字节,自上而下、从左到右,没有压缩,也没有行头。这 16 位按帧的 Format 解码:
ARGB4444(Format == 4)
bit 15 ──────────────────────────── bit 0
┌─────┬─────┬─────┬─────┐
│ A │ R │ G │ B │ 4 bits each
└─────┴─────┴─────┴─────┘
15-12 11-8 7-4 3-0
每个 4 位通道通过位复制(v<<4 | v)扩展为 8 位,因此 0xF → 0xFF,0x0 → 0x00。这种扩展是完全可逆的,kbot 正是借此无损地往返像素。
ARGB1555(Format == 5)
bit 15 ──────────────────────────── bit 0
┌─┬───────┬───────┬───────┐
│A│ R │ G │ B │ 1 + 5 + 5 + 5
└─┴───────┴───────┴───────┘
15 14-10 9-5 4-0
单个 alpha 位非此即彼(0 → 完全透明,1 → 完全不透明);每个 5 位颜色通道通过复制(v<<3 | v>>2)扩展。只需要 1 位镂空(cutout)的硬边精灵图用 ARGB1555;需要 16 级柔和 alpha(烟雾、光晕、渐隐)时用 ARGB4444。
Important
TAF 不需要调色板。 GAF 只存储索引,依赖 palettes/palette.pal 或 TA:K 的附属 PCX;而 TAF 的每个像素都已包含自己的颜色和 alpha。渲染就是直接把 16 位扩展为 32 位,不涉及任何查找表。
区分带调色板的 TAF 与真彩色 TAF
两种 .taf 共用 GAF 的文件头和版本字,所以无法凭前 12 个字节区分它们。判别依据是 FrameInfo 偏移量 0x09 处的帧 Format/Compressed 字节:
| 0x09 处的字节 | 文件是 | 读取方式 |
|---|---|---|
0 或 1 |
带调色板的 GAF 式动画(0/1 表示未压缩/已压缩) | GAF 加载器 + 调色板 |
4 或 5 |
真彩色动画(ARGB4444 / ARGB1555) | 本页(formats/tsf) |
kbot taf 会校验这个字节,遇到带调色板的文件时给出明确的错误并拒绝处理;这种情况请改用 kbot gaf。大多数 TA:K 真彩色 TAF 也遵循一个文件名约定——带 _4444 或 _1555 后缀(manabomb_1555.taf、bluefire_4444.taf)——但起决定作用的是格式字节,而不是名称。
TSF——文本形式
TSF(TAF Source Format,TAF 源格式)是用花括号分隔、类似 INI 的脚本表达的同一个动画。横扫千军:王国的界面加载器会直接读取 TSF 作为菜单背景;它也是 kbot 反编译 TAF 时输出的交换格式:每个动画一个节,每帧一个嵌套的 Frame,每帧一个引用外部图像文件的 Layer。
/* anims/titlescreen.tsf — abridged */
[JPGTest]
{
Looping = 0;
[Frame0]
{
Delay = 0;
[Layer0]
{
AnchorX = 0;
AnchorY = 0;
Filename = TitleScreen.jpg;
}
}
[Frame1] { /* … */ }
}
| 记号 | 对应 | 说明 |
|---|---|---|
[Name] { … } |
动画序列 | 每个文档一个;Name 成为 TAF 的序列名。 |
Looping |
(仅供参考) | 制作时的提示;二进制 TAF 中没有循环字段。 |
[FrameN] |
一个 Frame |
Delay 是以游戏 tick 计的时长。 |
Format |
Frame.Format |
ARGB4444(省略时的默认值)或 ARGB1555。 |
[LayerN] |
该帧的图像 | Filename 引用一个外部 PNG/JPG;AnchorX/Y 成为帧的原点。 |
编译后的 TAF 会把每帧唯一的图层拍平为一张真彩色位图。kbot 要求每帧恰好一个图层(多图层合成是界面制作时的便利功能,由编译器在构建时解决)——kbot tsf lint 会标出任何无法顺利编译的内容。
Tip
不碰图像,直接查看一个 TSF。
kbot tsf info anims/titlescreen.tsf # animation, frames, layer filenames
kbot tsf lint anims/titlescreen.tsf # shape check; non-zero exit on errors
lint 只校验结构——要走一遍完整的图像加载管线,请使用 kbot taf compile。
实例解析——manabomb_1555.taf
$ kbot taf info anims/manabomb_1555.taf
TAF: anims/manabomb_1555.taf
Sequence: "manabomb"
Frames: 10
Duration: 20 ticks (0.67s)
# Size Origin Format Duration Flag
─ ──── ────── ────── ──────── ────
0 50x48 25,24 ARGB1555 2 0x00
1 50x48 25,24 ARGB1555 2 0x00
...
9 50x48 25,24 ARGB1555 2 0x00
十个 50×48 的帧,每帧都锚定在其中心(25,24),各保持 2 个 tick——一个持续 ⅔ 秒的爆炸星芒。整个文件 48,376 字节:每帧 50 × 48 × 2 = 4,800 个像素字节 × 10,加上文件头、序列头、帧列表和十条 24 字节的信息记录。
命令行工作流
kbot taf 命令树涵盖查看、渲染、转换以及 TAF↔TSF 编译循环;kbot tsf 负责文本形式。每条命令都通过当前的 kbot 上下文或显式的 --vfs 挂载来解析输入,与 kbot gaf 和 kbot studio 完全一样,所以像 manabomb_1555.taf 这样的裸文件名会在所有已挂载的压缩包中查找。
查看与渲染
kbot taf info anims/bluefire_4444.taf # per-frame table
kbot taf list anims/bluefire_4444.taf # one-line summary
kbot taf render anims/bluefire_4444.taf --frame 4 -o frame4.png
kbot taf sheet anims/bluefire_4444.taf --cols 6 --bg "#202830" -o sheet.png
kbot taf export anims/bluefire_4444.taf --format apng -o bluefire.png # or --format gif
export 默认输出 APNG,它保留完整的 alpha 通道;--format gif 会量化为一张 255 色的颜色表外加一个透明槽(1 位镂空),适合预览和聊天客户端。
反编译 ↔ 编译往返
kbot taf decompile anims/manabomb_1555.taf --target ./manabomb
# → manabomb/manabomb.tsf + manabomb_0.png … manabomb_9.png
kbot taf compile ./manabomb/manabomb.tsf --images ./manabomb -o rebuilt.taf
kbot taf diff anims/manabomb_1555.taf rebuilt.taf # exits 0 — identical
decompile 写出一个 TSF 加上每帧一个 PNG;compile 把它们读回为二进制 TAF。对于 kbot 解析过的任何 TAF,往返都是逐字节一致的——kbot taf roundtrip 和 kbot taf diff 都会验证这一点。
从常见格式导入
kbot taf from-gif boom.gif --format argb4444 --name Boom -o boom.taf
kbot taf from-sheet strip.png --frame-width 64 --frame-height 64 \
--count 8 --delay 3 --format argb1555 -o strip.taf
from-gif 把一个 GIF 动画(遵循其处置方式和逐帧延迟)拍平为真彩色 TAF;from-sheet 把固定网格的 PNG/JPG 精灵图条切分成帧。两者都可以选择目标像素格式。
校验
kbot taf lint anims/manabomb_1555.taf # structural findings; non-zero exit on errors
MCP 工具
同样的功能也通过 MCP 服务器提供,因此智能体可以直接查看并展示动画:
| 工具 | 用途 |
|---|---|
taf_info |
完整的逐帧 JSON:名称、尺寸、原点、格式、时长。 |
taf_list |
单行 JSON 摘要。 |
taf_render |
渲染一帧,并以 PNG 内联返回(也可选择同时保存)。 |
taf_sheet |
把所有帧渲染成一张缩略拼图(contact sheet),以 PNG 内联返回。 |
taf_export |
把 GIF 或 APNG 动画写到磁盘。 |
taf_lint |
以 JSON 返回结构诊断。 |
tsf_info |
概述一个 TSF 文档(动画、帧、图层文件名)。 |
tsf_lint |
检查一个 TSF 文档的结构。 |
taf_render 和 taf_sheet 返回一个图像内容块,所以模型无需再单独读一次文件就能预览法术特效。路径的解析方式与 kbot 的其他所有 MCP 工具相同,都相对于配置的 game-data 文件夹。
坑点
Warning
.taf 有多重含义。 一个 .taf 可能是带调色板的 GAF,也可能是真彩色动画。一定要检查帧格式字节(0x09),而不是看扩展名或文件名——见区分两者。
- 恰好一个序列。 真彩色 TAF 总是只含一个动画;
SequenceCount不是1的话,你看到的就是另一种文件(很可能是带调色板的)。 - 从不压缩。 每一帧都是一张扁平的
Width×Height16 位位图,没有 GAF 那样的 RLE 路径——大型火焰特效(composite02flame)能达到约 600 KB,正是因为什么都没有压缩。 - 名称字段可能含有垃圾数据。 在几个零售版文件中,结尾 NUL 之后的字节未经初始化;要实现精确的往返就保留全部 32 个字节,但读取时只读到 NUL 为止。
0x0B标志字节的含义不明。 观察到的值为0x00或0xFF。kbot 原样保留它;不要把它规范化,否则往返结果就不再一致。- GIF 导出有损。 GIF 只能容纳 1 位镂空和 255 种颜色——ARGB4444 的柔和 alpha 会变成硬边。要保留真正的 alpha 通道请用 APNG。
Delay/Duration为 0 是合法的。 与 GAF 一样,零 tick 的帧由事件驱动(菜单的JPGTest示例对静态背景使用Delay = 0)。不要把它“修正”成某个默认值。
典型大小
| 资源 | 在 TA:K 的 TAF 中观察到的范围 |
|---|---|
小型特效(bluefire_4444,12 帧,每帧约 32 像素) |
约 30–60 KB |
爆炸(manabomb_1555,10 × 50×48) |
约 48 KB |
火球 / 法术(fireballa_1555) |
约 50–120 KB |
大型合成火焰(composite02flame) |
约 600 KB(未压缩的真彩色积少成多) |
| 每帧像素数据 | 恰好 Width × Height × 2 字节 |
另见
- GAF——TAF 所基于的带调色板容器;先读它,带调色板的
.taf文件也用它处理。 - PCX——带调色板的 TA:K 资源如何获得颜色(真彩色 TAF 不需要)。
- 横扫千军:王国界面——其加载器直接读取 TSF 作为动态背景的菜单系统。
- TA 与 TA:K 格式对比——两款游戏共有什么、在哪里分道扬镳。
- 术语表——origin、tick、sub-frame。