Skip to content

Implementing Chunk Definitions

Stefan Baumann edited this page Apr 19, 2020 · 2 revisions

For almost all use cases, you should never have the need to implement additional chunks for parsing. If you should need additional functionality or just want to contribute to the project to make the parser more complete, let's see how implementing chunk definitions is done:

This library uses various methods to make implementing chunk and custom struct layouts as easy as possible. For most chunks, it's sufficient to just define the chunk/struct as a class with the fields decorated with the [Property] and the chunk/struct with the [Chunk(0xCHUNKID)]/[CustomStruct] attribute.

A typical example of how an implementation of a chunk looks like is the chunk 0x03043308:

//The Chunk attribute sets the chunk id that is parseable with this layout and whether the chunk is skippable
//If multiple chunk ids map to the same class, multiple Chunk attributes can be applied to the same class
[Chunk(0x3043008)]
public class MapAuthorChunk
    : Chunk //Chunks have to inherit from Chunk, which is a superclass of Node
{
    //A property that corresponds to a value in the chunk is indicated by the [Property] attribute
    //The fields are parsed from the file in the same order as the properties are defined
    [Property]
    public uint Version { get; set; }

    [Property]
    public uint AuthorVersion { get; set; }

    [Property]
    public string Login { get; set; }

    [Property]
    public string Nick { get; set; }

    [Property]
    public string Zone { get; set; }

    [Property]
    public string ExtraInfo { get; set; }
}

That's all you need! All of the code for parsing this chunk will be generated for you automatically.

Implementation of complex chunks and structs

Of course it isn't always as easy as the previous example - some chunks have different versions, fields that are only parsed depending on various conditions, custom structs etc. Those are supported via the same api, so that you have to write as little code as possible:

If the property type is ambiguous to multiple data types in gbx files, you can specify the type as a part of the [Property] attribute (a property of type string will default to a non-lookback string and a property of a type [derived of] Node will default to the node just being there - if you want to parse a lookback-string or node reference, you have to specify it in the attribute). Arrays are supported via the [Array] attribute with the length of the array either being read from the file as a uint before the start of the array if nothing is specified, hardcoded or read from another property of the chunk. Furthermore, conditional parsing is also available via the [Condition] attribute. If a section of your chunk/struct is too complex to be parsed by the automatically generated parser, you can use the [CustomParserMethod] attribute to specify a method, which takes a GameBoxReader object and returns the parsed value of the property.

A great example of how the advanced features can be used to parse even the most complex chunks is the chunk 0x0304301F:

[Chunk(0x0304301F)]
public class MapChunk
    : Chunk
{
    //This property is a lookback string instead of a normal string, so we have to specify that
    [Property(SpecialPropertyType.LookbackString)]
    public string Uid { get; set; }

    [Property(SpecialPropertyType.LookbackString)]
    public string Environment { get; set; }

    [Property(SpecialPropertyType.LookbackString)]
    public string Author { get; set; }

    //This property is just a normal string so we don't have to specify anything
    [Property]
    public string Name { get; set; }

    [Property(SpecialPropertyType.LookbackString)]
    public string TimeOfDay { get; set; }

    [Property(SpecialPropertyType.LookbackString)]
    public string DecorationEnvironment { get; set; }

    [Property(SpecialPropertyType.LookbackString)]
    public string DecorationEnvironmentAuthor { get; set; }

    //Some special data types are also supported by the automatic parser, e.g. file references, 2D and 3D sizes and 2D and 3D vectors
    [Property]
    public Size3D Size { get; set; }

    [Property]
    public bool NeedsUnlock { get; set; }

    [Property]
    public uint Version { get; set; }

    //The layout of this property in the file cannot be represented with the attributes, so we specify a custom method to parse this property
    [Property, CustomParserMethod(nameof(MapChunk.ParseBlocks))]
    public Block[] Blocks { get; set; }

    //This is the custom parser method for the property above. It has to be a public instance method that takes a GameBoxReader and returns a value of the same type as the property
    public Block[] ParseBlocks(GameBoxReader reader)
    {
        //The block struct can be parsed by the automatically generated parser
        CustomStructParser<Block> blockParser = ParserFactory.GetCustomStructParser<Block>();

        //This count of blocks that specified the length of the array does not count blocks with empty flags, so we have to read them one by one and check if they are actually counted
        Block[] blocks = new Block[reader.ReadUInt32()];
        for (int i = 0; i < blocks.Length; i++)
        {
            //Use the auto-generated block parser to parse a block instance
            Block block = blockParser.Parse(reader);
            if (block.Flags.HasFlag(BlockFlags.Null))
            {
                i--; //Ignore parsed block
            }
            else
            {
                blocks[i] = block;
            }
        }

        return blocks;
    }
}

//This is a custom struct used in the chunk above - custom structs have to have the CustomStruct attribute to be able to automatically generate parsers for them
[CustomStruct]
public class Block
{
    [Property(SpecialPropertyType.LookbackString)]
    public string Name { get; set; }

    [Property]
    public byte Rotation { get; set; }

    [Property]
    public byte X { get; set; }

    [Property]
    public byte Y { get; set; }

    [Property]
    public byte Z { get; set; }

    //This is a property that can be represented by an enum. The parser does not support enums out of the box, so support is implemented via one property in which the value is put by the parser and a wrapper property that converts that value to the enum type
    [Property]
    public uint FlagsU { get; set; }
    public BlockFlags Flags => (BlockFlags)this.FlagsU;
    
    //This is a conditional property - it only gets parsed if the condition is fulfilled (in this case if the Flags property of the current Block instance has the CustomBlock flag)
    [Property, Condition(nameof(Block.Flags), ConditionOperator.DoesNotHaveFlag, BlockFlags.Null), Condition(nameof(Block.Flags), ConditionOperator.HasFlag, BlockFlags.CustomBlock)]
    public string Author { get; set; }

    //This is a subnode inside of a chunk/struct. If it's not a node reference, nothing special has to be specified and if the type of node/chunk that is present is known, the specific type can be specified
    [Property(SpecialPropertyType.NodeReference), Condition(nameof(Block.Flags), ConditionOperator.DoesNotHaveFlag, BlockFlags.Null), Condition(nameof(Block.Flags), ConditionOperator.HasFlag, BlockFlags.CustomBlock)]
    public Node Skin { get; set; }
    
    [Property(SpecialPropertyType.NodeReference), Condition(nameof(Block.Flags), ConditionOperator.DoesNotHaveFlag, BlockFlags.Null), Condition(nameof(Block.Flags), ConditionOperator.HasFlag, BlockFlags.HasBlockParameters)]
    public Node BlockParameters { get; set; }
}

//This is just a regular flags enum, nothing special here
[Flags]
public enum BlockFlags
    : uint
{
    Null = uint.MaxValue,
    CustomBlock = 0x8000,
    HasBlockParameters = 0x100000
}