Alle Formatseiten ↓
Karten und Gelände

TNT – Kartengelände

Aus coreprime/kbot @ 0ab97bf · Auf GitHub bearbeiten

Eine .tnt-Datei ist eine vollständige spielbare Karte: Kachelgrafiken, Geländehöhen, Platzierungen von Kartenobjekten (Bäume, Felsen, Hydrationsknoten, …), Meeresspiegel und eine 252×252-Minikarte, alles in einer einzigen, in sich geschlossenen Datei. Für die Geometrie gibt es keine externen Abhängigkeiten – leg eine .tnt in einen Spielordner, und sie lädt. (Die zugehörige .ota-Datei enthält nur Metadaten: Name, Beschreibung, Hinweise für die KI, Startpositionen.)

Karte Metal Heck, gerendert mit Objekt-Sprites und Startmarkierungen
maps/metal heck.tnt – vollständige Vorschau mit Objekt-Sprites, zusammengesetzt aus features/*.tdf,
und Markierungen der Startpositionen aus der zugehörigen .ota

Eingebettete Minikarte    Normalisierte Höhenkarte
Eingebettete Minikarte (links) und normalisiertes Höhenraster (rechts).

Tip

Probier es selbst.

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

Siehe die kbot tnt-Referenz der CLI.

Aus Go heraus. Verwende 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)

Auf einen Blick

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

Header (64 Byte)

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

Es gibt drei Versionswörter. 0x2000 ist die Karte von Total Annihilation, die jedes Werkzeug schreibt. 0x1020 ist ein älterer Aufbau von TA, den TA 3.1c noch liest: Seine Attribut-Datensätze sind 8 Byte groß (Höhe, dann ein Objekt als einzelnes Byte an Byte 2, wobei 0xFC und darüber „keines“ bedeutet), und Zeiger und Flags der Minikarte stehen bei 0x38/0x3c; kbot liest ihn, und beim Speichern wird 0x2000 geschrieben. 0x4000 ist eine Karte von TA: Kingdoms, die TA nicht laden kann (siehe TA:K-Karten); kbot tnt describe, das MCP-Werkzeug tnt_describe und die Kartenauswahl Open Map des Studios weisen darauf hin.

Important

Width und Height zählen Attributzellen zu 16 Pixeln, keine Kacheln. Teile durch 2, um die Kachelanzahl zu erhalten; multipliziere mit 16 für die Maße in Pixeln. Eine Karte von 4.096×4.096 px hat also Width = Height = 256 (Zellen) und 128 × 128 = 16384 Kacheln in ihrem Indexraster.


Raster der Kachelindizes (PtrMapData)

(Width/2) × (Height/2) Einträge vom Typ uint16, zeilenweise (Y außen, X innen). Jeder Eintrag ist ein Index in das Array der Kachelgrafiken. Kacheln sind 32 × 32 px groß; das Raster ist das Mosaik, das das Spiel blittet, um das Spielfeld zu zeichnen.

Note

Cavedogs Werkzeuge füllen diesen Block mit Speicherresten auf eine 16-Byte-Grenze auf. kbot tnt unpack hält die Füllbytes unverändert in metadata.json fest (wenn --lossless gesetzt ist), damit der Round-Trip byte-identisch ist.

Attributraster (PtrMapAttr)

Width × Height Zellen zu je 4 Byte, zeilenweise. Pro Zelle:

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;

Das Spiel platziert ein Kartenobjekt nur bei einem Wort unterhalb der Größe der Objekttabelle (TileAnims); 0xFFFC kennzeichnet eine Leerzelle, und jedes andere Wort platziert nichts. Wörter ab 0xFFFB sind unabhängig von der Tabellengröße Sentinel-Werte, eine Tabelle muss also weniger als 0xFFFB Einträge haben. kbot tnt describe, kbot tnt features und der Asset-Explorer zählen nur die Platzierungen, die das Spiel tatsächlich vornimmt.

Das Attributraster ist in jeder Richtung doppelt so fein wie das Kachelraster – jede Kachel hat 4 Attributzellen, eine pro Teilkachel zu 16 Pixeln. Dadurch kann die Engine Wegfindung, Bauplatzierung und Waffenkollisionen mit einer Auflösung von 16 Pixeln berechnen.

Warning

0xFFFE und 0xFFFD kommen in Inhalten der Verkaufsversion vor (Lava Run, Metal Heck und ein paar frühe Kampagnenkarten). Sie sind weder Objektindizes noch Leerzellen: Das Spiel platziert dort nichts, und die Zellen bleiben bebaubar. kbot bewahrt beim Round-Trip den ursprünglichen Wert.

Kachelgrafiken (PtrTileGfx)

Tiles × 1024 Byte Palettenindizes, innerhalb jedes 32×32-Blocks von oben nach unten und von links nach rechts, die Blöcke lückenlos hintereinander. Die Karte bringt ihre gesamte Kachelgrafik selbst mit – leg eine TNT in den Spielordner, und sie ist spielbar.

Objektnamen (PtrTileAnim)

TileAnims Einträge der Form:

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;

Diese Namen werden über die [FEATURE]-Abschnitte in features/**/*.tdf aufgelöst (siehe TDF) – so weiß die Engine, welches .3do-Mesh und welches .gaf-Sprite sie für „MetalTower10“ oder „ArmTreeFoo“ zeichnen muss. Der TDF-Eintrag des Kartenobjekts enthält außerdem Grundfläche, Schaden, Rückgewinnungswert usw.

Note

Cavedogs Werkzeuge lassen im hinteren Teil jedes Name[128] nach dem NUL-Terminator nicht initialisierte Speicherreste stehen. kbot tnt unpack schreibt (standardmäßig) nur den druckbaren Teil in features.csv; der Round-Trip baut saubere Tabellen neu auf. Mit --lossless bleiben die rohen Bytes für ein byte-identisches Packen exakt erhalten.

Minikarte (PtrMiniMap)

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

Das Spiel liest die gespeicherte Minikarte nur, wenn Bit 0 des Header-Worts bei 0x2c (MinimapFlags; 0x3c bei einer 0x1020-Karte) gesetzt ist; andernfalls baut es das Radarbild aus den Kacheln auf. Alle 275 Karten der Verkaufsversion setzen es, ebenso jede Karte, die das Studio speichert. Das Radar des Spiels verwendet eine gespeicherte Minikarte nur, wenn beide Seiten mindestens 252 lang sind.

Die Karte wird in die linke obere Ecke der Minikarte gezeichnet, der Rest ist Auffüllung mit dem Palettenindex 0x64 (ein paar Kampagnenkarten füllen mit 0 auf). Der Bereich zeigt die sichtbare Karte, die 32 Pixel schmaler und 128 Pixel niedriger ist als die Kacheln (das Spiel zeigt diese Ränder nie): Ihre längere Seite ist 252, die andere behält das Seitenverhältnis der sichtbaren Karte, abgerundet:

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 Kacheln ergeben also einen Bereich von 252×216, 128×96 (ac04) ergibt 252×182 und 96×128 ergibt 193×252. Der Bereich ergibt sich aus der Größe der Karte, nicht aus den Pixeln: Manche Minikarten der Verkaufsversion haben überhaupt keine Auffüllung (ac01, ac04 bis ac11 füllen 252×256), und der Bereich ist trotzdem der berechnete. Map.MinimapContentBounds und MinimapContentSize aus kbot-io berechnen ihn; der Asset-Explorer setzt die StartPos-Markierungen hinein, und das Studio speichert eine so erstellte Minikarte (Map.BuildMinimap: Jedes Pixel der Minikarte ist die Durchschnittsfarbe der Kartenpixel, die es abdeckt).


Beispiel im Detail – 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
  ...

So liest man das: Metal Heck ist eine Karte von 4.192×4.192 Pixeln aus 583 einzigartigen Kacheln, mit 74 Platzierungen von Kartenobjekten aus 28 verschiedenen Objektdefinitionen. Das Weltarchiv „Metal“ liefert die .tdf-Definitionen für diese Kartenobjekte.


Entpacken und bearbeiten

kbot tnt unpack <file.tnt> --target <dir> erzeugt einen Aufbau, den der passende Befehl pack wieder einliest:

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

Bearbeite irgendetwas davon und packe neu:

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

Important

Standardmäßig ist das Entpacken verlustbehaftet. Die Namenstabelle der Kartenobjekte fehlt in metadata.json und wird beim Packen aus den eindeutigen Namen in features.csv neu aufgebaut. Dabei gehen alle Speicherreste verloren, die Cavedogs Werkzeuge hinten in der Tabelle hinterlassen haben. Für byte-identische Round-Trips übergib --lossless, um die rohen Objektbytes festzuhalten:

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

Zugehörige .ota-Dateien

Zu jeder .tnt gehört eine .ota-Begleitdatei („Online Total Annihilation“) im selben Verzeichnis. Dort stehen die Missions-Metadaten: lesbarer Name, Planetenname, Wetter, Schwerkraft, Startpositionen, KI-Briefing usw.

Siehe TDF – .ota hat dasselbe INI-ähnliche Textformat wie .fbi und .tdf. kbot tnt preview liest sie automatisch, um die nummerierten Markierungen der Startpositionen in das Vorschaubild zu zeichnen. Jede Ansicht einer Karte in kbot liest die Schemata und Startpositionen der .ota so, wie das Spiel es tut (siehe TDF – OTA); der Karteneditor des Studios bearbeitet die .ota einer geöffneten Karte beim Speichern direkt.


Stolperfallen

Warning

Der Karteneditor (TAE) schreibt in manchen Versionen Height = actual_height + 6 – Sarumans Dokumentation von 1997 merkte dazu an: „weird, 6 more than used“. Moderne Werkzeuge behandeln die Maße im Header als maßgeblich und ignorieren die unbenutzten Zeilen am Ende. Der Leser von kbot verhält sich genauso.

  • SeaLevel == 0 bedeutet NICHT „kein Wasser“ – es bedeutet, dass Höhenbyte 0 die höchste Unterwasserhöhe ist, also nichts unter Wasser liegt. Karten ohne Wasser verwenden typischerweise SeaLevel = 1.
  • Kachelindizes sind uint16, eine einzelne Karte kann also bis zu 65.536 einzigartige Kacheln haben – in der Praxis treibt aber alles über ein paar Tausend die Engine unter Speicherdruck ins Auslagern. Metal Heck (583 Kacheln) liegt näher am Median als am Rand der Verteilung. Das Studio weigert sich, eine Karte zu speichern, die mehr braucht, und kbot tnt pack weist Werte in tilemap.csv ab, die keine Indizes der Kacheln der Karte sind (etwa -1 oder 70000, das zu 4.464 überlaufen würde). Eine Zelle, die eine Kachel jenseits des Kachelsatzes nennt, lädt (das Spiel liest über seinen Kachelsatz hinaus); kbot tnt lint meldet sie als bad-tile-index.
  • Die Auffüllung der Minikarte ist Palettenindex 0x64; die Engine ignoriert Pixel außerhalb des Kartenbereichs.
  • Bit 0 von MinimapFlags (0x2c, bzw. 0x3c bei einer 0x1020-Karte) muss gesetzt sein, sonst ignoriert das Spiel die gespeicherte Minikarte. Lass die übrigen Bits, wie sie sind.
  • Die vier pad-Felder sind immer null. Manche Editoren von Drittanbietern schreiben Werte ungleich null hinein; das Spiel scheint sie zu ignorieren, aber kbot bewahrt, was es vorfindet.
  • TA: Kingdoms verwendet ein verwandtes, aber inkompatibles Kartenformat (.btm- / .bmp-Höhenkarten + Kachelseiten). Siehe TA:K-Karten.

Typische Größen

Asset Beobachteter Bereich in Cavedogs maps/*.tnt
Dateigröße 0,3–7 MB
Kartenmaße (Kacheln) 64×64 – 256×256 (also 2.048–8.192 px Seitenlänge)
Einzigartige Kacheln 200–2.000
Platzierungen von Kartenobjekten 10–600
Objektdefinitionen in der Tabelle 5–50
Header-Overhead immer 64 Byte
Minikarte (252×252 mit Palette; 33 Karten der Verkaufsversion 252×256) 63.504 Byte
Textgröße der zugehörigen .ota 1–8 KB

Echte Beispiele im Referenzkatalog

Karten, die einen genaueren Blick lohnen – aus jeder TA-Installation unter maps/<name>.tnt zu entpacken:

  • metal heck.tnt – das Beispiel im Detail oben. 4.192×4.192 px, 583 einzigartige Kacheln, 74 Platzierungen von Kartenobjekten. Legendäre, rohstoffreiche Karte.
  • a gentle time.tnt – kleine Karte für 4 Spieler; ein guter Einstieg, wenn du einen Prototyp eines TNT-Betrachters oder einer Render-Pipeline baust.
  • lava run.tnt – hat ungewöhnliche Objekt-Sentinel-Werte 0xFFFE in den Attributzellen (eine der Karten, die die Randfälle des frühen Reverse Engineering geprägt haben).
  • acid foursome.tnt – große Karte für 8 Spieler; zeigt weit verteilte Startpositionen von [Schema 0] in der zugehörigen .ota.
  • example.tnt – die kleinste ausgelieferte TNT (~263 KB); eine minimale funktionsfähige Karte, um den Round-Trip mit kbot tnt unpack / pack zu testen.

Zu den TNT-Varianten von TA: Kingdoms (anderes IDVersion-Wort) siehe die Forschungsnotizen zu TA:K-Karten.


Siehe auch

  • SCT – die Bausteine, die TAE zu TNTs zusammensetzt.
  • TDF – Objektdefinitionen, die über den Namen aufgelöst werden, und das Format der zugehörigen .ota.
  • PAL – die Palette, mit der Kachelpixel und Minikarte gerendert werden.
  • GAF – Objekt-Sprites, die kbot tnt preview zusammensetzt.
  • TA:K-Karten – die ganz andere Karten-Pipeline von TA: Kingdoms.
  • Glossar – attribute cell, sea level, void cell.