MonoScriptCleaner

From Unify Community Wiki
Jump to: navigation, search

Author: Fredrik Ludvigsen (Steinbitglis)

Contents

Description

Adds a monoscript data cleaner window under Window/MonoScript data cleaner.

MonoScriptCleaner.png

Usage

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

What it does

The tl;dr:

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

This window will search your asset folder for default data in .cs.meta files, before showing you which scripts may contain outdated data that you may choose to clean out.

MonoScript default 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 MonoScript meta file(*.cs.meta), it will be kept around forever.

similar to, but slightly different from prefab instance data (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.

MonoScript data cleaner can be used to cherry pick that data out of your script meta files.

MonoScriptDataCleaner-Example.png

C# - MonoScriptCleaner.cs

using System;
using System.Collections.Generic;
using System.Linq;
 
using UnityEngine;
using UnityEngine.Serialization;
 
using UnityEditor;
 
public class MonScriptCleaner : EditorWindow {
 
    [MenuItem("Window/MonoScript data cleaner")]
    static void Init() {
        var window = EditorWindow.GetWindow<MonScriptCleaner>("MonoScript default data cleaner");
    }
 
    void OnEnable() {
        if (monoScripts == null) monoScripts = new MonoScriptList();
        if (defaultData == null) defaultData = new ModList();
 
        monoScripts.AlsoClears(defaultData);
        defaultData.AlsoClears();
 
        MakeMonoScriptIndexes();
    }
 
    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 fieldName;
        public int index;
    }
 
    [Serializable] class MonoScriptList : objectList<MonoScript> {};
    [Serializable] class ModList        : objectList<mod>        {};
 
 
    [SerializeField] MonoScriptList monoScripts;
    [SerializeField] ModList        defaultData;
 
    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 (defaultData.Elements != null)
        for (int i = 0; i < defaultData.Elements.Length; i++) {
            List<int> ppIndexes;
            var mod        = defaultData.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 IEnumerable<MonoScript> MonoScripts { set { monoScripts.SetElements(value); } }
    private IEnumerable<mod>        DefaultData { set { defaultData.SetElements(value); } }
 
    private bool analysisRequested;
 
    void OnGUI() {
        if (monoScripts.Elements == null && GUILayout.Button("Load Script Meta")) {
            var scriptsFound = new List<MonoScript>();
            var modsFound    = new List<mod>();
            foreach (var guid in AssetDatabase.FindAssets("t:MonoScript",  new[] { "Assets" })) {
                var path       = AssetDatabase.GUIDToAssetPath(guid);
 
                var type       = AssetDatabase.GetMainAssetTypeAtPath(path);
                if (type != typeof(MonoScript))
                    continue;
 
                var importer   = AssetImporter.GetAtPath(path);
                var monoscript = (MonoScript) AssetDatabase.LoadAssetAtPath(path, type);
                if (importer != null) {
                    var so = new SerializedObject(importer);
                    var iterator = so.FindProperty("m_DefaultReferences");
                    if (iterator == null)
                        continue;
                    if (iterator.arraySize != 0) {
                        var numElements = iterator.arraySize;
 
                        var monoscriptAdded = false;
 
                        for (int i = 0; i < numElements; i++) {
                            var element = iterator.GetArrayElementAtIndex(i);
 
                            var first  = element.FindPropertyRelative("first" );
                            //var second = element.FindPropertyRelative("second");
 
                            if (monoscript.GetClass().GetField(first.stringValue) == null) {
                                if (!monoscriptAdded) {
                                    monoscriptAdded = true;
                                    scriptsFound.Add(monoscript);
                                }
                                modsFound.Add(new mod { monoScript = monoscript, fieldName = $"{monoscript.GetClass().Name}.{first.stringValue}", index = i });
                            }
                        }
                    }
                }
            }
            MonoScripts = scriptsFound;
            DefaultData = modsFound;
            MakeMonoScriptIndexes();
        }
 
 
        using (var h = new EditorGUILayout.HorizontalScope()) {
            listArray(monoScripts, checkChanged: (monoScript, checkStatus) => {
                    var pp = propertyPathsPerMonoScript[monoScript];
                    for (int i = 0; i < pp.Count; i++)
                        defaultData.checkmarks[pp[i]] = checkStatus;
            });
            listModArray(defaultData, checkChanged: (mod, checkStatus) => {
                    var i = monoScriptIndexPerPropertyPath[mod];
                    var pp = propertyPathsPerMonoScript[monoScripts.Elements[i]];
                    var numPathsForScript = pp.Count;
                    var numPathsActive    = pp.Count(p_i => defaultData.checkmarks[p_i] != CheckStatus.None);
                    monoScripts.checkmarks[i] = numPathsActive == 0 ? CheckStatus.None : numPathsActive < numPathsForScript ? CheckStatus.Some : CheckStatus.All;
            });
        }
 
        if (defaultData.Elements != null && GUILayout.Button("Clean script default data")) {
            foreach (var msGroup in defaultData.SelectedElements.GroupBy(mod => mod.monoScript)) {
                var monoScript = msGroup.Key;
                var importer   = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(monoScript));
                if (importer != null) {
                    var so = new SerializedObject(importer);
                    var iterator = so.FindProperty("m_DefaultReferences");
                    foreach (var index in msGroup.Select(m => m.index).OrderByDescending(i => i))
                        iterator.DeleteArrayElementAtIndex(index);
                    so.ApplyModifiedProperties();
                    importer.SaveAndReimport();
                    Debug.Log("Scripts cleaned");
                }
            }
        }
 
        return;
 
        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.fieldName), 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;
                            }
                        }
                    }
                }
            }
        }
    }
 
}
Personal tools
Namespaces

Variants
Actions
Navigation
Extras
Toolbox