diff --git a/Datamodel.NET.sln b/Datamodel.NET.sln index 4113062..8e09549 100644 --- a/Datamodel.NET.sln +++ b/Datamodel.NET.sln @@ -6,6 +6,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Datamodel.NET", "Datamodel. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{4C928D60-5E48-4C0D-9C7E-C75D9734CD58}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElementFactoryGenerator", "ElementFactoryGenerator\ElementFactoryGenerator.csproj", "{FE0CDDEB-F817-0758-1DFC-A472CC81F0F3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -25,6 +27,12 @@ Global {4C928D60-5E48-4C0D-9C7E-C75D9734CD58}.Documentation|Any CPU.Build.0 = Debug|Any CPU {4C928D60-5E48-4C0D-9C7E-C75D9734CD58}.Release|Any CPU.ActiveCfg = Release|Any CPU {4C928D60-5E48-4C0D-9C7E-C75D9734CD58}.Release|Any CPU.Build.0 = Release|Any CPU + {FE0CDDEB-F817-0758-1DFC-A472CC81F0F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE0CDDEB-F817-0758-1DFC-A472CC81F0F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE0CDDEB-F817-0758-1DFC-A472CC81F0F3}.Documentation|Any CPU.ActiveCfg = Release|Any CPU + {FE0CDDEB-F817-0758-1DFC-A472CC81F0F3}.Documentation|Any CPU.Build.0 = Release|Any CPU + {FE0CDDEB-F817-0758-1DFC-A472CC81F0F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE0CDDEB-F817-0758-1DFC-A472CC81F0F3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Datamodel.NET/Arrays.cs b/Datamodel.NET/Arrays.cs index 21f2590..e51d6cd 100644 --- a/Datamodel.NET/Arrays.cs +++ b/Datamodel.NET/Arrays.cs @@ -181,8 +181,8 @@ internal set if (elem.Owner == null) { var importedElement = OwnerDatamodel.ImportElement(elem, Datamodel.ImportRecursionMode.Stubs, Datamodel.ImportOverwriteMode.Stubs); - - if(importedElement is not null) + + if (importedElement is not null) { Inner[i] = importedElement; } @@ -201,8 +201,8 @@ protected override void Insert_Internal(int index, Element item) if (item.Owner == null) { var importedElement = OwnerDatamodel.ImportElement(item, Datamodel.ImportRecursionMode.Recursive, Datamodel.ImportOverwriteMode.Stubs); - - if(importedElement is not null) + + if (importedElement is not null) { item = importedElement; } diff --git a/Datamodel.NET/AttributeList.cs b/Datamodel.NET/AttributeList.cs index d0648aa..40a0eba 100644 --- a/Datamodel.NET/AttributeList.cs +++ b/Datamodel.NET/AttributeList.cs @@ -29,7 +29,7 @@ private ICollection GetPropertyBasedAttributes(bool useSerializationN var result = new List(); foreach (DictionaryEntry entry in PropertyInfos) { - if(entry.Value is null) + if (entry.Value is null) { throw new InvalidDataException("Property value can not be null"); } @@ -135,7 +135,7 @@ public void Add(string key, object? value) { var attrib = Inner[key]; - if(attrib is null) + if (attrib is null) { return null; } @@ -269,7 +269,7 @@ public virtual object? this[string name] { // were actually fine with this being null, it will just set the value to null // but need to check so the type check doesn't fail if it is null - if(value != null) + if (value != null) { var valueType = value.GetType(); @@ -307,7 +307,7 @@ public virtual object? this[string name] return; } - + Attribute? old_attr; Attribute? new_attr; int old_index = -1; @@ -343,7 +343,7 @@ public AttrKVP this[int index] { var attr = (Attribute?)Inner[index]; - if(attr is null) + if (attr is null) { throw new InvalidOperationException($"attribute at index {index} doesn't exist"); } @@ -367,7 +367,7 @@ public void RemoveAt(int index) { attr = (Attribute?)Inner[index]; - if(attr is not null) + if (attr is not null) { attr.Owner = null; Inner.RemoveAt(index); @@ -571,7 +571,7 @@ public static ValueComparer Default public new bool Equals(object? x, object? y) { - if(x is null || y is null) + if (x is null || y is null) { return false; } diff --git a/Datamodel.NET/Codecs/Binary.cs b/Datamodel.NET/Codecs/Binary.cs index daf1c82..585dcdc 100644 --- a/Datamodel.NET/Codecs/Binary.cs +++ b/Datamodel.NET/Codecs/Binary.cs @@ -5,8 +5,8 @@ using System.Numerics; using System.IO; using System.Diagnostics; -using System.Reflection; using System.Runtime.CompilerServices; +using System.Threading; namespace Datamodel.Codecs { @@ -249,96 +249,116 @@ public void Encode(Datamodel dm, string encoding, int encoding_version, Stream s encoder.Encode(); } - float[] ReadVector(int dim, BinaryReader reader) + private static readonly Dictionary TypeMap = new Dictionary { - var output = new float[dim]; - foreach (int i in Enumerable.Range(0, dim)) - output[i] = reader.ReadSingle(); - return output; + { typeof(Element).TypeHandle, 0 }, + { typeof(int).TypeHandle, 1 }, + { typeof(float).TypeHandle, 2 }, + { typeof(bool).TypeHandle, 3 }, + { typeof(string).TypeHandle, 4 }, + { typeof(byte[]).TypeHandle, 5 }, + { typeof(TimeSpan).TypeHandle, 6 }, + { typeof(Color).TypeHandle, 7 }, + { typeof(Vector2).TypeHandle, 8 }, + { typeof(Vector3).TypeHandle, 9 }, + { typeof(QAngle).TypeHandle, 10 }, + { typeof(Vector4).TypeHandle, 11 }, + { typeof(Quaternion).TypeHandle, 12 }, + { typeof(Matrix4x4).TypeHandle, 13 }, + { typeof(byte).TypeHandle, 14 }, + { typeof(UInt64).TypeHandle, 15 } + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + object? ReadValue(Datamodel dm, int typeIndex, bool raw_string, BinaryReader reader) + { + return typeIndex switch + { + 0 => ReadElement(dm, reader), + 1 => reader.ReadInt32(), + 2 => reader.ReadSingle(), + 3 => reader.ReadBoolean(), + 4 => raw_string ? ReadString_Raw(reader) : StringDict!.ReadString(reader), + 5 => reader.ReadBytes(reader.ReadInt32()), + 6 => TimeSpan.FromTicks(reader.ReadInt32() * (TimeSpan.TicksPerSecond / DatamodelTicksPerSecond)), + 7 => ReadColor(reader), + 8 => ReadVector2(reader), + 9 => ReadVector3(reader), + 10 => ReadQAngle(reader), + 11 => ReadVector4(reader), + 12 => ReadQuaternion(reader), + 13 => ReadMatrix4x4(reader), + 14 => reader.ReadByte(), + 15 => reader.ReadUInt64(), + _ => throw new ArgumentException("Cannot read value of type") + }; } - object? ReadValue(Datamodel dm, Type? type, bool raw_string, BinaryReader reader) + // Specialized methods to avoid repeated vector allocations + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private object? ReadElement(Datamodel dm, BinaryReader reader) { - if (type == typeof(Element)) + var index = reader.ReadInt32(); + return index switch { - var index = reader.ReadInt32(); - if (index == -1) - return null; - else if (index == -2) - { - var id = new Guid(ReadString_Raw(reader)); // yes, it's in ASCII! - return dm.AllElements[id] ?? new Element(dm, id); - } - else - return dm.AllElements[index]; - } - if (type == typeof(int)) - return reader.ReadInt32(); - if (type == typeof(float)) - return reader.ReadSingle(); - if (type == typeof(bool)) - return reader.ReadBoolean(); - if (type == typeof(string)) - return raw_string ? ReadString_Raw(reader) : StringDict!.ReadString(reader); - - if (type == typeof(byte[])) - return reader.ReadBytes(reader.ReadInt32()); - if (type == typeof(TimeSpan)) - return TimeSpan.FromTicks(reader.ReadInt32() * (TimeSpan.TicksPerSecond / DatamodelTicksPerSecond)); + -1 => null, + -2 => dm.AllElements[new Guid(StringDict.ReadString(reader))] ?? new Element(dm, new Guid(StringDict.ReadString(reader))), + _ => dm.AllElements[index] + }; + } - if (type == typeof(Color)) - { - var rgba = reader.ReadBytes(4); - return Color.FromBytes(rgba); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Color ReadColor(BinaryReader reader) + { + var rgba = reader.ReadBytes(4); + return Color.FromBytes(rgba); + } - if (type == typeof(Vector2)) - { - var ords = ReadVector(2, reader); - return new Vector2(ords[0], ords[1]); - } - if (type == typeof(Vector3)) - { - var ords = ReadVector(3, reader); - return new Vector3(ords[0], ords[1], ords[2]); - } - if (type == typeof(QAngle)) - { - var ords = ReadVector(3, reader); - return new QAngle(ords[0], ords[1], ords[2]); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector2 ReadVector2(BinaryReader reader) + { + return new Vector2(reader.ReadSingle(), reader.ReadSingle()); + } - if (type == typeof(Vector4)) - { - var ords = ReadVector(4, reader); - return new Vector4(ords[0], ords[1], ords[2], ords[3]); - } - if (type == typeof(Quaternion)) - { - var ords = ReadVector(4, reader); - return new Quaternion(ords[0], ords[1], ords[2], ords[3]); - } - if (type == typeof(Matrix4x4)) - { - var ords = ReadVector(4 * 4, reader); - return new Matrix4x4( - ords[0], ords[1], ords[2], ords[3], - ords[4], ords[5], ords[6], ords[7], - ords[8], ords[9], ords[10], ords[11], - ords[12], ords[13], ords[14], ords[15]); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector3 ReadVector3(BinaryReader reader) + { + return new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + } - if (type == typeof(byte)) - return reader.ReadByte(); - if (type == typeof(UInt64)) - return reader.ReadUInt64(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static QAngle ReadQAngle(BinaryReader reader) + { + return new QAngle(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + } - throw new ArgumentException(type == null ? "No type provided to GetValue()" : "Cannot read value of type " + type.Name); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 ReadVector4(BinaryReader reader) + { + return new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Quaternion ReadQuaternion(BinaryReader reader) + { + return new Quaternion(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Matrix4x4 ReadMatrix4x4(BinaryReader reader) + { + // Read all 16 floats directly without intermediate array + return new Matrix4x4( + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); } public Datamodel Decode(string encoding, int encoding_version, string format, int format_version, Stream stream, DeferredMode defer_mode, ReflectionParams reflectionParams) { - var types = CodecUtilities.GetReflectionTypes(reflectionParams); + var elementFactoryTypes = CodecUtilities.GetIElementFactoryClasses(); + var elementFactory = (IElementFactory)Activator.CreateInstance(elementFactoryTypes.First()); stream.Seek(0, SeekOrigin.Begin); while (true) @@ -349,7 +369,8 @@ public Datamodel Decode(string encoding, int encoding_version, string format, in var dm = new Datamodel(format, format_version); EncodingVersion = encoding_version; - Reader = new BinaryReader(stream, Datamodel.TextEncoding); + + Reader = new BinaryReader(stream); if (EncodingVersion >= 9) { @@ -377,13 +398,14 @@ public Datamodel Decode(string encoding, int encoding_version, string format, in var id_bits = Reader.ReadBytes(16); var id = new Guid(BitConverter.IsLittleEndian ? id_bits : id_bits.Reverse().ToArray()); - if (!CodecUtilities.TryConstructCustomElement(types, dm, type, name, id, out _)) + if (!CodecUtilities.TryConstructCustomElement(elementFactory, reflectionParams, dm, type, name, id, out _)) { // note: constructing an element, adds it to the datamodel.AllElements _ = new Element(dm, name, id, type); } } + // read attributes (or not, if we're deferred) foreach (var elem in dm.AllElements.ToArray()) { @@ -414,12 +436,12 @@ public Datamodel Decode(string encoding, int encoding_version, string format, in public object? DeferredDecodeAttribute(Datamodel dm, long offset) { - if(Reader is null) + if (Reader is null) { throw new InvalidDataException("Tried to read a deferred attribute but the reader is invalid"); } - Reader.BaseStream.Seek(offset, SeekOrigin.Begin); + Reader.BaseStream.Seek((int)offset, SeekOrigin.Begin); return DecodeAttribute(dm, false, Reader); } @@ -428,15 +450,16 @@ public Datamodel Decode(string encoding, int encoding_version, string format, in var types = IdToType(reader.ReadByte()); if (types.Item2 == null) - return ReadValue(dm, types.Item1, EncodingVersion < 4 || prefix, reader); + return ReadValue(dm, TypeMap[types.Item1.TypeHandle], EncodingVersion < 4 || prefix, reader); else { var count = reader.ReadInt32(); var inner_type = types.Item2; var array = CodecUtilities.MakeList(inner_type, count); + var typeId = TypeMap[inner_type.TypeHandle]; foreach (var x in Enumerable.Range(0, count)) - array.Add(ReadValue(dm, inner_type, true, reader)); + array.Add(ReadValue(dm, typeId, true, reader)); return array; } @@ -449,7 +472,7 @@ void SkipAttribute(BinaryReader reader) int count = 1; Type? type = types.Item1; - if(type is null) + if (type is null) { throw new InvalidDataException("Failed to match id to type"); } @@ -554,7 +577,7 @@ public void Encode() int CountChildren(Element? elem, HashSet counter) { - if(elem is null) + if (elem is null) { return 0; } diff --git a/Datamodel.NET/Codecs/KeyValues2.cs b/Datamodel.NET/Codecs/KeyValues2.cs index ee33199..4ba2cda 100644 --- a/Datamodel.NET/Codecs/KeyValues2.cs +++ b/Datamodel.NET/Codecs/KeyValues2.cs @@ -137,7 +137,7 @@ public void Flush() void CountReferences(Element? elem) { - if(elem is null) + if (elem is null) { return; } @@ -309,7 +309,7 @@ void WriteAttribute(string name, int encodingVersion, Type type, object value, b private bool ShouldBeReferenced(Element? elem) { - if(elem is null) + if (elem is null) { return false; } @@ -336,7 +336,7 @@ void WriteElement(Element element, int encodingVersion, KV2Writer writer) foreach (var attr in element.GetAllAttributesForSerialization()) { - if (attr.Value != null) + if (attr.Value != null) WriteAttribute(attr.Key, encodingVersion, attr.Value.GetType(), attr.Value, false, writer); } @@ -362,7 +362,7 @@ public void Encode(Datamodel dm, string encoding, int encodingVersion, Stream st writer.Indent++; writer.WriteTokenLine("id", "elementid", Guid.NewGuid().ToString()); foreach (var attr in dm.PrefixAttributes) - if(attr.Value != null) + if (attr.Value != null) { WriteAttribute(attr.Key, encodingVersion, attr.Value.GetType(), attr.Value, false, writer); } @@ -374,7 +374,7 @@ public void Encode(Datamodel dm, string encoding, int encodingVersion, Stream st if (SupportsReferenceIds) CountReferences(dm.Root); - if(dm.Root != null) + if (dm.Root != null) { WriteElement(dm.Root, encodingVersion, writer); } @@ -408,7 +408,7 @@ private class IntermediateData public void HandleElementProp(Element? element, string attrName, Guid id) { - if(element is null) + if (element is null) { throw new InvalidDataException("Trying to handle the propery of an invalid element"); } @@ -483,15 +483,13 @@ string Decode_NextToken(StreamReader reader) } } - Element? Decode_ParseElement(string class_name, ReflectionParams reflectionParams, StreamReader reader, Datamodel dataModel, IntermediateData intermediateData) + Element? Decode_ParseElement(IElementFactory elementFactory, string class_name, ReflectionParams reflectionParams, StreamReader reader, Datamodel dataModel, IntermediateData intermediateData) { string elem_class = class_name ?? Decode_NextToken(reader); string elem_name = string.Empty; string elem_id = string.Empty; Element? elem = null; - var types = CodecUtilities.GetReflectionTypes(reflectionParams); - string next = Decode_NextToken(reader); if (next != "{") throw new CodecException($"Expected Element opener, got '{next}'."); while (true) @@ -509,7 +507,7 @@ string Decode_NextToken(StreamReader reader) var id = new Guid(elem_id); if (elem_class != "$prefix_element$") { - CodecUtilities.TryConstructCustomElement(types, dataModel, elem_class, elem_name, id, out elem); + CodecUtilities.TryConstructCustomElement(elementFactory, reflectionParams, dataModel, elem_class, elem_name, id, out elem); elem ??= new Element(dataModel, elem_name, id, elem_class); } @@ -538,7 +536,7 @@ string Decode_NextToken(StreamReader reader) object? attr_value = null; if (attr_type == null) - attr_value = Decode_ParseElement(attr_type_s, reflectionParams, reader, dataModel, intermediateData); + attr_value = Decode_ParseElement(elementFactory, attr_type_s, reflectionParams, reader, dataModel, intermediateData); else if (attr_type_s.EndsWith("_array")) { var array = CodecUtilities.MakeList(attr_type, 5); // assume 5 items @@ -561,19 +559,19 @@ string Decode_NextToken(StreamReader reader) } } // inline Element - else if (attr_type == typeof(Element)) + else if (attr_type == typeof(Element)) { - array.Add(Decode_ParseElement(next, reflectionParams, reader, dataModel, intermediateData)); + array.Add(Decode_ParseElement(elementFactory, next, reflectionParams, reader, dataModel, intermediateData)); } // normal value else { - array.Add(Decode_ParseValue(attr_type, next, reflectionParams, reader, dataModel, intermediateData)); + array.Add(Decode_ParseValue(elementFactory, attr_type, next, reflectionParams, reader, dataModel, intermediateData)); } } } else - attr_value = Decode_ParseValue(attr_type, Decode_NextToken(reader), reflectionParams, reader, dataModel, intermediateData); + attr_value = Decode_ParseValue(elementFactory, attr_type, Decode_NextToken(reader), reflectionParams, reader, dataModel, intermediateData); if (elem != null) elem.Add(attr_name, attr_value); @@ -583,7 +581,7 @@ string Decode_NextToken(StreamReader reader) return elem; } - object? Decode_ParseValue(Type type, string value, ReflectionParams reflectionParams, StreamReader reader, Datamodel dataModel, IntermediateData intermediateData) + object? Decode_ParseValue(IElementFactory elementFactory, Type type, string value, ReflectionParams reflectionParams, StreamReader reader, Datamodel dataModel, IntermediateData intermediateData) { if (type == typeof(string)) return value; @@ -591,7 +589,7 @@ string Decode_NextToken(StreamReader reader) value = value.Trim(); if (type == typeof(Element)) - return Decode_ParseElement(value, reflectionParams, reader, dataModel, intermediateData); + return Decode_ParseElement(elementFactory, value, reflectionParams, reader, dataModel, intermediateData); if (type == typeof(int)) return int.Parse(value, CultureInfo.InvariantCulture); else if (type == typeof(float)) @@ -661,6 +659,9 @@ string Decode_NextToken(StreamReader reader) public Datamodel Decode(string encoding, int encoding_version, string format, int format_version, Stream stream, DeferredMode defer_mode, ReflectionParams reflectionParams) { + var elementFactoryTypes = CodecUtilities.GetIElementFactoryClasses(); + var elementFactory = (IElementFactory)Activator.CreateInstance(elementFactoryTypes.First()); + var dataModel = new Datamodel(format, format_version); if (encoding == "keyvalues2_noids") @@ -682,7 +683,7 @@ public Datamodel Decode(string encoding, int encoding_version, string format, in { break; } try - { Decode_ParseElement(next, reflectionParams, reader, dataModel, intermediateData); } + { Decode_ParseElement(elementFactory, next, reflectionParams, reader, dataModel, intermediateData); } catch (Exception err) { throw new CodecException($"KeyValues2 decode failed on line {Line}:\n\n{err.Message}", err); } } @@ -700,7 +701,8 @@ public Datamodel Decode(string encoding, int encoding_version, string format, in { foreach (var id in list.Value) { - list.Key.Add(dataModel.AllElements[id]); + var elemToAdd = dataModel.AllElements[id]; + list.Key.Add(elemToAdd); } } diff --git a/Datamodel.NET/Datamodel.ElementList.cs b/Datamodel.NET/Datamodel.ElementList.cs index e08ad74..d069244 100644 --- a/Datamodel.NET/Datamodel.ElementList.cs +++ b/Datamodel.NET/Datamodel.ElementList.cs @@ -219,7 +219,7 @@ public void Trim() { foreach (var elem in this.Except(used).ToArray()) { - if(elem != null) + if (elem != null) { store.Remove(elem.ID); elem.Owner = null; @@ -241,7 +241,7 @@ public void Trim() protected void WalkElemTree(Element? elem, HashSet found) { - if(elem is null) + if (elem is null) { return; } diff --git a/Datamodel.NET/Datamodel.NET.csproj b/Datamodel.NET/Datamodel.NET.csproj index 89ea7bf..c19242d 100644 --- a/Datamodel.NET/Datamodel.NET.csproj +++ b/Datamodel.NET/Datamodel.NET.csproj @@ -4,7 +4,7 @@ Library Datamodel KeyValues2 - 0.8 + 0.9-beta enable COPYING README.md @@ -51,4 +51,9 @@ - \ No newline at end of file + + + + + + diff --git a/Datamodel.NET/Datamodel.cs b/Datamodel.NET/Datamodel.cs index edc4fdf..1dba887 100644 --- a/Datamodel.NET/Datamodel.cs +++ b/Datamodel.NET/Datamodel.cs @@ -167,7 +167,7 @@ private static ICodec GetCodec(string encoding, int encoding_version) var codecConstructor = codec_type.GetConstructor(Type.EmptyTypes); - if(codecConstructor is null) + if (codecConstructor is null) { throw new InvalidOperationException("Failed to get codec constructor."); } @@ -241,18 +241,18 @@ public void Save(string path, string encoding, int encoding_version) /// How to handle deferred loading. public static Datamodel Load(Stream stream, DeferredMode defer_mode = DeferredMode.Automatic) { - return Load_Internal(stream, Assembly.GetCallingAssembly(), defer_mode, null); + return Load_Internal(stream, defer_mode, null); } /// /// Loads a Datamodel from a . - /// + /// /// The input Stream. /// How to handle deferred loading. /// Type hint for what the Root of this datamodel should be when using reflection public static Datamodel Load(Stream stream, DeferredMode defer_mode = DeferredMode.Automatic, ReflectionParams? reflectionParams = null) where T : Element { - return Load_Internal(stream, Assembly.GetCallingAssembly(), defer_mode, reflectionParams); + return Load_Internal(stream, defer_mode, reflectionParams); } /// @@ -262,7 +262,7 @@ public static Datamodel Load(Stream stream, DeferredMode defer_mode = Deferre /// How to handle deferred loading. public static Datamodel Load(byte[] data, DeferredMode defer_mode = DeferredMode.Automatic) { - return Load_Internal(new MemoryStream(data, true), Assembly.GetCallingAssembly(), defer_mode); + return Load_Internal(new MemoryStream(data, true), defer_mode); } /// /// Loads a Datamodel from a byte array. @@ -273,7 +273,7 @@ public static Datamodel Load(byte[] data, DeferredMode defer_mode = DeferredMode public static Datamodel Load(byte[] data, ReflectionParams? reflectionParams = null) where T : Element { - return Load_Internal(new MemoryStream(data, true), Assembly.GetCallingAssembly(), DeferredMode.Disabled, reflectionParams); + return Load_Internal(new MemoryStream(data, true), DeferredMode.Disabled, reflectionParams); } /// @@ -287,7 +287,7 @@ public static Datamodel Load(string path, DeferredMode defer_mode = DeferredMode Datamodel? dm = null; try { - dm = Load_Internal(stream, Assembly.GetCallingAssembly(), defer_mode); + dm = Load_Internal(stream, defer_mode); return dm; } finally @@ -304,20 +304,37 @@ public static Datamodel Load(string path, ReflectionParams? reflectionParams where T : Element { using var stream = File.OpenRead(path); - return Load_Internal(stream, Assembly.GetCallingAssembly(), DeferredMode.Disabled, reflectionParams); + return Load_Internal(stream, DeferredMode.Disabled, reflectionParams); } - private static Datamodel Load_Internal(Stream stream, Assembly callingAssembly, DeferredMode defer_mode = DeferredMode.Automatic, ReflectionParams? reflectionParams = null) + private static Datamodel Load_Internal(Stream stream, DeferredMode defer_mode = DeferredMode.Automatic, ReflectionParams? reflectionParams = null) where T : Element { - reflectionParams ??= new (); + reflectionParams ??= new(); + + var templateType = typeof(T); + + if (templateType is null) + { + throw new InvalidDataException("Template type can't be null"); + } - if(typeof(T) == typeof(Element)) + if (templateType == typeof(Element)) { reflectionParams.AttemptReflection = false; } - reflectionParams.AssembliesToSearch.Add(callingAssembly); + // if user doesnt specify these assume assembly and namespace of root node + if (reflectionParams.Assembly == string.Empty) + { + reflectionParams.Assembly = templateType.Assembly.GetName().Name!; + } + + if (reflectionParams.Namespace == string.Empty) + { + reflectionParams.Namespace = templateType.Namespace!; + } + stream.Seek(0, SeekOrigin.Begin); var header = string.Empty; @@ -344,6 +361,9 @@ private static Datamodel Load_Internal(Stream stream, Assembly callingAssembl ICodec codec = GetCodec(encoding, encoding_version); + var typeNamespace = typeof(T).Namespace; + var typeAssembly = typeof(T).Assembly; + var dm = codec.Decode(encoding, encoding_version, format, format_version, stream, defer_mode, reflectionParams); if (defer_mode == DeferredMode.Automatic && codec is IDeferredAttributeCodec deferredCodec) { @@ -369,7 +389,7 @@ private static Datamodel Load_Internal(Stream stream, Assembly callingAssembl { result = StubRequest(id); - if(result is null) + if (result is null) { throw new InvalidDataException("Stub request failed, result was null"); } @@ -464,7 +484,7 @@ public string Format get => _Format; set { - if(value is null) + if (value is null) { throw new InvalidDataException("Format can not be null"); } @@ -499,7 +519,7 @@ public string Encoding get => _Encoding; set { - if(value is null) + if (value is null) { throw new InvalidDataException("Encoding can not be null"); } @@ -759,7 +779,7 @@ public ImportJob(ImportRecursionMode import_mode, ImportOverwriteMode overwrite_ local_element = null; } - if(local_element is null) + if (local_element is null) { return null; } @@ -779,7 +799,7 @@ public ImportJob(ImportRecursionMode import_mode, ImportOverwriteMode overwrite_ var list = (System.Collections.ICollection)attr.Value; var inner_type = GetArrayInnerType(list.GetType()); - if(inner_type is null) + if (inner_type is null) { throw new InvalidOperationException("Failed to get inner_type while importing element"); } @@ -915,7 +935,7 @@ internal DestubException(ElementArray array, int index, Exception innerException : base("An exception occured while destubbing an array item.", innerException) { var arrayOwner = array.Owner; - if(arrayOwner is not null) + if (arrayOwner is not null) { Data.Add("Element", ((Element)arrayOwner).ID); } diff --git a/Datamodel.NET/ICodec.cs b/Datamodel.NET/ICodec.cs index cef6071..4c2aed7 100644 --- a/Datamodel.NET/ICodec.cs +++ b/Datamodel.NET/ICodec.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Data; namespace Datamodel.Codecs { @@ -45,8 +46,9 @@ public interface ICodec public class ReflectionParams(bool attemptReflection = true, List? additionalTypes = null, List? assembliesToSearch = null) { public bool AttemptReflection = attemptReflection; - public List AdditionalTypes = additionalTypes ??= []; - public List AssembliesToSearch = assembliesToSearch ??= []; + + public string Assembly = string.Empty; + public string Namespace = string.Empty; } @@ -189,82 +191,41 @@ public static void AddDeferredAttribute(Element elem, string key, long offset) elem.Add(key, offset); } - public static Dictionary GetReflectionTypes(ReflectionParams reflectionParams) + public static bool TryConstructCustomElement(IElementFactory elementFactory, ReflectionParams reflectionParams, Datamodel dataModel, string elem_class, string elem_name, Guid elem_id, out Element? elem) { - Dictionary types = []; + elem = (Element?)elementFactory.GetClass(reflectionParams.Assembly, reflectionParams.Namespace, elem_class); - if (reflectionParams.AttemptReflection) + if (elem is null) { - foreach (var assembly in reflectionParams.AssembliesToSearch) - { - foreach (var classType in assembly.DefinedTypes) - { - if (classType.IsSubclassOf(typeof(Element))) - { - types.TryAdd(classType.Name, classType); - } - } - } - - foreach (var type in reflectionParams.AdditionalTypes) - { - if (type.IsSubclassOf(typeof(Element))) - { - types.TryAdd(type.Name, type); - } - } - } - - return types; - } - - public static bool TryConstructCustomElement(Dictionary types, Datamodel dataModel, string elem_class, string elem_name, Guid elem_id, out Element? elem) - { - var matchedType = types.TryGetValue(elem_class, out var classType); - - if (!matchedType || classType is null) - { - elem = null; return false; } - Type derivedType = classType; + elem.ID = elem_id; + elem.Owner = dataModel; + elem.Name = elem_name; + elem.ClassName = elem_class; - ConstructorInfo? elementConstructor = typeof(Element).GetConstructor( - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, - null, - [typeof(Datamodel), typeof(string), typeof(Guid), typeof(string)], - null - ); - - var customClassInitializer = derivedType.GetConstructor( - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, - null, - [], - null - ); - - if (elementConstructor == null) - { - throw new InvalidOperationException("Failed to get constructor while attemption reflection based deserialisation"); - } - - if (customClassInitializer == null) - { - throw new InvalidOperationException("Failed to get custom element constructor."); - } - - object uninitializedObject = RuntimeHelpers.GetUninitializedObject(derivedType); - - // this will initialize values such as - // public Datamodel.ElementArray Children { get; } = []; - customClassInitializer.Invoke(uninitializedObject, []); - - elementConstructor.Invoke(uninitializedObject, [dataModel, elem_name, elem_id, elem_class]); - - elem = (Element?)uninitializedObject; return true; } + + public static IEnumerable GetIElementFactoryClasses() + { + return AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assembly => + { + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + return ex.Types.Where(t => t != null); + } + }) + .Where(type => type.IsClass && + !type.IsAbstract && + type.GetInterfaces().Contains(typeof(IElementFactory))); + } } [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)] @@ -296,4 +257,9 @@ public CodecFormatAttribute(string name, int version) public string Name { get; private set; } public int[] Versions { get; private set; } } + + public interface IElementFactory + { + public object? GetClass(string assembly, string nameSpace, string classname); + } } diff --git a/ElementFactoryGenerator/COPYING b/ElementFactoryGenerator/COPYING new file mode 100644 index 0000000..0856733 --- /dev/null +++ b/ElementFactoryGenerator/COPYING @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Tom Edwards contact@steamreview.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/ElementFactoryGenerator/ElementFactory.cs b/ElementFactoryGenerator/ElementFactory.cs new file mode 100644 index 0000000..6cfc5d4 --- /dev/null +++ b/ElementFactoryGenerator/ElementFactory.cs @@ -0,0 +1,290 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; + +[Generator] +public class ElementFactoryGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var provider = context.SyntaxProvider.CreateSyntaxProvider( + + predicate: static (node, _) => node is ClassDeclarationSyntax, + transform: static (ctx, _) => (ClassDeclarationSyntax)ctx.Node + + ).Where(m => m is not null); + + var compilation = context.CompilationProvider.Combine(provider.Collect()); + + context.RegisterSourceOutput(compilation, Execute); + } + + private void Execute(SourceProductionContext context, (Compilation Left, ImmutableArray Right) tuple) + { + StringBuilder elementFactory = new(); + + var assemblies = new List(); + + elementFactory.Append( + """" + public class ElementFactory : Datamodel.Codecs.IElementFactory + { + public object? GetClass(string assembly, string nameSpace, string classname) + { + switch (assembly) + { + + """"); + + var (compilation, classDeclList) = tuple; + + var runningAssembly = new FactoryAssembly(compilation.AssemblyName); + foreach (var classDecl in classDeclList) + { + var type = compilation.GetSemanticModel(classDecl.SyntaxTree).GetDeclaredSymbol(classDecl); + + if (type is not null) + { + runningAssembly.AddType(type.ContainingNamespace.ToDisplayString(), type); + } + } + assemblies.Add(runningAssembly); + + foreach (var assemblySymbol in compilation.SourceModule.ReferencedAssemblySymbols) + { + if (assemblySymbol.Kind == SymbolKind.NetModule) + { + continue; + } + + var referencedAssembly = new FactoryAssembly(assemblySymbol.Name); + + var assemblyTypes = GetAllClassesFromAssembly(assemblySymbol); + foreach (var assemblyType in assemblyTypes) + { + referencedAssembly.AddType(assemblyType.ContainingNamespace.ToDisplayString(), assemblyType); + } + + assemblies.Add(referencedAssembly); + } + + foreach (var assembly in assemblies) + { + var namespaceStringBuilder = new StringBuilder(); + + namespaceStringBuilder.Append( + """" + + switch (nameSpace) + { + """"); + + var validNamespaces = 0; + foreach (var nameSpace in assembly.Namespaces) + { + var typeseStringBuilder = new StringBuilder(); + + typeseStringBuilder.Append( + """" + + switch (classname) + { + """"); + var validTypes = 0; + foreach (var type in nameSpace.Types) + { + if (ValidateType(type, compilation)) + { + validTypes++; + + typeseStringBuilder.AppendLine( + $"""" + + case "{type.Name}": + return new {type.ToDisplayString()}(); + """"); + } + + } + + typeseStringBuilder.AppendLine( + """" + } + """"); + + if (validTypes > 0) + { + validNamespaces++; + namespaceStringBuilder.AppendLine( + $"""" + + case "{nameSpace.Name}": + {typeseStringBuilder.ToString()} + break; + """"); + } + + } + + namespaceStringBuilder.AppendLine( + """" + } + """"); + + if (validNamespaces > 0) + { + elementFactory.AppendLine( + $""" + case "{assembly.Name}" : + {namespaceStringBuilder.ToString()} + break; + + """); + } + + } + + elementFactory.AppendLine( + """" + } + + return null; + } + + }; + """"); + + context.AddSource("ElementFactory.g.cs", elementFactory.ToString()); + } + + private static bool ValidateType(INamedTypeSymbol type, Compilation compilation) + { + if (InheritsFromFullName(type, "Datamodel.Element")) + { + // no nonsense please + if (type.IsAbstract || type.IsVirtual || type.TypeParameters.Length > 0) + { + return false; + } + + if (type.DeclaredAccessibility != Accessibility.Internal && type.DeclaredAccessibility != Accessibility.Public) + { + return false; + } + + // only internal classes in execution assembly are fine + if (type.DeclaredAccessibility == Accessibility.Internal) + { + if (!SymbolEqualityComparer.Default.Equals(compilation.Assembly, type.ContainingAssembly)) + { + return false; + } + } + + return true; + } + + return false; + } + + private static IEnumerable GetAllClassesFromAssembly(IAssemblySymbol assembly) + { + return GetAllTypesFromNamespace(assembly.GlobalNamespace) + .Where(t => t.TypeKind == TypeKind.Class); + } + + public static bool InheritsFromFullName(INamedTypeSymbol type, string fullBaseClassName) + { + var current = type.BaseType; + while (current != null) + { + if (current.ToDisplayString() == fullBaseClassName) + return true; + current = current.BaseType; + } + return false; + } + + private static IEnumerable GetAllTypesFromNamespace(INamespaceSymbol namespaceSymbol) + { + // Get all types directly in this namespace + foreach (var type in namespaceSymbol.GetTypeMembers()) + { + yield return type; + + // Get nested types recursively + foreach (var nestedType in GetNestedTypes(type)) + { + yield return nestedType; + } + } + + // Recursively process child namespaces + foreach (var childNamespace in namespaceSymbol.GetNamespaceMembers()) + { + foreach (var type in GetAllTypesFromNamespace(childNamespace)) + { + yield return type; + } + } + } + + private static IEnumerable GetNestedTypes(INamedTypeSymbol type) + { + foreach (var nestedType in type.GetTypeMembers()) + { + yield return nestedType; + + // Recursively get nested types within nested types + foreach (var deeplyNestedType in GetNestedTypes(nestedType)) + { + yield return deeplyNestedType; + } + } + } + + private class FactoryAssembly(string name) + { + public string Name = name; + + public HashSet Namespaces = new(); + + public void AddType(string nameSpaceName, INamedTypeSymbol type) + { + NamespacesContainsNamespace(nameSpaceName, out FactoryNamespace? foundNameSpace); + + if (foundNameSpace == null) + { + foundNameSpace = new FactoryNamespace(nameSpaceName); + Namespaces.Add(foundNameSpace); + } + + foundNameSpace.Types.Add(type); + } + + private bool NamespacesContainsNamespace(string namespaceName, out FactoryNamespace? outNameSpace) + { + foreach (var nameSpace in Namespaces) + { + if (nameSpace.Name == namespaceName) + { + outNameSpace = nameSpace; + return true; + } + } + + outNameSpace = null; + return false; + } + } + + private class FactoryNamespace(string name) + { + public string Name = name; + public List Types = new(); + } +} \ No newline at end of file diff --git a/ElementFactoryGenerator/ElementFactoryGenerator.csproj b/ElementFactoryGenerator/ElementFactoryGenerator.csproj new file mode 100644 index 0000000..624a59b --- /dev/null +++ b/ElementFactoryGenerator/ElementFactoryGenerator.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + latest + 0.1 + COPYING + README.md + enable + true + snupkg + true + + + + + + + + + + + + \ No newline at end of file diff --git a/ElementFactoryGenerator/Properties/launchSettings.json b/ElementFactoryGenerator/Properties/launchSettings.json new file mode 100644 index 0000000..16bcb10 --- /dev/null +++ b/ElementFactoryGenerator/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Profile 1": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\Datamodel.NET\\Datamodel.NET.csproj" + } + } +} \ No newline at end of file diff --git a/ElementFactoryGenerator/README.md b/ElementFactoryGenerator/README.md new file mode 100644 index 0000000..6ce7f24 --- /dev/null +++ b/ElementFactoryGenerator/README.md @@ -0,0 +1 @@ +Code generator for reflection based deserialisation in Datamodel.NET \ No newline at end of file diff --git a/Tests/Tests.cs b/Tests/Tests.cs index 4fbc427..845c5f7 100644 --- a/Tests/Tests.cs +++ b/Tests/Tests.cs @@ -9,7 +9,7 @@ using System.Numerics; using DM = Datamodel.Datamodel; using System.Globalization; -using VMAP; +using Tests.VMAP; namespace Datamodel_Tests { @@ -20,7 +20,7 @@ public class DatamodelTests protected FileStream Binary_4_File = File.OpenRead(TestContext.CurrentContext.TestDirectory + "/Resources/binary4.dmx"); protected FileStream KeyValues2_1_File = File.OpenRead(TestContext.CurrentContext.TestDirectory + "/Resources/taunt05.dmx"); - const string GameBin = @"C:/Program Files (x86)/Steam/steamapps/common/Counter-Strike Global Offensive/game/bin/win64"; + const string GameBin = @"D:/Steam/steamapps/common/Counter-Strike Global Offensive/game/bin/win64"; static readonly string DmxConvertExe = Path.Combine(GameBin, "dmxconvert.exe"); static readonly bool DmxConvertExe_Exists = File.Exists(DmxConvertExe); @@ -321,7 +321,7 @@ private static void Validate_Vmap_Reflection(Datamodel.Datamodel unserialisedVma Assert.AreEqual(vertexData.size, 8); Assert.AreEqual(vertexData.streams[0]["semanticName"], "position"); - var typedPolygonMeshData = (VMAP.CDmePolygonMeshDataStream)vertexData.streams[0]; + var typedPolygonMeshData = (CDmePolygonMeshDataStream)vertexData.streams[0]; Assert.AreEqual(typedPolygonMeshData.semanticName, "position"); var typedPolygonMeshDataStream = typedPolygonMeshData.data as Vector3Array; diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index b052fc3..5eef346 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -22,4 +22,8 @@ + + + + \ No newline at end of file diff --git a/Tests/ValveMap.cs b/Tests/ValveMap.cs index 8ce70c5..9037024 100644 --- a/Tests/ValveMap.cs +++ b/Tests/ValveMap.cs @@ -2,7 +2,7 @@ using System.Numerics; using DMElement = Datamodel.Element; -namespace VMAP; +namespace Tests.VMAP; #nullable enable