diff --git a/Assets/Resources/Newtonsoft.Json-for-Unity.Converters.asset b/Assets/Resources/Newtonsoft.Json-for-Unity.Converters.asset index 1266bae..b06496c 100644 --- a/Assets/Resources/Newtonsoft.Json-for-Unity.Converters.asset +++ b/Assets/Resources/Newtonsoft.Json-for-Unity.Converters.asset @@ -19,142 +19,188 @@ MonoBehaviour: unityConverters: - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Addressables.AssetReferenceConverter + converterAssembly: Newtonsoft.Json.UnityConverters.Addressables settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.AI.NavMesh.NavMeshQueryFilterConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.AI.NavMesh.NavMeshTriangulationConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Camera.CullingGroupEventConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Geometry.BoundsConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Geometry.BoundsIntConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Geometry.PlaneConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Geometry.RectConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Geometry.RectIntConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Geometry.RectOffsetConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Graphics.ResolutionConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Hashing.Hash128Converter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Math.Color32Converter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Math.ColorConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Math.Matrix4x4Converter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Math.QuaternionConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Math.SphericalHarmonicsL2Converter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Math.Vector2Converter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Math.Vector2IntConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Math.Vector3Converter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Math.Vector3IntConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Math.Vector4Converter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.NativeArray.NativeArrayConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Physics.JointDriveConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Physics.JointLimitsConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Physics.SoftJointLimitConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Physics2D.ColliderDistance2DConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Physics2D.ContactFilter2DConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Random.RandomStateConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Scripting.LayerMaskConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] - enabled: 1 converterName: Newtonsoft.Json.UnityConverters.Scripting.RangeIntConverter + converterAssembly: Newtonsoft.Json.UnityConverters settings: [] useAllJsonNetConverters: 0 jsonNetConverters: - enabled: 1 converterName: Newtonsoft.Json.Converters.StringEnumConverter + converterAssembly: Newtonsoft.Json settings: [] - enabled: 1 converterName: Newtonsoft.Json.Converters.VersionConverter + converterAssembly: Newtonsoft.Json settings: [] - enabled: 0 converterName: Newtonsoft.Json.Converters.BinaryConverter + converterAssembly: Newtonsoft.Json settings: [] - enabled: 0 converterName: Newtonsoft.Json.Converters.BsonObjectIdConverter + converterAssembly: Newtonsoft.Json settings: [] - enabled: 0 converterName: Newtonsoft.Json.Converters.DataSetConverter + converterAssembly: Newtonsoft.Json settings: [] - enabled: 0 converterName: Newtonsoft.Json.Converters.DataTableConverter + converterAssembly: Newtonsoft.Json settings: [] - enabled: 0 converterName: Newtonsoft.Json.Converters.DiscriminatedUnionConverter + converterAssembly: Newtonsoft.Json settings: [] - enabled: 0 converterName: Newtonsoft.Json.Converters.EntityKeyMemberConverter + converterAssembly: Newtonsoft.Json settings: [] - enabled: 0 converterName: Newtonsoft.Json.Converters.ExpandoObjectConverter + converterAssembly: Newtonsoft.Json settings: [] - enabled: 0 converterName: Newtonsoft.Json.Converters.IsoDateTimeConverter + converterAssembly: Newtonsoft.Json settings: [] - enabled: 0 converterName: Newtonsoft.Json.Converters.JavaScriptDateTimeConverter + converterAssembly: Newtonsoft.Json settings: [] - enabled: 0 converterName: Newtonsoft.Json.Converters.KeyValuePairConverter + converterAssembly: Newtonsoft.Json settings: [] - enabled: 0 converterName: Newtonsoft.Json.Converters.RegexConverter + converterAssembly: Newtonsoft.Json settings: [] - enabled: 0 converterName: Newtonsoft.Json.Converters.UnixDateTimeConverter + converterAssembly: Newtonsoft.Json settings: [] - enabled: 0 converterName: Newtonsoft.Json.Converters.XmlNodeConverter + converterAssembly: Newtonsoft.Json settings: [] autoSyncConverters: 1 diff --git a/Build/version.json b/Build/version.json index 6e22d3e..3b5f4db 100644 --- a/Build/version.json +++ b/Build/version.json @@ -1,7 +1,7 @@ { "Major": 1, "Minor": 6, - "Patch": 2, + "Patch": 3, "Suffix": "", - "AutoDeployLiveRun": true + "AutoDeployLiveRun": false } diff --git a/CHANGELOG.md b/CHANGELOG.md index d5b6cd7..f015054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Unity Converters for Newtonsoft.Json changelog +## 1.6.3 (WIP) + +- Fixed converter lookups collisions when multiple assemblies converters + with the same name, as it was not resolving assemblies in an exact way + nor deterministic order: + + - Added assembly name field to `ConverterConfig` for each converter. + + - Changed TypeCache to sort assemblies based on `FullName` + and some heuristics. + + - Changed TypeCache to lookup type in correct assembly, + based on the assembly's name. + + Thanks [@Erifirin](https://github.com/Erifirin) for the pull request ([#90](https://github.com/jilleJr/Newtonsoft.Json-for-Unity.Converters/pull/90)) + ## 1.6.2 (2024-01-08) - Fixed typo in the new Unity.Mathematics QuaternionConverter's namespace: diff --git a/Packages/Newtonsoft.Json-for-Unity.Converters/Configuration/ConverterConfig.cs b/Packages/Newtonsoft.Json-for-Unity.Converters/Configuration/ConverterConfig.cs index f1551e9..fda5161 100644 --- a/Packages/Newtonsoft.Json-for-Unity.Converters/Configuration/ConverterConfig.cs +++ b/Packages/Newtonsoft.Json-for-Unity.Converters/Configuration/ConverterConfig.cs @@ -12,11 +12,13 @@ public struct ConverterConfig : IEquatable public string converterName; + public string converterAssembly; + public List settings; public override string ToString() { - return $"{{enabled={enabled}, converterName={converterName}, settings=[{settings?.Count ?? 0}]}}"; + return $"{{enabled={enabled}, converterName={converterName}, assembly={converterAssembly}, settings=[{settings?.Count ?? 0}]}}"; } public override bool Equals(object obj) @@ -28,6 +30,7 @@ public bool Equals(ConverterConfig other) { return enabled == other.enabled && converterName == other.converterName && + converterAssembly == other.converterAssembly && EqualityComparer>.Default.Equals(settings, other.settings); } @@ -35,9 +38,10 @@ public bool Equals(ConverterConfig other) public override int GetHashCode() #pragma warning restore S2328 // "GetHashCode" should not reference mutable fields { - int hashCode = 1016066258; + int hashCode = 913629501; hashCode = hashCode * -1521134295 + enabled.GetHashCode(); hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(converterName); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(converterAssembly); hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(settings); return hashCode; } diff --git a/Packages/Newtonsoft.Json-for-Unity.Converters/Editor/UnityConvertersConfigEditor.cs b/Packages/Newtonsoft.Json-for-Unity.Converters/Editor/UnityConvertersConfigEditor.cs index 1e6e7ce..8c293fd 100644 --- a/Packages/Newtonsoft.Json-for-Unity.Converters/Editor/UnityConvertersConfigEditor.cs +++ b/Packages/Newtonsoft.Json-for-Unity.Converters/Editor/UnityConvertersConfigEditor.cs @@ -150,11 +150,27 @@ public override void OnInspectorGUI() private void AddAndSetupConverters(SerializedProperty arrayProperty, IList converterTypes, bool newAreEnabledByDefault) { - var elements = EnumerateArrayElements(arrayProperty); + var elements = EnumerateArrayElements(arrayProperty).ToArray(); var elementTypes = elements - .Select(e => TypeCache.FindType(e.FindPropertyRelative(nameof(ConverterConfig.converterName)).stringValue)) + .Select(e => TypeCache.FindType( + name: e.FindPropertyRelative(nameof(ConverterConfig.converterName)).stringValue, + assemblyName: e.FindPropertyRelative(nameof(ConverterConfig.converterAssembly)).stringValue + )) .ToArray(); + // Refresh missing fields on existing types + for (int i = 0; i < elements.Length; i++) + { + SerializedProperty elem = elements[i]; + Type type = elementTypes[i]; + + var assemblyNameProp = elem.FindPropertyRelative(nameof(ConverterConfig.converterAssembly)); + if (string.IsNullOrEmpty(assemblyNameProp.stringValue)) + { + assemblyNameProp.stringValue = type.Assembly.GetName().Name; + } + } + Type[] missingConverters = converterTypes .Where(type => !elementTypes.Contains(type)) .ToArray(); @@ -166,7 +182,7 @@ private void AddAndSetupConverters(SerializedProperty arrayProperty, IList { continue; } - var typeName = arrayProperty.GetArrayElementAtIndex(i).FindPropertyRelative(nameof(ConverterConfig.converterName)).stringValue; + string typeName = arrayProperty.GetArrayElementAtIndex(i).FindPropertyRelative(nameof(ConverterConfig.converterName)).stringValue; Debug.Log($"Removed type from JsonConverter list: \"{typeName}\"", target); arrayProperty.DeleteArrayElementAtIndex(i); } @@ -179,9 +195,11 @@ private void AddAndSetupConverters(SerializedProperty arrayProperty, IList SerializedProperty elemProp = arrayProperty.GetArrayElementAtIndex(nextIndex); SerializedProperty enabledProp = elemProp.FindPropertyRelative(nameof(ConverterConfig.enabled)); SerializedProperty converterNameProp = elemProp.FindPropertyRelative(nameof(ConverterConfig.converterName)); + SerializedProperty assemblyNameProp = elemProp.FindPropertyRelative(nameof(ConverterConfig.converterAssembly)); enabledProp.boolValue = newAreEnabledByDefault; converterNameProp.stringValue = converterType.FullName; + assemblyNameProp.stringValue = converterType.Assembly.GetName().Name; } } @@ -236,7 +254,10 @@ private void FoldoutConvertersList(SerializedProperty property, AnimBool fadedAn var allConfigsWithType = EnumerateArrayElements(property) .Select(o => ( serializedProperty: o, - type: TypeCache.FindType(o.FindPropertyRelative(nameof(ConverterConfig.converterName)).stringValue) + type: TypeCache.FindType( + name: o.FindPropertyRelative(nameof(ConverterConfig.converterName)).stringValue, + assemblyName: o.FindPropertyRelative(nameof(ConverterConfig.converterAssembly)).stringValue + ) )) .Where(o => o.type != null) .OrderBy(o => o.type.FullName); diff --git a/Packages/Newtonsoft.Json-for-Unity.Converters/UnityConverters/UnityConverterInitializer.cs b/Packages/Newtonsoft.Json-for-Unity.Converters/UnityConverters/UnityConverterInitializer.cs index 901d605..e73ddcf 100644 --- a/Packages/Newtonsoft.Json-for-Unity.Converters/UnityConverters/UnityConverterInitializer.cs +++ b/Packages/Newtonsoft.Json-for-Unity.Converters/UnityConverters/UnityConverterInitializer.cs @@ -157,15 +157,15 @@ internal static ConverterGrouping FindGroupedConverters(UnityConvertersConfig co } return new ConverterGrouping { - outsideConverters = config.outsideConverters.Select(x => GetTypeOrLog(x.converterName)).WhereNotNullRef().ToList(), - unityConverters = config.unityConverters.Select(x => GetTypeOrLog(x.converterName)).WhereNotNullRef().ToList(), - jsonNetConverters = config.jsonNetConverters.Select(x => GetTypeOrLog(x.converterName)).WhereNotNullRef().ToList(), + outsideConverters = config.outsideConverters.Select(x => GetTypeOrLog(x.converterName, x.converterAssembly)).WhereNotNullRef().ToList(), + unityConverters = config.unityConverters.Select(x => GetTypeOrLog(x.converterName, x.converterAssembly)).WhereNotNullRef().ToList(), + jsonNetConverters = config.jsonNetConverters.Select(x => GetTypeOrLog(x.converterName, x.converterAssembly)).WhereNotNullRef().ToList(), }; } - private static Type GetTypeOrLog(string name) + private static Type GetTypeOrLog(string name, string assemblyName) { - var type = TypeCache.FindType(name); + var type = TypeCache.FindType(name, assemblyName); if (type == null) { Debug.LogWarning($"Failed to lookup JsonConverter type. Ignoring it. Type name: \"{name}\""+ @@ -215,7 +215,7 @@ private static IEnumerable ApplyConfigFilter(IEnumerable types, bool var typesOfEnabledThroughConfig = configs .Where(o => o.enabled) - .Select(o => Utility.TypeCache.FindType(o.converterName)) + .Select(o => Utility.TypeCache.FindType(o.converterName, o.converterAssembly)) .Where(o => o != null); var hashMap = new HashSet(typesOfEnabledThroughConfig); diff --git a/Packages/Newtonsoft.Json-for-Unity.Converters/Utility/TypeCache.cs b/Packages/Newtonsoft.Json-for-Unity.Converters/Utility/TypeCache.cs index bef86ae..5c2be85 100644 --- a/Packages/Newtonsoft.Json-for-Unity.Converters/Utility/TypeCache.cs +++ b/Packages/Newtonsoft.Json-for-Unity.Converters/Utility/TypeCache.cs @@ -7,44 +7,142 @@ namespace Newtonsoft.Json.UnityConverters.Utility { internal static class TypeCache { - private static readonly Dictionary _typeByName - = new Dictionary(); - - private static readonly Assembly[] _assemblies - = AppDomain.CurrentDomain.GetAssemblies() - // Reversing so we get last imported assembly first. - // When searching for types we want to look in mscorlib last - // and Newtonsoft.Json up as the first ones - .Reverse() - .ToArray(); - - public static Type FindType(string name) + private static readonly Dictionary, Type> _typeByNameAndAssembly + = new Dictionary, Type>(); + private static readonly Dictionary _assemblyByName + = new Dictionary(); + + private static readonly Assembly[] _assemblies; + + static TypeCache() { - if (_typeByName.TryGetValue(name, out var type)) + var assemblies = AppDomain.CurrentDomain.GetAssemblies() + .Select(x => new NamedAssembly(x)) + .Where(x => x.name != "Microsoft.GeneratedCode") + .OrderBy(AssemblyOrderBy).ThenBy(x => x.name) + .ToList(); + + _assemblies = new Assembly[assemblies.Count]; + _assemblyByName = new Dictionary(assemblies.Count); + + for (int i = 0; i < assemblies.Count; i++) { - return type; + NamedAssembly assembly = assemblies[i]; + _assemblies[i] = assembly.assembly; + + // Adding them like this because the LINQ ToDictionary does not like duplicate keys + _assemblyByName[assembly.name] = assembly.assembly; } - else + } + + private static int AssemblyOrderBy(NamedAssembly record) + { + // Newtonsoft.Json converters should be among the first, as they're most commonly referenced + if (record.name.StartsWith("Newtonsoft.Json")) { - // Check this assembly, or if it has AssemblyQualifiedName - type = Type.GetType(name); - if (type != null) + return -10; + } + + // Relies on the heuristic that "assembly name == namespace" + switch (GetRootNamespace(record.name)) + { + // User-defined code gets sent to the top + case "Assembly-CSharp": + return -1; + case "Assembly-CSharp-Editor": + return -2; + + // Unity standard library does not contain any converters + case "Unity": + return 10; + case "UnityEngine": + return 11; + case "UnityEditor": + return 12; + + // .NET standard library does not contain any converters, so just put it at the end + case "System": + return 20; + case "netstandard": + return 21; + case "mscorlib": + return 22; + + default: + return 0; + } + } + + private static string GetRootNamespace(string name) + { + int index = name.IndexOf('.'); + if (index > 0) + { + return name.Substring(0, index); + } + + return name; + } + + public static Type FindType(string name, string assemblyName) + { + Type type; + if (assemblyName != null) + { + if (_typeByNameAndAssembly.TryGetValue((name, assemblyName), out type)) { - _typeByName[name] = type; return type; } - // Check all the other assemblies, from last imported to first - foreach (var assembly in _assemblies) + if (_assemblyByName.TryGetValue(assemblyName, out var asm)) { - type = assembly.GetType(name); + type = asm.GetType(name); if (type != null) { - _typeByName[name] = type; + _typeByNameAndAssembly[(name, assemblyName)] = type; return type; } } - return null; + } + else + { + if (_typeByNameAndAssembly.TryGetValue((name, null), out type)) + { + return type; + } + } + + // Check this assembly, or if it has AssemblyQualifiedName + type = Type.GetType(name); + if (type != null) + { + _typeByNameAndAssembly[(name, null)] = type; + return type; + } + + // Check all the other assemblies, from last imported to first + foreach (var assembly in _assemblies) + { + type = assembly.GetType(name); + if (type != null) + { + _typeByNameAndAssembly[(name, null)] = type; + return type; + } + } + + return null; + } + + private readonly struct NamedAssembly + { + public Assembly assembly { get; } + public string name { get; } + + public NamedAssembly(Assembly assembly) + { + this.assembly = assembly; + name = assembly.GetName().Name; } } }