Skip to content

Extracting basic map data

Petr Pivoňka edited this page May 26, 2024 · 11 revisions

Getting some of the basic information about a certain map is probably the easiest thing you can do with GBX.NET. But I will also dedicate this tutorial to explain where it's good to use header parse over full parse methods.

In this tutorial, we will try to extract:

In the README, you can see the table of recommended Gbx types to start with. Map has an extension *.Map.Gbx or *.Challenge.Gbx - the type we want to work with is CGameCtnChallenge.

Full Gbx parse, or is header enough?

As it always will, the code starts with the parse of the Gbx map file. It is easier to work with ParseNode when we know what kind of node to expect. But do we choose ParseNode or ParseHeaderNode in this situation?

Most of the time, you are better off parsing the full Gbx file. There are only a few node types that have header chunks:

  • CGameCtnChallenge
  • CGameCtnReplayRecord
  • Anything that inherits CGameCtnCollector

Gbx of a map (*.Map.Gbx or *.Challenge.Gbx) is known to have some basic information at the top of the file, so we will try getting the information with ParseHeaderNode first:

var map = GameBox.ParseHeaderNode<CGameCtnChallenge>("Path/To/My/Track.Map.Gbx");

Does header have what we need?

Warning

This does not apply for GBX.NET 2 at the moment, only up to 1.2.6, but a solution is in works.

If the node has a good Gbx header reading support can be determined if the node inherits the INodeHeader interface. It's not an interface you can see directly, however, you can identify it easily in two ways:

  • map object has a method called GetHeaderMembers()
  • map object has a property called HeaderChunks (in 0.16.0+)

If none of these are present, you will receive no information about the node with the GameBox.Parse...Header() methods, only about the Gbx serialization.

With GetHeaderMembers(), thanks to the IntelliSense, you can see what members can be available in the header. Some of the things we want to read from the map are not available in the header, so we will deal with this later.

GetHeaderMembers() just returns this, and is not available in Release configuration. It should be only used to review what can or cannot be changed using the header.

Reading from the map object

This was definitely a tough beginning of the library but it was needed to resolve all the question marks related to full parse and header parse. It gets very easy from now on.

Name of the map

...is in the header!

string mapName = map.MapName;

Console.WriteLine(mapName);

UID of the map

...is in the header!

string mapUid = map.MapUid;

Console.WriteLine(mapUid);

Medal times

...are in the header! Now it's 4 properties though, and with a type we haven't talked about before.

TimeInt32? bronzeMedal = map.BronzeTime;
TimeInt32? silverMedal = map.SilverTime;
TimeInt32? goldMedal = map.GoldTime;
TimeInt32? authorMedal = map.AuthorTime;

Console.WriteLine($"Bronze medal: {bronzeMedal.ToTmString()}");
Console.WriteLine($"Silver medal: {silverMeda.ToTmString()}");
Console.WriteLine($"Gold medal: {goldMedal.ToTmString()}");
Console.WriteLine($"Author medal: {authorMedal.ToTmString()}");

Medal properties work a bit specially. If only the header is parsed, the medal values are normally part of CGameCtnChallenge. But if fully parsed, these values are handled through the CGameCtnChallengeParameters object stored in ChallengeParameters property. That means this approach would be valid as well:

TimeInt32? bronzeMedal = map.ChallengeParameters.BronzeTime;
TimeInt32? silverMedal = map.ChallengeParameters.SilverTime;
TimeInt32? goldMedal = map.ChallengeParameters.GoldTime;
TimeInt32? authorMedal = map.ChallengeParameters.AuthorTime;

TimeInt32?

Race times, or anything with a decimal millisecond precision, will return either TimeInt32 or TimeInt32?.

TimeInt32 is a struct and it represents a similar type to TimeSpan, except that it uses 4 bytes to store instead of 8 bytes that TimeSpan uses. TimeInt32? is just a nullable variant, therefore it can store null. TimeInt32 also provides functionality that suits the Trackmania situation better, especially with string formatting. ToString() is overriden to use the formatting style from the TmEssentials dependency, but for TimeInt32? this doesn't apply, due to wrap into Nullable<TimeInt32> class, and you have to call ToTmString() there instead to apply the idea of no time. No time is meant to be something like -:--.--- when the time is not present (in Trackmania, it's presented as -1 or 0xFFFFFFFF).

  • If you see TimeInt32 type, it means that it's usually a race time or any other time that has decimal representation.
  • If you see TimeInt32? type, it means that the time can also not be available.

For more details, see TimeInt32 and TimeSingle.

Thumbnail

Thumbnail is in the header and is simply a JPEG piece of data. Inside the object, it is stored in a byte[] in a property called Thumbnail.

If the map has no thumbnail (ESWC and below), this property is null.

TMS has a different system of thumbnails that don't have them embedded inside the node object. Instead, they are present as BIK/WEBM files named like the Map.Gbx file. Fun fact: Thanks to the reuse of the code, you can use this feature in every single Trackmania version since TMU.

byte[]? thumbnail = map.Thumbnail;

if (thumbnail == null)
{
    Console.WriteLine("Map has no thumbnail.");
}

If you want to work with the thumbnail picture through the Bitmap object from the System.Drawing namespace, you can reference the GBX.NET.Imaging package that will give additional extension methods to CGameCtnChallenge like GetThumbnailBitmap(), ExportThumbnail(), or ImportThumbnail(). You can also use Linux-compatible alternatives like GBX.NET.Imaging.SkiaSharp or GBX.NET.Imaging.ImageSharp.

Environment

  • Environment is accessible from the header.
  • Environment is presented as collection internally.
  • The easiest way to get the environment is to use GetEnvironment() method.
  • The property Collection just translates the MapInfo.Collection into simpler access.
  • Id can be implicitly casted to string. GetEnvironment() does the same with an additional null check.

Once you know this, environment-related job becomes easy.

string environment = map.GetEnvironment();

Console.WriteLine(environment);

You can also get the block size of the environment with a special method GetBlockSize():

Int3 blockSize = map.Collection.GetBlockSize();

Console.WriteLine(blockSize);

Stunts time limit

(TODO) Now if you look into GetHeaderMembers(), there is nothing that really hints towards a time limit of anything.

For this, you would need to switch to full parse method:

var map = Gbx.ParseNode<CGameCtnChallenge>("Path/To/My/Track.Map.Gbx");

This property is specifically stored in ChallengeParameters.

GBX.NET is a lot about looking around the IntelliSense and the API, seeing what's possible or not. The idea will always start from the Parse...() method.

TimeInt32 timeLimit = map.ChallengeParameters.TimeLimit;

Console.WriteLine(timeLimit);

Notice how every single map (no matter the gamemode) has a time limit of 1 minute! You can find a lot of other similarly unused values all around the Gbx files, and it's one of the fun parts of GBX.NET.

Mod (texture pack) download URL

This is another case that you would expect to be in the header, but actually really isn't.

You can technically maybe use the map.XML contents to see it, but if you would want to change it later, then you will come into issues, as the mod definition in the body is the prefered way for the game.

A new type called PackDesc comes in:

PackDesc mod = map.ModPackDesc;

Console.WriteLine(mod.LocatorUrl);

It stores the file path, checksum, and the locator URL to use for downloading the content or using the offline content from the drive. This idea applies to sign skins or MediaTracker images too for example, and you can change these for displaying custom online content.

Checksum only seems to save some performance or network traffic, you don't have to generally care about it as things will work without touching the Checksum property.

Final code

var map = Gbx.ParseNode<CGameCtnChallenge>("Path/To/My/Track.Map.Gbx");

string mapName = map.MapName;

Console.WriteLine(mapName);

string mapUid = map.MapUid;

Console.WriteLine(mapUid);

TimeInt32? bronzeMedal = map.BronzeTime;
TimeInt32? silverMedal = map.SilverTime;
TimeInt32? goldMedal = map.GoldTime;
TimeInt32? authorMedal = map.AuthorTime;

Console.WriteLine($"Bronze medal: {bronzeMedal.ToTmString()}");
Console.WriteLine($"Silver medal: {silverMedal.ToTmString()}");
Console.WriteLine($"Gold medal: {goldMedal.ToTmString()}");
Console.WriteLine($"Author medal: {authorMedal.ToTmString()}");

byte[]? thumbnail = map.Thumbnail;

if (thumbnail is null)
{
    Console.WriteLine("Map has no thumbnail.");
}

string environment = map.GetEnvironment();

Console.WriteLine(environment);

Int3 blockSize = map.Collection.GetBlockSize();

Console.WriteLine(blockSize);

TimeInt32 timeLimit = map.ChallengeParameters.TimeLimit;

Console.WriteLine(timeLimit);

FileRef mod = map.ModPackDesc;

Console.WriteLine(mod.LocatorUrl);

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