Skip to content

Working with script metadata

Petr Pivoňka edited this page May 23, 2024 · 20 revisions

Script metadata is a set of variables that can be used in Trackmania 2, Trackmania 2020, or Shootmania gamemodes and editor plugins. This feature was added in ManiaPlanet and is unavailable in TMUF and older TM games. It makes it possible to read additional info from the map in scripts like this:

declare metadata Integer TimeLimit for Map;
TimeLimit = 60000;

You cannot set the value inline due to the ManiaScript language limitation.

This tutorial applies to GBX.NET 1.1.0+!

The older metadata API is not supported, however, the migration process is explained at the bottom.

Introduction

Script metadata can be applied via the ScriptMetadata property on two classes currently:

  • CGameCtnChallenge - A map
  • CGameCtnMacroBlockInfo - A macroblock model

The class is called CScriptTraitsMetadata. Inside, it contains a set of named variables, internally called "traits".

You cannot store null into traits.

Possible types of traits to use

  • bool (Boolean)
  • int (Integer)
  • float (Real)
  • string (Text)
  • Vec2
  • Vec3
  • Int3
  • Int2
  • List (array) with any of the types above
  • Dictionary (associative array) with any combination of the types above
  • Struct (ScriptStructTrait) with any members of the types above

These types are strictly defined in the source-generated code and source-generated method names. No generic handling is exposed by default, so it's not easily possible to create any other trait types than these above.

Declaring a trait

You can declare traits by adding new elements to the Traits dictionary, but there are Declare methods to majorly simplify this:

var map = Gbx.ParseNode<CGameCtnChallenge>("MyMap.Map.Gbx");
var metadata = map.ScriptMetadata;

metadata.Declare("IsValid", true); // bool
metadata.Declare("TimeLimit", 60000); // int
metadata.Declare("Percentage", 0.6547f); // float
metadata.Declare("OriginalLogin", "bigbang1112"); // string
metadata.Declare("ScreenPos", new Vec2(0.85f, 0.6f)); // Vec2
metadata.Declare("WorldPos", new Vec3(654.65f, 42.64f, 459.1f)); // Vec3
metadata.Declare("BlockPos", new Int3(16, 12, 21)); // Int3
metadata.Declare("MousePos", new Int2(1643, 731)); // Int2

metadata.Declare("Authors", new List<string> // Enumerable (array) with any of the types above
{
    "BigBang1112",
    "ThaumicTom",
    "Arkady"
});

metadata.Declare("BlocksPlaced", new Dictionary<string, int> // Dictionary (associative array) with any combination of the types above
{
    { "BigBang1112", 420 },
    { "ThaumicTom", 69 },
    { "Arkady", 42 }
});

For struct creation, see Defining/Declaring a struct below.

Receiving a trait

A lot of methods to easily read metadata traits are available.

The GetX(string name) method returns the value of name with type X. If it's not available in the trait list, null is returned.

var isValid = metadata.GetBoolean("IsValid"); // bool
var timeLimit = metadata.GetInteger("TimeLimit"); // int
var percentage = metadata.GetReal("Percentage"); // float
var originalLogin = metadata.GetText("OriginalLogin"); // string

The TryGetX(string name) method returns the value of name with type X through the out parameter. If it's available, true is returned, otherwise false.

if (metadata.TryGetVec2("ScreenPos", out Vec2 value))
{
    Console.WriteLine(value);
}

if (metadata.TryGetVec2("WorldPos", out Vec3 value))
{
    Console.WriteLine(value);
}

For lists/arrays, the methods are called GetXArray(string name) and TryGetXArray(string name) where X is the type of array.

For dictionaries, the methods are called GetXYAssociativeArray(string name) and TryGetXYAssociativeArray(string name) where X is the key and Y is the value.

Defining/Declaring a struct

There are two approaches to this problem where you may prefer one over another depending on your situation:

  • Declare a struct with assigned values right away
  • Define a type first and then reuse that type on different struct traits (it's easier to manage multiple struct instances with different values across, but you still cannot pre-define default values currently)

The other approach would look something like this:

const string timeLimit = "TimeLimit"; // To not repeat strings

// Define a struct type builder that you wanna reuse in multiple cases
var structTypeBuilder = CScriptTraitsMetadata.DefineStruct("SComplexStruct")
    .WithBoolean("IsValid")
    .WithInteger(timeLimit)
    .WithReal("Percentage")
    .WithText("OriginalLogin");

// Set TimeLimit to 60000, while rest will stay at default values
var structTrait = structTypeBuilder.Set() // Use Set() on type builder to instantiate
    .WithInteger(timeLimit, 60000)
    .Build();

// Set TimeLimit to 33333, while rest will stay at default values
var structTrait2 = structTypeBuilder.Set() // Use Set() on type builder to instantiate
    .WithInteger(timeLimit, 33333)
    .Build();

// Declare them
metadata.Declare("Complex", structTrait);
metadata.Declare("Complex2", structTrait2);

It does not give a whole lot of benefits, as you have to set the members by repeating the strings anyway, but if you have a case where you wanna set only a few members of a struct multiple times, this becomes ideal.

The first approach is much shorter and very often more viable:

var structTrait = CScriptTraitsMetadata.CreateStruct("SComplexStruct")
    .WithBoolean("IsValid", true)
    .WithInteger("TimeLimit", 60000)
    .WithReal("Percentage", 0.6547f)
    .WithText("OriginalLogin", "bigbang1112")
    .Build();
        
metadata.Declare("Complex", structTrait);

Defining a struct in a struct

The builder pattern (fluent API) allows to chain structs in structs. But note that recursion is not possible (you can attempt it, but it won't work ingame).

var structTrait = CScriptTraitsMetadata.CreateStruct("SComplexStruct")
    .WithBoolean("IsValid", true)
    .WithInteger("TimeLimit", 60000)
    .WithStruct("MoreComplex", CScriptTraitsMetadata.CreateStruct("SMoreComplexStruct")
        .WithText("Name", "Very complex indeed")
        .WithVec3("Position", default)
        .WithInt2("Whatever", (1, 2)))
    .WithReal("Percentage", 0.6547f)
    .WithText("OriginalLogin", "bigbang1112")
    .Build();
        
metadata.Declare("Complex", structTrait);

Receiving a struct

Helper methods GetStruct() and TryGetStruct() exist, but they give out ScriptStructTrait, so that needs to be explained.

  • Type - An immutable identifier of the struct. It can be used for equality comparison.
    • It is always ScriptStructType. It contains the struct members inside, but I don't recommend playing with it much.
  • Value - A dictionary where the key is the name of the member and the value is the member's value.
    • Member value is presented as ScriptTrait, which doesn't have a Value exposed. There are more approaches, but GetValue() returning object is the easiest one.

Writing out all members of a struct:

if (metadata.TryGetStruct("Complex", out var structTrait))
{
    foreach (var (name, trait) in structTrait.Value)
    {
        Console.WriteLine($"{name}: {trait.GetValue()}");
    }
}

Solving the flaw of nested structs:

if (src.TryGetStruct("Complex", out var structTrait))
{
    WriteStruct(structTrait);
}
        
void WriteStruct(CScriptTraitsMetadata.ScriptStructTrait structTrait, int nesting = 0)
{
    foreach (var (name, trait) in structTrait.Value)
    {
        Console.Write($"{new string(' ', nesting)}{name}:");

        if (trait is CScriptTraitsMetadata.ScriptStructTrait nestedStructTrait)
        {
            Console.WriteLine();
            WriteStruct(nestedStructTrait, nesting: nesting + 1);
            continue;
        }
                
        Console.WriteLine($" {trait.GetValue()}");
    }
}

Arrays may not look nice either, but you can solve them in a similar way.

If you wanna receive specific types, you can use either pattern matching on GetValue():

if (src.TryGetStruct("Complex", out var structTrait))
{
    foreach (var (name, trait) in structTrait.Value)
    {
        switch (trait.GetValue())
        {
            case int intValue:
                // Do something with int
                break;
            case float floatValue:
                // Do something with float
                break;
            case IList<CScriptTraitsMetadata.ScriptTrait> list:
                // Do something with list
                break;
            case IDictionary<CScriptTraitsMetadata.ScriptTrait, CScriptTraitsMetadata.ScriptTrait> dictionary:
                // Do something with dictionary
                break;
            case IDictionary<string, CScriptTraitsMetadata.ScriptTrait> structMembers:
                // Do something with struct members
                break;
        }
    }
}

Or you can go the efficient yet more confusing path:

if (src.TryGetStruct("Complex", out var structTrait))
{
    foreach (var (name, trait) in structTrait.Value)
    {
        switch (trait)
        {
            case CScriptTraitsMetadata.ScriptTrait<int> integerTrait:
                int intValue = integerTrait.Value; // Do something with int
                break;
            case CScriptTraitsMetadata.ScriptTrait<float> realTrait:
                float floatValue = realTrait.Value; // Do something with float
                break;
            case CScriptTraitsMetadata.ScriptArrayTrait arrayTrait:
                IList<CScriptTraitsMetadata.ScriptTrait> list = arrayTrait.Value; // Do something with list
                break;
            case CScriptTraitsMetadata.ScriptDictionaryTrait dictionaryTrait:
                IDictionary<CScriptTraitsMetadata.ScriptTrait, CScriptTraitsMetadata.ScriptTrait> dictionary = dictionaryTrait.Value; // Do something with dictionary
                break;
            case CScriptTraitsMetadata.ScriptStructTrait nestedStructTrait:
                IDictionary<string, CScriptTraitsMetadata.ScriptTrait> structMembers = nestedStructTrait.Value; // Do something with struct
                break;
        }
    }
}

Migration from the old system to 1.1

If you have been just using the Declare(), Remove(), and ClearMetadata() methods, you can safely update without any issues.

  • Class ScriptVariable was renamed to ScriptTrait.
    • Name is no longer part of the class. Use the key from the dictionary.
    • Value's alternative currently is the GetValue() method. This method is boxing the value from the ScriptTrait<T> : ScriptTrait where T : notnull class, so maybe it's preferred to pattern match with the generic class.
  • Property List<ScriptVariable> Metadata has been changed to IDictionary<string, ScriptTrait> Traits.
    • Most of the arrays and lists have moved to the dictionary strategy where the key is always the string name of the trait, as that's the most commonly indexed way to get metadata.
  • Method Get(string name) returns the new ScriptTrait now.
  • Class ScriptArray was renamed to ScriptArrayTrait and heavily changed.
    • Reference no longer exists.
    • Instead of always using the dictionary, the behaviour was split into two classes, where ScriptArrayTrait uses IList and ScriptDictionaryTrait uses IDictionary, for a more straightforward experience.
  • Class ScriptStruct was renamed to ScriptStructTrait and the type-related properties have been migrated to ScriptStructType:
    • StructName can be accessed with ((ScriptStructType)Type).Name.
    • Members was transferred from an array to IDictionary<string, ScriptTrait> - similar, only indexable with member names instead of numbers.

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