Essentials - BigBang1112/gbx-net GitHub Wiki

This page applies to GBX.NET 1.2.X and lower - updated 2.x.x info coming soon...

Gbx is a serialized object in the GameBox engine. This object can be either shared around or is loaded from internal game files and used by the game. The library tries to follow the object orientation like in the actual engine.

User-generated Gbx files are always compressed, while internal Gbx files can be uncompressed. All compression-related job is handled by the library (after you reference GBX.NET.LZO in the project).

Basics about parsing/reading

Gbx is effectively deserialized from a data stream to an object when calling the GameBox.Parse...() methods. This type of object is called node (internally in the game engine) and always inherits CMwNod. With the GameBox.ParseNode() method, you get the node directly returned, while GameBox.Parse wraps the node into GameBox<T> where T : Node object that contains technical Gbx file parameters regarding its serialization.

Both file names and streams are supported by the Parse... methods.

In versions of 0.15.X, GameBox and the main Node class were requiring you to use the using keyword, but this change was reverted later in 0.16.0+! Be aware of the version you're using!

CMwNod inherits the Node class, where the Node class is more used across the GBX.NET API.

The type of the node is determined by the class ID contained in the .Gbx file, not by the extension. All possible types are contained in the GBX.NET.Engines namespace. For example, a class ID 0x03043000 corresponds to the CGameCtnChallenge type (class).

If a class ID is implemented in this namespace:

  • GameBox.Parse() method returns a new generic GameBox<T> object, where T is the type of the class ID found in Gbx.
  • GameBox.ParseNode() method calls the GameBox.Parse() method but returns the object directly. GameBox object can be accessed with GetGbx() method.

If a class ID is not implemented in this namespace:

  • GameBox.Parse() method returns a non-generic GameBox object, where Node is null, and you only get the header and reference table information.
  • GameBox.ParseNode() method calls the GameBox.Parse() method but always returns null, without having access to any Gbx parameters. The GameBox object is still created but is going to be garbage collected. Don't use GameBox.ParseNode() for analyzing unknown Gbx files.

The rules above apply to both generic and non-generic overloads of the methods.

Header parse methods (GameBox.Parse...Header()) return the same kind of objects, except the body parse (and decompression) is ignored. They are 8-130 times faster than full parse methods (parse speed is more consistent), so it is recommended to use the GameBox.Parse...Header() methods for things like file lists and other simple visual overviews of many Gbx files at once.

Implicit parse

Implicit parse is represented by the non-generic GameBox.Parse...() methods. At compile-time, the type of the Gbx is unknown and needs to be checked with pattern matching.

Type is determined from the class ID, then instantiated using reflection (versions below 1.1) or via source-generated switch statement (versions +1.1).

// Returns GameBox - possible to cast or pattern match with generic GameBox<T>
GameBox gbx = GameBox.Parse("RandomMap.Map.Gbx");

Node node = gbx.Node; // Not specified at compile time, but should store object of type CGameCtnChallenge

if (gbx is GameBox<CGameCtnReplayRecord> gbxReplay)
{
    CGameCtnReplayRecord replay = gbxReplay.Node; 
}
else if (gbx is GameBox<CGameCtnChallenge> gbxMap)
{
    // We should reach here
    CGameCtnChallenge map = gbxMap.Node; 
}

switch (gbx)
{
    case GameBox<CGameCtnReplayRecord> gbxReplay:
        CGameCtnReplayRecord replay = gbxReplay.Node; 
        break;
    case GameBox<CGameCtnChallenge> gbxMap:
        // We should reach here
        CGameCtnChallenge map = gbxMap.Node; 
        break;
}

string name = gbx switch
{
    GameBox<CGameCtnReplayRecord> gbxReplay => "Replay",
    GameBox<CGameCtnChallenge> gbxMap => "Map", // We should reach here
    _ => "Unknown"
};
// Returns Node - possible to cast or pattern match with GBX.NET.Engines types
Node node = GameBox.ParseNode("RandomMap.Map.Gbx");

if (node is CGameCtnReplayRecord replay)
{
    
}
else if (node is CGameCtnChallenge map)
{
    // We should reach here
}

switch (node)
{
    case CGameCtnReplayRecord replay:

        break;
    case CGameCtnChallenge map:
        // We should reach here
        break;
}

string name = node switch
{
    CGameCtnReplayRecord replay => "Replay",
    CGameCtnChallenge map => "Map", // We should reach here
    _ => "Unknown"
};

Explicit parse

Explicit parse is represented by the generic GameBox.Parse...<T>() where T : Node methods. It behaves identically like the implicit parse, except the type is known at compile-time, therefore there's no pattern matching involved.

If the type determined from the Parse method doesn't equal typeof(T), InvalidCastException is thrown.

GameBox<CGameCtnChallenge> gbx = GameBox.Parse<CGameCtnChallenge>("RandomMap.Map.Gbx");
CGameCtnChallenge node = GameBox.ParseNode<CGameCtnChallenge>("RandomMap.Map.Gbx");

In practice, implicit parse is more flexible to use in production, while explicit parse is better suitable for testing.

There's no real performance benefit by using the explicit parse from the implicit parse.

Basics about saving/writing

Save() method on the GameBox/Node object is the method behind writing Gbx files.

var gbx = GameBox.Parse<CGameCtnChallenge>("RandomMap.Map.Gbx");

// Do some stuff...

gbx.Save("ModifiedMap.Map.Gbx");
var node = GameBox.ParseNode<CGameCtnChallenge>("RandomMap.Map.Gbx");

// Do some stuff...

node.Save("ModifiedMap.Map.Gbx");

Both file names and streams are supported by the Save... methods.

You can in fact save any supported Node object to a Gbx file! Supported means that it has all ReadWrite/Write methods fully coded. Some nodes can be explicitly not supported with the WritingNotSupportedAttribute, like the CGameCtnReplayRecord.

If Node.Save() variant is used, the Node is wrapped to a new GameBox<T> object, unless its private gbx field is already set from before.

If you're modifying the main node, then to be directly saved, GameBox.Header property information is ignored and is replaced with a new one in the Gbx binary, while GameBox.Header property is still going to be unchanged.

Chunks

Engine objects (nodes) are serialized back into Gbx by using data chunks.

Each node should include at least 1 chunk in its Chunks property, to be written properly. If a chunk is not included, the node loses information contained in that chunk - default values are going to be used on the node.

Chunks have a backwards compatibility feature. That means the newer Trackmania games can recognize older chunks, but older Trackmania games cannot recognize newer chunks - so if you want to guarantee the highest accessibility of the Gbx file, choose the oldest chunks possible that serializes the information you need (but not too old as very very old chunks - like 15+ years super obsolete information - get discarded over time). The older the chunk is, the lower the chunk part of the ID is (last 3 digits of the ID).

You don't have to deal with the chunk choice if you use node builders from the GBX.NET.Builders namespace (the .Create() static method on certain nodes). It is the recommended approach, but of course if that node builder is available.

For more information related to chunks, see Chunks in-depth - why certain properties lag?.

Troubleshooting of GBX.NET.LZO detection issues

Compressed Gbx files require to include the GBX.NET.LZO library (or any similar implementation, but there are no other compatible at the moment). In most cases, the LZO compression is automatically detected after just referencing the library in the project. You don't require to have a using GBX.NET.LZO; anywhere.

On specific platforms like Blazor WebAssembly though, the dependency system works differently, and (currently) GBX.NET struggles to automatically detect the LZO library.

In these cases (if just following the MissingLzoException message didn't solve the problem), add this line above the first attempt of parsing the Gbx. It should be called just once.

GBX.NET.Lzo.SetLzo(typeof(GBX.NET.LZO.MiniLZO));