TNT — рельеф карты
Источник: coreprime/kbot @ 0ab97bf · Редактировать на GitHub
Файл
.tnt— это полная карта, готовая к игре: графика тайлов, высоты рельефа, расстановка объектов (деревья, камни, узлы гидратации, …), уровень моря и миникарта 252×252 — всё в одном самодостаточном файле. Для геометрии не нужно никаких внешних зависимостей: положите.tntв папку игры, и карта загрузится. (Парный файл.otaсодержит только метаданные: название, описание, подсказки для ИИ, стартовые позиции.)
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
См. справочник по 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
Существует три слова версии. 0x2000 — карта Total Annihilation,
которую пишут все инструменты. 0x1020 — более старая структура TA,
которую TA 3.1c всё ещё читает: её записи атрибутов занимают 8 байт
(высота, затем однобайтовый объект в байте 2, где 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 пикселей Width = Height = 256
(ячеек), а в сетке индексов 128 × 128 = 16384 тайлов.
Сетка индексов тайлов (PtrMapData)
(Width/2) × (Height/2) элементов uint16 построчно (внешний цикл по Y,
внутренний по X). Каждый элемент — индекс в массиве графики тайлов.
Тайлы имеют размер 32 × 32 пикселя; сетка — это мозаика, которую игра
выводит, чтобы нарисовать игровое поле.
Note
Инструменты Cavedog дополняют этот блок до границы 16 байт мусором
из рабочей памяти. kbot tnt unpack записывает байты дополнения как есть
в metadata.json (если указан --lossless), чтобы round-trip был
побайтно идентичным.
Сетка атрибутов (PtrMapAttr)
Width × Height ячеек по 4 байта, построчно. Для каждой ячейки:
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 и обозреватель
ресурсов учитывают только те объекты, которые действительно ставит игра.
Сетка атрибутов вдвое плотнее сетки тайлов по каждому измерению — на каждый тайл приходится 4 ячейки атрибутов, по одной на 16-пиксельный подтайл. Благодаря этому движок разрешает поиск пути юнитов, размещение построек и столкновения оружия с точностью до 16 пикселей.
Warning
0xFFFE и 0xFFFD встречаются в оригинальном контенте (Lava Run,
Metal Heck и пара ранних карт кампании). Это не индексы объектов и не
пустота: игра ничего туда не ставит, и на этих ячейках можно строить.
kbot сохраняет исходное значение при round-trip.
Графика тайлов (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;
Эти имена разрешаются по секциям [FEATURE] в features/**/*.tdf (см.
TDF) — так движок узнаёт, какую сетку .3do и какой спрайт
.gaf рисовать для «MetalTower10» или «ArmTreeFoo». Запись TDF объекта
также содержит площадь основания, урон, ценность при утилизации и т. д.
Note
Инструменты Cavedog оставляют неинициализированный мусор в хвосте
каждого Name[128] после завершающего NUL. kbot tnt unpack (по умолчанию) записывает в features.csv только печатаемую
часть, а при round-trip таблицы собираются чистыми. Передайте
--lossless, чтобы сохранить сырые байты в точности для побайтно
идентичной упаковки.
Миникарта (PtrMiniMap)
uint32 Width; // 252
uint32 Height; // 252 (33 of the 275 retail maps store 256)
uint8 pixels[Width * Height]; // Palette indices
Игра читает записанную миникарту, только если установлен бит 0
слова заголовка по смещению 0x2c (MinimapFlags; 0x3c в карте
0x1020); иначе она строит изображение радара по тайлам. Во всех 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), и область всё равно вычисляется по
формуле. Её вычисляют Map.MinimapContentBounds и MinimapContentSize в
kbot-io; обозреватель ресурсов ставит в ней маркеры 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 уникальных
тайлов, с 74 расставленными объектами 28 разных определений. Определения
.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
Распаковка по умолчанию идёт с потерями. Таблица имён объектов не
попадает в metadata.json и при упаковке собирается заново из
уникальных имён в features.csv. При этом теряется мусор, оставленный
в таблице инструментами Cavedog. Для побайтно идентичного round-trip
передайте --lossless, чтобы записать сырые байты объектов:
kbot tnt unpack "metal heck.tnt" -t ./metal_heck --lossless
Парные файлы .ota
Каждый .tnt поставляется с сопутствующим файлом .ota («Online Total
Annihilation») в том же каталоге. В нём хранятся метаданные миссии:
читаемое название, название планеты, погода, гравитация, стартовые
позиции, задание для ИИ и т. д.
См. TDF: .ota — тот же текстовый формат в стиле INI, что и
.fbi с .tdf. kbot tnt preview автоматически читает его, чтобы
нарисовать на превью пронумерованные маркеры стартовых позиций. Все
представления карты в kbot читают схемы и стартовые позиции .ota так
же, как игра (см. TDF — OTA); редактор карт
студии при сохранении изменяет .ota открытой карты на месте.
Подводные камни
Warning
Редактор карт (TAE) в некоторых версиях записывает
Height = actual_height + 6 — в документации Saruman 1997 года было
отмечено: «странно, на 6 больше, чем используется». Современные
инструменты считают размеры из заголовка определяющими и игнорируют
неиспользуемые строки в конце. Читатель kbot ведёт себя так же.
SeaLevel == 0НЕ означает «нет воды» — это значит, что байт высоты 0 — наибольшая подводная высота, то есть под водой ничего нет. Карты без воды обычно используютSeaLevel = 1.- Индексы тайлов —
uint16, так что на одной карте может быть до 65 536 уникальных тайлов, но на практике всё, что больше нескольких тысяч, загонит движок в своп из-за нехватки памяти. Metal Heck (583 тайла) ближе к медиане, чем к хвосту распределения. Студия отказывается сохранять карту, которой нужно больше, аkbot tnt packотвергает значения вtilemap.csv, не являющиеся индексами тайлов карты (например,-1или70000, которое превратилось бы в 4464). Ячейка, указывающая на тайл за пределами набора тайлов, загружается (игра читает за концом своего набора тайлов);kbot tnt lintсообщает о ней как оbad-tile-index. - Заполнение миникарты — индекс палитры
0x64; движок игнорирует пиксели вне области карты. - Бит 0
MinimapFlags(0x2c, или0x3cв карте0x1020) должен быть установлен, иначе игра игнорирует записанную миникарту. Остальные биты оставляйте такими, какими они были. - Четыре поля
padвсегда равны нулю. Некоторые сторонние редакторы пишут туда ненулевые значения; игра, по-видимому, их игнорирует, но kbot сохранит всё, что найдёт. - TA: Kingdoms использует родственный, но несовместимый формат карт
(карты высот
.btm/.bmp+ страницы тайлов). См. Карты TA:K.
Типичные размеры
| Ресурс | Диапазон в maps/*.tnt от Cavedog |
|---|---|
| Размер файла | 0,3 – 7 МБ |
| Размеры карты (в тайлах) | 64×64 – 256×256 (то есть стороны 2048 – 8192 пикселя) |
| Уникальных тайлов | 200 – 2000 |
| Расставленных объектов | 10 – 600 |
| Определений объектов в таблице | 5 – 50 |
| Заголовок | всегда 64 байта |
| Миникарта (252×252 с палитрой; в 33 оригинальных картах 252×256) | 63504 байта |
Размер текста парного .ota |
1 – 8 КБ |
Живые примеры из справочного каталога
Примечательные карты для изучения — их можно извлечь из любой установки
TA по пути maps/<name>.tnt:
metal heck.tnt— разобрана в примере выше. 4192×4192 пикселя, 583 уникальных тайла, 74 расставленных объекта. Культовая карта, богатая ресурсами.a gentle time.tnt— небольшая карта на 4 игроков; хорошая отправная точка при создании прототипа просмотрщика TNT или конвейера отрисовки.lava run.tnt— в ячейках атрибутов есть необычные сигнальные значения объектов0xFFFE(одна из карт, породивших пограничные случаи на заре реверс-инжиниринга).acid foursome.tnt— большая карта на 8 игроков, показывает далеко разнесённые стартовые позиции[Schema 0]в парном.ota.example.tnt— самый маленький поставляемый TNT (~263 КБ); минимальная рабочая карта для проверки round-tripkbot tnt unpack/pack.
Варианты TNT из TA: Kingdoms (с другим словом IDVersion) описаны в исследовательских заметках о картах TA:K.
См. также
- SCT — строительные блоки, из которых TAE собирает TNT.
- TDF — определения объектов, разрешаемые по имени, и формат
парного файла
.ota. - PAL — палитра для отрисовки пикселей тайлов и миникарты.
- GAF — спрайты объектов, которые накладывает
kbot tnt preview. - Карты TA:K — совсем другой конвейер карт TA: Kingdoms.
- Глоссарий — attribute cell, sea level, void cell.