Skip to content

Essentials

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

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.

Basics about parsing/reading

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

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

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:

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

If a class ID is not implemented in this namespace:

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

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

Header parse methods (Gbx.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 Gbx.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 Gbx.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 via source-generated switch statement (versions +1.1).

using GBX.NET;
using GBX.NET.Engines.Game;
using GBX.NET.Engines.MwFoundations;

Gbx.LZO = new GBX.NET.LZO.MiniLZO();

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

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

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

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

string name = gbx switch
{
    Gbx<CGameCtnReplayRecord> gbxReplay => "Replay",
    Gbx<CGameCtnChallenge> gbxMap => "Map", // We should reach here
    _ => "Unknown"
};
using GBX.NET;
using GBX.NET.Engines.Game;
using GBX.NET.Engines.MwFoundations;

Gbx.LZO = new GBX.NET.LZO.MiniLZO();

// Returns CMwNod - possible to cast or pattern match with GBX.NET.Engines types
CMwNod node = Gbx.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 : CMwNod methods.

It runs through a slightly modified code that does slightly simpler things than the Implicit parse. If the type cannot match typeof(T), InvalidCastException is thrown.

using GBX.NET;
using GBX.NET.Engines.Game;

Gbx.LZO = new GBX.NET.LZO.MiniLZO();

GameBox<CGameCtnChallenge> gbx = GameBox.Parse<CGameCtnChallenge>("RandomMap.Map.Gbx");
using GBX.NET;
using GBX.NET.Engines.Game;

Gbx.LZO = new GBX.NET.LZO.MiniLZO();

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

You cannot use Explicit parse on unknown Gbx files.

Smaller switch statement of classes allows effective trimming that can reduce the library size much more than it is able to with the Implicit parse.

Explicit parse is still considered fairly experimental and it might sometimes fail its job.

In practice, implicit parse is more flexible to use in production, while explicit parse is better suited for simplicity, lightweightness, critical-performance tasks, or testing.

Basics about saving/writing

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

using GBX.NET;
using GBX.NET.Engines.Game;

Gbx.LZO = new GBX.NET.LZO.MiniLZO();

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

// Do some stuff...

gbx.Save("ModifiedMap.Map.Gbx");
using GBX.NET;
using GBX.NET.Engines.Game;

Gbx.LZO = new GBX.NET.LZO.MiniLZO();

var node = Gbx.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 CMwNod 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 CMwNod.Save() variant is used, the CMwNod is wrapped to a new GameBox<T> object.

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 serialize 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.

Parsing before 2.0

Applies to GBX.NET 1.2.6 and below.

Instead of Gbx class, the GameBox class is used.

Implicit parse

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

Explicit parse

It behaves identically like the implicit parse, except the type is known at compile-time, therefore there's no pattern matching involved.

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

Saving

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.

Property lag

Certain properties lag in old GBX.NET versions. This is caused by the Discovery feature that was removed in v2. It loads data from stored buffers to "improve performance". This was newly replaced with chunk ignoring.

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