TNT — マップの地形
.tntファイルはプレイ可能なマップ一式です。タイル画像、地形の高さ、地形物の配置(木、岩、給水ノードなど)、海面、252×252のミニマップが、1つの自己完結したファイルにすべて入っています。形状については外部への依存がなく、.tntをゲームのフォルダーに置けば読み込まれます。(対になる.otaファイルはメタデータだけです。名前、説明、AIへのヒント、開始位置が入っています。)
maps/metal heck.tnt — features/*.tdfの地形物スプライトを合成し、
対になる.otaの開始位置マーカーを加えた完全なプレビュー
埋め込みのミニマップ(左)と、正規化した標高グリッド(右)。
Tip
自分で試す
kbot tnt describe "maps/metal heck.tnt"
kbot tnt minimap "maps/metal heck.tnt" -t mini.png
kbot tnt heightmap "maps/metal heck.tnt" --normalize -t h.png
kbot tnt image "maps/metal heck.tnt" -t map.png # raw tile mosaic
kbot tnt preview "maps/metal heck.tnt" -t preview.jpg # + feature sprites + StartPos
# And the reverse: decompose into editable files, edit, repack.
kbot tnt unpack "maps/metal heck.tnt" -t ./metal_heck
kbot tnt pack ./metal_heck -t metal_heck.tnt
CLIのkbot tntリファレンスを参照してください。
Goから使う場合はformats/tntを使います。
import "github.com/coreprime/kbot/formats/tnt"
f, _ := os.Open("maps/metal heck.tnt")
defer f.Close()
m, _ := tnt.LoadFromReader(f)
fmt.Printf("%dx%d tiles, %d unique, %d features\n",
m.TileW, m.TileH, m.Header.Tiles, m.Header.TileAnims)
概要
┌─ Header (64 B) ─────────────────────────────────────────────┐
│ IDVersion (0x2000) Width Height (in 16-px attribute cells)│
│ PtrMapData PtrMapAttr PtrTileGfx Tiles TileAnims │
│ PtrTileAnim SeaLevel PtrMiniMap MinimapFlags pad×4 │
└──────────────────────────────┬──────────────────────────────┘
│ offsets are absolute
┌─────────────────┬─────┴────────┬──────────────────────┐
▼ ▼ ▼ ▼
┌─ Map data ────┐ ┌─ Attributes ─┐ ┌─ Tile graphics ─┐ ┌─ Tile animations ─┐
│ uint16 grid │ │ 4 B per 16px │ │ Tiles × 1024 B │ │ TileAnims × 132 B │
│ (TileW×TileH)│ │ cell: │ │ (32×32 idx) │ │ name[128] each │
│ │ │ h, feat, _ │ │ │ │ │
└───────────────┘ └──────────────┘ └─────────────────┘ └───────────────────┘
▼
┌─ Minimap (252×252 paletted) ─┐
└──────────────────────────────┘
ヘッダー(64バイト)
typedef struct {
uint32 IDVersion; // 0x2000 (TA); 0x1020 is the older TA layout
uint32 Width; // In 16-pixel attribute cells (tile width = Width/2)
uint32 Height; // In 16-pixel attribute cells
uint32 PtrMapData; // → tile-index grid
uint32 PtrMapAttr; // → per-cell attributes
uint32 PtrTileGfx; // → tile pixel data
uint32 Tiles; // unique tile count
uint32 TileAnims; // feature table count (must be < 0xFFFB)
uint32 PtrTileAnim; // → feature name table
uint32 SeaLevel; // heights below this = underwater
uint32 PtrMiniMap; // → embedded 252×252 minimap
uint32 MinimapFlags; // 0x2c: bit 0 set = the game reads the minimap
uint32 pad1, pad2, pad3, pad4; // observed all zero
} TNTHeader;
Note
バージョンワードは3つあります。0x2000は、どのツールも書き出すTotal Annihilationのマップです。0x1020は、TA 3.1cが今も読む古いTAの配置です。その属性レコードは8バイト(高さ、続いてバイト2に1バイトの地形物があり、0xFC以上はなしを意味します)で、ミニマップのポインターとフラグは0x38/0x3cにあります。kbotはこれを読み、保存すると0x2000で書き出します。0x4000はTA: Kingdomsのマップで、TAでは読み込めません(TA:Kのマップを参照)。kbot tnt describe、MCPのtnt_describeツール、スタジオのOpen Mapの選択画面は、そのことを表示します。
Important
WidthとHeightはタイルではなく、16ピクセルの属性セル単位です。タイル数を得るには2で割り、ピクセル寸法を得るには16を掛けます。つまり、4096×4096 pxのマップはWidth = Height = 256(セル)で、インデックスグリッドには128 × 128 = 16384枚のタイルがあります。
タイルインデックスのグリッド(PtrMapData)
(Width/2) × (Height/2)個のuint16のエントリで、行優先(外側がY、内側がX)です。各エントリは、タイル画像の配列を指すインデックスです。タイルは32 × 32 pxで、このグリッドは、ゲームがプレイフィールドを描くために転送するモザイクです。
Note
Cavedogのツールはこのブロックを、作業用メモリの内容で16バイト境界まで埋めています。kbot tnt unpackは(--losslessを指定したとき)その埋め草のバイトをmetadata.jsonにそのまま記録するので、ラウンドトリップはバイト単位で同一になります。
属性グリッド(PtrMapAttr)
各4バイトのセルがWidth × Height個、行優先で並びます。セルごとの内容は次のとおりです。
typedef struct {
uint8 Height; // Elevation (0..255)
uint16 Feature; // Feature placement index into the feature table,
// or 0xFFFF if no feature,
// or 0xFFFC if the cell is void / impassable
uint8 _pad; // Zero; alignment to 4 bytes
} TNTAttrCell;
ゲームが地形物を置くのは、ワードが地形物テーブルの数(TileAnims)より小さいときだけです。0xFFFCはボイド(void)のセルを示し、それ以外のワードでは何も置かれません。0xFFFB以上のワードはテーブルの大きさにかかわらず番兵値なので、テーブルの項目数は0xFFFB未満でなければなりません。kbot tnt describe、kbot tnt features、アセットエクスプローラーは、ゲームが実際に行う配置だけを数えます。
属性グリッドは、タイルのグリッドより各次元で2倍細かくなっています。各タイルには、16ピクセルのサブタイルごとに1つ、合計4つの属性セルがあります。これにより、エンジンはユニットの経路探索、建設の配置、武器の衝突判定を16ピクセル単位で解決できます。
Warning
製品版のデータには0xFFFEと0xFFFDが現れます(Lava Run、Metal Heck、初期のキャンペーンマップのいくつか)。これらは地形物のインデックスでもボイドでもありません。ゲームはそこに何も置かず、セルは建設可能なままです。kbotはラウンドトリップで元の値を保持します。
タイル画像(PtrTileGfx)
Tiles × 1024バイトのパレットインデックスで、各32×32のブロック内では上から下、左から右の順に並び、ブロックは連続して格納されます。マップはタイルの絵をすべて自身の中に持っているので、TNTをゲームのフォルダーに置けば、そのまま遊べます。
地形物の名前(PtrTileAnim)
次のエントリがTileAnims個並びます。
typedef struct {
uint32 Index; // Matches array position; safe to ignore on read
char Name[128]; // NUL-padded, references a [FEATURE] section
// in features/*.tdf
} TNTFeatureEntry;
これらの名前は、features/**/*.tdfの[FEATURE]セクション(TDFを参照)に対して解決されます。こうしてエンジンは、「MetalTower10」や「ArmTreeFoo」にどの.3doメッシュとどの.gafスプライトを描けばよいかを知ります。地形物のTDFの項目には、フットプリント、ダメージ、回収したときの価値なども入っています。
Note
Cavedogのツールは、各Name[128]の末尾部分、NUL終端の後に、初期化されていない作業用のバイトを残しています。kbot tnt unpackは(既定では)表示可能な部分だけをfeatures.csvに書き出し、ラウンドトリップではきれいなテーブルを作り直します。バイト単位で同一にパックするために生のバイトを正確に保持するには、--losslessを渡してください。
ミニマップ(PtrMiniMap)
uint32 Width; // 252
uint32 Height; // 252 (33 of the 275 retail maps store 256)
uint8 pixels[Width * Height]; // Palette indices
ゲームが格納されたミニマップを読むのは、0x2cにあるヘッダーのワード(MinimapFlags。0x1020のマップでは0x3c)のビット0がセットされているときだけです。そうでなければ、タイルからレーダーの画像を作ります。製品版の275のマップはすべてこれをセットしており、スタジオが保存するマップもすべてそうです。ゲームのレーダーが格納されたミニマップを使うのは、その両辺が252以上のときだけです。
マップはミニマップの左上の隅に描かれ、残りはパレットインデックス0x64の余白です(いくつかのキャンペーンマップは0で埋めています)。この領域が示すのは見えるマップで、タイルより幅が32ピクセル、高さが128ピクセル小さくなっています(ゲームはその端を決して表示しません)。領域の長いほうの辺は252で、もう一方の辺は見えるマップの縦横比を保って切り捨てた値です。
visible_w = Width * 16 - 32; // Width/Height in attribute cells
visible_h = Height * 16 - 128;
if (visible_w >= visible_h) { region_w = 252; region_h = 252 * visible_h / visible_w; }
else { region_h = 252; region_w = 252 * visible_w / visible_h; }
したがって、225×196タイルでは領域が252×216、128×96(ac04)では252×182、96×128では193×252になります。領域はピクセルではなく、マップの大きさで決まります。製品版のミニマップには余白がまったくないもの(ac01、ac04〜ac11は252×256を埋めています)もありますが、それでも領域は計算したとおりです。kbot-ioのMap.MinimapContentBoundsとMinimapContentSizeがこれを計算します。アセットエクスプローラーはこの領域の中にStartPosのマーカーを置き、スタジオはこの方法で作ったミニマップを保存します(Map.BuildMinimap。ミニマップの各ピクセルは、それが覆うマップのピクセルの平均色です)。
実例 — metal heck.tnt
$ kbot tnt describe "maps/metal heck.tnt"
File Size: 973176 bytes
Header:
IDVersion: 0x2000 (Total Annihilation)
Width: 262 (16px cells) -> 131 tiles, 4192 pixels
Height: 262 (16px cells) -> 131 tiles, 4192 pixels
SeaLevel: 1
Tiles: 583 unique
Features: 28 in table, 74 placements
Minimap: 252x252
MinimapFlags: 1 (bit 0 set, the game reads the stored minimap)
Pads: 0 0 0 0
Elevation:
min=13 max=181 mean=24.7 cells below sealevel: 0 (0.00%)
Top features:
[ 6] MetalTower10 count=10
[ 14] MetalTower09 count=9
[ 5] MetalVent01 count=8
...
この出力の読み方は次のとおりです。Metal Heckは4192×4192ピクセルのマップで、583種類のタイルからできており、28種類の地形物の定義から74個の地形物が配置されています。それらの地形物の.tdf定義は、「Metal」ワールドのアーカイブが提供します。
分解と編集
kbot tnt unpack <file.tnt> --target <dir>は、対応するpackコマンドが読み戻せる構成を作ります。
metal_heck/
├── map.png full RGBA render of the tile mosaic
├── heightmap.png 8-bit grayscale, pixel value = raw elevation byte
├── minimap.png paletted PNG of the embedded minimap
├── tiles/<n>.png paletted 32×32 per unique tile
├── tilemap.csv 2D grid of tile indices (rows = y, cols = x)
├── features.csv feature_index,name,attr_x,attr_y per placement
└── metadata.json header constants + round-trip data
上のどれでも編集して、パックし直します。
kbot tnt pack ./metal_heck --target metal_heck.tnt
Important
既定のunpackは非可逆です。地形物の名前テーブルはmetadata.jsonから除かれ、パックの際にfeatures.csvにある一意な名前から作り直されます。そのため、Cavedogのツールがテーブルに残した末尾の作業用バイトは失われます。バイト単位で同一のラウンドトリップを行うには、--losslessを渡して地形物の生のバイトを記録してください。
kbot tnt unpack "metal heck.tnt" -t ./metal_heck --lossless
対になる.otaファイル
どの.tntにも、同じディレクトリに.ota(「Online Total Annihilation」)のサイドカーが付いています。ミッションのメタデータはそこにあります。人が読める名前、惑星名、天候、重力、開始位置、AIへの指示などです。
TDFを参照してください。.otaは.fbiや.tdfと同じINI風のテキスト形式です。kbot tnt previewはこれを自動的に読み、プレビュー画像に番号付きの開始位置マーカーを描きます。kbotでマップを表示する画面はどれも、.otaのスキーマと開始位置をゲームと同じ方法で読みます(TDF — OTAを参照)。スタジオのマップエディターは、開いたマップの.otaを保存時にその場で書き換えます。
注意点
Warning
マップエディター(TAE)は、バージョンによってはHeight = actual_height + 6を書き込みます。Sarumanによる1997年の文書にも「なぜか、使われているより6多い」と記されています。現代のツールはヘッダーの寸法を正式なものとして扱い、使われない末尾の行を無視します。kbotのリーダーもこの振る舞いに合わせています。
SeaLevel == 0は「水がない」という意味ではありません。高さのバイト0が水中の最も高い高さだという意味で、つまり水中にあるものはありません。水のないマップでは、ふつうSeaLevel = 1を使います。- タイルインデックスは
uint16です。そのため、1つのマップは最大65,536種類のタイルを持てますが、実際には数千を超えると、エンジンはメモリ不足でスワップに追い込まれます。Metal Heck(583タイル)は、分布の裾よりも中央値に近いところにあります。スタジオはそれ以上が必要なマップの保存を拒否し、kbot tnt packは、マップのタイルのインデックスでないtilemap.csvの値(-1や、4464に折り返されてしまう70000など)を拒否します。タイルセットの範囲を超えたタイルを指すセルは読み込まれます(ゲームはタイルセットの先まで読みます)。kbot tnt lintはこれをbad-tile-indexとして報告します。 - ミニマップの余白はパレットインデックス
0x64です。エンジンはマップの領域外のピクセルを無視します。 MinimapFlags(0x2c、0x1020のマップでは0x3c)のビット0はセットされていなければなりません。そうでないと、ゲームは格納されたミニマップを無視します。ほかのビットは見つけたとおりに保ってください。- 4つの
padフィールドは常に0です。一部のサードパーティのエディターは0以外の値を書き込みます。ゲームはそれを無視するようですが、kbotは見つけた値をそのまま保持します。 - TA: Kingdomsは、関連はあるが互換性のないマップ形式を使います(
.btm/.bmpのハイトマップとタイルページ)。TA:Kのマップを参照してください。
典型的なサイズ
| アセット | Cavedogのmaps/*.tntで観測された範囲 |
|---|---|
| ファイルサイズ | 0.3〜7 MB |
| マップの寸法(タイル) | 64×64〜256×256(一辺2048〜8192 px) |
| 一意なタイル | 200〜2000 |
| 地形物の配置 | 10〜600 |
| テーブル内の地形物の定義 | 5〜50 |
| ヘッダーのオーバーヘッド | 常に64バイト |
| ミニマップ(252×252のパレット付き。製品版の33のマップは252×256) | 63504バイト |
対になる.otaのテキストサイズ |
1〜8 KB |
リファレンスカタログにある実例
研究に向いた主なマップです。どのTAのインストールからでも、maps/<name>.tntから取り出せます。
metal heck.tnt— 上の実例で取り上げたマップ。4192×4192 px、583種類のタイル、74個の地形物の配置。資源の豊富な、象徴的なマップです。a gentle time.tnt— 小さな4人用マップ。TNTビューアーや描画処理の試作を始めるのに向いています。lava run.tnt— 属性セルに、珍しい0xFFFEの地形物番兵値があります(初期のリバースエンジニアリングで、端のケースを洗い出すきっかけになったマップの1つ)。acid foursome.tnt— 大きな8人用マップ。対になる.otaの[Schema 0]で、開始位置が遠く離れて配置されている例です。example.tnt— 出荷されている最小のTNT(約263 KB)。kbot tnt unpack/packのラウンドトリップを試すための最小限のマップです。
TA: KingdomsのTNTの変種(IDVersionのワードが異なる)については、TA:Kのマップの調査メモを参照してください。