Skip to content

Adding support for new Unreal classes

EliotVU edited this page Dec 15, 2014 · 7 revisions

New classes can be registered using either the UnrealPackage method RegisterClass or the attribute UnrealRegisterClass, if the class resides in the UELib itself.

In this article we will use the Unreal class UTexture which is the class that represents textures from the Unreal Engine 1, and 2. The support seen in the examples are made up to demonstrate how to support new data structures. The actual data structure may vary from game and engine version.

RegisterClass

With the RegisterClass method you can register new classes dynamically at runtime but it requires more effort to setup and maintain.

public class UMyNewClassName : UObject
{   
}

var package = UnrealLoader.LoadPackage( "FilePathToUPK", System.IO.FileAccess.Read );
if( package != null )
{
    package.RegisterClass( "UTexture", typeof(UMyNewClassName) );
    package.InitializePackage();
}

For every object of class type UTexture found in the package a new instance of class UMyNewClassName will be created.

UnrealRegisterClass

Registering classes is a lot easier with the UnrealRegisterClass attribute, but be aware that the library only scans for this attribute on classes that reside in the Eliot.UELib.dll.

[UnrealRegisterClass]
public class UTexture : UObject
{
}

Whenever an object of class type UTexture, a new instance of class UTexture will be created.

Deserializing Support

Every UObject has a function called Deserialize, this function is called for every UObject after the summary of the package and its objects have been initialized. In this function we can read the data from with an IUnrealStream interface.

The Deserialize function is protected and virtual so it can't be called except via the public BeginDeserializing method which is already instigated by the library itself.

protected override void Deserialize()
{
}

It is important to call the base Deserialize function as well because it reads the common data structure that most UObjects have for you!

protected override void Deserialize()
{
    base.Deserialize();
}

The IUnrealStream can be accessed through the public or protected property called Buffer, and _Buffer. It is recommended to use the _Buffer version as the Buffer is meant for public access where needed, this may change in future builds.

We can now support the data structure of a UTexture object by reading its MipMaps first, in early builds of UE2 a UTexture object data begins with the count of mipmaps followed by a series of MipMap data. To support this we can define a new property called MipMaps of type UArray which is an extended List that eases the steps of reading repitive data. But to add this new field we need to create a new class called MipMap first.

public class MipMap : IUnrealSerializableClass
{
    public void Serialize( IUnrealStream stream )
    {
        throw new NotImplementedException();
    }

    public void Deserialize( IUnrealStream stream )
    {
        // ... perform reading calls here.
        // The mipmap data can be quite complicated so this step is skipped.
    }
}

What is this IUnrealSerializableClass? It is an interface that helps the UArray to understand your class, when the UArray is initialized with an IUnrealStream instance it will repeat this Deserialize call for every item in the array and pass its IUnrealStream reference.

Let's get back to the UTexture, Deserialize function and initialize our UArray of MipMaps. First declare the "public UArray MipMaps;" property.

protected override void Deserialize()
{
    base.Deserialize();

    MipMaps = new UArray<MipMap>( _Buffer );
}

Easy right? When the UTexture is asked to deserialize itself it will create a new UArray, and pass a reference to the texture's IUnrealStream instance. The UArray will then read the count of mipmaps and from the current stream position and repeat the Deserialize function in the MipMap class for every item in the array.

If necessary, we can communicate back to the texture from the MipMap by calling the Deserialize function ourselves after initializing the array with no IUnrealStream, by passing a reference to "this", as seen in this example:

public class MipMap : IUnrealSerializableClass
{
    internal UTexture Owner;
    ...
}

MipMaps = new UArray<MipMap>();
MipMaps.Deserialize( _Buffer, mm => mm.Owner = this );

That's it! Now the library will recognize the UTexture as a supported class and call its Deserializing function when called for, now it is up to you to support the complete data structure of the UTexture objects.

Complete Example

Disclaimer: UTexture exists already within the UELib although support is strictly limited, it is recommended that you try this guide on a unsupported class or temporary use another name for UTexture.

[UnrealRegisterClass]
public class UTexture : UObject
{
    public UArray<MipMap> MipMaps{ get; private set; }

    public UTexture()
    {
        // Optimization, means don't call Deserialize automatically, 
        // - has to be called by explicit using the BeginDeserializing method.
        ShouldDeserializeOnDemand = true;
    }

    protected override void Deserialize()
    {
        base.Deserialize();

        MipMaps = new UArray<MipMap>();
        MipMaps.Deserialize( _Buffer, mm => mm.Owner = this );
    }

    public class MipMap : IUnrealSerializableClass
    {
        internal UTexture Owner;

        public uint WidthOffset;
        public uint Width;
        public uint Height;
        public int[] Pixels;

        public void Serialize( IUnrealStream stream )
        {
            throw new NotImplementedException();
        }

        public void Deserialize( IUnrealStream stream )
        {
            if( stream.Version >= 63 )
            {
                // Offset to (Width = ...)
                WidthOffset = stream.ReadUInt32();

                long opos = stream.Position;
                stream.Seek( WidthOffset, System.IO.SeekOrigin.Begin );
                Width = stream.ReadUInt32();
                Height = stream.ReadUInt32();
                stream.Seek( opos, System.IO.SeekOrigin.Begin );
            }

            // Pixels structure varies based on the Format's property value,
            // - which can be read from the "Owner" 
            // - as "Owner.Properties.Find( "Format" ).Decompile()"
            int mipMapSize = stream.ReadIndex();
            Pixels = new int[mipMapSize];
        }
    }
}

Decompiling

Although the above covers support completely, there's an extra thing called Decompilation which is the opposite of what a compiler does but doesn't make as much sense for textures, nonetheless we can add decompilation support for debugging or as to show extra raw data for the end-user such as what "View Object" outputs in UE Explorer.

Add the Decompile method from IUnrealDecompilable:

public override string Decompile()
{
}

Return some data:

// Properties:
//     Format: DXT1
public override string Decompile()
{
    var formatProp = Properties.Find( "Format" );

    var output = String.Empty;
    output = "Properties:";

    output += "\tFormat: " + 
        (formatProp != null ? formatProp.Decompile() : "None") + 
    ";";
    return output;
}

Now if you were to "View Object" this object through the UE Explorer's interface you'd see:

Properties:
    Format: DXT1

Good luck :)