PrefabDataCleaner

From Unify Community Wiki
Jump to: navigation, search

Author: Fredrik Ludvigsen (Steinbitglis)

Contents

Description

Adds a prefab data cleaner window under Window/Prefab data cleaner.

PrefabDataCleaner-Window.png

Usage

  • Place the scripts in a folder named Editor in your project's Assets folder.
  • Open the window by selecting Window/Prefab data cleaner.

What it does

The tl;dr:

  • Finds prefab instance data that isn't likely being used
  • Lets you choose which data to clean out

This window will open and analyze all the scenes that you currently have open, before showing you which prefabs may contain outdated data that you might choose to clean out.

Optionally, the window can also search your scenes for links to prefabs that have either nested prefabs or that are variants of other prefabs, since these may contain prefab instance data too.

Prefab instance data

Unity will serialize data fields if, in general, the fields are simple and public. But when you change your scripts during the course of a project lifetime, fields usually get renamed or removed altogether. Unity will then no longer serialize that data, as can be expected.

However.

When data lives in a prefab instance, it will be kept around forever.

(issues, 1191638 and 1191643)

https://forum.unity.com/threads/m_modification-doesnt-clear-old-references-when-the-prefab-is-modified.761219/#post-5069048

Currently, due to a bug, linked objects will also be pulled into builds and asset bundles.

Prefab data cleaner can be used to cherry pick that data out of your prefab instances.

PrefabDataCleaner-Example.png

C# - PrefabDataCleaner.cs

using Type = System.Type;
using System;
using System.Collections.Generic;
using System.Linq;
 
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Serialization;
 
using UnityEditor;
using UnityEditor.SceneManagement;
 
public class PrefabDataCleaner : EditorWindow {
 
    [MenuItem("Window/Prefab data cleaner")]
    static void Init() {
        var window = EditorWindow.GetWindow<PrefabDataCleaner>("Prefab data cleaner");
    }
 
    void OnEnable() {
        if (scenes        == null) scenes        = new SceneAssetList();
        if (parentPrefabs == null) parentPrefabs = new GameObjectList();
        if (monoScripts   == null) monoScripts   = new MonoScriptList();
        if (sourcePrefabs == null) sourcePrefabs = new GameObjectList();
        if (propertyPaths == null) propertyPaths = new ModList();
 
        scenes       .AlsoClears(propertyPaths, sourcePrefabs, monoScripts, parentPrefabs);
        parentPrefabs.AlsoClears(propertyPaths, sourcePrefabs, monoScripts);
        sourcePrefabs.AlsoClears(propertyPaths, monoScripts);
        monoScripts  .AlsoClears(propertyPaths);
        propertyPaths.AlsoClears();
 
        MakeMonoScriptIndexes();
 
        EditorSceneManager.sceneOpened += NewSceneList;
        EditorSceneManager.sceneClosed += NewSceneList;
    }
 
    void OnDisable() {
        EditorSceneManager.sceneOpened -= NewSceneList;
        EditorSceneManager.sceneClosed -= NewSceneList;
    }
 
    void NewSceneList(Scene scene)                     { scenes.Clear(); }
    void NewSceneList(Scene scene, OpenSceneMode mode) { NewSceneList(scene); }
 
    enum CheckStatus {
        None,
        All,
        Some,
    };
 
    interface iobjectList {
        void Clear();
    }
    class objectList<T> : iobjectList, ISerializationCallbackReceiver {
        public  Vector2        scrollPosition;
        public  CheckStatus[]  checkmarks;
        public  bool           hasContent;
 
        public void OnBeforeSerialize() {}
        public void OnAfterDeserialize() { if (!hasContent) { checkmarks = null; elements = null; } }
 
        private T[] elements;
        public T[] Elements {
            get { return elements;  }
            set {
                this.elements   = value;
                this.checkmarks = new CheckStatus[value.Length];
                this.hasContent = true;
            }
        }
 
        public void SetElements (IEnumerable<T> elements) {
            Elements = elements.ToArray();
        }
 
        public IEnumerable<T> SelectedElements =>
            elements == null ? Enumerable.Empty<T>() :
            Enumerable.Range (0, elements.Length)
                      .Where (i => checkmarks[i] != CheckStatus.None)
                      .Select(i => elements  [i]);
 
        private iobjectList[] children;
        public void AlsoClears(params iobjectList[] children) {
            this.children = children;
        }
 
        public void Clear() {
            elements   = null;
            checkmarks = null;
            hasContent = false;
 
            Array.ForEach(children, c => c.Clear());
        }
    }
 
    [Serializable]
    class mod {
        public MonoScript monoScript;
        public string propertyPath;
    }
 
    [Serializable] class SceneAssetList : objectList<SceneAsset> {};
    [Serializable] class GameObjectList : objectList<GameObject> {};
    [Serializable] class MonoScriptList : objectList<MonoScript> {};
    [Serializable] class ModList        : objectList<mod>        {};
 
    [SerializeField] SceneAssetList scenes;
    [SerializeField] GameObjectList parentPrefabs;
 
    [SerializeField] MonoScriptList monoScripts;
    [SerializeField] GameObjectList sourcePrefabs;
    [SerializeField] ModList        propertyPaths;
 
    private Dictionary <MonoScript, List<int>> propertyPathsPerMonoScript;
    private Dictionary <mod, int>              monoScriptIndexPerPropertyPath;
 
    void MakeMonoScriptIndexes() {
        var monoScriptIndexes      = new Dictionary <MonoScript, int>();
        if (monoScripts.Elements != null)
        for (int i = 0; i < monoScripts.Elements.Length; i++)
            monoScriptIndexes[monoScripts.Elements[i]] = i;
 
        propertyPathsPerMonoScript     = new Dictionary <MonoScript, List<int>>();
        monoScriptIndexPerPropertyPath = new Dictionary <mod, int>();
        if (propertyPaths.Elements != null)
        for (int i = 0; i < propertyPaths.Elements.Length; i++) {
            List<int> ppIndexes;
            var mod        = propertyPaths.Elements[i];
            var monoScript = mod.monoScript;
            if (!propertyPathsPerMonoScript.TryGetValue(monoScript, out ppIndexes))
                propertyPathsPerMonoScript[monoScript] = ppIndexes = new List<int>();
            ppIndexes.Add(i);
 
            monoScriptIndexPerPropertyPath[mod] = monoScriptIndexes[monoScript];
        }
    }
 
    private Dictionary<Type, HashSet<string>> getDeniedPropertyPaths() {
        var result = new Dictionary <Type, HashSet<string>>();
        foreach (var mod in propertyPaths.SelectedElements) {
            HashSet<string> pPaths;
            var type = mod.monoScript.GetClass();
            if (!result.TryGetValue(type, out pPaths))
                result[type] = pPaths = new HashSet<string>();
            pPaths.Add(mod.propertyPath);
        }
        return result;
    }
 
 
    private IEnumerable<SceneAsset> Scenes        { set { scenes       .SetElements(value); } }
    private IEnumerable<GameObject> ParentPrefabs { set { parentPrefabs.SetElements(value); } }
    private IEnumerable<MonoScript> MonoScripts   { set { monoScripts  .SetElements(value); } }
    private IEnumerable<GameObject> SourcePrefabs { set { sourcePrefabs.SetElements(value); } }
    private IEnumerable<mod>        PropertyPaths { set { propertyPaths.SetElements(value); } }
 
    Vector2 prefabScrollPosition, monoScriptScrollPosition;
 
    private bool analysisRequested;
 
    void OnGUI() {
        if (scenes.Elements == null && GUILayout.Button("Load Scenes")) {
            var loadedScenes = Enumerable.Range(0, EditorSceneManager.sceneCount).Select(i => EditorSceneManager.GetSceneAt(i)).Where(s => s.isLoaded);
            Scenes = loadedScenes.Select(s => AssetDatabase.LoadAssetAtPath<SceneAsset>(s.path));
        }
 
        var numActiveScenes = scenes.checkmarks == null ? 0 : scenes.checkmarks.Count(m => m != CheckStatus.None);
 
        if (scenes       .Elements != null &&
            parentPrefabs.Elements == null   ) {
 
            GUI.enabled = numActiveScenes != 0;
            if (GUILayout.Button("Load Related Nested Prefabs")) {
                var sceneDepPaths = new HashSet<string>();
                for (int i = 0, c = 0; i < scenes.Elements.Length; i++)
                if  (scenes.checkmarks[i] != CheckStatus.None) {
                    EditorUtility.DisplayProgressBar("Collecting dependencies", scenes.Elements[i].name, c / (float) numActiveScenes);
                    sceneDepPaths.UnionWith(AssetDatabase.GetDependencies(AssetDatabase.GetAssetPath(scenes.Elements[i])));
                    c++;
                }
 
                EditorUtility.ClearProgressBar();
                var allPrefabsFromDependencies = new HashSet<GameObject>(
                        sceneDepPaths.Where(p => AssetDatabase.GetMainAssetTypeAtPath(p) == typeof(GameObject))
                                     .Select(p => (GameObject) AssetDatabase.LoadMainAssetAtPath(p))
                                     .Where (o => { var type = PrefabUtility.GetPrefabAssetType(o);
                                                    var isRegularPrefab  = type == PrefabAssetType.Regular ||
                                                                           type == PrefabAssetType.Variant   ;
                                                    return isRegularPrefab && hasNestedPrefabs(o); }));
 
                ParentPrefabs = allPrefabsFromDependencies;
            }
            GUI.enabled = true;
        }
 
        if ((scenes       .Elements != null ||
             parentPrefabs.Elements != null   ) && analysisRequested) {
 
            analysisRequested = false;
 
            IEnumerable<GameObject> instanceRoots = null;
            try { instanceRoots = getConnectedInstanceRoots(); }
            catch (NoSceneOrPrefabSelectedException) {
                //Debug.LogException(e);
                sourcePrefabs.Clear();
            }
            if (instanceRoots != null) {
                var (scriptToPropertyPathChange, changedInstances) = SerializationAnalysis.GetEliminatedPropertyPaths(instanceRoots);
 
                SourcePrefabs = changedInstances.Select(PrefabUtility.GetCorrespondingObjectFromOriginalSource).Distinct().ToList();
                MonoScripts   = scriptToPropertyPathChange.Keys;
                PropertyPaths = scriptToPropertyPathChange.SelectMany(kvp => kvp.Value.Select(p => new mod { propertyPath = p, monoScript = kvp.Key }));
 
                MakeMonoScriptIndexes();
            }
        }
 
        using (var h = new EditorGUILayout.HorizontalScope()) {
            using (var v = new EditorGUILayout.VerticalScope()) {
                listArray(scenes, checkChanged: (scene, checkStatus) => analysisRequested = true, showAll: true);
                listArray(parentPrefabs, checkChanged: (scene, checkStatus) => analysisRequested = true, showClear: true);
            }
            listArray(sourcePrefabs, showCheckmarks: false);
            listArray(monoScripts, checkChanged: (monoScript, checkStatus) => {
                    var pp = propertyPathsPerMonoScript[monoScript];
                    for (int i = 0; i < pp.Count; i++)
                        propertyPaths.checkmarks[pp[i]] = checkStatus;
            });
            listModArray(propertyPaths, checkChanged: (mod, checkStatus) => {
                    var i = monoScriptIndexPerPropertyPath[mod];
                    var pp = propertyPathsPerMonoScript[monoScripts.Elements[i]];
                    var numPathsForScript = pp.Count;
                    var numPathsActive    = pp.Count(p_i => propertyPaths.checkmarks[p_i] != CheckStatus.None);
                    monoScripts.checkmarks[i] = numPathsActive == 0 ? CheckStatus.None : numPathsActive < numPathsForScript ? CheckStatus.Some : CheckStatus.All;
            });
        }
 
        if (propertyPaths.Elements != null && GUILayout.Button("Clean prefab instances")) {
 
            IEnumerable<GameObject> instanceRoots = null;
            try { instanceRoots = getConnectedInstanceRoots(); }
            catch (NoSceneOrPrefabSelectedException) {
                // No scene or prefab selected
            }
            if (instanceRoots != null) {
                var removedMods = getDeniedPropertyPaths();
 
                foreach (var instanceRoot in instanceRoots)
                    SerializationAnalysis.EliminatePropertyModifications(instanceRoot, removedMods);
            }
        }
 
        return;
 
 
        IEnumerable<GameObject> getConnectedInstanceRoots() {
            var numActiveScenesAndPrefabs = (scenes       .Elements == null ? 0 : scenes.checkmarks.Count(m => m != CheckStatus.None)) +
                                            (parentPrefabs.Elements == null ? 0 : scenes.checkmarks.Count(m => m != CheckStatus.None));
            if (numActiveScenesAndPrefabs == 0)
                throw new NoSceneOrPrefabSelectedException();
 
            var validScenes            = scenes.SelectedElements       .Select    (s => EditorSceneManager.GetSceneByPath(AssetDatabase.GetAssetPath(s))).Where(s => s.IsValid() && s.isLoaded);
            var allSceneGameObjects    = validScenes                   .SelectMany(s => s.GetRootGameObjects().SelectMany(go => go.GetComponentsInChildren<Transform>().Select(t => t.gameObject)) );
            var allSceneInstanceRoots  = allSceneGameObjects           .Where     (go => isInstanceWithOverrides(go));
 
            var allPrefabGameObjects   = parentPrefabs.SelectedElements.SelectMany(p => p.GetComponentsInChildren<Transform>().Select(t => t.gameObject));
            var allPrefabInstanceRoots = allPrefabGameObjects          .Where     (go => isInstanceWithOverrides(go));
 
            return allSceneInstanceRoots.Concat(allPrefabInstanceRoots);
        }
 
        bool hasNestedPrefabs(GameObject gameObject) {
            if (PrefabUtility.IsPartOfPrefabInstance(gameObject))
                return true;
 
            var parents   = new Stack<Transform>(); parents  .Push(gameObject.transform);
            var nextChild = new Stack<int      >(); nextChild.Push(0);
 
            while (parents.Count != 0) {
                var current   = parents.Peek();
                var nextIndex = nextChild.Pop();
                if (nextIndex == current.childCount) {
                    parents.Pop();
                    continue;
                }
                var child = current.GetChild(nextIndex);
                if (PrefabUtility.IsPartOfPrefabInstance(child))
                    return true;
 
                nextChild.Push(nextIndex+1);
 
                parents  .Push(child);
                nextChild.Push(0);
            }
            return false;
        }
 
        void listArray<T> (objectList<T> l, Action<T, CheckStatus> checkChanged = null, bool showCheckmarks = true, bool showAll = false, bool showClear = false) where T : UnityEngine.Object {
            listArrayImpl(l, checkChanged, e => EditorGUILayout.ObjectField(e, typeof(T), true), showCheckmarks, showAll, showClear);
        }
        void listModArray (objectList<mod>l, Action<mod, CheckStatus> checkChanged = null, bool showCheckmarks = true, bool showAll = false, bool showClear = false) {
            listArrayImpl(l, checkChanged, e => EditorGUILayout.LabelField(e.propertyPath), showCheckmarks, showAll, showClear);
        }
 
        void listArrayImpl<T> (objectList<T> l, Action<T, CheckStatus> checkChanged, Action<T> showField, bool showCheckmarks, bool showAll, bool showClear) {
            if (l.Elements == null) return;
            var layoutOptions = (showAll ? new[] { GUILayout.Height(18f * l.Elements.Length + 24f) } : new GUILayoutOption[0]);
 
            using (var scrollView = new EditorGUILayout.ScrollViewScope(l.scrollPosition, layoutOptions)) {
                l.scrollPosition = scrollView.scrollPosition;
 
                if (l.Elements != null) {
                    if (showClear && GUILayout.Button("Clear"))
                        l.Clear();
                    else {
                        for (int i = 0; i < l.Elements.Length; i++) {
                            using (var h = new EditorGUILayout.HorizontalScope()) {
                                if (showCheckmarks)
                                using (var check = new EditorGUI.ChangeCheckScope()) {
                                    EditorGUI.showMixedValue = l.checkmarks[i] == CheckStatus.Some;
                                    var newToggleValue = EditorGUILayout.Toggle(l.checkmarks[i] != CheckStatus.None, GUILayout.Width(18f));
                                    if (check.changed) {
                                        var newCheckStatus = newToggleValue ? CheckStatus.All : CheckStatus.None;
                                        l.checkmarks[i]    = newCheckStatus;
                                        checkChanged?.Invoke(l.Elements[i], newCheckStatus);
                                    }
                                    EditorGUI.showMixedValue = false;
                                }
                                GUI.enabled = false;
                                showField(l.Elements[i]);
                                GUI.enabled = true;
                            }
                        }
                    }
                }
            }
        }
 
        bool isInstanceWithOverrides(GameObject o, bool onlyPrefabs = true) {
            if (!PrefabUtility.IsAnyPrefabInstanceRoot(o))
                return false;
 
            var assetType = PrefabUtility.GetPrefabAssetType(o);
            if (!(assetType == PrefabAssetType.Regular ||
                  assetType == PrefabAssetType.Variant ||
                  (assetType == PrefabAssetType.Model && !onlyPrefabs)))
                return false;
 
            return PrefabUtility.HasPrefabInstanceAnyOverrides(o, false);
        }
    }
 
}
 
public class NoSceneOrPrefabSelectedException : Exception {
    public NoSceneOrPrefabSelectedException()                                : base("No scenes or prefabs selected") {}
    public NoSceneOrPrefabSelectedException(string message)                  : base(message)        {}
    public NoSceneOrPrefabSelectedException(string message, Exception inner) : base(message, inner) {}
}

C# - SerializationAnalysis.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
 
using UnityEngine;
using UnityEditor;
 
public static class SerializationAnalysis {
 
    private static Predicate<Type> isBasicType = new HashSet<Type> {
        typeof(bool),    typeof(byte),
        typeof(sbyte),   typeof(char),
        typeof(decimal), typeof(double),
        typeof(float),   typeof(int),
        typeof(uint),    typeof(long),
        typeof(ulong),   typeof(short),
        typeof(ushort),  typeof(string),
    }.Contains;
 
    private static Predicate<Type> isKnownStruct = new HashSet<Type> {
        typeof(Color),      typeof(Vector2),
        typeof(Vector3),    typeof(Vector4),
        typeof(Rect),       typeof(AnimationCurve),
        typeof(Bounds),     typeof(Gradient),
        typeof(Quaternion), typeof(Vector2Int),
        typeof(Vector3Int), typeof(RectInt),
        typeof(BoundsInt),
    }.Contains;
 
    private static bool isSingleValueType (Type type) {
        return typeof(UnityEngine.Object).IsAssignableFrom(type) ||
               type.IsEnum                                       ||
               isBasicType(type)                                   ;
    }
 
    private static bool isSerializableType           (Type      type) => Attribute.IsDefined(type, typeof(SerializableAttribute));
    private static bool hasSerializableAttribute     (FieldInfo fi  ) => Attribute.IsDefined(fi,   typeof(SerializeField));
    private static bool hasNonSerializedAttribute    (FieldInfo fi  ) => Attribute.IsDefined(fi,   typeof(NonSerializedAttribute));
    private static bool fieldDeclarationWillSerialize(FieldInfo fi  ) => fi.IsPublic && !hasNonSerializedAttribute(fi) || hasSerializableAttribute(fi);
 
    private static bool isSerializableStruct (Type type) => isKnownStruct    (type) || isSerializableType  (type);
    private static bool isCompatibleType     (Type type) => isSingleValueType(type) || isSerializableStruct(type);
 
 
    private static bool        fieldWillSerialize     (FieldInfo fi) => fieldDeclarationWillSerialize(fi) && isCompatibleType(fi.FieldType);
    private static FieldInfo[] getSerializableFields  (Type      t ) => Array.FindAll(GetFieldInfosIncludingBaseClasses(t), fieldWillSerialize);
 
    private const BindingFlags instanceFieldFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
 
    private static Type[] reservedTypes = new[] { typeof(MonoBehaviour), typeof(Component),
                                                  typeof(Behaviour),     typeof(ValueType),
                                                  typeof(System.Object) };
 
    public static FieldInfo[] GetFieldInfosIncludingBaseClasses (Type type) {
        FieldInfo[] fieldInfos = type.GetFields(instanceFieldFlags);
 
        // If this class doesn't have a base, don't waste any time
        if (reservedTypes.Contains(type.BaseType) || type == typeof(System.Object))
            return fieldInfos;
 
        var fieldInfoList = new List<FieldInfo>(fieldInfos);
        do {
            fieldInfoList.AddRange(type.GetFields(instanceFieldFlags));
            type = type.BaseType;
        } while (!reservedTypes.Contains(type));
 
        return fieldInfoList.ToArray();
    }
 
    private static bool tryGetArrayType(Type t, out Type elementType) {
        if      (t.IsArray)                                                         { elementType = t.GetElementType();         return true; }
        else if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(List<>)) { elementType = t.GetGenericArguments()[0]; return true; }
 
        elementType = default(Type); return false;
    }
 
    private static IEnumerable<string> getGeneralizedPropertyPaths(FieldInfo fi) {
        var fieldType = fi.FieldType;
        if (tryGetArrayType(fieldType, out var elementType)) {
            yield return $"{fi.Name}.Array.size";
            if (isSingleValueType(elementType))
                yield return $"{fi.Name}.Array.data[*]";
            else
                foreach (var elementField in getSerializableFields(elementType))
                    foreach (var aboutString in getGeneralizedPropertyPaths(elementField))
                        yield return $"{fi.Name}.Array.data[*].{aboutString}";
        }
 
        else if (isSingleValueType(fieldType))
            yield return fi.Name;
 
        else if (isSerializableStruct(fieldType)) {
            foreach (var elementField in getSerializableFields(fieldType))
                foreach (var aboutString in getGeneralizedPropertyPaths(elementField))
                    yield return $"{fi.Name}.{aboutString}";
        }
 
        else {
            Debug.LogWarning($"Unexpected field: {fi.Name}");
            yield return $"UNEXPECTED: {fi.Name}";
        }
    }
 
    static HashSet<string> getExpectedPropertyPaths (Type type) {
        var result = new HashSet<string> (getSerializableFields(type).SelectMany(getGeneralizedPropertyPaths));
        if (typeof(Behaviour).IsAssignableFrom(type))
            result.Add("m_Enabled");
        return result;
    }
 
    static IEnumerable<MonoBehaviour> targetsOfInstance (GameObject root) {
        // root is always included, but other prefab instance roots aren't
        var parents   = new Stack<Transform>(); parents  .Push(root.transform);
        var nextChild = new Stack<int      >(); nextChild.Push(0);
        while (parents.Count != 0) {
            var current   = parents.Peek();
            var nextIndex = nextChild.Pop();
 
            foreach (var b in current.GetComponents<MonoBehaviour>())
                if (!PrefabUtility.IsAddedComponentOverride(b))
                    yield return b;
 
            if (nextIndex == current.childCount) {
                parents.Pop();
                continue;
            }
            var child = current.GetChild(nextIndex);
 
            nextChild.Push(nextIndex+1);
 
            if (!PrefabUtility.IsAnyPrefabInstanceRoot  (child.gameObject) &&
                !PrefabUtility.IsAddedGameObjectOverride(child.gameObject)   ) {
                parents  .Push(child);
                nextChild.Push(0);
            }
        }
    }
 
    public static (Dictionary<MonoScript, HashSet<string>> mods, IEnumerable<GameObject> changedInstances) GetEliminatedPropertyPaths(IEnumerable<GameObject> instanceRoots, bool debug = false) {
        var prefabRoots = new HashSet<GameObject>( instanceRoots.Select    (PrefabUtility.GetCorrespondingObjectFromSource) );
        var types       = instanceRoots.SelectMany(targetsOfInstance).Select(mb => mb.GetType()).Distinct();
 
        var mods                  = new Dictionary<MonoScript, HashSet<string>>();
        var acceptedPropertyPaths = types.ToDictionary(t => t, getExpectedPropertyPaths);
 
        var changedInstances = new List<GameObject>();
 
        foreach (var ir in instanceRoots) {
            foreach (var mb in targetsOfInstance(ir)) {
 
                var originalPrefabModifications = PrefabUtility.GetPropertyModifications        (ir);
                var targetObjectInAsset         = PrefabUtility.GetCorrespondingObjectFromSource(mb);
 
                var generalizedProperties = new HashSet<string> (originalPrefabModifications.Where (mod => mod.target == targetObjectInAsset)
                                                                                            .Select(mod => Regex.Replace(mod.propertyPath, @"(?<=\.Array.data\[)[0-9]+(?=\])", "*")));
                generalizedProperties.ExceptWith(acceptedPropertyPaths[mb.GetType()]);
 
                if (generalizedProperties.Count != 0) {
                    changedInstances.Add(ir);
 
                    HashSet<string> changeSet;
                    var monoScript = MonoScript.FromMonoBehaviour(mb);
                    if (!mods.TryGetValue(monoScript, out changeSet))
                        mods[monoScript] = changeSet = new HashSet<string>();
 
                    changeSet.UnionWith(generalizedProperties);
                }
            }
        }
 
        return (mods, changedInstances);
    }
 
    public static void EliminatePropertyModifications(GameObject instanceRoot, Dictionary<Type, HashSet<string>> removedMods) {
        if (!PrefabUtility.IsAnyPrefabInstanceRoot(instanceRoot)) {
            Debug.LogWarning($"game object {instanceRoot} is not a prefab instance root", instanceRoot);
            return;
        }
        if (!PrefabUtility.HasPrefabInstanceAnyOverrides(instanceRoot, false)) {
            Debug.LogWarning($"no overrides in {instanceRoot}", instanceRoot);
            return;
        }
 
        var prefabHandle    = PrefabUtility.GetPrefabInstanceHandle (instanceRoot);
        var mods_with_index = PrefabUtility.GetPropertyModifications(instanceRoot).Select((mod, i) => (mod: mod, index:i)).ToList();
        var numOriginalMods = mods_with_index.Count;
 
        var elementsRemoved = 0;
 
        foreach (var mb in targetsOfInstance(instanceRoot))
        if (removedMods.TryGetValue(mb.GetType(), out var pathsForType))
            elementsRemoved += mods_with_index.RemoveAll(mwi => mwi.mod.target == PrefabUtility.GetCorrespondingObjectFromSource(mb) && pathsForType.Contains(mwi.mod.propertyPath));
 
        if (elementsRemoved != 0) {
            var modsToRemove = new List<int>();
 
            for (int i = 0, mod_i = 0; i < numOriginalMods; i++)
            if (mod_i >= mods_with_index.Count || i != mods_with_index[mod_i].index)
                modsToRemove.Add(i);
            else
                mod_i++;
 
            var so   = new SerializedObject(prefabHandle);
            var prop = so.FindProperty("m_Modification.m_Modifications");
 
            for (int i = modsToRemove.Count-1; i >= 0; i--)
                prop.DeleteArrayElementAtIndex(modsToRemove[i]);
 
            so.ApplyModifiedPropertiesWithoutUndo();
 
            EditorUtility.SetDirty(instanceRoot);
        }
    }
}
Personal tools
Namespaces

Variants
Actions
Navigation
Extras
Toolbox