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.tntnuma pasta do jogo e ele carrega. (O arquivo.otaque o acompanha só tem metadados: nome, descrição, dicas para a IA, posições iniciais.)
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 (à 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 == 0NÃ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 usarSeaLevel = 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, ekbot tnt packrecusa valores detilemap.csvque não sejam índices de blocos do mapa (como-1ou70000, 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 linta informa comobad-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, ou0x3cnum mapa0x1020) precisa estar ligado, senão o jogo ignora o minimapa guardado. Mantenha os outros bits como estão. - Os quatro campos
padsã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 elemento0xFFFEincomuns 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.otairmão.example.tnt: o menor TNT distribuído (~263 KB). O mapa mínimo viável para testar a ida e volta dekbot 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).