diff --git a/Assets/Editor/CheckBundleDupeDependenciesV2.cs b/Assets/Editor/CheckBundleDupeDependenciesV2.cs index b705728..488f226 100644 --- a/Assets/Editor/CheckBundleDupeDependenciesV2.cs +++ b/Assets/Editor/CheckBundleDupeDependenciesV2.cs @@ -1,44 +1,29 @@ +#if UNITY_EDITOR /// This AnalyzeRule is based on the built-in rule CheckBundleDupeDependencies /// This rule finds assets in Addressables that will be duplicated across multiple AssetBundles /// Instead of placing all problematic assets in a shared Group, this rule results in fewer AssetBundles /// being created by placing assets with the same AssetBundle parents into the same label and AssetBundle using System; -using System.Collections.Generic; using System.Linq; +using System.Collections.Generic; + +using UnityEngine; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEditor.Build.Pipeline; using UnityEditor.AddressableAssets.Build; +using UnityEditor.AddressableAssets.Build.AnalyzeRules; using UnityEditor.AddressableAssets.Build.DataBuilders; using UnityEditor.AddressableAssets.Settings; using UnityEditor.AddressableAssets.Settings.GroupSchemas; -using UnityEditor.Build.Pipeline; -using UnityEditor.SceneManagement; -using UnityEngine; -using UnityEditor; -using UnityEditor.AddressableAssets.Build.AnalyzeRules; class CheckBundleDupeDependenciesV2 : BundleRuleBase { - struct DuplicateResult - { - public AddressableAssetGroup Group; - public string DuplicatedFile; - public string AssetPath; - public GUID DuplicatedGroupGuid; - } - - // Return true because we have added an automated way of fixing these problems with the FixIssues() function - public override bool CanFix - { - get { return true; } - } + public override bool CanFix => true; - // The name that appears in the Editor UI - public override string ruleName - { get { return "Check Duplicate Bundle Dependencies V2"; } } + public override string ruleName => "Check Duplicate Bundle Dependencies V2"; - [NonSerialized] - internal readonly Dictionary>> m_AllIssues = new Dictionary>>(); - [SerializeField] - internal Dictionary, List> duplicateAssetsAndParents = new Dictionary, List>(); + internal Dictionary, List> duplicateAssetsByParents = new(HashSet.CreateSetComparer()); // The function that is called when the user clicks "Analyze Selected Rules" in the Analyze window public override List RefreshAnalysis(AddressableAssetSettings settings) @@ -49,164 +34,79 @@ public override List RefreshAnalysis(AddressableAssetSettings set List CheckForDuplicateDependencies(AddressableAssetSettings settings) { - // Create a container to store all our AnalyzeResults - List retVal = new List(); - - // Quit if the opened scene is not saved - if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) + if(!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) { - Debug.LogError("Cannot run Analyze with unsaved scenes"); - retVal.Add(new AnalyzeResult { resultName = ruleName + "Cannot run Analyze with unsaved scenes" }); - return retVal; + Debug.LogWarning("Cannot run Analyze with unsaved scenes"); + return new() { new AnalyzeResult() { severity = MessageType.Warning,resultName = ruleName + "Cannot run Analyze with unsaved scenes" } }; } - // Internal Addressables function that populates m_AllBundleInputDefs with what all our bundles will look like CalculateInputDefinitions(settings); - // Early return if we found no bundles to build - if (m_AllBundleInputDefs.Count <= 0) + if(AllBundleInputDefs.Count > 0) { - return retVal; - } + var context = GetBuildContext(settings); + ReturnCode exitCode = RefreshBuild(context); + if(exitCode < ReturnCode.Success) + { + Debug.LogError("Analyze build failed. " + exitCode); + return new() { new AnalyzeResult() { severity = MessageType.Error,resultName = ruleName + " Analyze build failed. " + exitCode } }; + } - var context = GetBuildContext(settings); - ReturnCode exitCode = RefreshBuild(context); - if (exitCode < ReturnCode.Success) - { - Debug.LogError("Analyze build failed. " + exitCode); - retVal.Add(new AnalyzeResult { resultName = ruleName + "Analyze build failed. " + exitCode }); - return retVal; + List retVal = CheckForDuplicateDependencies(context); + if(retVal.Count > 0) + return retVal; } - var implicitGuids = GetImplicitGuidToFilesMap(); - - // Actually calculate the duplicates - var dupeResults = CalculateDuplicates(implicitGuids, context); - BuildImplicitDuplicatedAssetsSet(dupeResults); - - retVal = (from issueGroup in m_AllIssues - from bundle in issueGroup.Value - from item in bundle.Value - select new AnalyzeResult - { - resultName = ruleName + kDelimiter + - issueGroup.Key + kDelimiter + - ConvertBundleName(bundle.Key, issueGroup.Key) + kDelimiter + - item, - severity = MessageType.Warning - }).ToList(); - - if (retVal.Count == 0) - retVal.Add(noErrors); - - return retVal; + return new() { noErrors }; } - IEnumerable CalculateDuplicates(Dictionary> implicitGuids, AddressableAssetsBuildContext aaContext) + List CheckForDuplicateDependencies(AddressableAssetsBuildContext context) { - duplicateAssetsAndParents.Clear(); - - //Get all guids that have more than one bundle referencing them - IEnumerable>> validGuids = - from dupeGuid in implicitGuids - where dupeGuid.Value.Distinct().Count() > 1 - where IsValidPath(AssetDatabase.GUIDToAssetPath(dupeGuid.Key.ToString())) - select dupeGuid; + var validGuids = GetImplicitGuidToFilesMap().Select((dupeGuid) => { + return (guid: dupeGuid.Key,path: AssetDatabase.GUIDToAssetPath(dupeGuid.Key), assetParents: dupeGuid.Value.Distinct().ToHashSet()); + }).Where((pair) => { + return IsValidPath(pair.path) && pair.assetParents.Count > 1; + }).ToList(); // Key = a set of bundle parents // Value = asset paths that share the same bundle parents // e.g. <{"bundle1", "bundle2"} , {"Assets/Sword_D.tif", "Assets/Sword_N.tif"}> - - foreach (var entry in validGuids) - { - string assetPath = AssetDatabase.GUIDToAssetPath(entry.Key.ToString()); - // Grab the list of bundle parents - List assetParents = entry.Value; - - // Purge duplicate parents (assets inside a Scene can show multiple copies of the Scene AssetBundle as a parent) - List nonDupeParents = new List(); - foreach (var parent in assetParents) - { - if (nonDupeParents.Contains(parent)) - continue; - nonDupeParents.Add(parent); - } - assetParents = nonDupeParents; - - // Add this pair to the dictionary - bool found = false; - foreach (var bundleParentSetup in duplicateAssetsAndParents.Keys) - { - // If this set of bundle parents equals our set of bundle parents, add this asset to this dictionary entry - if (Enumerable.SequenceEqual(bundleParentSetup, assetParents)) - { - duplicateAssetsAndParents[bundleParentSetup].Add(assetPath); - found = true; - break; - } - } - if (!found) - { - // We failed to find an existing set of matching bundle parents. Add a new entry - duplicateAssetsAndParents.Add(assetParents, new List() { assetPath }); - } - } - - return - from guidToFile in validGuids - from file in guidToFile.Value - - //Get the files that belong to those guids - let fileToBundle = m_ExtractData.WriteData.FileToBundle[file] - - //Get the bundles that belong to those files - let bundleToGroup = aaContext.bundleToAssetGroup[fileToBundle] - - //Get the asset groups that belong to those bundles - let selectedGroup = aaContext.Settings.FindGroup(findGroup => findGroup != null && findGroup.Guid == bundleToGroup) - - select new DuplicateResult - { - Group = selectedGroup, - DuplicatedFile = file, - AssetPath = AssetDatabase.GUIDToAssetPath(guidToFile.Key.ToString()), - DuplicatedGroupGuid = guidToFile.Key - }; - } - - void BuildImplicitDuplicatedAssetsSet(IEnumerable dupeResults) - { - foreach (var dupeResult in dupeResults) - { - // Add the data to the AllIssues container which is shown in the Analyze window - Dictionary> groupData; - if (!m_AllIssues.TryGetValue(dupeResult.Group.Name, out groupData)) - { - groupData = new Dictionary>(); - m_AllIssues.Add(dupeResult.Group.Name, groupData); - } - - // TODO: why is this necessary? - List assets; - if (!groupData.TryGetValue(m_ExtractData.WriteData.FileToBundle[dupeResult.DuplicatedFile], out assets)) - { - assets = new List(); - groupData.Add(m_ExtractData.WriteData.FileToBundle[dupeResult.DuplicatedFile], assets); - } - - assets.Add(dupeResult.AssetPath); - } + duplicateAssetsByParents.Clear(); + foreach (var guidToFile in validGuids.GroupBy((pair) => pair.assetParents,(pair) => pair.guid,HashSet.CreateSetComparer())) + duplicateAssetsByParents.Add(guidToFile.Key,guidToFile.ToList()); + + return validGuids.SelectMany((guidToFile) => { + return guidToFile.assetParents.Join(ExtractData.WriteData.FileToBundle,(file) => file,(pair) => pair.Key,(file,pair) => { + return (fileToBundle: pair.Value, assetPath: guidToFile.path); + }); + }).GroupBy((dupeResult) => dupeResult.fileToBundle,(dupeResult) => dupeResult.assetPath).GroupBy((dupeResultGroup) => { + string bundleToGroup = context.bundleToAssetGroup[dupeResultGroup.Key]; + var group = context.Settings.FindGroup(findGroup => findGroup != null && findGroup.Guid == bundleToGroup); + return group.Name; + }).SelectMany((issueGroup) => { + return issueGroup.SelectMany((bundle) => { + string bundleName = ConvertBundleName(bundle.Key,issueGroup.Key); + return bundle.Select((item) => { + return new AnalyzeResult() { + resultName = string.Join(kDelimiter,issueGroup.Key,bundleName,item), + severity = MessageType.Warning + }; + }); + }); + }).ToList(); } // The function that is called when the user clicks "Fix Issues" in the Analyze window public override void FixIssues(AddressableAssetSettings settings) { - // If we have no duplicate data, run the check again - if (duplicateAssetsAndParents == null || duplicateAssetsAndParents.Count == 0) + if(duplicateAssetsByParents == null) + duplicateAssetsByParents = new(); + + if(duplicateAssetsByParents.Count == 0) CheckForDuplicateDependencies(settings); // If we have found no duplicates, return - if (duplicateAssetsAndParents.Count == 0) + if(duplicateAssetsByParents.Count == 0) return; // Setup a new Addressables Group to store all our duplicate assets @@ -214,63 +114,44 @@ public override void FixIssues(AddressableAssetSettings settings) AddressableAssetGroup group = settings.FindGroup(desiredGroupName); if (group == null) { - group = settings.CreateGroup(desiredGroupName, false, false, false, null, typeof(BundledAssetGroupSchema), typeof(ContentUpdateGroupSchema)); + group = settings.CreateGroup(desiredGroupName, false, false, false, null!, typeof(BundledAssetGroupSchema), typeof(ContentUpdateGroupSchema)); var bundleSchema = group.GetSchema(); // Set to pack by label so that assets with the same label are put in the same AssetBundle bundleSchema.BundleMode = BundledAssetGroupSchema.BundlePackingMode.PackTogetherByLabel; } - EditorUtility.DisplayProgressBar("Setting up De-Duplication Group...", "", 0f / duplicateAssetsAndParents.Count); + EditorUtility.DisplayProgressBar("Setting up De-Duplication Group...", "", 0f / duplicateAssetsByParents.Count); // Iterate through each duplicate asset int bundleNumber = 1; - foreach (var entry in duplicateAssetsAndParents) + foreach (var (parents,entry) in duplicateAssetsByParents) { - EditorUtility.DisplayProgressBar("Setting up De-Duplication Group...", "Creating Label Group", ((float)bundleNumber) / duplicateAssetsAndParents.Count); + EditorUtility.DisplayProgressBar("Setting up De-Duplication Group...", "Creating Label Group", ((float)bundleNumber) / duplicateAssetsByParents.Count); // Create a new Label string desiredLabelName = "Bundle" + bundleNumber; - List entriesToAdd = new List(); - // Put each asset in the shared Group - foreach (string assetPath in entry.Value) - { - entriesToAdd.Add(settings.CreateOrMoveEntry(AssetDatabase.AssetPathToGUID(assetPath).ToString(), group, false, false)); - } - - // Set the label for this selection of assets so they get packed into the same AssetBundle settings.AddLabel(desiredLabelName); - SetLabelValueForEntries(settings, entriesToAdd, desiredLabelName, true); - bundleNumber++; - } - - settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, null, true, true); - } + List entriesToAdd = entry.Select((guid) => { + // Set the label for this selection of assets so they get packed into the same AssetBundle + var e = settings.CreateOrMoveEntry(guid.ToString(),group,false,false); + e.SetLabel(desiredLabelName,true,false); + return e; + }).ToList(); - // Helper function for adding labels to Addressable assets - void SetLabelValueForEntries(AddressableAssetSettings settings, List entries, string label, bool value, bool postEvent = true) - { - if (value) - settings.AddLabel(label); + settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryModified, entriesToAdd, true, true); - foreach (var e in entries) - e.SetLabel(label, value, false); + bundleNumber++; + } - settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryModified, entries, postEvent, true); + settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, null!, true, true); } // The function that is run when the user clicks "Clear Selected Rules" in the Analyze window public override void ClearAnalysis() { - m_AllIssues.Clear(); - duplicateAssetsAndParents.Clear(); + duplicateAssetsByParents.Clear(); base.ClearAnalysis(); } -} -// Boilerplate to add our rule to the AnalyzeSystem's list of rules -[InitializeOnLoad] -class RegisterCheckBundleDupeDependenciesV2 -{ - static RegisterCheckBundleDupeDependenciesV2() - { - AnalyzeSystem.RegisterNewRule(); - } -} \ No newline at end of file + [InitializeOnLoadMethod] + static void Register() => AnalyzeSystem.RegisterNewRule(); +} +#endif