Running with Code Like with scissors, only more dangerous

17Nov/150

Exploring .dbc files with C# Dynamic Code Generation, part 2: The naive implementation

Last time, we defined the problem of viewing a DBC file from World of Warcraft and created the shape of an API we’d like to use for that purpose. This time, we’ll implement a basic parser which uses reflection but no dynamic code generation to populate the individual records.

Let’s make some assumptions about where we are:

  • A DBC file is accessible via a random-access stream
  • We’re only handling int, float, and string. The others are variations of int.

Here’s a quick enumeration outlining the different types that we might set:

        internal enum TargetType
        {
            String,
            Int32,
            Float32,
        }

In DbcReader, I actually supported both read-write properties and fields; but, for simplicity, we’ll just support fields. For the most part, they work the same. We’re going to create a simple type that keeps track of the relevant information — that is, the Reflection FieldInfo of the field, the column Position of the field in the row, and the TargetType.

        internal class TargetInfo
        {
            public FieldInfo Field;
            public int Position;
            public TargetType Type;

            // inputVal is the int that was 4-byte value read from the underlying stream
            // for this column.
            public void SetValue<TTarget>(TTarget target, int inputVal, DbcTable table)
                where TTarget : class
            {
                switch (Type)
                {
                    case TargetType.Int32:
                        SetValue(target, inputVal);
                        break;
                    case TargetType.Float32:
                        byte[] bits = BitConverter.GetBytes(inputVal);
                        SetValue(target, BitConverter.ToSingle(bits, 0));
                        break;
                    case TargetType.String:
                        string tmp = table.GetString(inputVal);
                        SetValue(target, tmp);
                        break;
                }
            }

            public void SetValue<TValue, TTarget>(TTarget target, TValue inputVal)
                where TTarget : class
            {
                Field.SetValue(target, inputVal);
            }
        }

This class provides a convenient way to set the value of a field in a TTarget entity. So how is this used? From within a DbcTable<T>, this produces a record:

        private static void ConvertSlow(BinaryReader reader, int fieldsPerRecord, DbcTable table, T target)
        {
            int[] values = new int[fieldsPerRecord];
            for (int i = 0; i < fieldsPerRecord; i++)
            {
                values[i] = reader.ReadInt32();
            }

            Type t = typeof(T);
            foreach (var targetInfo in DbcTableCompiler.GetTargetInfoForType(t))
            {
                targetInfo.SetValue(target, values[targetInfo.Position], table);
            }
        }

The only thing that needs to be implemented from this point is the GetTargetInfoForType function. It isn’t terribly complex; it’s just enumerating the fields. Because we enter this function with the int[] of all columns, the array is random-access, so ordering isn’t particularly important of how the fields get enumerated.

        internal static IEnumerable<TargetInfo> GetTargetInfoForType(Type type)
        {
            // Get all public instance fields
            foreach (FieldInfo fi in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
            {
                // Get DbcRecordPosition attributes but don't inherit
                var attr = fi.GetCustomAttribute<DbcRecordPositionAttribute>(false);
                if (attr != null)
                {
                    var result = new TargetInfo
                    {
                        Field = fi,
                        Position = attr.Position,
                        Type = GetTargetTypeFromType(fi.FieldType),
                    };
                    yield return result;
                }
            }
        }

This is a simple enumerator that looks at each field on a class which is public, then yields it to the coroutine that’s invoked the enumeration.

So how does this all fit together?

We have a class DbcTable and child class DbcTable<T>. The DbcTable owns and operates the underlying Stream and a BinaryReader that lives atop the Stream. The ConvertSlow function manages all of that work, so the implementation of GetAt (which provides the record at a specified index) is very simple:

        public T GetAt(int index)
        {
            if (_store == null)
                throw new ObjectDisposedException("DbcTable");

            // _store is the Stream
            _store.Seek(_perRecord * index + _headerLength, SeekOrigin.Begin);

            T target = new T(); // DbcTable<T> must be declared as where T: class, new()
            ConvertSlow(_reader, _recordLength, this, target);
            return target;
        }

That’s all there is to it! Next time, we’ll actually start getting into dynamic code generation. We’re going to cheat and write it by hand, then reverse engineer that out.

Comments (0) Trackbacks (0)

No comments yet.


Leave a comment

ERROR: si-captcha.php plugin says GD image support not detected in PHP!

Contact your web host and ask them why GD image support is not enabled for PHP.

ERROR: si-captcha.php plugin says imagepng function not detected in PHP!

Contact your web host and ask them why imagepng function is not enabled for PHP.

No trackbacks yet.