Todas as páginas de formatos ↓
Mapas e terreno

TNT: o terreno do mapa

Fonte: coreprime/kbot @ 0ab97bf · Editar no GitHub

Um arquivo .tnt é um mapa jogável completo: gráficos dos blocos, alturas do terreno, posicionamento dos elementos (árvores, rochas, nós de hidratação, …), nível do mar e um minimapa de 252×252, tudo num único arquivo autossuficiente. A geometria não depende de nada externo: ponha um .tnt numa pasta do jogo e ele carrega. (O arquivo .ota que o acompanha só tem metadados: nome, descrição, dicas para a IA, posições iniciais.)

Mapa Metal Heck renderizado com os sprites dos elementos e os marcadores de início
maps/metal heck.tnt: prévia completa com os sprites dos elementos compostos a partir de features/*.tdf
e os marcadores de posição inicial do .ota irmão

Minimapa embutido    Mapa de altura normalizado
Minimapa embutido (à esquerda) e grade de elevação normalizada (à direita).

Tip

Experimente você mesmo.

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

Veja a referência do kbot tnt da linha de comando.

Em Go. Use 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)

Visão geral

┌─ 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) ─┐
                                              └──────────────────────────────┘

Cabeçalho (64 bytes)

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

Existem três valores de versão. 0x2000 é o mapa do Total Annihilation que todas as ferramentas gravam. 0x1020 é uma estrutura mais antiga do TA que o TA 3.1c ainda lê: os seus registros de atributo têm 8 bytes (a altura e depois um elemento de um byte no byte 2, em que 0xFC ou mais significa nenhum), e o ponteiro e as flags do minimapa ficam em 0x38/0x3c. O kbot o lê, e salvá-lo grava 0x2000. 0x4000 é um mapa do TA: Kingdoms, que o TA não consegue carregar (veja Mapas do TA:K). kbot tnt describe, a ferramenta MCP tnt_describe e o seletor Open Map do studio avisam isso.

Important

Width e Height são medidos em células de atributo de 16 pixels, não em blocos. Divida por 2 para obter o número de blocos e multiplique por 16 para ter as dimensões em pixels. Assim, um mapa de 4.096×4.096 px tem Width = Height = 256 (células) e 128 × 128 = 16384 blocos na sua grade de índices.


Grade de índices de blocos (PtrMapData)

(Width/2) × (Height/2) entradas uint16, linha a linha (Y por fora, X por dentro). Cada entrada é um índice no array de gráficos dos blocos. Os blocos têm 32 × 32 px. A grade é o mosaico que o jogo copia para desenhar a área de jogo.

Note

As ferramentas da Cavedog completam este bloco até um limite de 16 bytes com restos de memória. kbot tnt unpack registra os bytes de preenchimento tal como estão em metadata.json (quando --lossless é usado), para que a ida e volta seja idêntica byte a byte.

Grade de atributos (PtrMapAttr)

Width × Height células de 4 bytes cada, linha a linha. Por célula:

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;

O jogo só posiciona um elemento quando o valor é menor que o número de entradas da tabela de elementos (TileAnims). 0xFFFC marca uma célula de vazio, e qualquer outro valor não posiciona nada. Os valores a partir de 0xFFFB são sentinelas, qualquer que seja o tamanho da tabela, por isso uma tabela precisa ter menos de 0xFFFB entradas. kbot tnt describe, kbot tnt features e o explorador de recursos contam só os posicionamentos que o jogo de fato faz.

A grade de atributos é 2× mais densa que a grade de blocos em cada dimensão: cada bloco tem 4 células de atributo, uma por sub-bloco de 16 pixels. É isso que permite ao motor resolver os caminhos das unidades, o posicionamento de construções e a colisão das armas com granularidade de 16 pixels.

Warning

0xFFFE e 0xFFFD aparecem no conteúdo da versão comercial (Lava Run, Metal Heck e alguns mapas iniciais da campanha). Não são índices de elementos nem vazio: o jogo não posiciona nada ali, e as células continuam permitindo construções. O kbot preserva o valor original na ida e volta.

Gráficos dos blocos (PtrTileGfx)

Tiles × 1024 bytes de índices da paleta, de cima para baixo e da esquerda para a direita dentro de cada bloco de 32×32, com os blocos guardados em sequência. O mapa traz toda a arte dos seus blocos embutida: ponha um TNT na pasta do jogo e ele roda.

Nomes dos elementos (PtrTileAnim)

TileAnims entradas de:

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;

Esses nomes são resolvidos nas seções [FEATURE] de features/**/*.tdf (veja TDF): é assim que o motor sabe qual malha .3do e qual sprite .gaf desenhar para "MetalTower10" ou "ArmTreeFoo". A entrada TDF do elemento também traz a área ocupada, o dano, o valor de reciclagem etc.

Note

As ferramentas da Cavedog deixam bytes de memória não inicializados no final de cada Name[128], depois do terminador NUL. kbot tnt unpack (padrão) grava só a parte imprimível em features.csv, e a ida e volta reconstrói tabelas limpas. Passe --lossless para preservar os bytes brutos exatamente e empacotar de forma idêntica byte a byte.

Minimapa (PtrMiniMap)

uint32 Width;   // 252
uint32 Height;  // 252 (33 of the 275 retail maps store 256)
uint8  pixels[Width * Height];  // Palette indices

O jogo só lê o minimapa guardado quando o bit 0 do valor do cabeçalho em 0x2c (MinimapFlags; 0x3c num mapa 0x1020) está ligado. Caso contrário, monta a imagem do radar a partir dos blocos. Todos os 275 mapas da versão comercial o ligam, assim como todo mapa que o studio salva. O radar do jogo só usa um minimapa guardado quando os dois lados dele têm pelo menos 252.

O mapa é desenhado no canto superior esquerdo do minimapa, e o resto é preenchimento, com o índice da paleta 0x64 (alguns mapas da campanha preenchem com 0). A região mostra o mapa visível, que é 32 pixels mais estreito e 128 pixels mais baixo que os blocos (o jogo nunca mostra essas bordas): o seu lado maior mede 252 e o outro mantém a proporção do mapa visível, arredondada para baixo:

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; }

Assim, 225×196 blocos dão uma região de 252×216, 128×96 (ac04) dá 252×182 e 96×128 dá 193×252. A região é fixada pelo tamanho do mapa, não pelos pixels: alguns minimapas da versão comercial não têm preenchimento nenhum (ac01, ac04 a ac11 preenchem 252×256), e a região continua sendo a calculada. Map.MinimapContentBounds e MinimapContentSize do kbot-io a calculam. O explorador de recursos posiciona os marcadores de StartPos nela, e o studio salva um minimapa montado dessa forma (Map.BuildMinimap: cada pixel do minimapa é a cor média dos pixels do mapa que ele cobre).


Exemplo comentado: 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
  ...

Como ler isto: Metal Heck é um mapa de 4.192×4.192 pixels feito de 583 blocos únicos, com 74 posicionamentos de elementos tirados de 28 definições de elementos distintas. O arquivo compactado do mundo "Metal" fornece as definições .tdf desses elementos.


Desempacotar e editar

kbot tnt unpack <file.tnt> --target <dir> produz uma estrutura que o comando pack correspondente lê de volta:

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

Edite qualquer um desses arquivos e reempacote:

kbot tnt pack ./metal_heck --target metal_heck.tnt

Important

O desempacotamento padrão tem perdas. A tabela de nomes de elementos fica de fora de metadata.json e é reconstruída no empacotamento a partir dos nomes únicos de features.csv. Com isso se perdem os bytes de memória que as ferramentas da Cavedog deixaram no final da tabela. Para idas e voltas idênticas byte a byte, passe --lossless para registrar os bytes brutos dos elementos:

kbot tnt unpack "metal heck.tnt" -t ./metal_heck --lossless

Os arquivos .ota irmãos

Todo .tnt vem com um arquivo acompanhante .ota ("Online Total Annihilation") na mesma pasta. É ali que ficam os metadados da missão: nome legível, nome do planeta, clima, gravidade, posições iniciais, instruções para a IA etc.

Veja TDF: o .ota tem o mesmo formato de texto no estilo INI de .fbi e .tdf. kbot tnt preview o lê automaticamente para desenhar os marcadores numerados de posição inicial na imagem de prévia. Todas as visualizações de mapa do kbot leem os esquemas e as posições iniciais do .ota da mesma forma que o jogo (veja TDF: OTA), e o editor de mapas do studio edita o .ota de um mapa aberto no próprio arquivo ao salvar.


Armadilhas

Warning

O editor de mapas (TAE) grava Height = actual_height + 6 em algumas versões: a documentação de Saruman, de 1997, já notava "estranho, 6 a mais do que o usado". As ferramentas modernas tratam as dimensões do cabeçalho como a referência e ignoram as linhas finais não usadas. O leitor do kbot se comporta da mesma forma.

  • SeaLevel == 0 NÃO significa "sem água": significa que o byte de altura 0 é a maior altura debaixo d'água, ou seja, nada fica debaixo d'água. Mapas sem água costumam usar SeaLevel = 1.
  • Os índices de blocos são uint16, então um único mapa pode ter até 65.536 blocos únicos, mas na prática qualquer coisa além de alguns milhares leva o motor a trocar memória com o disco. Metal Heck (583 blocos) está mais perto da mediana do que do extremo. O studio se recusa a salvar um mapa que precise de mais, e kbot tnt pack recusa valores de tilemap.csv que não sejam índices de blocos do mapa (como -1 ou 70000, que dariam a volta para 4.464). Uma célula que aponta para um bloco além do conjunto de blocos carrega (o jogo lê além do seu conjunto de blocos). kbot tnt lint a informa como bad-tile-index.
  • O preenchimento do minimapa usa o índice da paleta 0x64. O motor ignora os pixels fora da região do mapa.
  • O bit 0 de MinimapFlags (0x2c, ou 0x3c num mapa 0x1020) precisa estar ligado, senão o jogo ignora o minimapa guardado. Mantenha os outros bits como estão.
  • Os quatro campos pad são sempre zero. Alguns editores de terceiros gravam valores diferentes de zero. O jogo parece ignorá-los, mas o kbot preserva o que encontrar.
  • O TA: Kingdoms usa um formato de mapa parecido, mas incompatível (mapas de altura .btm / .bmp + páginas de blocos). Veja Mapas do TA:K.

Tamanhos típicos

Recurso Faixa observada em maps/*.tnt da Cavedog
Tamanho do arquivo 0,3 – 7 MB
Dimensões do mapa (blocos) 64×64 – 256×256 (ou seja, lados de 2.048 – 8.192 px)
Blocos únicos 200 – 2.000
Posicionamentos de elementos 10 – 600
Definições de elementos na tabela 5 – 50
Custo do cabeçalho sempre 64 bytes
Minimapa (252×252 com paleta; 33 mapas comerciais têm 252×256) 63.504 bytes
Tamanho do texto do .ota acompanhante 1 – 8 KB

Exemplos reais no catálogo de referência

Mapas que vale a pena estudar. Extraia-os de qualquer instalação do TA em maps/<name>.tnt:

  • metal heck.tnt: aparece no exemplo comentado acima. 4.192×4.192 px, 583 blocos únicos, 74 posicionamentos de elementos. Um mapa clássico, rico em recursos.
  • a gentle time.tnt: mapa pequeno para 4 jogadores. Bom ponto de partida ao prototipar um visualizador de TNT ou um processo de renderização.
  • lava run.tnt: tem sentinelas de elemento 0xFFFE incomuns nas células de atributo (um dos mapas que trouxeram os casos-limite do início da engenharia reversa).
  • acid foursome.tnt: mapa grande para 8 jogadores. Mostra posições iniciais [Schema 0] bem espalhadas no .ota irmão.
  • example.tnt: o menor TNT distribuído (~263 KB). O mapa mínimo viável para testar a ida e volta de kbot tnt unpack / pack.

Para as variantes de TNT do TA: Kingdoms (outro valor de IDVersion), veja as notas de pesquisa sobre mapas do TA:K.


Veja também

  • SCT: os blocos de montagem que o TAE junta para formar TNTs.
  • TDF: as definições de elementos resolvidas pelo nome, e o formato do arquivo irmão .ota.
  • PAL: a paleta usada para renderizar os pixels dos blocos e o minimapa.
  • GAF: os sprites de elementos compostos por kbot tnt preview.
  • Mapas do TA:K: o processo de mapas bem diferente do TA: Kingdoms.
  • Glossário: attribute cell (célula de atributo), sea level (nível do mar), void cell (célula de vazio).