diff --git a/Il2CppInspector.Common/Cpp/CppType.cs b/Il2CppInspector.Common/Cpp/CppType.cs index bdcffb0..5987f44 100644 --- a/Il2CppInspector.Common/Cpp/CppType.cs +++ b/Il2CppInspector.Common/Cpp/CppType.cs @@ -115,14 +115,70 @@ namespace Il2CppInspector.CppUtils } // A struct, union or class type (type with fields) - public class CppComplexType : CppType + public class CppComplexType : CppType, IEnumerable { + // Various enumerators + public List this[int byteOffset] => Fields[byteOffset * 8]; + + public CppField this[string fieldName] => Fields.Values.SelectMany(f => f).FirstOrDefault(f => f.Name == fieldName); + + public IEnumerator GetEnumerator() => Fields.Values.SelectMany(f => f).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + // Collection which flattens all nested fields, calculating their direct bit offsets from the start of the type + // Unions can still cause some offsets to have multiple values + public class FlattenedFieldsCollection : IEnumerable + { + public SortedDictionary> Fields; + + public FlattenedFieldsCollection(CppComplexType t) => Fields = getFlattenedFields(t); + + private SortedDictionary> getFlattenedFields(CppComplexType t) { + var flattened = new SortedDictionary>(); + + foreach (var field in t.Fields.Values.SelectMany(f => f)) { + if (field.Type is CppComplexType ct) { + var baseOffset = field.Offset; + var fields = ct.Flattened.Fields.Select(kl => new { + Key = kl.Key + baseOffset, + Value = kl.Value.Select(f => new CppField { Name = f.Name, Type = f.Type, BitfieldSize = f.BitfieldSize, Offset = f.Offset + baseOffset }).ToList() + }).ToDictionary(kv => kv.Key, kv => kv.Value); + + flattened = new SortedDictionary>(flattened.Union(fields).ToDictionary(kv => kv.Key, kv => kv.Value)); + } else { + if (flattened.ContainsKey(field.Offset)) + flattened[field.Offset].Add(field); + else + flattened.Add(field.Offset, new List { field }); + } + } + return flattened; + } + + public List this[int byteOffset] => Fields[byteOffset * 8]; + + public CppField this[string fieldName] => Fields.Values.SelectMany(f => f).FirstOrDefault(f => f.Name == fieldName); + + public IEnumerator GetEnumerator() => Fields.Values.SelectMany(f => f).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + private FlattenedFieldsCollection flattenedFields; + + public FlattenedFieldsCollection Flattened { + get { + if (flattenedFields == null) + flattenedFields = new FlattenedFieldsCollection(this); + return flattenedFields; + } + } + // The compound type public CompoundType CompoundType; // Dictionary of byte offset in the type to each field // Unions and bitfields can have more than one field at the same offset - public Dictionary> Fields { get; internal set; } = new Dictionary>(); + public SortedDictionary> Fields { get; internal set; } = new SortedDictionary>(); public CppComplexType(CompoundType compoundType) : base("", 0) => CompoundType = compoundType; @@ -147,7 +203,7 @@ namespace Il2CppInspector.CppUtils if (alignmentBytes > 0 && field.OffsetBytes % alignmentBytes != 0) field.Offset += (alignmentBytes - field.OffsetBytes % alignmentBytes) * 8; - if (Fields.ContainsKey(field.Offset)) + if (Fields.ContainsKey(field.Offset)) Fields[field.Offset].Add(field); else Fields.Add(field.Offset, new List { field }); @@ -216,7 +272,7 @@ namespace Il2CppInspector.CppUtils var offset = $"/* 0x{OffsetBytes:x2} - 0x{OffsetBytes + SizeBytes - 1:x2} (0x{SizeBytes:x2}) */"; var field = Type switch { - // nested anonymouse types + // nested anonymous types CppComplexType t when string.IsNullOrEmpty(t.Name) => "\n" + t.ToString()[..^1] + " " + Name, // function pointers CppFnPtrType t when string.IsNullOrEmpty(t.Name) => $" {t.ReturnType} (*{Name})({t.Arguments})", diff --git a/Il2CppTests/TestCppTypes.cs b/Il2CppTests/TestCppTypes.cs index a489ebb..23d22db 100644 --- a/Il2CppTests/TestCppTypes.cs +++ b/Il2CppTests/TestCppTypes.cs @@ -35,6 +35,53 @@ namespace Il2CppInspector foreach (var cppType in cppTypes.Types) Debug.WriteLine("// " + cppType.Key + "\n" + cppType.Value + "\n"); } + + // Do a few sanity checks taken from real applications + // NOTE: Does not provide full code coverage! + + var cppTypes2 = CppTypes.FromUnityVersion(new UnityVersion("2019.3.1f1"), 64); + + CppComplexType ct; + CppField field; + + // Un-nested class + ct = (CppComplexType) cppTypes2["Il2CppClass"]; + + field = ct[0xD8].First(); + + Assert.AreEqual(field.Name, "cctor_finished"); + + field = ct[0x128].First(); + + Assert.AreEqual(field.Name, "vtable"); + + field = ct["cctor_finished"]; + + Assert.AreEqual(field.OffsetBytes, 0xD8); + + field = ct["vtable"]; + + Assert.AreEqual(field.OffsetBytes, 0x128); + + // Nested class + ct = (CppComplexType) cppTypes2["Il2CppClass_Merged"]; + var fields = ct.Flattened; + + field = fields[0xD8].First(); + + Assert.AreEqual(field.Name, "cctor_finished"); + + field = fields[0x128].First(); + + Assert.AreEqual(field.Name, "vtable"); + + field = fields["cctor_finished"]; + + Assert.AreEqual(field.OffsetBytes, 0xD8); + + field = fields["vtable"]; + + Assert.AreEqual(field.OffsetBytes, 0x128); } } }