Angepinnt (Roh-)aufbau einer Teeworlds Map

    Diese Seite verwendet Cookies. Durch die Nutzung unserer Seite erklären Sie sich damit einverstanden, dass wir Cookies setzen. Weitere Informationen

    • (Roh-)aufbau einer Teeworlds Map

      Aufgeteilt auf 2 Beiträge.


      Hey,

      da ich gerade an einem erweiterten Map-editor arbeite (github.com/Malekblubb/twl | github.com/Malekblubb/twlu), wollte ich die Gelengeheit nutzen, um in diesem "Tutorial" etwas näher auf den Aufbau einer Teeworlds Map einzugehen.
      Das einzige was ich zu diesem Thema in Internet gefunde habe, war die TML dokumentation von SushiTee (sushitee.github.io/tml/mapformat.html), die meiner Meinung nach etwas kurz ist.

      Etwas Erfahrung in der Programmierung(bits, bytes, byteorder, usw.) wären beim lesen von Vorteil.


      Inhalt:
      • 0. Teeworlds Map Format
      • 1. Grundstruktur einer Map
        • 1.0 Bereiche
          • 1.0.0 Infobereich
            • 1.0.0.0 Header
            • 1.0.0.1 Item Infos
            • 1.0.0.2 Item Offsets
            • 1.0.0.3 Komprimierte Daten Offsets
            • 1.0.0.4 Unkomprimierte Daten Längen
          • 1.0.1 Itembereich
            • 1.0.1.0 Itemsortierung
            • 1.0.1.1 Item: Version
            • 1.0.1.2 Item: Info
            • 1.0.1.3 Item: Bild
            • 1.0.1.4 Item: Envelope
            • 1.0.1.5 Item: Gruppe
            • 1.0.1.6 Item: Ebene/Layer
            • 1.0.1.7 Item: Envpunkt
          • 1.0.2 Datenbereich
            • 1.0.2.0 Komprimierte Daten
      • 2. Lesen einer Map in C++


      0. Teeworlds Map Format

      Das Teeworlds Map Format beseteht wie jedes andere "Datafile" Format rein aus Binären bzw., für den Menschen besser lesbar, Hexdezimalen "Code".
      Damit man die rohen Daten besser verstehen kann, habe ich die Map in verschiedene Bereiche aufgeteilt (s.o.).
      Zu Beginn ein kleiner Einblick in eine Map-Datei via Hex-Editor.



      1. Grundstruktur einer Map
      1.0 Bereiche
      Wie ich zuvor schon geschrieben habe, habe ich die Map-Datei in verschiedene Bereiche eingeteilt.
      Rot, Dunkelgrün, Blau, Orange und Cyan gehören zum "Info-Hauptbereich", der Hellgrün eingerahmte "Code" ist der Item-Hauptbereich und der Rote Teil ist der Daten-Hauptbereich(der nich ganz abgebebildet ist).
      Alle bereiche haben eine Variable Länge, die logischerweise von der Anzahl an Items/Daten abhängt.

      1.0.0 Infobereich
      1.0.0.0 Header
      Der Header (oben Rot) ist einer der wichtigsten Bereiche,
      er enthält wichtige Informationen und ist (mit Ausnahme der Items) der einzige Bereich, dessen Länge immer gleich ist, nämlich 36Bytes.
      Die Ersten 4Bytes sollten der Zeichenkette "DATA" (oder in Big endian "ATAD") entsprechen, das ist mehr oder weniger die Kennung einer Teeworlds Map.
      Der nächste Integer(4 Bytes) enthält die Version der Map, die in der jetztigen TW-Version 0.6.x, 4 sein sollte.
      Integer 5-9 enthalten wichtige Informationen, die zum lesen/parsen der Map notwendig sind.

      Mit Hilfe des Headers kann errechnet werden, wie weit man lesen muss, um den Itembereich und den Datenbereich vollständig zu lesen.
      Folgende "Formel" ist anzuwenden:

      Quellcode

      1. itembereich = (num_itemtypes * 12) + (num_items * 4) + ((num_rawdata * 4) * 2) + item_area_size + data_area_size
      Die Zahl 12 in der Formel kommt vom datentyp "map_datafile_iteminfo" und wird in Punkt 1.0.0.1 "Item Infos" erklärt.

      Der Header zusammengefasst in einem POD

      [cpp]
      struct map_datafile_header
      {
      char m_sig[4];
      int m_ver;
      int m_size;
      int m_swaplen;
      int m_num_itemtypes;
      int m_num_items;
      int m_num_rawdata;
      int m_item_area_size;
      int m_data_area_size;

      bool valid() const noexcept {return ((std::memcmp(m_sig, "DATA", 4) == 0) && (m_ver == 4));}
      int body_size() const noexcept
      {
      return (m_num_itemtypes * 12) +
      (m_num_items * sizeof(int)) +
      ((m_num_rawdata * sizeof(int)) * 2) +
      m_item_area_size + m_data_area_size;
      }
      };
      [/cpp]


    • 1.0.0.1 Item Infos
      Der Item Infos Bereich(oben Dunkelgrün) ist insgesamt 12 * num_itemtypes lang.
      Er beinhaltet Informationen über die jeweiligen Items.
      Beispiel anhand der obigen Map:

      Quellcode

      1. 00000000 00000000 01000000 (= 12Bytes, Typ=Version)
      2. ^ ^ ^
      3. Typ Start Anzahl
      Für jedes Item gibt es jeweils die dazugehörige Info nur einmal.
      Die verschiedenen Typen können in der TW-Source (src/game/mapitems.h) nachgelesen werden.

      map_datafile_iteminfo POD
      [cpp]
      struct map_datafile_iteminfo
      {
      int m_type;
      int m_start;
      int m_num;
      };
      [/cpp]



      1.0.0.2 Item Offsets

      Der Item Offset Bereich(oben Blau) hat eine Länge von num_items * 4, wobei 4 die größe eines Integers ist.
      Die Offsets, die aus diesem Bereich gelesen werden können, beginnen erst ab dem Itembereich.
      Aus ihnen kann die länge der jeweiligen Items errechnet werden (Rechenweg in Punkt 1.0.0.3).


      1.0.0.3 Komprimierte Daten Offsets
      Für diesen Bereich(oben Orange) gilt das selbe wie für 1.0.0.2.
      Rechenweg:
      [cpp]
      int data_area_len = header.data_area_size; // länge des gesamten datenbereichs
      std::vector<int> offset_cnt; // container, der die offsets gespeichert hat
      std::vector<int> ret_vec; // container, der die errechneten längen speichert
      std::size_t size{offset_cnt.size()};
      for(std::size_t i{0}; i < size; ++i)
      {
      if(i != size - 1)
      {
      ret_vec.push_back(offset_cnt.at(i + 1) - offset_cnt.at(i));
      continue;
      }

      // last element
      ret_vec.push_back(data_area_len - offset_cnt.at(i));
      }
      [/cpp]


      1.0.0.4 Unkomprimierte Daten Längen
      Dieser Bereich(oben Cyan) enthält die Längen der komprimierten Daten im Datenbereich nachdem sie "entpackt" wurden.
      Er
      ist nur dann wichtig, wenn man Daten, wie z.b Bilder, Tiles, Quads
      auslesen möchte, um den notwendigen Speicher zu reservieren.


      1.0.1 Itembereich
      Im Itembereich(oben Hellgrün) sind die Items folgendermaßen gespeichert:
      1. Integer: type_and_id, 2. Integer: Länge des nachfolgenden Datenbereichs
      Beispiel:

      Quellcode

      1. 00000000 04000000 01000000 (Typ=Version)
      2. ^ ^ ^
      3. type_and_id länge daten

      Der Integer type_and_id kann mithilfe von bitmasking auf 2 Integer aufgeteilt werden:
      [cpp]
      int type_and_id{0x02040000};
      int type{(type_and_id >> 16) & 0xff}; // type == 4
      int id{(type_and_id >> 24) & 0xff}; // id == 2
      [/cpp]


      1.0.1.0 Itemsortierung
      Wenn eine Map im Standard-Client gespeichert wird, werden die zuvor hinzugefügten Items sortiert.
      Sortierung: siehe auflistung oben.


      1.0.1.1
      Item: Version
      Datenlänge: 4Bytes
      Daten: Versionsnummer (4Bytes)


      1.0.1.2 Item: Info
      Datenlänge: 16Bytes
      Daten: int author_string, map_version_string, creadits_string, license_string
      Sonstiges: Die obigen Integer speichern nur den Index der Komprimierten Daten.


      1.0.1.3 Item: Bild
      Datenlänge: 24Bytes
      Daten: int version, width, height, external, image_name, image_data
      Sonstiges: siehe 1.0.1.2


      1.0.1.4 Item: Envelopes
      Datenlänge: 52Bytes
      Daten: int version, channels, start_point, num_point, name, syncronized
      Sonstiges: Der name kann mithilfe der funktion IntsToStr in "src/game/gamecore.h" zu einem c-style string umgewandelt werden.


      1.0.1.5 Item: Gruppe
      Datenlänge: 60Bytes
      Sonstiges: siehe 1.0.1.4


      1.0.1.6 Item: Ebene/Layer
      Tiles:
      Datenlänge: 72Bytes

      Quads:
      Datenlänge: 40Bytes

      Sonstiges: sieht 1.0.1.4


      1.0.1.7 Item: Envpunkt
      Dantelänge: 24Bytes
      Daten: time, curvetype, values


      1.0.2 Datenbereich
      1.0.2.0 Komprimierte Daten

      Hier(oben Rot, letzter Bereich) werden die Komprimierten Daten, wie Bilder, Tiles, Quads, Namen, usw. gespeichert.
      Die Daten können mithilfe der zuvor ausgerechneten Längen(siehe 1.0.0.3) dekomprimiert werden.
      Bsp.:
      [cpp]
      #include <zlib.h> // needs zlib

      void some_function(const std::vector<unsigned char>& compressed_data)
      {
      std::uint64_t unpacked_size{size}; // size == zuvor errechnete länge
      std::vector<unsigned char> tmp(unpacked_size); // hier wird gespeichert

      int z_error{uncompress(reinterpret_cast<Bytef*>(&tmp[0]), &unpacked_size,
      reinterpret_cast<Bytef*>(&compressed_data[0]), compressed_data.size())};

      if(z_error != Z_OK)
      {/*zlib error handling*/}
      }
      [/cpp]



      2. Lesen einer Map in C++
      Grundsetzlich
      sollte man jetzt in der Lage sein eine Map in den Speicher zu lesen und
      die verschiedenen Bereiche zu parsen(vorausgesetzt man kann C++).
      Simples beispiel zum lesen des Headers und der iteminfos:
      Spoiler anzeigen
      [cpp]
      #include <fstream>


      struct header
      {
      char m_sig[4];
      int m_ver;
      int m_size;
      int m_swaplen;
      int m_num_itemtypes;
      int m_num_items;
      int m_num_rawdata;
      int m_item_area_size;
      int m_data_area_size;

      bool valid() const noexcept {return ((std::memcmp(m_sig, "DATA", 4) == 0) && (m_ver == 4));}
      int body_size() const noexcept
      {
      return (m_num_itemtypes * 12) +
      (m_num_items * sizeof(int)) +
      ((m_num_rawdata * sizeof(int)) * 2) +
      m_item_area_size + m_data_area_size;
      }
      };

      struct item_info
      {
      int m_type;
      int m_num;
      int m_start;
      };


      int main()
      {

      std::ifstream strm{"/path/to/my/map.map", std::ios::in | std::ios::binary};
      if(!strm.is_open())
      return 1;

      header h;
      strm.read(reinterpret_cast<char*>(&h), sizeof(header));
      if(!h.valid())
      return 2;

      std::vector<item_info> infos(h.m_num_itemtypes);
      for(int i{0}; i < h.m_num_itemtypes; ++i)
      strm.read(reinterpret_cast<char*>(&infos), sizeof(item_info));

      return 0;
      }
      [/cpp]


      Ansonsten kann man sich noch die TW-Source durchlesen, das low-level lese-zeug
      findet man in "src/engine/shared/datafile.cpp", oder meine lib, die noch
      nicht fertig ist: github.com/Malekblubb/twl/tree/master/include/twl/files .