Embedding custom items to a map - BigBang1112/gbx-net GitHub Wiki
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.
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)
-
Collection ZIP folder ('MidBarrierTech.zip' for example)
-
(club ID folders - '56909' for example)
- Blocks
(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 openZipArchive
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. For club items, you can see it as <fake>/MemoryTemp/FavoriteClubItems
.
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.
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!)
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"
.
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);
});
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.
In theory,
SmallestSize
should be the most efficient, but for some reason, it may compress worse thanOptimal
(the equivalent of not specifying theCompressionLevel
). You may need to seek third-party libraries and operate directly on theEmbeddedZipData
if you want to compress more efficiently.
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.