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
Див. довідник 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
Існує три слова версії. 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 px має
Width = Height = 256 (клітинок) і 128 × 128 = 16384 тайли в сітці
індексів.
Сітка індексів тайлів (PtrMapData)
(Width/2) × (Height/2) елементів uint16 у порядку рядків (Y —
зовнішній цикл, X — внутрішній). Кожен елемент — це індекс у масиві
графіки тайлів. Тайли мають розмір 32 × 32 px; сітка — це мозаїка, яку
гра виводить (blit), щоб намалювати ігрове поле.
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) — так рушій дізнається, яку 3D-сітку .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 px) |
| Унікальні тайли | 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 px, 583 унікальні тайли, 74 розміщення об'єктів. Культова мапа, багата на ресурси.a gentle time.tnt— невелика мапа на 4 гравців; добрий початок, коли робите прототип переглядача TNT або конвеєра рендерингу.lava run.tnt— має незвичні сторожові значення об'єктів0xFFFEу клітинках атрибутів (одна з мап, на яких виявилися граничні випадки під час раннього реверс-інжинірингу).acid foursome.tnt— велика мапа на 8 гравців, демонструє широко розкидані стартові позиції[Schema 0]у сусідньому.ota.example.tnt— найменший постачуваний TNT (~263 КБ); мінімальна робоча мапа для перевірки round-trip черезkbot 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.