using System; using System.Collections.Generic; using UnityEditor; using UnityEngine; namespace Unity.GraphAuthoring.Editor.ProviderSystem { internal class ProviderLibrary { internal static event Action> OnProvidersReloaded; internal static void NotifyProvidersReloaded(IReadOnlyList modifiedAssetIDs) => OnProvidersReloaded?.Invoke(modifiedAssetIDs); static ProviderLibrary s_instance; internal static bool IsInitialized => s_instance != null; internal static bool TryGetInstance(out ProviderLibrary instance) { if (AssetDatabase.IsAssetImportWorkerProcess()) { instance = null; return false; } if (s_instance is null) { s_instance = new ProviderLibrary(); ProviderTypeCache.PopulateLibrary(s_instance); ShaderReflectionAssetPostProcessor.PopulateFromFiles(s_instance); } instance = s_instance; return true; } // Note: The provider key + version concept is being used heavily here, but is never exposed to the public. Dictionary<(string key, string version), IProvider> m_providers = new(); Dictionary<(string key, string version), List> m_conflictedProviders = new(); Dictionary> m_providersByVersion = new(); Dictionary> m_providersByAsset = new(); Dictionary> m_providersByGroup = new(); IComparer m_versionComparer = null; IComparer<(int priority, (string key, string version) vk)> m_priorityComparer = null; static int CompareVersion(string a, string b) { if (a == Hints.Version.kUnversioned && b == Hints.Version.kUnversioned) return 0; if (a == Hints.Version.kUnversioned) return 1; if (b == Hints.Version.kUnversioned) return -1; bool ap = Version.TryParse(a, out var pa); bool bp = Version.TryParse(b, out var pb); return -((ap && bp) ? pa.CompareTo(pb) : string.Compare(a, b, StringComparison.Ordinal)); } static int ComparePriority((int priority, (string key, string version) vk) a, (int priority, (string key, string version) vk) b) { var cmpPriority = -a.priority.CompareTo(b.priority); return cmpPriority != 0 ? cmpPriority : a.vk.key.CompareTo(b.vk.key); } internal IEnumerable> MainProviders() where T : IDefinition { foreach (var group in m_providersByGroup.Values) { foreach (var gke in group) { if (m_providers[gke.vk] is IProvider typedProvider) { yield return typedProvider; break; } } } // Only emit the latest version iff it is ungrouped. We don't fall back to earlier ungrouped versions. foreach (var pkv in m_providersByVersion) { string version = null; foreach (var v in pkv.Value) { version = v; break; } // .First() var provider = m_providers[(pkv.Key, version)]; if (provider is IProvider typedProvider && provider.Group == null) yield return typedProvider; } } internal IEnumerable> ProvidersByGroup(string groupKey) where T : IDefinition { if (m_providersByGroup.TryGetValue(groupKey, out var group)) { foreach (var gke in group) { var provider = m_providers[gke.vk]; if (provider is IProvider typedProvider) yield return typedProvider; } } } internal IEnumerable> ProvidersByVersion(string key) where T : IDefinition { if (m_providersByVersion.TryGetValue(key, out var entries)) foreach (var version in entries) { var provider = m_providers[(key, version)]; if (provider is IProvider typedProvider) yield return typedProvider; } } internal void ClearByAssetID(GUID assetID) { if (m_providersByAsset.Remove(assetID, out var affected)) foreach (var vk in affected) RemoveProvider(m_providers[vk]); List conflictsToRemove = new(); foreach (var conflicts in m_conflictedProviders.Values) foreach (var conflict in conflicts) if (conflict.AssetID == assetID) conflictsToRemove.Add(conflict); foreach (var conflict in conflictsToRemove) RemoveProvider(conflict); } internal IEnumerable GetTrackedAssetIDs() => m_providersByAsset.Keys; internal bool TryAdd(IProvider provider) // TODO(SVFXG-918): Improve feedback and logging for conflicts. { // provider is malformed. if (provider == null || string.IsNullOrEmpty(provider.ProviderKey) || string.IsNullOrEmpty(provider.Version)) { return false; } var vk = (provider.ProviderKey, provider.Version); // Key is already conflicted, so we can just add this to the conflicted set. if (m_conflictedProviders.TryGetValue(vk, out var byConflict)) { byConflict.Add(provider); return false; } // An unconflicted key already exists, so now this new one and the original are in conflict. if (!m_providers.TryAdd(vk, provider)) { m_conflictedProviders.TryAdd(vk, new()); m_conflictedProviders[vk].Add(provider); m_conflictedProviders[vk].Add(m_providers[vk]); RemoveProvider(m_providers[vk]); return false; } // Looks good, update bookkeeping. // Lookup by Version. m_providersByVersion.TryAdd(provider.ProviderKey, new(m_versionComparer ??= Comparer.Create(CompareVersion))); m_providersByVersion[provider.ProviderKey].Add(provider.Version); // Lookup by Asset ID. if (provider.AssetID != default) { m_providersByAsset.TryAdd(provider.AssetID, new()); m_providersByAsset[provider.AssetID].Add(vk); } // Lookup by Group Key. if (provider.Group != null) { m_providersByGroup.TryAdd(provider.Group, new(m_priorityComparer ??= Comparer<(int, (string, string))>.Create(ComparePriority))); var group = m_providersByGroup[provider.Group]; // We only want the latest version of a provider in a group, so we need to check if one is currently in the group first. var currentFound = false; var currentVersion = Hints.Version.kUnversioned; var currentPriority = 0; foreach (var gke in group) if (gke.vk.key == provider.ProviderKey) { currentFound = true; currentVersion = gke.vk.version; currentPriority = gke.priority; break; } if (!currentFound) { group.Add((provider.GroupPriority, vk)); } else if (CompareVersion(provider.Version, currentVersion) > 0) { group.Remove((currentPriority, (provider.ProviderKey, currentVersion))); group.Add((provider.GroupPriority, vk)); } // else: the version we're adding is actually older than current, so we should do nothing. } return true; } internal bool TryGet(string key, string version, out IProvider provider) where T : IDefinition { provider = null; if (m_providers.TryGetValue((key, version), out var untypedProvider)) { if (untypedProvider is IProvider typedProvider) { provider = typedProvider; return true; } } return false; } internal bool TryGet(string key, out IProvider provider) where T : IDefinition { provider = null; if (m_providersByVersion.TryGetValue(key, out var byVersion)) { foreach(var version in byVersion) { if (m_providers[(key, version)] is IProvider typedProvider) { provider = typedProvider; return true; } } } return false; } void RemoveProvider(IProvider provider) { var vk = (provider.ProviderKey, provider.Version); if (m_providers.Remove(vk)) { if (m_providersByAsset.TryGetValue(provider.AssetID, out var byAsset)) { byAsset.Remove(vk); if (byAsset.Count == 0) m_providersByAsset.Remove(provider.AssetID); } if (m_providersByVersion.TryGetValue(provider.ProviderKey, out var byVersion)) { byVersion.Remove(provider.Version); if (byVersion.Count == 0) m_providersByVersion.Remove(provider.ProviderKey); } if (provider.Group != null && m_providersByGroup.TryGetValue(provider.Group, out var byGroup)) { byGroup.Remove((provider.GroupPriority, vk)); if (byVersion.Count > 0) { foreach(var lkg in byVersion) { if (m_providers.TryGetValue((provider.ProviderKey, lkg), out var nextBestProvider)) { if (nextBestProvider.Group == provider.Group) { byGroup.Add((nextBestProvider.GroupPriority, (nextBestProvider.ProviderKey, nextBestProvider.Version))); break; } } } } if (byGroup.Count == 0) { m_providersByGroup.Remove(provider.Group); } } } else if (m_conflictedProviders.ContainsKey(vk)) { // If we remove a conflicted provider, find its index. for(int i = 0; i < m_conflictedProviders[vk].Count; ++i) { var conflict = m_conflictedProviders[vk][i]; if (conflict == provider || conflict.AssetID == provider.AssetID) { // remove it. m_conflictedProviders[vk].RemoveAt(i); // if there's only one left it is now uncontested and can be added to the library. if (m_conflictedProviders[vk].Count == 1) { var unconflicted = m_conflictedProviders[vk][0]; m_conflictedProviders.Remove(vk); TryAdd(unconflicted); // Conflict Resolved. } // Conflict still remains. break; } } } } } }