TNT — 맵 지형
.tnt파일은 그대로 플레이할 수 있는 완전한 맵입니다: 타일 그래픽, 지형 높이, 지형지물 배치(나무, 바위, 수화 노드 등), 해수면, 252×252 미니맵이 모두 자체 완결된 파일 하나에 들어 있습니다. 지형 형상에는 외부 의존성이 없습니다 —.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
버전 워드는 세 가지가 있습니다. 0x2000은 모든 도구가 쓰는 Total
Annihilation 맵입니다. 0x1020은 TA 3.1c가 여전히 읽는 예전 TA 구조입니다: 속성
레코드가 8바이트이고(높이, 그다음 바이트 2에 한 바이트짜리 지형지물, 0xFC
이상은 없음을 뜻함), 미니맵 포인터와 플래그가 0x38/0x3c에 있습니다. kbot은
이 구조를 읽을 수 있고, 저장하면 0x2000으로 씁니다. 0x4000은 TA가 로드할 수
없는 TA: Kingdoms 맵이며(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픽셀 서브타일마다 하나씩, 속성 칸이 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 항목에는
풋프린트(footprint), 피해, 회수 가치 등도 들어 있습니다.
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은 고유 타일 583개로 만든 4192×4192픽셀 맵이며, 서로 다른
지형지물 정의 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년 문서는 "weird, 6 more than used"(이상하게도 실제보다 6
많음)라고 적어 두었습니다. 요즘 도구는 헤더의 크기를 기준으로 삼고, 쓰이지
않는 뒤쪽 행은 무시합니다. kbot의 리더도 이렇게 동작합니다.
SeaLevel == 0은 "물 없음"이 아닙니다 — 높이 바이트 0이 물속인 가장 높은 높이라는 뜻이며, 결국 물속에 있는 것이 없습니다. 물이 없는 맵은 보통SeaLevel = 1을 씁니다.- **타일 인덱스는
uint16**이므로 맵 하나에 고유 타일이 최대 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을 반드시 설정해야 합니다. 그렇지 않으면 게임이 저장된 미니맵을 무시합니다. 다른 비트는 원래 값대로 두세요.- 네 개의
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지형지물 감시값이 있습니다(초기 리버스 엔지니어링에서 예외 사례를 드러낸 맵 중 하나).acid foursome.tnt— 큰 8인용 맵. 짝이 되는.ota에서[Schema 0]시작 위치가 멀리 흩어져 있는 예를 보여 줍니다.example.tnt— 정식 출시된 TNT 중 가장 작은 파일(약 263 KB).kbot tnt unpack/pack왕복 변환 테스트에 쓸 수 있는 최소한의 맵입니다.
TA: Kingdoms의 TNT 변형(IDVersion 워드가 다름)은 TA:K 맵 연구 노트를 참고하세요.