-
-
Notifications
You must be signed in to change notification settings - Fork 20
Extracting basic map data
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:
- Name of the map
- UID of the map
- Medal times
- Thumbnail
- Environment
- Stunts time limit
- Mod (texture pack) download URL
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
.
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");
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 calledGetHeaderMembers()
-
map
object has a property calledHeaderChunks
(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 returnsthis
, and is not available in Release configuration. It should be only used to review what can or cannot be changed using the header.
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.
...is in the header!
string mapName = map.MapName;
Console.WriteLine(mapName);
...is in the header!
string mapUid = map.MapUid;
Console.WriteLine(mapUid);
...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;
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 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 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 theMapInfo.Collection
into simpler access. -
Id
can be implicitly casted tostring
.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);
(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.
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.
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);
- Beginner
- Extracting basic map data
- Extracting ghosts from replays
- Extracting input data from ghosts
- Extracting checkpoints from ghosts (soon)
- Builders - create new nodes extremely easily (soon)
- Intermediate
- Basic map modification (soon)
- Embedding custom items to a map
- Working with script metadata
- Extracting samples from ghosts (later)
- Fast header reading and writing (later)
- Advanced
- Advanced map modification (soon)
- Creating a MediaTracker clip from scratch (soon)
- Lightmap modification (later)
- Integrate GBX.NET with other languages (later)
-
TimeInt32
andTimeSingle
(soon) - Chunks in depth - why certain properties lag? (soon)
- High-performance parsing (later)
- Purpose of Async methods (soon)
- Compatibility, class ID remapping (soon)
- Class structure (soon)
- Class verification (soon)
- Class documentation
- Gbx from noob to master
- Reading chunks in your parser