com.hidano.missing-reference-finder 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,7 +24,7 @@ namespace Hidano.MissingReferenceFinder.Editor
24
24
  /// ログには「どのファイルの / どの GameObject (階層パス) の / 上から何番目のどのコンポーネントの / どのフィールド」が
25
25
  /// 壊れているかと、その対処法を出力する。Prefab インスタンス内のオブジェクトは元 Prefab を読み込んで名前を解決する。
26
26
  ///
27
- /// 有効/無効はメニュー (Hidano/Tools/Check Missing References On Build) または
27
+ /// 有効/無効はメニュー (Tools/Hidano/Check Missing References On Build) または
28
28
  /// <see cref="MissingReferenceFinderWindow"/> から切り替えられる。
29
29
  /// </summary>
30
30
  public sealed class MissingReferenceBuildCheck : IPreprocessBuildWithReport
@@ -304,6 +304,7 @@ namespace Hidano.MissingReferenceFinder.Editor
304
304
  private static int CheckBuildScenes(out string breakdown)
305
305
  {
306
306
  var total = 0;
307
+ var unusedOverrideTotal = 0;
307
308
  var scenePaths = new List<string>();
308
309
  var prefabPaths = new List<string>();
309
310
  var prefabUsedBy = new Dictionary<string, List<string>>();
@@ -351,7 +352,7 @@ namespace Hidano.MissingReferenceFinder.Editor
351
352
  foreach (var scenePath in scenePaths)
352
353
  {
353
354
  EditorUtility.DisplayProgressBar("Missing Reference Build Check", scenePath, (float)index++ / totalFiles);
354
- var count = CheckYamlFile(scenePath, true, null);
355
+ var count = CheckYamlFile(scenePath, true, null, ref unusedOverrideTotal);
355
356
  if (count > 0)
356
357
  {
357
358
  fileCounts.Add((scenePath, count));
@@ -363,7 +364,7 @@ namespace Hidano.MissingReferenceFinder.Editor
363
364
  foreach (var prefabPath in prefabPaths)
364
365
  {
365
366
  EditorUtility.DisplayProgressBar("Missing Reference Build Check", prefabPath, (float)index++ / totalFiles);
366
- var count = CheckYamlFile(prefabPath, false, prefabUsedBy[prefabPath]);
367
+ var count = CheckYamlFile(prefabPath, false, prefabUsedBy[prefabPath], ref unusedOverrideTotal);
367
368
  if (count > 0)
368
369
  {
369
370
  fileCounts.Add((prefabPath, count));
@@ -392,6 +393,11 @@ namespace Hidano.MissingReferenceFinder.Editor
392
393
  breakdown = breakdownBuilder.Length > 0 ? breakdownBuilder.ToString() : "なし";
393
394
 
394
395
  var summary = $"{LogPrefix} 検査完了: {scenePaths.Count} シーン / {prefabPaths.Count} Prefab / Missing {total} 件";
396
+ if (unusedOverrideTotal > 0)
397
+ {
398
+ summary += $" / 未使用オーバーライド {unusedOverrideTotal} 件 (警告のみ・ビルドは継続)";
399
+ }
400
+
395
401
  if (total > 0)
396
402
  {
397
403
  summary += $"\n内訳: {breakdown}";
@@ -452,8 +458,11 @@ namespace Hidano.MissingReferenceFinder.Editor
452
458
  public bool IsTransform => ClassId == TransformClassId || ClassId == RectTransformClassId;
453
459
  }
454
460
 
455
- /// <summary>シーンまたは Prefab ファイルを 1 つ検査し、Missing の数を返す。</summary>
456
- private static int CheckYamlFile(string assetPath, bool isScene, List<string> usedByScenes)
461
+ /// <summary>
462
+ /// シーンまたは Prefab ファイルを 1 つ検査し、Missing の数を返す。
463
+ /// 警告扱いにした未使用オーバーライドの数は <paramref name="unusedOverrideCount"/> に加算する。
464
+ /// </summary>
465
+ private static int CheckYamlFile(string assetPath, bool isScene, List<string> usedByScenes, ref int unusedOverrideCount)
457
466
  {
458
467
  var context = new FileContext(assetPath, isScene, ParseYamlFile(assetPath), usedByScenes);
459
468
 
@@ -462,6 +471,14 @@ namespace Hidano.MissingReferenceFinder.Editor
462
471
  {
463
472
  foreach (var entry in doc.Missing)
464
473
  {
474
+ // 存在しないプロパティへのオーバーライドは Unity が適用しない (Inspector にも出ない) ので、ビルドは止めずに警告のみ
475
+ if (context.IsUnusedOverride(doc, entry))
476
+ {
477
+ unusedOverrideCount++;
478
+ Debug.LogWarning(context.BuildUnusedOverrideReport(doc, entry), context.GetContextObject(doc, entry));
479
+ continue;
480
+ }
481
+
465
482
  count++;
466
483
  Debug.LogError(context.BuildReport(doc, entry), context.GetContextObject(doc, entry));
467
484
  }
@@ -1026,6 +1043,72 @@ namespace Hidano.MissingReferenceFinder.Editor
1026
1043
  $"正しい{fieldTypeName ?? "アセット"}を設定し直すか、フィールドを右クリック → Revert でオーバーライドを解除して Prefab の値に戻してください。");
1027
1044
  }
1028
1045
 
1046
+ /// <summary>
1047
+ /// PrefabInstance のオーバーライドが、元 Prefab のコンポーネントに存在しないプロパティを指しているか。
1048
+ /// フィールド名の変更 (例: Cinemachine 3 の CinemachineVolumeSettings で FocusTarget → m_FocusTarget) や
1049
+ /// フィールド削除で取り残されたオーバーライドは Unity が適用せず Inspector にも表示されないため、
1050
+ /// Inspector から設定し直しても消えず、参照先が消えたアセットでも実害がない。
1051
+ /// 判定できない場合 (コンポーネントを読めない / 配列要素 / SerializeReference) は false (= Missing 扱い) にする。
1052
+ /// </summary>
1053
+ public bool IsUnusedOverride(DocumentInfo doc, MissingEntry entry)
1054
+ {
1055
+ if (doc.ClassId != PrefabInstanceClassId || entry.Key == SourcePrefabKey || entry.TargetFileId == 0)
1056
+ {
1057
+ return false;
1058
+ }
1059
+
1060
+ if (!(PrefabAssetCache.GetObject(doc.SourcePrefabGuid, entry.TargetFileId) is Component target))
1061
+ {
1062
+ return false;
1063
+ }
1064
+
1065
+ var propertyPath = entry.Key;
1066
+ if (propertyPath.StartsWith("managedReferences", StringComparison.Ordinal))
1067
+ {
1068
+ return false;
1069
+ }
1070
+
1071
+ // 配列要素はインスタンス側でサイズが拡張されている場合があるため、配列プロパティ自体の存在だけを確認する
1072
+ var arrayIndex = propertyPath.IndexOf(".Array.data[", StringComparison.Ordinal);
1073
+ if (arrayIndex >= 0)
1074
+ {
1075
+ propertyPath = propertyPath.Substring(0, arrayIndex);
1076
+ }
1077
+
1078
+ try
1079
+ {
1080
+ using var serializedObject = new SerializedObject(target);
1081
+ return serializedObject.FindProperty(propertyPath) == null;
1082
+ }
1083
+ catch (Exception)
1084
+ {
1085
+ return false;
1086
+ }
1087
+ }
1088
+
1089
+ public string BuildUnusedOverrideReport(DocumentInfo instance, MissingEntry entry)
1090
+ {
1091
+ var targetComponent = (Component)PrefabAssetCache.GetObject(instance.SourcePrefabGuid, entry.TargetFileId);
1092
+ AssetDatabase.TryGetGUIDAndLocalFileIdentifier(targetComponent.gameObject, out _, out long gameObjectLocalId);
1093
+ var ownerPath = GetPrefabInstanceObjectPath(instance, gameObjectLocalId, 0);
1094
+ var typeName = targetComponent.GetType().Name;
1095
+ var sourcePrefabPath = PrefabAssetCache.GetPath(instance.SourcePrefabGuid) ?? instance.SourcePrefabGuid;
1096
+ var location = _isScene
1097
+ ? $"シーン {_fileLabel} を開き、Hierarchy で上記 Prefab インスタンスを右クリック"
1098
+ : $"Prefab {_fileLabel} を開き、Hierarchy で上記 Prefab インスタンスを右クリック";
1099
+
1100
+ return Compose(
1101
+ $"[Unused Override] {_fileLabel} | {ownerPath} | {typeName} | {entry.Key} (存在しないプロパティへのオーバーライド)",
1102
+ "ファイル: " + _assetPath,
1103
+ DescribeUsedBy(),
1104
+ $"GameObject: {ownerPath} (Prefab {sourcePrefabPath} のインスタンス)",
1105
+ "コンポーネント: " + typeName,
1106
+ $"プロパティ: {entry.Key} は {typeName} に存在しません (フィールド名の変更・削除で取り残されたオーバーライド)",
1107
+ DescribeGuid(entry.Guid),
1108
+ "影響: このオーバーライドは Unity に適用されず Inspector にも表示されないため、ビルドは止めません。",
1109
+ $"対処: {location} → Prefab → Remove Unused Overrides でファイルから削除できます。");
1110
+ }
1111
+
1029
1112
  private string OpenInstruction => _isScene
1030
1113
  ? $"シーン {_fileLabel} を開いて"
1031
1114
  : $"Prefab {_fileLabel} を Project ウィンドウでダブルクリックして開き、";
@@ -9,7 +9,7 @@ namespace Hidano.MissingReferenceFinder.Editor
9
9
  /// </summary>
10
10
  public sealed class MissingReferenceFinderWindow : EditorWindow
11
11
  {
12
- private const string MenuPath = "Hidano/Tools/Missing Reference Finder";
12
+ private const string MenuPath = "Tools/Hidano/Missing Reference Finder";
13
13
 
14
14
  [SerializeField] private bool _reportNone = true;
15
15
  [SerializeField] private bool _monoBehaviourOnly = true;
@@ -0,0 +1,274 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Linq;
4
+ using System.Text;
5
+ using UnityEditor;
6
+ using UnityEditor.SceneManagement;
7
+ using UnityEngine;
8
+ using UnityEngine.Playables;
9
+ using UnityEngine.SceneManagement;
10
+ using Object = UnityEngine.Object;
11
+
12
+ namespace Hidano.MissingReferenceFinder.Editor.TimelineBindings
13
+ {
14
+ /// <summary>
15
+ /// Timeline (.playable) の保存時に、その Timeline を再生する PlayableDirector の m_SceneBindings へ
16
+ /// 未登録トラックの空 Binding (value: None) を追加する。
17
+ ///
18
+ /// PlayableDirector の Inspector は Bindings 欄を表示した時点で、未登録の出力に空の Binding を作る
19
+ /// (UnityEditor.DirectorEditor.UpdatePlayableBindingsIfRequired)。トラックを追加した人がその Prefab の
20
+ /// Inspector を表示していないと、後で別の人が表示したときに意図しない差分として現れる。その差分をコミットせずに
21
+ /// 抱えたまま pull すると、コンフリクト解決時に他の人が設定した Binding を上書きして消す事故につながる。
22
+ /// トラック追加と同じタイミングで空 Binding を作っておき、同じコミットに含めることでこれを防ぐ。
23
+ ///
24
+ /// 対象は Inspector と同じく、playableAsset.outputs のうち出力先の型を持つもの。Inspector は出力先の型を持たない
25
+ /// 出力の Binding を削除もするが、要素の位置がずれて Prefab インスタンスのオーバーライド (配列番号で記録される) を
26
+ /// 崩しうるため、ここでは末尾への追加のみを行う。Prefab インスタンス上の PlayableDirector は
27
+ /// 元 Prefab 側で処理されるので対象外 (インスタンス側に追加すると不要なオーバーライドになる)。
28
+ ///
29
+ /// 有効/無効はメニュー (Tools/Hidano/Fill Timeline Binding Entries On Save) で切り替えられる。
30
+ /// </summary>
31
+ public sealed class TimelineBindingEntryFiller : AssetPostprocessor
32
+ {
33
+ private const string ToggleMenuPath = "Tools/Hidano/Fill Timeline Binding Entries On Save";
34
+ private const string RunMenuPath = "Tools/Hidano/Fill Timeline Binding Entries In All Prefabs";
35
+ private const string EditorPrefsKey = "Hidano.TimelineBindingEntryFiller.Enabled";
36
+ private const string LogPrefix = "[TimelineBindingEntryFiller]";
37
+
38
+ private const string PlayableExtension = ".playable";
39
+ private const string AssetsFolder = "Assets";
40
+ private const string SceneBindingsProperty = "m_SceneBindings";
41
+ private const string BindingKeyProperty = "key";
42
+ private const string BindingValueProperty = "value";
43
+
44
+ /// <summary>delayCall 待ちの間に保存された Timeline のパス。</summary>
45
+ private static readonly HashSet<string> PendingTimelinePaths = new HashSet<string>();
46
+
47
+ public static bool Enabled
48
+ {
49
+ get => EditorPrefs.GetBool(EditorPrefsKey, true);
50
+ set => EditorPrefs.SetBool(EditorPrefsKey, value);
51
+ }
52
+
53
+ [MenuItem(ToggleMenuPath)]
54
+ private static void ToggleEnabled()
55
+ {
56
+ Enabled = !Enabled;
57
+ }
58
+
59
+ [MenuItem(ToggleMenuPath, true)]
60
+ private static bool ToggleEnabledValidate()
61
+ {
62
+ Menu.SetChecked(ToggleMenuPath, Enabled);
63
+ return true;
64
+ }
65
+
66
+ /// <summary>保存を待たずに、Timeline を参照する全 Prefab と開いているシーンに同じ処理を行う。</summary>
67
+ [MenuItem(RunMenuPath)]
68
+ private static void RunManually()
69
+ {
70
+ var addedCount = Fill(_ => true);
71
+ if (addedCount == 0)
72
+ {
73
+ Debug.Log($"{LogPrefix} 追加が必要な Binding はありませんでした。");
74
+ }
75
+ }
76
+
77
+ private static void OnPostprocessAllAssets(
78
+ string[] importedAssets,
79
+ string[] deletedAssets,
80
+ string[] movedAssets,
81
+ string[] movedFromAssetPaths)
82
+ {
83
+ if (!Enabled)
84
+ {
85
+ return;
86
+ }
87
+
88
+ var added = false;
89
+ foreach (var path in importedAssets)
90
+ {
91
+ if (IsProjectTimelinePath(path))
92
+ {
93
+ added |= PendingTimelinePaths.Add(path);
94
+ }
95
+ }
96
+
97
+ if (added)
98
+ {
99
+ // インポート処理中に他のアセットを書き換えないよう、インポート完了後に回す
100
+ EditorApplication.delayCall -= ProcessPending;
101
+ EditorApplication.delayCall += ProcessPending;
102
+ }
103
+ }
104
+
105
+ private static bool IsProjectTimelinePath(string path)
106
+ {
107
+ return path.StartsWith(AssetsFolder + "/", StringComparison.Ordinal) &&
108
+ path.EndsWith(PlayableExtension, StringComparison.OrdinalIgnoreCase);
109
+ }
110
+
111
+ private static void ProcessPending()
112
+ {
113
+ if (PendingTimelinePaths.Count == 0)
114
+ {
115
+ return;
116
+ }
117
+
118
+ var timelinePaths = new HashSet<string>(PendingTimelinePaths);
119
+ PendingTimelinePaths.Clear();
120
+ Fill(timelinePaths.Contains);
121
+ }
122
+
123
+ /// <summary>
124
+ /// <paramref name="isTargetTimeline"/> に該当する Timeline を参照する Prefab と、開いているシーンを処理する。
125
+ /// </summary>
126
+ /// <returns>追加した Binding の総数。</returns>
127
+ private static int Fill(Func<string, bool> isTargetTimeline)
128
+ {
129
+ var addedCount = 0;
130
+ var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
131
+
132
+ foreach (var guid in AssetDatabase.FindAssets("t:Prefab", new[] { AssetsFolder }))
133
+ {
134
+ var prefabPath = AssetDatabase.GUIDToAssetPath(guid);
135
+ var referencesTarget = AssetDatabase.GetDependencies(prefabPath, false)
136
+ .Any(path => IsProjectTimelinePath(path) && isTargetTimeline(path));
137
+ if (!referencesTarget)
138
+ {
139
+ continue;
140
+ }
141
+
142
+ // Prefab モードで開いている Prefab をファイル側から書き換えると編集中の内容と食い違うので、ステージ上で処理する
143
+ if (prefabStage != null && prefabStage.assetPath == prefabPath)
144
+ {
145
+ var stageAdded = FillDirectors(prefabStage.prefabContentsRoot, isTargetTimeline, prefabPath);
146
+ if (stageAdded > 0)
147
+ {
148
+ EditorSceneManager.MarkSceneDirty(prefabStage.scene);
149
+ addedCount += stageAdded;
150
+ }
151
+
152
+ continue;
153
+ }
154
+
155
+ var root = PrefabUtility.LoadPrefabContents(prefabPath);
156
+ try
157
+ {
158
+ var prefabAdded = FillDirectors(root, isTargetTimeline, prefabPath);
159
+ if (prefabAdded > 0)
160
+ {
161
+ PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
162
+ addedCount += prefabAdded;
163
+ }
164
+ }
165
+ finally
166
+ {
167
+ PrefabUtility.UnloadPrefabContents(root);
168
+ }
169
+ }
170
+
171
+ for (var i = 0; i < SceneManager.sceneCount; i++)
172
+ {
173
+ var scene = SceneManager.GetSceneAt(i);
174
+ if (!scene.isLoaded)
175
+ {
176
+ continue;
177
+ }
178
+
179
+ var sceneAdded = scene.GetRootGameObjects()
180
+ .Sum(root => FillDirectors(root, isTargetTimeline, scene.path));
181
+ if (sceneAdded > 0)
182
+ {
183
+ EditorSceneManager.MarkSceneDirty(scene);
184
+ addedCount += sceneAdded;
185
+ }
186
+ }
187
+
188
+ return addedCount;
189
+ }
190
+
191
+ private static int FillDirectors(GameObject root, Func<string, bool> isTargetTimeline, string context)
192
+ {
193
+ var addedCount = 0;
194
+ foreach (var director in root.GetComponentsInChildren<PlayableDirector>(true))
195
+ {
196
+ if (PrefabUtility.IsPartOfPrefabInstance(director))
197
+ {
198
+ continue;
199
+ }
200
+
201
+ var playableAsset = director.playableAsset;
202
+ if (playableAsset == null || !isTargetTimeline(AssetDatabase.GetAssetPath(playableAsset)))
203
+ {
204
+ continue;
205
+ }
206
+
207
+ var addedKeys = FillDirector(director, playableAsset);
208
+ if (addedKeys.Count == 0)
209
+ {
210
+ continue;
211
+ }
212
+
213
+ addedCount += addedKeys.Count;
214
+ Debug.Log(
215
+ $"{LogPrefix} {context} の {GetHierarchyPath(director.transform)} に空の Binding を {addedKeys.Count} 件追加しました " +
216
+ $"({playableAsset.name}: {string.Join(", ", addedKeys.Select(key => key.name))})。" +
217
+ "Timeline の変更と一緒にコミットしてください。");
218
+ }
219
+
220
+ return addedCount;
221
+ }
222
+
223
+ /// <summary>
224
+ /// 出力先の型を持つ出力のうち m_SceneBindings に無いものを、value が None の要素として末尾に追加する。
225
+ /// UnityEditor.DirectorEditor.UpdatePlayableBindingsIfRequired と同じ対象・同じ順序になる。
226
+ /// </summary>
227
+ private static List<Object> FillDirector(PlayableDirector director, PlayableAsset playableAsset)
228
+ {
229
+ var serializedDirector = new SerializedObject(director);
230
+ var bindings = serializedDirector.FindProperty(SceneBindingsProperty);
231
+
232
+ var boundKeys = new HashSet<Object>();
233
+ for (var i = 0; i < bindings.arraySize; i++)
234
+ {
235
+ boundKeys.Add(bindings.GetArrayElementAtIndex(i).FindPropertyRelative(BindingKeyProperty).objectReferenceValue);
236
+ }
237
+
238
+ var addedKeys = new List<Object>();
239
+ foreach (var output in playableAsset.outputs)
240
+ {
241
+ var key = output.sourceObject;
242
+ if (key == null || output.outputTargetType == null || !boundKeys.Add(key))
243
+ {
244
+ continue;
245
+ }
246
+
247
+ var index = bindings.arraySize;
248
+ bindings.InsertArrayElementAtIndex(index);
249
+ var element = bindings.GetArrayElementAtIndex(index);
250
+ element.FindPropertyRelative(BindingKeyProperty).objectReferenceValue = key;
251
+ element.FindPropertyRelative(BindingValueProperty).objectReferenceValue = null;
252
+ addedKeys.Add(key);
253
+ }
254
+
255
+ if (addedKeys.Count > 0)
256
+ {
257
+ serializedDirector.ApplyModifiedPropertiesWithoutUndo();
258
+ }
259
+
260
+ return addedKeys;
261
+ }
262
+
263
+ private static string GetHierarchyPath(Transform transform)
264
+ {
265
+ var builder = new StringBuilder(transform.name);
266
+ for (var parent = transform.parent; parent != null; parent = parent.parent)
267
+ {
268
+ builder.Insert(0, parent.name + "/");
269
+ }
270
+
271
+ return builder.ToString();
272
+ }
273
+ }
274
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 457746b482d64933b3c146165b573958
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -0,0 +1,8 @@
1
+ fileFormatVersion: 2
2
+ guid: 74892d32ee9c404896081c5ac4c235e6
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant:
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "com.hidano.missing-reference-finder",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "displayName": "Missing Reference Finder",
5
- "description": "A tool to find missing references in Unity projects.",
5
+ "description": "A tool to find missing references in Unity projects. Also fills empty PlayableDirector binding entries when a Timeline is saved, so they are committed together with the Timeline change.",
6
6
  "unity": "6000.0",
7
7
  "author": {
8
8
  "name": "Hidano"