using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using CodeGenerator; using Kaitai; using static Kaitai.BlendFile; namespace BlendFile; public class Reader { string _path; private readonly Dictionary dnaTypes = new(); private readonly Dictionary dnaTypesDb = new(); private Dictionary<(IntPtr, Type), object> objects = new(); public Dictionary<(IntPtr, Type), object> Objects => objects; public Dictionary InstantiatedObjects = new(); /// /// A dictionary that contains pointers to pointers /// /// /// Key: Memory address of the pointer /// Value: Memory address of the object we are pointing to /// private Dictionary pointers = new(); public List GetObjects() => objects.Values.ToList(); public List GetObjects() => objects.Values.OfType().ToList(); private SortedDictionary memBlocks = new(); /// /// Gets the block at the specified memory address /// /// memory address in current system endianness /// A object public FileBlock? GetBlock(long memAddr) => memBlocks.SkipWhile(x => x.Key < memAddr).FirstOrDefault().Value; /// /// Creates a new instance of the class /// /// A containing a path to a blend file that will be read. public Reader(string path) : this() { _path = path; } /// /// Creates a new instance of the class /// public Reader() { _path = ""; var types = Assembly.GetExecutingAssembly().DefinedTypes; foreach (var type in types) { var attrib = type.GetCustomAttribute(); if (attrib == null) continue; dnaTypes.Add(attrib.OriginalIndex, type); dnaTypesDb.Add(attrib.OriginalName, type); } } public void Read(string path) { _path = path; Read(); } public void Read() { var file = new KaitaiStream(_path); var blend = new Kaitai.BlendFile(file); Console.WriteLine($"Start offset: 0x{blend.Blocks[0].MemAddr.ToPointer():X}"); bool isLe = blend.Hdr.Endian == Kaitai.BlendFile.Endian.Le; //TODO: two blocks somehow have the same mem address... this sounds wrong. blend.Blocks.ForEach(block => memBlocks.TryAdd(block.MemAddr.ToMemAddr(isLe), block)); foreach (var block in blend.Blocks) { //We need to read all blocks of data regardless of the type //Checks if the block has a known SDNA type, meaning that we know how it is structured if (!dnaTypes.ContainsKey((int)block.SdnaIndex)) continue; //Get the type of the block dnaTypesDb.TryGetValue(block.SdnaStruct.Type, out Type? t); if (t == null) continue; //should never happen // How many objects are in the block var count = block.Count; //offset for the next object in the block var blockOffset = 0; //for each expected object in the block for (var i = 0; i < count; i++) { //create an instance of type "t" (the dna type we inferred before) var obj = ActivateInstance(block.SdnaStruct.Type); if (obj == null) continue; //should never happen //fill the object with the data from the block FillObject(block, ref obj, t.GetFields(), blockOffset); //move the offset to the next object in the block blockOffset += t.GetCustomAttribute()!.Size; } } foreach (var obj in objects) { // for each converted object // get the fields of the object FieldInfo[] fieldInfo = obj.Value.GetType().GetFields(); // get all fields that are pointers var list = fieldInfo.Where(fldInfo => fldInfo.GetCustomAttribute()!.IsPointer).ToList(); // for each pointer field foreach (var f in list) { // get the pointer value var addr = GetBlockFieldDataOffset(obj.Key.Item1, f.GetCustomAttribute()!.OriginalIndex, fieldInfo); // get the object that the pointer is pointing to if we've already converted it var newobj = objects.GetValueOrDefault((addr, f.FieldType)); if (newobj != null) f.SetValue(obj.Value, newobj); else // if we haven't converted the object yet, we need to convert it now { newobj = ActivateInstance(f.FieldType); if (newobj != null) { FillObject(addr, ref newobj, f.FieldType.GetFields()); f.SetValue(obj.Value, newobj); Objects.Add((addr, f.FieldType), newobj); } // should never happen else throw new NotSupportedException($"Type \"{f.FieldType}\" is unknown"); } } } foreach(var ptr in pointers) { var obj = objects.GetValueOrDefault((ptr.Key, typeof(object))); if (obj == null) continue; InstantiatedObjects.Add(ptr.Key, obj); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private long GetBlockFieldDataOffset(long blockAddress, int fieldIndex, FieldInfo[] fieldMetadata) => blockAddress + GetFieldDataOffset(fieldIndex, fieldMetadata); [MethodImpl(MethodImplOptions.AggressiveInlining)] private IntPtr GetBlockFieldDataOffset(IntPtr blockAddress, int fieldIndex, FieldInfo[] fieldMetadata) => new(blockAddress + GetFieldDataOffset(fieldIndex, fieldMetadata)); /// /// Gets the offset of the data of a field in a block /// /// index of the field in the structure /// array of metadata /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private long GetFieldDataOffset(int fieldIndex, FieldInfo[] fieldMetadata) => fieldMetadata.First(x => x.GetCustomAttribute()!.OriginalIndex == fieldIndex) .GetCustomAttribute()!.MemoryOffset; /// /// Creates an instance of a given type /// /// A containing the name of the type to create /// An object of the type specified in the parameter or null [MethodImpl(MethodImplOptions.AggressiveInlining)] private object? ActivateInstance(string type) => dnaTypesDb.TryGetValue(type, out Type? t) ? Activator.CreateInstance(t) : null; [MethodImpl(MethodImplOptions.AggressiveInlining)] private object? ActivateInstance(Type type) => Activator.CreateInstance(type); /// /// Filles a given object with the data from a block, starting to read it from the specified offset /// /// data block from where to read the values /// object of same struct type as the one that is being decoded /// Array of s containing attributes /// offset in bytes from where structure starts in the block private void FillObject(FileBlock block, ref object? obj, FieldInfo[] fieldMetadata, IntPtr startOffset = 0) { if (block.Code == "ENDB") return; // ENDB is a special block that does not contain any data foreach (var field in fieldMetadata) { object? value; //Get the DNAFieldAttribute of the current field var attrib = field.GetCustomAttribute(); switch (attrib) { case DNAFieldAttribute fieldAttribute: value = ConvertNormalField(block, fieldAttribute, startOffset); break; case DNAListAttribute listAttribute: value = ConvertListField(block, field, listAttribute, startOffset); break; default: continue; //should never happen. } field.SetValue(obj, value); //Add the freshly handled object to the database objects.Add((block.MemAddr.ToPointer() + startOffset, obj!.GetType()), obj!); } //Add the freshly handled object to the database objects.Add((block.MemAddr.ToPointer() + startOffset, obj!.GetType()), obj!); } private void FillObject(IntPtr ptr, ref object? obj, FieldInfo[] fieldMetadata) { var block = GetBlock(ptr.ToInt64()); if (block == null) throw new($"Block for pointer {ptr.ToInt64():X} not found"); var blockOffset = ptr.ToInt64() - block.MemAddr.ToPointer(); FillObject(block, ref obj, fieldMetadata, new IntPtr(blockOffset)); } private object? ConvertNormalField(FileBlock block, DNAFieldAttribute attrib, IntPtr startOffset) { //Calculate the offset from where the data of the field starts. //Because the order of the fields is not guaranteed we need to compute it each time IntPtr offset = attrib.MemoryOffset + startOffset; //Grab data size, create a container and copy the data from the block to the container int size = attrib.Size; var data = new byte[size]; Array.Copy((byte[])block.Body, offset, data, 0, size); //Convert the data to the correct type if it's a base type object? value = ConvertFieldData(data, attrib.OriginalType); if (value != null) return value; //Check if the field is a pointer to another DNA structure if (dnaTypesDb.ContainsKey(attrib.OriginalType)) { if (!attrib.IsPointer) { //It's a structure //Create a new instance of the DNA structure type object? newObj = ActivateInstance(attrib.OriginalType); //should never happen... type is missing? if (newObj == null) throw new NotSupportedException($"Type \"{attrib.OriginalType}\" is unknown"); //Get the information of the fields of the new object var fieldInfo = newObj.GetType().GetFields(); //relative offset to this block for the structure IntPtr relAddr = block.MemAddr.ToPointer() + offset; //If the object is already created, we can just assign it if (objects.TryGetValue((relAddr, newObj.GetType()), out object? o)) return o; //Fill the object with the data from the block (this is recursive) FillObject(block, ref newObj, fieldInfo, offset); } else { //It's a pointer IntPtr memAddr = data.ToPointer(); if (memAddr == 0) return null; //nullPointer, no need to store the reference pointers.TryAdd(block.MemAddr.ToPointer() + offset, data.ToPointer()); } } throw new NotSupportedException($"Unknown type \"{attrib.OriginalType}\""); } private object? ConvertListField(FileBlock block, FieldInfo field, DNAListAttribute attrib, IntPtr startOffset) { IntPtr countOffset = attrib.CountMemoryOffset + startOffset; IntPtr ptrOffset = attrib.PtrMemoryOffset + startOffset; var countLen = attrib.CountFieldName.ParseFSize(); var countData = new byte[countLen]; Array.Copy((byte[])block.Body, countOffset, countData, 0, countLen); var tmpCount = ConvertFieldData(countData, attrib.CountFieldName); int count; switch (tmpCount) { case int i: count = i; break; case short s: count = s; break; default: throw new NotSupportedException($"Unknown type \"{attrib.CountFieldName}\""); } List objList = new(); for(IntPtr ptr = ptrOffset; ptr < ptrOffset + count * sizeof(Int64); ptr += sizeof(Int64)) { var data = new byte[sizeof(Int64)]; Array.Copy((byte[])block.Body, ptr, data, 0, sizeof(Int64)); IntPtr memAddr = new IntPtr(BitConverter.ToInt64(data, 0)); if (memAddr == IntPtr.Zero) { objList.Add(null); continue; } var obj = ActivateInstance(attrib.UnderlyingType); if (obj == null) throw new NotSupportedException($"Type \"{attrib.UnderlyingType}\" is unknown"); var fieldMetadata = obj.GetType().GetFields(); FillObject(memAddr, ref obj, fieldMetadata); objList.Add(obj); } return objList; } private object? ConvertFieldData(byte[] data, string type) { return type switch { "char" => (char)data[0], "short" => BitConverter.ToInt16(data, 0), "int" => BitConverter.ToInt32(data, 0), "float" => BitConverter.ToSingle(data, 0), "double" => BitConverter.ToDouble(data, 0), "string" => Encoding.UTF8.GetString(data), // utf8? "void" => null, // object? "ushort" => BitConverter.ToUInt16(data, 0), "uchar" => data[0], "int64_t" => BitConverter.ToInt64(data, 0), "int8_t" => (sbyte)data[0], "uint64_t" => BitConverter.ToUInt64(data, 0), _ => null }; } }