Working with script metadata - BigBang1112/gbx-net GitHub Wiki
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.
The older metadata API is not supported, however, the migration process is explained at the bottom.
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.
- 
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.
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.
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"); // stringThe 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.
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);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);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.
 
- It is always 
- 
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 aValueexposed. There are more approaches, butGetValue()returningobjectis the easiest one.
 
- Member value is presented as 
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;
        }
    }
}If you have been just using the Declare(), Remove(), and ClearMetadata() methods, you can safely update without any issues.
- Class ScriptVariablewas renamed toScriptTrait.- 
Nameis no longer part of the class. Use the key from the dictionary.
- 
Value's alternative currently is theGetValue()method. This method is boxing the value from theScriptTrait<T> : ScriptTrait where T : notnullclass, so maybe it's preferred to pattern match with the generic class.
 
- 
- Property List<ScriptVariable> Metadatahas been changed toIDictionary<string, ScriptTrait> Traits.- Most of the arrays and lists have moved to the dictionary strategy where the key is always the stringname of the trait, as that's the most commonly indexed way to get metadata.
 
- Most of the arrays and lists have moved to the dictionary strategy where the key is always the 
- Method Get(string name)returns the newScriptTraitnow.
- Class ScriptArraywas renamed toScriptArrayTraitand heavily changed.- 
Referenceno longer exists.
- Instead of always using the dictionary, the behaviour was split into two classes, where ScriptArrayTraitusesIListandScriptDictionaryTraitusesIDictionary, for a more straightforward experience.
 
- 
- Class ScriptStructwas renamed toScriptStructTraitand the type-related properties have been migrated toScriptStructType:- 
StructNamecan be accessed with((ScriptStructType)Type).Name.
- 
Memberswas transferred from an array toIDictionary<string, ScriptTrait>- similar, only indexable with member names instead of numbers.
 
- 
