-
-
Notifications
You must be signed in to change notification settings - Fork 20
Working with script metadata
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"); // 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.
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 aValue
exposed. There are more approaches, butGetValue()
returningobject
is 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
ScriptVariable
was renamed toScriptTrait
.-
Name
is 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 : notnull
class, so maybe it's preferred to pattern match with the generic class.
-
- Property
List<ScriptVariable> Metadata
has been changed toIDictionary<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.
- Most of the arrays and lists have moved to the dictionary strategy where the key is always the
- Method
Get(string name)
returns the newScriptTrait
now. - Class
ScriptArray
was renamed toScriptArrayTrait
and heavily changed.-
Reference
no longer exists. - Instead of always using the dictionary, the behaviour was split into two classes, where
ScriptArrayTrait
usesIList
andScriptDictionaryTrait
usesIDictionary
, for a more straightforward experience.
-
- Class
ScriptStruct
was renamed toScriptStructTrait
and the type-related properties have been migrated toScriptStructType
:-
StructName
can be accessed with((ScriptStructType)Type).Name
. -
Members
was transferred from an array toIDictionary<string, ScriptTrait>
- similar, only indexable with member names instead of numbers.
-
- 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