모든 형식 페이지 ↓
맵·지형

TNT — 맵 지형

출처: coreprime/kbot @ 0ab97bf · GitHub에서 편집

.tnt 파일은 그대로 플레이할 수 있는 완전한 맵입니다: 타일 그래픽, 지형 높이, 지형지물 배치(나무, 바위, 수화 노드 등), 해수면, 252×252 미니맵이 모두 자체 완결된 파일 하나에 들어 있습니다. 지형 형상에는 외부 의존성이 없습니다 — .tnt를 게임 폴더에 넣으면 바로 로드됩니다. (짝이 되는 .ota 파일은 이름, 설명, AI 힌트, 시작 위치 같은 메타데이터만 담습니다.)

지형지물 스프라이트와 시작 위치 표시를 함께 렌더링한 Metal Heck 맵
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 맵 연구 노트를 참고하세요.


함께 보기

  • SCT — TAE가 이어 붙여 TNT를 만드는 구성 요소.
  • TDF — 이름으로 찾는 지형지물 정의와, 짝이 되는 .ota 파일 형식.
  • PAL — 타일 픽셀과 미니맵을 렌더링하는 데 쓰는 팔레트.
  • GAF — kbot tnt preview가 합성하는 지형지물 스프라이트.
  • TA:K 맵 — 전혀 다른 TA: Kingdoms 맵 파이프라인.
  • 용어집 — attribute cell, sea level, void cell.