diff --git a/Assets/DirtWindowSettings.asset b/Assets/DirtWindowSettings.asset
new file mode 100644
index 0000000..3abef6f
--- /dev/null
+++ b/Assets/DirtWindowSettings.asset
@@ -0,0 +1,17 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 3b5401099ed7f1049a6f1339094e387e, type: 3}
+ m_Name: DirtWindowSettings
+ m_EditorClassIdentifier: Dirt::Dirt.DirtWindowSettings
+ modificationLimit: {x: 3, y: 10}
+ visibility: 3
+ exclusions: []
diff --git a/Assets/DirtWindowSettings.asset.meta b/Assets/DirtWindowSettings.asset.meta
new file mode 100644
index 0000000..8c38324
--- /dev/null
+++ b/Assets/DirtWindowSettings.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 949cf3c297246c54593be19eac457770
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Editor/Dirt.meta b/Assets/Scripts/Editor/Dirt.meta
new file mode 100644
index 0000000..8ed8e5e
--- /dev/null
+++ b/Assets/Scripts/Editor/Dirt.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 8484dc58e4e6dec4d8bbec756fa758bb
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Editor/Dirt/Dirt.asmdef b/Assets/Scripts/Editor/Dirt/Dirt.asmdef
new file mode 100644
index 0000000..bcc0c9e
--- /dev/null
+++ b/Assets/Scripts/Editor/Dirt/Dirt.asmdef
@@ -0,0 +1,16 @@
+{
+ "name": "Dirt",
+ "rootNamespace": "",
+ "references": [],
+ "includePlatforms": [
+ "Editor"
+ ],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Editor/Dirt/Dirt.asmdef.meta b/Assets/Scripts/Editor/Dirt/Dirt.asmdef.meta
new file mode 100644
index 0000000..6fcee6c
--- /dev/null
+++ b/Assets/Scripts/Editor/Dirt/Dirt.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 4e2a065fa28e72148b0fa49a1061dba9
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Editor/Dirt/DirtHeader.uxml b/Assets/Scripts/Editor/Dirt/DirtHeader.uxml
new file mode 100644
index 0000000..8511b6b
--- /dev/null
+++ b/Assets/Scripts/Editor/Dirt/DirtHeader.uxml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Assets/Scripts/Editor/Dirt/DirtHeader.uxml.meta b/Assets/Scripts/Editor/Dirt/DirtHeader.uxml.meta
new file mode 100644
index 0000000..1218dca
--- /dev/null
+++ b/Assets/Scripts/Editor/Dirt/DirtHeader.uxml.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 872d329bdd85298469aecb4acce3d6f2
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
diff --git a/Assets/Scripts/Editor/Dirt/DirtRow.uxml b/Assets/Scripts/Editor/Dirt/DirtRow.uxml
new file mode 100644
index 0000000..82d268a
--- /dev/null
+++ b/Assets/Scripts/Editor/Dirt/DirtRow.uxml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Assets/Scripts/Editor/Dirt/DirtRow.uxml.meta b/Assets/Scripts/Editor/Dirt/DirtRow.uxml.meta
new file mode 100644
index 0000000..96ad530
--- /dev/null
+++ b/Assets/Scripts/Editor/Dirt/DirtRow.uxml.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: e96daeb60bacf3d48b0e9e4ccbbe167c
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
diff --git a/Assets/Scripts/Editor/Dirt/DirtWindow.cs b/Assets/Scripts/Editor/Dirt/DirtWindow.cs
new file mode 100644
index 0000000..b12d77d
--- /dev/null
+++ b/Assets/Scripts/Editor/Dirt/DirtWindow.cs
@@ -0,0 +1,517 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEditor.UIElements;
+
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using UnityEngine.UIElements;
+
+namespace Dirt
+{
+ public class DirtWindow : EditorWindow
+ {
+ [SerializeField] VisualTreeAsset visualTreeAsset;
+ [SerializeField] VisualTreeAsset headerAsset;
+ [SerializeField] VisualTreeAsset rowAsset;
+
+ [MenuItem("Tools/Dirt")]
+ public static void Open() => GetWindow("Dirt", typeof(SceneView));
+
+ string activePath;
+ int refreshCountdown;
+
+ TextField searchField;
+ ScrollView content;
+
+ readonly List> cache = new();
+ readonly List visibleModifications = new();
+
+ int cacheIndexGUI;
+
+ public void CreateGUI()
+ {
+ DirtWindowSettings.Load();
+
+ if (!visualTreeAsset)
+ {
+ return;
+ }
+
+ var templateContainer = visualTreeAsset.Instantiate();
+ rootVisualElement.Add(templateContainer);
+ templateContainer.StretchToParentSize();
+
+ searchField = rootVisualElement.Q("Search");
+ searchField.RegisterValueChangedCallback(e => refreshCountdown = 3);
+
+ content = rootVisualElement.Q("Content");
+ content.RegisterCallback(e => OnMouseDown(e));
+
+ refreshCountdown = 2;
+ }
+
+ void Update()
+ {
+ var text = "Dirt";
+
+ if (cache.Count > 0)
+ {
+ var progress = Mathf.RoundToInt(
+ (float) cacheIndexGUI / cache.Count * 100
+ );
+
+ if (progress < 100)
+ {
+ text += $" - {progress}%";
+ }
+ }
+
+ if (text == "Dirt" && titleContent.text != text)
+ {
+ Repaint();
+ }
+
+ titleContent.text = text;
+
+ if (refreshCountdown > 0)
+ {
+ return;
+ }
+
+ UpdateGUI();
+ }
+
+ void OnInspectorUpdate()
+ {
+ var activePath = GetActivePath(out _);
+
+ if (this.activePath != activePath)
+ {
+ this.activePath = activePath;
+ refreshCountdown += 2;
+ }
+
+ if (refreshCountdown != 0)
+ {
+ if (refreshCountdown == 1)
+ {
+ this.activePath = GetActivePath(out var prefabStage);
+ var instanceOwners = FindInstanceOwners(prefabStage);
+ RefreshCache(instanceOwners);
+ ClearGUI();
+ }
+
+ refreshCountdown--;
+ }
+ }
+
+ static string GetActivePath(out PrefabStage prefabStage)
+ {
+ prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
+
+ if (prefabStage)
+ {
+ return prefabStage.assetPath;
+ }
+ else
+ {
+ return string.Join(";", Enumerable
+ .Range(0, SceneManager.sceneCount)
+ .Select(i => SceneManager.GetSceneAt(i).path)
+ );
+ }
+ }
+
+ static List FindInstanceOwners(PrefabStage prefabStage)
+ {
+ return (
+ prefabStage
+ ? prefabStage
+ .FindComponentsOfType()
+ .Where(x => x.hideFlags != HideFlags.HideAndDontSave)
+ .Select(x => x.gameObject)
+ : FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.InstanceID)
+ .AsEnumerable()
+ ).ToList();
+ }
+
+ void RefreshCache(List instanceOwners)
+ {
+ cache.Clear();
+
+ foreach (var instanceOwner in instanceOwners.Where(PrefabUtility.IsOutermostPrefabInstanceRoot))
+ {
+ var prefabOwner = PrefabUtility.GetCorrespondingObjectFromSource(instanceOwner);
+
+ cache.Add(
+ PrefabUtility.GetPropertyModifications(instanceOwner)
+ .Select(x => new Modification(prefabOwner, instanceOwner, x))
+ .Where(x => x.instance is not null)
+ .ToList()
+ );
+ }
+ }
+
+ void ClearGUI()
+ {
+ content.Clear();
+ visibleModifications.Clear();
+ cacheIndexGUI = 0;
+ }
+
+ void UpdateGUI()
+ {
+ if (cacheIndexGUI >= cache.Count)
+ {
+ return;
+ }
+
+ var settings = DirtWindowSettings.instance;
+
+ foreach (var modifications in cache.Skip(cacheIndexGUI))
+ {
+ cacheIndexGUI++;
+
+ var filteredModifications = modifications.Where(x =>
+ {
+ var s = searchField.value;
+ if (s == string.Empty)
+ {
+ return true;
+ }
+
+ var i = StringComparison.InvariantCultureIgnoreCase;
+
+ foreach (var value in x.instance.GetOwnerHierarchy())
+ {
+ if (value.name.Contains(s, i))
+ {
+ return true;
+ }
+ }
+
+ var t = x.instance.GetTarget();
+ if (t && (t.name.Contains(s, i) || t.GetType().Name.Contains(s, i)))
+ {
+ return true;
+ }
+
+ if (x.propertyPath.Contains(s, i))
+ {
+ return true;
+ }
+
+ if (x.prefab.targetPropertyValue.Contains(s, i))
+ {
+ return true;
+ }
+
+ if (x.instance.targetPropertyValue.Contains(s, i))
+ {
+ return true;
+ }
+
+ return false;
+
+ }).Where(x => x.IsUnchangedButDirty()
+ ? settings.visibility.HasFlag(VisibilityMask.UnchangedDirtyValues)
+ : settings.visibility.HasFlag(VisibilityMask.ChangedDirtyValues)
+ ).ToList();
+
+ if (filteredModifications.Count == 0)
+ {
+ continue;
+ }
+
+ visibleModifications.AddRange(filteredModifications);
+
+ var header = headerAsset.Instantiate();
+ content.Add(header);
+
+ var instanceOwnerHierarchyContainer = header.Q("InstanceOwnerHierarchy");
+
+ var fields = filteredModifications
+ .First()
+ .instance
+ .GetOwnerHierarchy()
+ .Select(x => new ObjectField
+ {
+ value = x,
+ style = {
+ flexShrink = 1,
+ marginTop = 10,
+ marginRight = 0,
+ marginBottom = 0,
+ marginLeft = 0
+ }
+ });
+
+ foreach (var field in fields)
+ {
+ if (instanceOwnerHierarchyContainer.childCount > 0)
+ {
+ instanceOwnerHierarchyContainer.Add(new Label
+ {
+ text = "/",
+ style = {
+ marginTop = 10,
+ marginRight = 2,
+ marginBottom = 0,
+ marginLeft = 2
+ }
+ });
+ }
+ instanceOwnerHierarchyContainer.Add(field);
+ }
+
+ var rowContent = new VisualElement();
+ content.Add(rowContent);
+
+ if (settings.modificationLimit.y > 0 && filteredModifications.Count <= settings.modificationLimit.y)
+ {
+ foreach (var m in filteredModifications)
+ {
+ rowContent.Add(CreateRow(m));
+ }
+ break;
+ }
+
+ foreach (var m in filteredModifications.Take(settings.modificationLimit.x))
+ {
+ rowContent.Add(CreateRow(m));
+ }
+
+ var moreModifications = filteredModifications
+ .Skip(settings.modificationLimit.x)
+ .ToList();
+
+ var button = new Button
+ {
+ text = $"Show More... ({moreModifications.Count})"
+ };
+
+ button.AddToClassList("link");
+ button.clicked += () =>
+ {
+ button.RemoveFromHierarchy();
+ foreach (var m in moreModifications)
+ {
+ rowContent.Add(CreateRow(m));
+ }
+ };
+
+ rowContent.Add(button);
+
+ break;
+ }
+ }
+
+ VisualElement CreateRow(Modification modification)
+ {
+ var row = rowAsset.Instantiate();
+ row.AddToClassList("row");
+
+ row.RegisterCallback(e => OnMouseDown(e, row, modification));
+ row.Q("InstanceTarget").value = modification.instance.GetTarget();
+
+ foreach (var (name, reference) in new[] {
+ ("Prefab", modification.prefab),
+ ("Instance", modification.instance)
+ })
+ {
+ var label = row.Q