Skip to content

Embedding custom items to a map

Petr Pivoňka edited this page Sep 10, 2024 · 10 revisions

GBX.NET can be effectively used to embed custom content into a map in ManiaPlanet and Trackmania (2020). You would typically want to embed:

  • Custom items (*.Item.Gbx)
  • Custom blocks (*.Block.Gbx)
  • Custom materials (*.Mat.Gbx)

This does not work for TMUF and older games. For custom blocks there, use the GameData exploit instead.

Concept of embedding

The way maps handle embedding is that an earlier data chunk (0x01F) contains ID references to the blocks on the map and their placement parameters. These usually refer to the official block set that has all of its data stored in Pak files. Items are then stored later in chunk 0x040 also as ID references with placement parameters. After that, in 0x054 data chunk, a ZIP "file" with additional embedded mesh data is stored.

The ZIP is structured the same way as UserData (the folder in My Documents, could be 'ManiaPlanet' or 'Trackmania2020') and the root is the inside of it:

  • (root)
    • Blocks
      • (folders and Block.Gbxs)
    • Items
      • (folders and Item.Gbxs)
    • Materials
      • (folders and Mat.Gbxs)
    • ClubItems
      • (club ID folders - '56909' for example)
        • Collection ZIP folder ('MidBarrierTech.zip' for example)
          • (folders and Item.Gbxs)

(root) means root literally, it is not a folder

The goal is to match these paths with the ID reference paths of blocks/items on the map.

To just explore the embedded ZIP, there are two options:

  • Use OpenReadEmbeddedZipData() on the map. It will open ZipArchive in read mode.
  • Export EmbeddedZipData byte array to a file and open the file in your favorite ZIP browser.

Note

In TM2020 there's a bug that sometimes the ZIP stores the full path in its fullest when you first save the map - the folders include your drive letter and also potentially the Windows username. It's nothing to be largely scared about though, as this problem dissappears after you reload the map editor and save again. You shouldn't store the full paths otherwise.

Placing custom items correctly

The easiest option to place a custom item currently is to use PlaceAnchoredObject on CGameCtnChallenge.

Use this if your item is supposed to be in the Items/MyFolder called MyItem.Item.Gbx:

map.PlaceAnchoredObject(new("MyFolder\\MyItem.Item.Gbx", 26, "my_login"), absolutePosition: (32, 24, 16), pitchYawRoll: (0, 1, 2));

The item reference (path) should match the folders where the item file is supposed to be, excluding the "root" Items.

  • Backslash is important here. GBX.NET won't do any corrections for you in case you provide the forwardslash.
  • You don't include the Items folder name at the beginning.
  • Adding the .Item.Gbx extension is also important.
  • To simplify repetitive backslashes, you can do @"MyFolder\MyItem.Item.Gbx".

Note

This page will not cover details about the item ID/reference problems. Here, the collection 26 means a typical TM2020 item.

Placing custom blocks correctly

The simplest way to place a custom block is to use PlaceBlock with the string blockModel overload on CGameCtnChallenge.

Use this if your block is supposed to be in the Blocks/MyFolder called MyBlock.Block.Gbx:

map.PlaceBlock("MyFolder\\MyBlock.Block.Gbx_CustomBlock", coord: (5, 6, 7), Direction.West);

The block reference (path) should match the folders where the block file is supposed to be, excluding the "root" Blocks and adding the _CustomBlock suffix.

  • Backslash is important here. GBX.NET won't do any corrections for you in case you provide the forwardslash.
  • You don't include the Blocks folder name at the beginning.
  • Adding the .Block.Gbx extension is also important. _CustomBlock should always be after though.
  • To simplify repetitive backslashes, you can do @"MyFolder\MyBlock.Block.Gbx_CustomBlock".

Custom blocks also tend to include the block author in the block.Author property, but this isn't a requirement to embed the block. (luckily!)

Placing club items correctly

The easiest option to place a custom item currently is to use PlaceAnchoredObject on CGameCtnChallenge.

Use this if your item is supposed to be in the MidBarrierTech folder called v_MidBarrierTechDiagMirror.Item.Gbx in the "Ealipse The Rock" club (ID: 37625) in the "MidBarrierTech" collection:

map.PlaceAnchoredObject(new("club:37625\\MidBarrierTech.zip\\MidBarrierTech\\v_MidBarrierTechDiagMirror.Item.Gbx", 26, "Ealipse"), absolutePosition: (32, 24, 16), pitchYawRoll: (0, 1, 2));
  • Backslash is important here. GBX.NET won't do any corrections for you in case you provide the forwardslash.
  • You don't include the Items folder name here at all like when embedding to the zip.
  • Adding the .Item.Gbx extension is also important.
  • To simplify repetitive backslashes, you can do @"club:37625\MidBarrierTech.zip\MidBarrierTech\v_MidBarrierTechDiagMirror.Item.Gbx".

Embedding data to the ZIP

To embed custom stuff, use UpdateEmbeddedZipData or UpdateEmbeddedZipDataAsync from CGameCtnChallenge. These methods ensure safe modification without much confusion, unlike working with ZipArchive from scratch. The Async method exists to allow async operations during the modification state.

Slashes don't matter here. This only affects the ZIP format, which should understand both slashes and other things are handled internally by GBX.NET.

Create entries directly with the relative path with all needed folders and then it's just a stream job.

map.UpdateEmbeddedZipData(zip =>
{
    var entry = zip.CreateEntry("Blocks/MyFolder/MyBlock.Item.Gbx");
    using var entryStream = entry.Open();
    using var fileStream = File.OpenRead("Any/Other/Path/MyBlock.Block.Gbx");
    fileStream.CopyTo(entryStream);
});

Here is the async example. Utilizing cancellation token is recommended.

await map.UpdateEmbeddedZipDataAsync(async (zip, cancellationToken) =>
{
    var entry = zip.CreateEntry("Blocks/MyFolder/MyBlock.Item.Gbx");
    using var entryStream = entry.Open();
    using var fileStream = File.OpenRead("Any/Other/Path/MyBlock.Block.Gbx");
    await fileStream.CopyToAsync(entryStream, cancellationToken);
});

Optimization tips

You can also use the ZipArchive.CreateEntry(String, CompressionLevel) overload with System.IO.Compression.CompressionLevel set as SmallestSize to squeeze as many bytes as possible.

It is also recommended to decompress the Gbx before packing it into the zip so that the deflate algorithm can kick in more efficiently:

map.UpdateEmbeddedZipData(zip =>
{
    var entry = zip.CreateEntry("Blocks/MyFolder/MyBlock.Item.Gbx", CompressionLevel.SmallestSize);
    using var entryStream = entry.Open();
    using var fileStream = File.OpenRead("Any/Other/Path/MyBlock.Block.Gbx");
    Gbx.Decompress(input: fileStream, output: entryStream);
});

Within UpdateEmbeddedZipData, you should also embed multiple items, preferably all of them, as unpacking and packing zip bytes for hundreds of individual items will end up being expensive and slow.

GBX.NET

Practical

Theoretical

  • TimeInt32 and TimeSingle (soon)
  • Chunks in depth - why certain properties lag? (soon)
  • High-performance parsing (later)
  • Purpose of Async methods (soon)
  • Compatibility, class ID remapping (soon)

Internal

External

  • Gbx from noob to master
  • Reading chunks in your parser
Clone this wiki locally