com.hidano.missing-reference-finder 0.0.0-stage → 1.0.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.
- package/Editor/Hidano.MissingReferenceFinder.asmdef +16 -0
- package/Editor/Hidano.MissingReferenceFinder.asmdef.meta +7 -0
- package/Editor/MissingReferenceBuildCheck.cs +1423 -0
- package/Editor/MissingReferenceBuildCheck.cs.meta +11 -0
- package/Editor/MissingReferenceFinderWindow.cs +79 -0
- package/Editor/MissingReferenceFinderWindow.cs.meta +11 -0
- package/Editor/MissingReferenceScanner.cs +256 -0
- package/Editor/MissingReferenceScanner.cs.meta +11 -0
- package/Editor.meta +8 -0
- package/package.json +11 -4
- package/package.json.meta +7 -0
- package/README.md +0 -3
|
@@ -0,0 +1,1423 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.IO;
|
|
4
|
+
using System.Reflection;
|
|
5
|
+
using System.Text;
|
|
6
|
+
using System.Text.RegularExpressions;
|
|
7
|
+
using UnityEditor;
|
|
8
|
+
using UnityEditor.Build;
|
|
9
|
+
using UnityEditor.Build.Reporting;
|
|
10
|
+
using UnityEngine;
|
|
11
|
+
using UnityEngine.SceneManagement;
|
|
12
|
+
using Object = UnityEngine.Object;
|
|
13
|
+
|
|
14
|
+
namespace Hidano.MissingReferenceFinder.Editor
|
|
15
|
+
{
|
|
16
|
+
/// <summary>
|
|
17
|
+
/// ビルド開始前に Build Settings に登録された各シーンファイル (.unity) と、
|
|
18
|
+
/// それらが依存する Prefab ファイル (.prefab) を直接読み、
|
|
19
|
+
/// 参照先アセットの GUID が AssetDatabase に存在しないもの (= Missing) を検出してビルドを中止する。
|
|
20
|
+
///
|
|
21
|
+
/// ビルド中に読み込まれるシーンでは Missing 参照が解決済みになっており instanceID ベースの判定が効かないため、
|
|
22
|
+
/// メモリ上の走査 (<see cref="MissingReferenceScanner"/>) ではなくファイル上の GUID を検査する。
|
|
23
|
+
///
|
|
24
|
+
/// ログには「どのファイルの / どの GameObject (階層パス) の / 上から何番目のどのコンポーネントの / どのフィールド」が
|
|
25
|
+
/// 壊れているかと、その対処法を出力する。Prefab インスタンス内のオブジェクトは元 Prefab を読み込んで名前を解決する。
|
|
26
|
+
///
|
|
27
|
+
/// 有効/無効はメニュー (Hidano/Tools/Check Missing References On Build) または
|
|
28
|
+
/// <see cref="MissingReferenceFinderWindow"/> から切り替えられる。
|
|
29
|
+
/// </summary>
|
|
30
|
+
public sealed class MissingReferenceBuildCheck : IPreprocessBuildWithReport
|
|
31
|
+
{
|
|
32
|
+
private const string ToggleMenuPath = "Tools/Hidano/Check Missing References On Build";
|
|
33
|
+
private const string RunMenuPath = "Tools/Hidano/Run Pre-Build Missing Reference Check";
|
|
34
|
+
private const string EditorPrefsKey = "Hidano.MissingReferenceBuildCheck.Enabled";
|
|
35
|
+
private const string LogPrefix = "[MissingReferenceBuildCheck]";
|
|
36
|
+
|
|
37
|
+
/// <summary>階層パス解決時の無限ループ防止用。</summary>
|
|
38
|
+
private const int MaxPathDepth = 64;
|
|
39
|
+
|
|
40
|
+
// 例: "--- !u!114 &123456789" / "--- !u!1 &123 stripped"
|
|
41
|
+
private static readonly Regex DocumentHeaderRegex =
|
|
42
|
+
new Regex(@"^--- !u!(\d+) &(-?\d+)", RegexOptions.Compiled);
|
|
43
|
+
|
|
44
|
+
// 例: " prefab: {fileID: 123, guid: 0123456789abcdef0123456789abcdef, type: 3}"
|
|
45
|
+
private static readonly Regex GuidRegex =
|
|
46
|
+
new Regex(@"guid:\s*([0-9a-fA-F]{32})", RegexOptions.Compiled);
|
|
47
|
+
|
|
48
|
+
// 例: " m_GameObject: {fileID: 123456}"
|
|
49
|
+
private static readonly Regex GameObjectRefRegex =
|
|
50
|
+
new Regex(@"^\s*m_GameObject:\s*\{fileID:\s*(-?\d+)\}", RegexOptions.Compiled);
|
|
51
|
+
|
|
52
|
+
// 例: " m_CorrespondingSourceObject: {fileID: 123, guid: ..., type: 3}"
|
|
53
|
+
private static readonly Regex CorrespondingSourceRegex =
|
|
54
|
+
new Regex(@"^\s*m_CorrespondingSourceObject:\s*\{fileID:\s*(-?\d+)", RegexOptions.Compiled);
|
|
55
|
+
|
|
56
|
+
// 例: " m_PrefabInstance: {fileID: 123}"
|
|
57
|
+
private static readonly Regex PrefabInstanceRefRegex =
|
|
58
|
+
new Regex(@"^\s*m_PrefabInstance:\s*\{fileID:\s*(-?\d+)\}", RegexOptions.Compiled);
|
|
59
|
+
|
|
60
|
+
// Transform / RectTransform の親 Transform。例: " m_Father: {fileID: 123}"
|
|
61
|
+
private static readonly Regex FatherRegex =
|
|
62
|
+
new Regex(@"^\s*m_Father:\s*\{fileID:\s*(-?\d+)\}", RegexOptions.Compiled);
|
|
63
|
+
|
|
64
|
+
// GameObject が持つコンポーネント一覧。例: " - component: {fileID: 123}"
|
|
65
|
+
private static readonly Regex ComponentListItemRegex =
|
|
66
|
+
new Regex(@"^\s*-\s*component:\s*\{fileID:\s*(-?\d+)\}", RegexOptions.Compiled);
|
|
67
|
+
|
|
68
|
+
// MonoBehaviour のスクリプト。例: " m_Script: {fileID: 11500000, guid: ..., type: 3}"
|
|
69
|
+
private static readonly Regex ScriptRegex =
|
|
70
|
+
new Regex(@"^\s*m_Script:\s*\{fileID:\s*-?\d+,\s*guid:\s*([0-9a-fA-F]{32})", RegexOptions.Compiled);
|
|
71
|
+
|
|
72
|
+
// PrefabInstance の元 Prefab。例: " m_SourcePrefab: {fileID: 100100000, guid: ..., type: 3}"
|
|
73
|
+
private static readonly Regex SourcePrefabRegex =
|
|
74
|
+
new Regex(@"^\s*m_SourcePrefab:\s*\{fileID:\s*-?\d+,\s*guid:\s*([0-9a-fA-F]{32})", RegexOptions.Compiled);
|
|
75
|
+
|
|
76
|
+
// PrefabInstance の親 Transform。例: " m_TransformParent: {fileID: 123}"
|
|
77
|
+
private static readonly Regex TransformParentRegex =
|
|
78
|
+
new Regex(@"^\s*m_TransformParent:\s*\{fileID:\s*(-?\d+)\}", RegexOptions.Compiled);
|
|
79
|
+
|
|
80
|
+
// PrefabInstance の m_Modifications 各要素。例: " - target: {fileID: 123, guid: ..., type: 3}"
|
|
81
|
+
private static readonly Regex ModificationTargetRegex =
|
|
82
|
+
new Regex(@"^\s*-\s*target:\s*\{fileID:\s*(-?\d+)", RegexOptions.Compiled);
|
|
83
|
+
|
|
84
|
+
private static readonly Regex ModificationPropertyPathRegex =
|
|
85
|
+
new Regex(@"^\s*propertyPath:\s*(.*)$", RegexOptions.Compiled);
|
|
86
|
+
|
|
87
|
+
private static readonly Regex ModificationValueRegex =
|
|
88
|
+
new Regex(@"^\s*value:\s*(.*)$", RegexOptions.Compiled);
|
|
89
|
+
|
|
90
|
+
// 例: " m_Name: Foo"
|
|
91
|
+
private static readonly Regex NameRegex =
|
|
92
|
+
new Regex(@"^\s*m_Name:\s*(.*)$", RegexOptions.Compiled);
|
|
93
|
+
|
|
94
|
+
// ドキュメント直下 (インデント 2) のキー。MonoBehaviour のフィールド名列挙に使う
|
|
95
|
+
private static readonly Regex TopLevelKeyRegex =
|
|
96
|
+
new Regex(@"^ ([A-Za-z_][A-Za-z0-9_]*):", RegexOptions.Compiled);
|
|
97
|
+
|
|
98
|
+
private static readonly Regex ArrayIndexRegex =
|
|
99
|
+
new Regex(@"\[\d*\]", RegexOptions.Compiled);
|
|
100
|
+
|
|
101
|
+
// Unity の SerializedProperty パス表記 "_list.Array.data[3]" → "_list[3]"
|
|
102
|
+
private static readonly Regex ArrayDataRegex =
|
|
103
|
+
new Regex(@"\.Array\.data\[", RegexOptions.Compiled);
|
|
104
|
+
|
|
105
|
+
/// <summary>
|
|
106
|
+
/// Unity YAML の行をインデントに基づいて追跡し、現在行の完全パス ("_entries[1]._material") と末尾キーを返す。
|
|
107
|
+
/// Unity は list 要素を親キーと同じインデントの "- " で書き、要素の中身はその 2 桁右に置く。
|
|
108
|
+
/// </summary>
|
|
109
|
+
private sealed class YamlPathTracker
|
|
110
|
+
{
|
|
111
|
+
private sealed class Frame
|
|
112
|
+
{
|
|
113
|
+
public int Indent;
|
|
114
|
+
public string Key;
|
|
115
|
+
public int Index = -1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 例: " - target: {...}" → indent=4, item="- ", rest="target: {...}"
|
|
119
|
+
private static readonly Regex LineRegex =
|
|
120
|
+
new Regex(@"^( *)(- +)?(.*)$", RegexOptions.Compiled);
|
|
121
|
+
|
|
122
|
+
private static readonly Regex RestKeyRegex =
|
|
123
|
+
new Regex(@"^([A-Za-z_][A-Za-z0-9_]*):", RegexOptions.Compiled);
|
|
124
|
+
|
|
125
|
+
private readonly List<Frame> _frames = new List<Frame>();
|
|
126
|
+
|
|
127
|
+
public string Path { get; private set; } = "?";
|
|
128
|
+
public string LeafKey { get; private set; } = "?";
|
|
129
|
+
|
|
130
|
+
public void Reset()
|
|
131
|
+
{
|
|
132
|
+
_frames.Clear();
|
|
133
|
+
Path = "?";
|
|
134
|
+
LeafKey = "?";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public void Feed(string line)
|
|
138
|
+
{
|
|
139
|
+
var match = LineRegex.Match(line);
|
|
140
|
+
var indent = match.Groups[1].Length;
|
|
141
|
+
var isListItem = match.Groups[2].Success;
|
|
142
|
+
var rest = match.Groups[3].Value;
|
|
143
|
+
|
|
144
|
+
// より深い階層から戻ってきたので、子フレームを捨てる
|
|
145
|
+
PopWhile(frame => frame.Indent > indent);
|
|
146
|
+
|
|
147
|
+
if (isListItem)
|
|
148
|
+
{
|
|
149
|
+
if (_frames.Count > 0 && _frames[_frames.Count - 1].Indent == indent)
|
|
150
|
+
{
|
|
151
|
+
_frames[_frames.Count - 1].Index++;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
indent += match.Groups[2].Length;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
var keyMatch = RestKeyRegex.Match(rest);
|
|
158
|
+
if (keyMatch.Success)
|
|
159
|
+
{
|
|
160
|
+
PopWhile(frame => frame.Indent >= indent);
|
|
161
|
+
|
|
162
|
+
// インデント 0 は "MonoBehaviour:" のようなクラス名行なのでパスには含めない
|
|
163
|
+
if (indent > 0)
|
|
164
|
+
{
|
|
165
|
+
_frames.Add(new Frame { Indent = indent, Key = keyMatch.Groups[1].Value });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
var builder = new StringBuilder();
|
|
170
|
+
foreach (var frame in _frames)
|
|
171
|
+
{
|
|
172
|
+
if (builder.Length > 0)
|
|
173
|
+
{
|
|
174
|
+
builder.Append('.');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
builder.Append(frame.Key);
|
|
178
|
+
if (frame.Index >= 0)
|
|
179
|
+
{
|
|
180
|
+
builder.Append('[').Append(frame.Index).Append(']');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
Path = builder.Length > 0 ? builder.ToString() : "?";
|
|
185
|
+
LeafKey = _frames.Count > 0 ? _frames[_frames.Count - 1].Key : "?";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private void PopWhile(Func<Frame, bool> predicate)
|
|
189
|
+
{
|
|
190
|
+
while (_frames.Count > 0 && predicate(_frames[_frames.Count - 1]))
|
|
191
|
+
{
|
|
192
|
+
_frames.RemoveAt(_frames.Count - 1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// <summary>Unity 組み込みリソースや null を表す GUID。AssetDatabase 上に存在しないので除外する。</summary>
|
|
198
|
+
private static readonly HashSet<string> BuiltinGuids = new HashSet<string>
|
|
199
|
+
{
|
|
200
|
+
"0000000000000000f000000000000000", // Resources/unity_builtin_extra
|
|
201
|
+
"0000000000000000e000000000000000", // Library/unity default resources
|
|
202
|
+
"00000000000000000000000000000000", // null (m_SceneGUID など)
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/// <summary>
|
|
206
|
+
/// Prefab インスタンス由来の内部参照。元 Prefab が消えている場合は PrefabInstance 側で 1 件にまとめて報告するので除外する。
|
|
207
|
+
/// </summary>
|
|
208
|
+
private static readonly HashSet<string> IgnoredKeys = new HashSet<string>
|
|
209
|
+
{
|
|
210
|
+
"m_CorrespondingSourceObject",
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/// <summary>MonoBehaviour が標準で持つキー。Missing Script の手掛かりとしてフィールド名を列挙する際に除外する。</summary>
|
|
214
|
+
private static readonly HashSet<string> StandardMonoBehaviourKeys = new HashSet<string>
|
|
215
|
+
{
|
|
216
|
+
"m_ObjectHideFlags",
|
|
217
|
+
"m_CorrespondingSourceObject",
|
|
218
|
+
"m_PrefabInstance",
|
|
219
|
+
"m_PrefabAsset",
|
|
220
|
+
"m_GameObject",
|
|
221
|
+
"m_Enabled",
|
|
222
|
+
"m_EditorHideFlags",
|
|
223
|
+
"m_Script",
|
|
224
|
+
"m_Name",
|
|
225
|
+
"m_EditorClassIdentifier",
|
|
226
|
+
"serializedVersion",
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/// <summary>組み込みコンポーネントの ClassID → 型名。実オブジェクトを解決できない場合の表示用。</summary>
|
|
230
|
+
private static readonly Dictionary<int, string> BuiltinClassNames = new Dictionary<int, string>
|
|
231
|
+
{
|
|
232
|
+
{ 1, "GameObject" }, { 4, "Transform" }, { 20, "Camera" }, { 21, "Material" }, { 23, "MeshRenderer" },
|
|
233
|
+
{ 33, "MeshFilter" }, { 54, "Rigidbody" }, { 64, "MeshCollider" }, { 65, "BoxCollider" },
|
|
234
|
+
{ 81, "AudioListener" }, { 82, "AudioSource" }, { 95, "Animator" }, { 96, "TrailRenderer" },
|
|
235
|
+
{ 108, "Light" }, { 111, "Animation" }, { 114, "MonoBehaviour" }, { 120, "LineRenderer" },
|
|
236
|
+
{ 135, "SphereCollider" }, { 136, "CapsuleCollider" }, { 137, "SkinnedMeshRenderer" },
|
|
237
|
+
{ 198, "ParticleSystem" }, { 199, "ParticleSystemRenderer" }, { 212, "SpriteRenderer" },
|
|
238
|
+
{ 215, "ReflectionProbe" }, { 218, "Terrain" }, { 220, "LightProbeGroup" }, { 222, "CanvasRenderer" },
|
|
239
|
+
{ 223, "Canvas" }, { 224, "RectTransform" }, { 225, "CanvasGroup" }, { 328, "VideoPlayer" },
|
|
240
|
+
{ 1001, "PrefabInstance" },
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
private const int GameObjectClassId = 1;
|
|
244
|
+
private const int TransformClassId = 4;
|
|
245
|
+
private const int MonoBehaviourClassId = 114;
|
|
246
|
+
private const int RectTransformClassId = 224;
|
|
247
|
+
private const int PrefabInstanceClassId = 1001;
|
|
248
|
+
|
|
249
|
+
private const string SourcePrefabKey = "m_SourcePrefab";
|
|
250
|
+
private const string ScriptKey = "m_Script";
|
|
251
|
+
|
|
252
|
+
public static bool Enabled
|
|
253
|
+
{
|
|
254
|
+
get => EditorPrefs.GetBool(EditorPrefsKey, true);
|
|
255
|
+
set => EditorPrefs.SetBool(EditorPrefsKey, value);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
public int callbackOrder => -100;
|
|
259
|
+
|
|
260
|
+
[MenuItem(ToggleMenuPath)]
|
|
261
|
+
private static void ToggleEnabled()
|
|
262
|
+
{
|
|
263
|
+
Enabled = !Enabled;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
[MenuItem(ToggleMenuPath, true)]
|
|
267
|
+
private static bool ToggleEnabledValidate()
|
|
268
|
+
{
|
|
269
|
+
Menu.SetChecked(ToggleMenuPath, Enabled);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// <summary>ビルドせずに同じチェックを手動実行する。</summary>
|
|
274
|
+
[MenuItem(RunMenuPath)]
|
|
275
|
+
private static void RunManually()
|
|
276
|
+
{
|
|
277
|
+
var missingCount = CheckBuildScenes(out _);
|
|
278
|
+
if (missingCount == 0)
|
|
279
|
+
{
|
|
280
|
+
Debug.Log($"{LogPrefix} Missing 参照はありませんでした。");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
public void OnPreprocessBuild(BuildReport report)
|
|
285
|
+
{
|
|
286
|
+
if (!Enabled)
|
|
287
|
+
{
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
var missingCount = CheckBuildScenes(out var breakdown);
|
|
292
|
+
if (missingCount > 0)
|
|
293
|
+
{
|
|
294
|
+
throw new BuildFailedException(
|
|
295
|
+
$"{LogPrefix} ビルド対象シーン (および依存 Prefab) に Missing 参照が {missingCount} 件あるためビルドを中止しました。\n" +
|
|
296
|
+
$"内訳: {breakdown}\n" +
|
|
297
|
+
"Console で \"[Missing\" を検索し、各エラーログを選択すると「GameObject / コンポーネント / フィールド / 対処」の詳細が表示されます。" +
|
|
298
|
+
"ログをクリックすると該当オブジェクトが選択されます (シーンを開いている場合)。\n" +
|
|
299
|
+
$"修正後は {RunMenuPath} でビルドせずに再確認できます。");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/// <summary>Build Settings の有効なシーンと依存 Prefab を全て検査し、Missing の総数を返す。</summary>
|
|
304
|
+
private static int CheckBuildScenes(out string breakdown)
|
|
305
|
+
{
|
|
306
|
+
var total = 0;
|
|
307
|
+
var scenePaths = new List<string>();
|
|
308
|
+
var prefabPaths = new List<string>();
|
|
309
|
+
var prefabUsedBy = new Dictionary<string, List<string>>();
|
|
310
|
+
var fileCounts = new List<(string Path, int Count)>();
|
|
311
|
+
|
|
312
|
+
foreach (var buildScene in EditorBuildSettings.scenes)
|
|
313
|
+
{
|
|
314
|
+
if (!buildScene.enabled)
|
|
315
|
+
{
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (string.IsNullOrEmpty(buildScene.path) || !File.Exists(buildScene.path))
|
|
320
|
+
{
|
|
321
|
+
Debug.LogWarning($"{LogPrefix} Build Settings に登録されたシーンが見つかりません: {buildScene.path}");
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
scenePaths.Add(buildScene.path);
|
|
326
|
+
|
|
327
|
+
foreach (var dependency in AssetDatabase.GetDependencies(buildScene.path, true))
|
|
328
|
+
{
|
|
329
|
+
if (!dependency.EndsWith(".prefab") || !File.Exists(dependency))
|
|
330
|
+
{
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!prefabUsedBy.TryGetValue(dependency, out var usedBy))
|
|
335
|
+
{
|
|
336
|
+
usedBy = new List<string>();
|
|
337
|
+
prefabUsedBy.Add(dependency, usedBy);
|
|
338
|
+
prefabPaths.Add(dependency);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
usedBy.Add(Path.GetFileNameWithoutExtension(buildScene.path));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
PrefabAssetCache.Clear();
|
|
346
|
+
try
|
|
347
|
+
{
|
|
348
|
+
var index = 0;
|
|
349
|
+
var totalFiles = scenePaths.Count + prefabPaths.Count;
|
|
350
|
+
|
|
351
|
+
foreach (var scenePath in scenePaths)
|
|
352
|
+
{
|
|
353
|
+
EditorUtility.DisplayProgressBar("Missing Reference Build Check", scenePath, (float)index++ / totalFiles);
|
|
354
|
+
var count = CheckYamlFile(scenePath, true, null);
|
|
355
|
+
if (count > 0)
|
|
356
|
+
{
|
|
357
|
+
fileCounts.Add((scenePath, count));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
total += count;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
foreach (var prefabPath in prefabPaths)
|
|
364
|
+
{
|
|
365
|
+
EditorUtility.DisplayProgressBar("Missing Reference Build Check", prefabPath, (float)index++ / totalFiles);
|
|
366
|
+
var count = CheckYamlFile(prefabPath, false, prefabUsedBy[prefabPath]);
|
|
367
|
+
if (count > 0)
|
|
368
|
+
{
|
|
369
|
+
fileCounts.Add((prefabPath, count));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
total += count;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
finally
|
|
376
|
+
{
|
|
377
|
+
EditorUtility.ClearProgressBar();
|
|
378
|
+
PrefabAssetCache.Clear();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
var breakdownBuilder = new StringBuilder();
|
|
382
|
+
foreach (var (path, count) in fileCounts)
|
|
383
|
+
{
|
|
384
|
+
if (breakdownBuilder.Length > 0)
|
|
385
|
+
{
|
|
386
|
+
breakdownBuilder.Append(", ");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
breakdownBuilder.Append(Path.GetFileName(path)).Append(": ").Append(count).Append(" 件");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
breakdown = breakdownBuilder.Length > 0 ? breakdownBuilder.ToString() : "なし";
|
|
393
|
+
|
|
394
|
+
var summary = $"{LogPrefix} 検査完了: {scenePaths.Count} シーン / {prefabPaths.Count} Prefab / Missing {total} 件";
|
|
395
|
+
if (total > 0)
|
|
396
|
+
{
|
|
397
|
+
summary += $"\n内訳: {breakdown}";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
Debug.Log(summary);
|
|
401
|
+
return total;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ------------------------------------------------------------------
|
|
405
|
+
// YAML ドキュメント
|
|
406
|
+
// ------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
private sealed class MissingEntry
|
|
409
|
+
{
|
|
410
|
+
/// <summary>フィールド名 (配列の場合は "name[index]")。PrefabInstance のオーバーライドの場合は propertyPath。</summary>
|
|
411
|
+
public string Key;
|
|
412
|
+
|
|
413
|
+
public string Guid;
|
|
414
|
+
|
|
415
|
+
/// <summary>PrefabInstance のオーバーライドが指す元 Prefab 内のコンポーネント fileID。それ以外は 0。</summary>
|
|
416
|
+
public long TargetFileId;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private sealed class DocumentInfo
|
|
420
|
+
{
|
|
421
|
+
public int ClassId;
|
|
422
|
+
public long FileId;
|
|
423
|
+
public string Name;
|
|
424
|
+
public long GameObjectFileId;
|
|
425
|
+
public bool IsStripped;
|
|
426
|
+
public long CorrespondingSourceFileId;
|
|
427
|
+
public long PrefabInstanceFileId;
|
|
428
|
+
|
|
429
|
+
/// <summary>Transform / RectTransform の親 Transform fileID。ルートは 0。</summary>
|
|
430
|
+
public long FatherFileId;
|
|
431
|
+
|
|
432
|
+
/// <summary>PrefabInstance の親 Transform fileID。ルートは 0。</summary>
|
|
433
|
+
public long TransformParentFileId;
|
|
434
|
+
|
|
435
|
+
/// <summary>PrefabInstance の元 Prefab GUID。</summary>
|
|
436
|
+
public string SourcePrefabGuid;
|
|
437
|
+
|
|
438
|
+
/// <summary>MonoBehaviour のスクリプト GUID。</summary>
|
|
439
|
+
public string ScriptGuid;
|
|
440
|
+
|
|
441
|
+
/// <summary>GameObject が持つコンポーネント fileID (Inspector 表示順)。</summary>
|
|
442
|
+
public readonly List<long> ComponentFileIds = new List<long>();
|
|
443
|
+
|
|
444
|
+
/// <summary>MonoBehaviour に保存されているユーザー定義フィールド名。</summary>
|
|
445
|
+
public readonly List<string> FieldNames = new List<string>();
|
|
446
|
+
|
|
447
|
+
/// <summary>PrefabInstance の m_Modifications による m_Name 上書き (元 Prefab 内 fileID → 名前)。</summary>
|
|
448
|
+
public readonly Dictionary<long, string> NameOverrides = new Dictionary<long, string>();
|
|
449
|
+
|
|
450
|
+
public readonly List<MissingEntry> Missing = new List<MissingEntry>();
|
|
451
|
+
|
|
452
|
+
public bool IsTransform => ClassId == TransformClassId || ClassId == RectTransformClassId;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/// <summary>シーンまたは Prefab ファイルを 1 つ検査し、Missing の数を返す。</summary>
|
|
456
|
+
private static int CheckYamlFile(string assetPath, bool isScene, List<string> usedByScenes)
|
|
457
|
+
{
|
|
458
|
+
var context = new FileContext(assetPath, isScene, ParseYamlFile(assetPath), usedByScenes);
|
|
459
|
+
|
|
460
|
+
var count = 0;
|
|
461
|
+
foreach (var doc in context.Documents)
|
|
462
|
+
{
|
|
463
|
+
foreach (var entry in doc.Missing)
|
|
464
|
+
{
|
|
465
|
+
count++;
|
|
466
|
+
Debug.LogError(context.BuildReport(doc, entry), context.GetContextObject(doc, entry));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return count;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private static List<DocumentInfo> ParseYamlFile(string assetPath)
|
|
474
|
+
{
|
|
475
|
+
var documents = new List<DocumentInfo>();
|
|
476
|
+
DocumentInfo current = null;
|
|
477
|
+
var pathTracker = new YamlPathTracker();
|
|
478
|
+
long modificationTarget = 0;
|
|
479
|
+
string modificationPropertyPath = null;
|
|
480
|
+
|
|
481
|
+
foreach (var line in File.ReadLines(assetPath))
|
|
482
|
+
{
|
|
483
|
+
var header = DocumentHeaderRegex.Match(line);
|
|
484
|
+
if (header.Success)
|
|
485
|
+
{
|
|
486
|
+
FinalizeDocument(current);
|
|
487
|
+
current = new DocumentInfo
|
|
488
|
+
{
|
|
489
|
+
ClassId = int.Parse(header.Groups[1].Value),
|
|
490
|
+
FileId = long.Parse(header.Groups[2].Value),
|
|
491
|
+
IsStripped = line.TrimEnd().EndsWith("stripped"),
|
|
492
|
+
};
|
|
493
|
+
documents.Add(current);
|
|
494
|
+
pathTracker.Reset();
|
|
495
|
+
modificationTarget = 0;
|
|
496
|
+
modificationPropertyPath = null;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (current == null)
|
|
501
|
+
{
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
switch (current.ClassId)
|
|
506
|
+
{
|
|
507
|
+
case GameObjectClassId:
|
|
508
|
+
{
|
|
509
|
+
var name = NameRegex.Match(line);
|
|
510
|
+
if (name.Success)
|
|
511
|
+
{
|
|
512
|
+
current.Name = name.Groups[1].Value.Trim();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
var component = ComponentListItemRegex.Match(line);
|
|
516
|
+
if (component.Success)
|
|
517
|
+
{
|
|
518
|
+
current.ComponentFileIds.Add(long.Parse(component.Groups[1].Value));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
case MonoBehaviourClassId:
|
|
524
|
+
{
|
|
525
|
+
var script = ScriptRegex.Match(line);
|
|
526
|
+
if (script.Success)
|
|
527
|
+
{
|
|
528
|
+
current.ScriptGuid = script.Groups[1].Value.ToLowerInvariant();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
var topLevelKey = TopLevelKeyRegex.Match(line);
|
|
532
|
+
if (topLevelKey.Success && !StandardMonoBehaviourKeys.Contains(topLevelKey.Groups[1].Value))
|
|
533
|
+
{
|
|
534
|
+
current.FieldNames.Add(topLevelKey.Groups[1].Value);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
case PrefabInstanceClassId:
|
|
540
|
+
{
|
|
541
|
+
var target = ModificationTargetRegex.Match(line);
|
|
542
|
+
if (target.Success)
|
|
543
|
+
{
|
|
544
|
+
modificationTarget = long.Parse(target.Groups[1].Value);
|
|
545
|
+
modificationPropertyPath = null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
var propertyPath = ModificationPropertyPathRegex.Match(line);
|
|
549
|
+
if (propertyPath.Success)
|
|
550
|
+
{
|
|
551
|
+
modificationPropertyPath = propertyPath.Groups[1].Value.Trim();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (modificationPropertyPath == "m_Name")
|
|
555
|
+
{
|
|
556
|
+
var value = ModificationValueRegex.Match(line);
|
|
557
|
+
if (value.Success)
|
|
558
|
+
{
|
|
559
|
+
current.NameOverrides[modificationTarget] = value.Groups[1].Value.Trim();
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
var transformParent = TransformParentRegex.Match(line);
|
|
564
|
+
if (transformParent.Success)
|
|
565
|
+
{
|
|
566
|
+
current.TransformParentFileId = long.Parse(transformParent.Groups[1].Value);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
var sourcePrefab = SourcePrefabRegex.Match(line);
|
|
570
|
+
if (sourcePrefab.Success)
|
|
571
|
+
{
|
|
572
|
+
current.SourcePrefabGuid = sourcePrefab.Groups[1].Value.ToLowerInvariant();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (current.IsTransform)
|
|
580
|
+
{
|
|
581
|
+
var father = FatherRegex.Match(line);
|
|
582
|
+
if (father.Success)
|
|
583
|
+
{
|
|
584
|
+
current.FatherFileId = long.Parse(father.Groups[1].Value);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// 所属 GameObject / Prefab インスタンス情報
|
|
589
|
+
var goRef = GameObjectRefRegex.Match(line);
|
|
590
|
+
if (goRef.Success)
|
|
591
|
+
{
|
|
592
|
+
current.GameObjectFileId = long.Parse(goRef.Groups[1].Value);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
var sourceRef = CorrespondingSourceRegex.Match(line);
|
|
596
|
+
if (sourceRef.Success)
|
|
597
|
+
{
|
|
598
|
+
current.CorrespondingSourceFileId = long.Parse(sourceRef.Groups[1].Value);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
var prefabRef = PrefabInstanceRefRegex.Match(line);
|
|
602
|
+
if (prefabRef.Success)
|
|
603
|
+
{
|
|
604
|
+
current.PrefabInstanceFileId = long.Parse(prefabRef.Groups[1].Value);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// キー追跡 (インデントから "_entries[1]._material" のような完全パスを組み立てる)
|
|
608
|
+
pathTracker.Feed(line);
|
|
609
|
+
var key = pathTracker.LeafKey;
|
|
610
|
+
|
|
611
|
+
if (IgnoredKeys.Contains(key))
|
|
612
|
+
{
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// GUID 参照
|
|
617
|
+
var guidMatch = GuidRegex.Match(line);
|
|
618
|
+
if (!guidMatch.Success)
|
|
619
|
+
{
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
var guid = guidMatch.Groups[1].Value.ToLowerInvariant();
|
|
624
|
+
if (BuiltinGuids.Contains(guid))
|
|
625
|
+
{
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (!string.IsNullOrEmpty(AssetDatabase.GUIDToAssetPath(guid)))
|
|
630
|
+
{
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
var entry = new MissingEntry { Guid = guid };
|
|
635
|
+
if (current.ClassId == PrefabInstanceClassId)
|
|
636
|
+
{
|
|
637
|
+
if (key == "objectReference" && !string.IsNullOrEmpty(modificationPropertyPath))
|
|
638
|
+
{
|
|
639
|
+
// 元 Prefab は存在するが、オーバーライド値が消えたアセットを指している
|
|
640
|
+
entry.Key = modificationPropertyPath;
|
|
641
|
+
entry.TargetFileId = modificationTarget;
|
|
642
|
+
}
|
|
643
|
+
else
|
|
644
|
+
{
|
|
645
|
+
// m_Modifications の各 target と m_SourcePrefab は全て元 Prefab の GUID を指すので 1 件にまとめる
|
|
646
|
+
entry.Key = SourcePrefabKey;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
else
|
|
650
|
+
{
|
|
651
|
+
entry.Key = pathTracker.Path;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// 1 行 = 1 参照なので原則そのまま全て報告する。
|
|
655
|
+
// 例外として元 Prefab 欠落 (m_SourcePrefab) は同じ GUID が何十行も並ぶため 1 件にまとめる
|
|
656
|
+
if (entry.Key == SourcePrefabKey)
|
|
657
|
+
{
|
|
658
|
+
var duplicate = false;
|
|
659
|
+
foreach (var existing in current.Missing)
|
|
660
|
+
{
|
|
661
|
+
if (existing.Key == SourcePrefabKey && existing.Guid == entry.Guid)
|
|
662
|
+
{
|
|
663
|
+
duplicate = true;
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (duplicate)
|
|
669
|
+
{
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
current.Missing.Add(entry);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
FinalizeDocument(current);
|
|
678
|
+
return documents;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/// <summary>元 Prefab が消えている PrefabInstance では、同じ GUID を指すオーバーライド報告を m_SourcePrefab の 1 件に畳む。</summary>
|
|
682
|
+
private static void FinalizeDocument(DocumentInfo doc)
|
|
683
|
+
{
|
|
684
|
+
if (doc == null || doc.ClassId != PrefabInstanceClassId)
|
|
685
|
+
{
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
string missingSourceGuid = null;
|
|
690
|
+
foreach (var entry in doc.Missing)
|
|
691
|
+
{
|
|
692
|
+
if (entry.Key == SourcePrefabKey)
|
|
693
|
+
{
|
|
694
|
+
missingSourceGuid = entry.Guid;
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (missingSourceGuid == null)
|
|
700
|
+
{
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
doc.Missing.RemoveAll(entry => entry.Key != SourcePrefabKey && entry.Guid == missingSourceGuid);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ------------------------------------------------------------------
|
|
708
|
+
// 元 Prefab の読み込みキャッシュ (Prefab インスタンス内オブジェクトの名前・階層解決用)
|
|
709
|
+
// ------------------------------------------------------------------
|
|
710
|
+
|
|
711
|
+
private static class PrefabAssetCache
|
|
712
|
+
{
|
|
713
|
+
private sealed class Info
|
|
714
|
+
{
|
|
715
|
+
public string AssetPath;
|
|
716
|
+
public GameObject Root;
|
|
717
|
+
public readonly Dictionary<long, Object> ObjectsById = new Dictionary<long, Object>();
|
|
718
|
+
public readonly Dictionary<long, GameObject> GameObjectsById = new Dictionary<long, GameObject>();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
private static readonly Dictionary<string, Info> Cache = new Dictionary<string, Info>();
|
|
722
|
+
|
|
723
|
+
public static void Clear()
|
|
724
|
+
{
|
|
725
|
+
Cache.Clear();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
public static string GetPath(string guid)
|
|
729
|
+
{
|
|
730
|
+
return Get(guid)?.AssetPath;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/// <summary>Prefab 内の fileID からオブジェクトを取得する。</summary>
|
|
734
|
+
public static Object GetObject(string guid, long localId)
|
|
735
|
+
{
|
|
736
|
+
var info = Get(guid);
|
|
737
|
+
if (info == null)
|
|
738
|
+
{
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return info.ObjectsById.TryGetValue(localId, out var obj) ? obj : null;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/// <summary>Prefab 内の fileID (GameObject または Transform) から GameObject を取得する。0 はルート。</summary>
|
|
746
|
+
public static GameObject GetGameObject(string guid, long localId)
|
|
747
|
+
{
|
|
748
|
+
var info = Get(guid);
|
|
749
|
+
if (info == null)
|
|
750
|
+
{
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (localId == 0)
|
|
755
|
+
{
|
|
756
|
+
return info.Root;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return info.GameObjectsById.TryGetValue(localId, out var go) ? go : null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/// <summary>Prefab ルートから <paramref name="gameObject"/> までの (fileID, 名前) をルート側から順に返す。</summary>
|
|
763
|
+
public static List<(long Id, string Name)> GetPathSegments(GameObject gameObject)
|
|
764
|
+
{
|
|
765
|
+
if (gameObject == null)
|
|
766
|
+
{
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
var segments = new List<(long, string)>();
|
|
771
|
+
var current = gameObject.transform;
|
|
772
|
+
var depth = 0;
|
|
773
|
+
while (current != null && depth++ < MaxPathDepth)
|
|
774
|
+
{
|
|
775
|
+
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(current.gameObject, out _, out long localId);
|
|
776
|
+
segments.Insert(0, (localId, current.gameObject.name));
|
|
777
|
+
current = current.parent;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return segments;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
private static Info Get(string guid)
|
|
784
|
+
{
|
|
785
|
+
if (string.IsNullOrEmpty(guid))
|
|
786
|
+
{
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (Cache.TryGetValue(guid, out var cached))
|
|
791
|
+
{
|
|
792
|
+
return cached;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// 元 Prefab は .prefab のほか、モデル (.fbx) の場合もある
|
|
796
|
+
Info info = null;
|
|
797
|
+
var path = AssetDatabase.GUIDToAssetPath(guid);
|
|
798
|
+
if (!string.IsNullOrEmpty(path))
|
|
799
|
+
{
|
|
800
|
+
info = new Info
|
|
801
|
+
{
|
|
802
|
+
AssetPath = path,
|
|
803
|
+
Root = AssetDatabase.LoadAssetAtPath<GameObject>(path),
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
foreach (var obj in AssetDatabase.LoadAllAssetsAtPath(path))
|
|
807
|
+
{
|
|
808
|
+
// スクリプトが Missing のコンポーネントは null で返る
|
|
809
|
+
if (obj == null)
|
|
810
|
+
{
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out _, out long localId))
|
|
815
|
+
{
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
info.ObjectsById[localId] = obj;
|
|
820
|
+
switch (obj)
|
|
821
|
+
{
|
|
822
|
+
case GameObject go:
|
|
823
|
+
info.GameObjectsById[localId] = go;
|
|
824
|
+
break;
|
|
825
|
+
case Transform transform:
|
|
826
|
+
info.GameObjectsById[localId] = transform.gameObject;
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
Cache[guid] = info;
|
|
833
|
+
return info;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// ------------------------------------------------------------------
|
|
838
|
+
// 1 ファイル分の解決・レポート生成
|
|
839
|
+
// ------------------------------------------------------------------
|
|
840
|
+
|
|
841
|
+
private sealed class FileContext
|
|
842
|
+
{
|
|
843
|
+
public readonly List<DocumentInfo> Documents;
|
|
844
|
+
|
|
845
|
+
private readonly string _assetPath;
|
|
846
|
+
private readonly bool _isScene;
|
|
847
|
+
private readonly string _fileGuid;
|
|
848
|
+
private readonly string _fileLabel;
|
|
849
|
+
private readonly Object _mainAsset;
|
|
850
|
+
private readonly bool _isSceneLoaded;
|
|
851
|
+
private readonly List<string> _usedByScenes;
|
|
852
|
+
private readonly Dictionary<long, DocumentInfo> _documentsById = new Dictionary<long, DocumentInfo>();
|
|
853
|
+
private readonly Dictionary<long, DocumentInfo> _transformByGameObject = new Dictionary<long, DocumentInfo>();
|
|
854
|
+
|
|
855
|
+
public FileContext(string assetPath, bool isScene, List<DocumentInfo> documents, List<string> usedByScenes)
|
|
856
|
+
{
|
|
857
|
+
Documents = documents;
|
|
858
|
+
_assetPath = assetPath;
|
|
859
|
+
_isScene = isScene;
|
|
860
|
+
_fileGuid = AssetDatabase.AssetPathToGUID(assetPath);
|
|
861
|
+
_fileLabel = Path.GetFileName(assetPath);
|
|
862
|
+
_mainAsset = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
|
863
|
+
_isSceneLoaded = isScene && SceneManager.GetSceneByPath(assetPath).isLoaded;
|
|
864
|
+
_usedByScenes = usedByScenes;
|
|
865
|
+
|
|
866
|
+
foreach (var doc in documents)
|
|
867
|
+
{
|
|
868
|
+
_documentsById[doc.FileId] = doc;
|
|
869
|
+
if (doc.IsTransform && doc.GameObjectFileId != 0)
|
|
870
|
+
{
|
|
871
|
+
_transformByGameObject[doc.GameObjectFileId] = doc;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// ---- レポート ----
|
|
877
|
+
|
|
878
|
+
public string BuildReport(DocumentInfo doc, MissingEntry entry)
|
|
879
|
+
{
|
|
880
|
+
if (doc.ClassId == PrefabInstanceClassId)
|
|
881
|
+
{
|
|
882
|
+
return entry.Key == SourcePrefabKey
|
|
883
|
+
? ReportMissingSourcePrefab(doc, entry)
|
|
884
|
+
: ReportMissingOverride(doc, entry);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
DocumentInfo owner;
|
|
888
|
+
if (doc.ClassId == GameObjectClassId)
|
|
889
|
+
{
|
|
890
|
+
owner = doc;
|
|
891
|
+
}
|
|
892
|
+
else if (!_documentsById.TryGetValue(doc.GameObjectFileId, out owner))
|
|
893
|
+
{
|
|
894
|
+
owner = null;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
var ownerPath = owner != null ? GetGameObjectPath(owner, 0) : $"(所属 GameObject 不明) &{doc.FileId}";
|
|
898
|
+
var ordinal = GetComponentOrdinal(doc, owner);
|
|
899
|
+
|
|
900
|
+
if (doc.ClassId == MonoBehaviourClassId && entry.Key == ScriptKey)
|
|
901
|
+
{
|
|
902
|
+
return ReportMissingScript(doc, ownerPath, ordinal, entry);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return ReportMissingField(doc, ownerPath, ordinal, entry);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private string ReportMissingField(DocumentInfo doc, string ownerPath, string ordinal, MissingEntry entry)
|
|
909
|
+
{
|
|
910
|
+
var typeName = GetComponentTypeName(doc);
|
|
911
|
+
var componentLabel = ordinal == null ? typeName : $"{typeName} ({ordinal})";
|
|
912
|
+
var fieldTypeName = doc.ClassId == MonoBehaviourClassId ? GetFieldTypeName(doc.ScriptGuid, entry.Key) : null;
|
|
913
|
+
var displayName = NicifyPropertyPath(entry.Key);
|
|
914
|
+
|
|
915
|
+
string fix;
|
|
916
|
+
if (doc.IsStripped)
|
|
917
|
+
{
|
|
918
|
+
fix = $"この参照は Prefab インスタンス内のものです。元の Prefab ({GetSourcePrefabPath(doc)}) を開いて修正してください。";
|
|
919
|
+
}
|
|
920
|
+
else
|
|
921
|
+
{
|
|
922
|
+
fix = $"{OpenInstruction} 上記 GameObject を選択し、Inspector の {typeName} にある「{displayName}」に" +
|
|
923
|
+
$"正しい{fieldTypeName ?? "アセット"}を設定し直すか、None に戻してください。";
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return Compose(
|
|
927
|
+
$"[Missing] {_fileLabel} | {ownerPath} | {componentLabel} | {entry.Key}",
|
|
928
|
+
"ファイル: " + _assetPath,
|
|
929
|
+
DescribeUsedBy(),
|
|
930
|
+
"GameObject: " + ownerPath,
|
|
931
|
+
"コンポーネント: " + componentLabel,
|
|
932
|
+
"フィールド: " + DescribeField(entry.Key, displayName, fieldTypeName),
|
|
933
|
+
DescribeGuid(entry.Guid),
|
|
934
|
+
"対処: " + fix);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
private string ReportMissingScript(DocumentInfo doc, string ownerPath, string ordinal, MissingEntry entry)
|
|
938
|
+
{
|
|
939
|
+
var position = ordinal ?? "位置不明";
|
|
940
|
+
var fieldHint = doc.FieldNames.Count > 0
|
|
941
|
+
? "手掛かり: このコンポーネントに保存されているフィールド名 = " + string.Join(", ", doc.FieldNames) +
|
|
942
|
+
" (どのスクリプトだったか推測できます)"
|
|
943
|
+
: "手掛かり: 保存されているフィールドはありません";
|
|
944
|
+
|
|
945
|
+
string fix;
|
|
946
|
+
if (doc.IsStripped)
|
|
947
|
+
{
|
|
948
|
+
fix = $"このコンポーネントは Prefab インスタンス内のものです。元の Prefab ({GetSourcePrefabPath(doc)}) を開いて修正してください。";
|
|
949
|
+
}
|
|
950
|
+
else
|
|
951
|
+
{
|
|
952
|
+
fix = $"{OpenInstruction} 上記 GameObject を選択し、Inspector で「Missing (Mono Script)」と表示されているコンポーネント ({position}) の" +
|
|
953
|
+
" Script 欄に正しいスクリプトを設定するか、右クリック → Remove Component で削除してください。";
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return Compose(
|
|
957
|
+
$"[Missing Script] {_fileLabel} | {ownerPath} | {position} のコンポーネントのスクリプトが見つかりません",
|
|
958
|
+
"ファイル: " + _assetPath,
|
|
959
|
+
DescribeUsedBy(),
|
|
960
|
+
"GameObject: " + ownerPath,
|
|
961
|
+
$"コンポーネント: Inspector で「Missing (Mono Script)」と表示されるもの ({position})",
|
|
962
|
+
$"元スクリプト: GUID {entry.Guid} のスクリプトはプロジェクト内に存在しません (削除・移動・meta 再生成の可能性)",
|
|
963
|
+
fieldHint,
|
|
964
|
+
"対処: " + fix);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
private string ReportMissingSourcePrefab(DocumentInfo instance, MissingEntry entry)
|
|
968
|
+
{
|
|
969
|
+
var ownerPath = GetPrefabInstanceObjectPath(instance, 0, 0);
|
|
970
|
+
return Compose(
|
|
971
|
+
$"[Missing Prefab] {_fileLabel} | {ownerPath} | 元の Prefab アセットが存在しません",
|
|
972
|
+
"ファイル: " + _assetPath,
|
|
973
|
+
DescribeUsedBy(),
|
|
974
|
+
$"GameObject: {ownerPath} (Hierarchy で「Missing Prefab」と表示されます)",
|
|
975
|
+
$"参照先: GUID {entry.Guid} の Prefab はプロジェクト内に存在しません (削除・移動・meta 再生成の可能性)",
|
|
976
|
+
$"対処: {OpenInstruction} 上記オブジェクトを削除するか、元の Prefab アセットを復元してください。" +
|
|
977
|
+
" 別の Prefab で置き換える場合は、削除してから新しい Prefab を配置し直してください。");
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
private string ReportMissingOverride(DocumentInfo instance, MissingEntry entry)
|
|
981
|
+
{
|
|
982
|
+
var targetComponent = PrefabAssetCache.GetObject(instance.SourcePrefabGuid, entry.TargetFileId) as Component;
|
|
983
|
+
var targetGameObject = targetComponent != null
|
|
984
|
+
? targetComponent.gameObject
|
|
985
|
+
: PrefabAssetCache.GetGameObject(instance.SourcePrefabGuid, entry.TargetFileId);
|
|
986
|
+
|
|
987
|
+
string ownerPath;
|
|
988
|
+
if (targetGameObject != null)
|
|
989
|
+
{
|
|
990
|
+
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(targetGameObject, out _, out long gameObjectLocalId);
|
|
991
|
+
ownerPath = GetPrefabInstanceObjectPath(instance, gameObjectLocalId, 0);
|
|
992
|
+
}
|
|
993
|
+
else
|
|
994
|
+
{
|
|
995
|
+
ownerPath = GetPrefabInstanceObjectPath(instance, 0, 0) + $"/(&{entry.TargetFileId})";
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
string typeName;
|
|
999
|
+
string componentLabel;
|
|
1000
|
+
string fieldTypeName = null;
|
|
1001
|
+
if (targetComponent != null)
|
|
1002
|
+
{
|
|
1003
|
+
typeName = targetComponent.GetType().Name;
|
|
1004
|
+
var ordinal = GetComponentOrdinal(targetComponent);
|
|
1005
|
+
componentLabel = ordinal == null ? typeName : $"{typeName} ({ordinal})";
|
|
1006
|
+
fieldTypeName = GetSerializedPropertyTypeName(targetComponent, entry.Key);
|
|
1007
|
+
}
|
|
1008
|
+
else
|
|
1009
|
+
{
|
|
1010
|
+
typeName = $"コンポーネント &{entry.TargetFileId}";
|
|
1011
|
+
componentLabel = typeName;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
var displayName = NicifyPropertyPath(entry.Key);
|
|
1015
|
+
var sourcePrefabPath = PrefabAssetCache.GetPath(instance.SourcePrefabGuid) ?? instance.SourcePrefabGuid;
|
|
1016
|
+
|
|
1017
|
+
return Compose(
|
|
1018
|
+
$"[Missing] {_fileLabel} | {ownerPath} | {componentLabel} | {entry.Key} (Prefab インスタンスのオーバーライド値)",
|
|
1019
|
+
"ファイル: " + _assetPath,
|
|
1020
|
+
DescribeUsedBy(),
|
|
1021
|
+
$"GameObject: {ownerPath} (Prefab {sourcePrefabPath} のインスタンス)",
|
|
1022
|
+
"コンポーネント: " + componentLabel,
|
|
1023
|
+
"フィールド: " + DescribeField(entry.Key, displayName, fieldTypeName) + " ※インスタンス側で上書きされた値",
|
|
1024
|
+
DescribeGuid(entry.Guid),
|
|
1025
|
+
$"対処: {OpenInstruction} 上記 GameObject を選択し、Inspector の {typeName} にある「{displayName}」に" +
|
|
1026
|
+
$"正しい{fieldTypeName ?? "アセット"}を設定し直すか、フィールドを右クリック → Revert でオーバーライドを解除して Prefab の値に戻してください。");
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
private string OpenInstruction => _isScene
|
|
1030
|
+
? $"シーン {_fileLabel} を開いて"
|
|
1031
|
+
: $"Prefab {_fileLabel} を Project ウィンドウでダブルクリックして開き、";
|
|
1032
|
+
|
|
1033
|
+
private string DescribeUsedBy()
|
|
1034
|
+
{
|
|
1035
|
+
if (_usedByScenes == null || _usedByScenes.Count == 0)
|
|
1036
|
+
{
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
return "使用シーン: " + string.Join(", ", _usedByScenes);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
private static string DescribeGuid(string guid)
|
|
1044
|
+
{
|
|
1045
|
+
return $"参照先: GUID {guid} のアセットはプロジェクト内に存在しません (削除・移動・meta 再生成の可能性)";
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
private static string DescribeField(string key, string displayName, string fieldTypeName)
|
|
1049
|
+
{
|
|
1050
|
+
var text = key;
|
|
1051
|
+
if (!string.Equals(displayName, key, StringComparison.Ordinal))
|
|
1052
|
+
{
|
|
1053
|
+
text += $" (Inspector 表示名: {displayName})";
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (!string.IsNullOrEmpty(fieldTypeName))
|
|
1057
|
+
{
|
|
1058
|
+
text += $" / 型: {fieldTypeName}";
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return text;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
private static string Compose(string header, params string[] lines)
|
|
1065
|
+
{
|
|
1066
|
+
var builder = new StringBuilder(header);
|
|
1067
|
+
foreach (var line in lines)
|
|
1068
|
+
{
|
|
1069
|
+
if (string.IsNullOrEmpty(line))
|
|
1070
|
+
{
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
builder.Append('\n').Append(" ").Append(line);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
return builder.ToString();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// ---- コンポーネント情報 ----
|
|
1081
|
+
|
|
1082
|
+
private string GetComponentTypeName(DocumentInfo doc)
|
|
1083
|
+
{
|
|
1084
|
+
if (doc.ClassId == MonoBehaviourClassId)
|
|
1085
|
+
{
|
|
1086
|
+
var scriptPath = string.IsNullOrEmpty(doc.ScriptGuid) ? null : AssetDatabase.GUIDToAssetPath(doc.ScriptGuid);
|
|
1087
|
+
if (string.IsNullOrEmpty(scriptPath))
|
|
1088
|
+
{
|
|
1089
|
+
return "Missing Script";
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
var script = AssetDatabase.LoadAssetAtPath<MonoScript>(scriptPath);
|
|
1093
|
+
var scriptClass = script != null ? script.GetClass() : null;
|
|
1094
|
+
return scriptClass != null ? scriptClass.Name : Path.GetFileNameWithoutExtension(scriptPath);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (ResolveObject(doc) is Component component)
|
|
1098
|
+
{
|
|
1099
|
+
return component.GetType().Name;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
return BuiltinClassNames.TryGetValue(doc.ClassId, out var name) ? name : $"ClassID {doc.ClassId}";
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/// <summary>"上から N 番目 / 全 M 個" 形式。GameObject のコンポーネント一覧が取れない場合は null。</summary>
|
|
1106
|
+
private static string GetComponentOrdinal(DocumentInfo doc, DocumentInfo owner)
|
|
1107
|
+
{
|
|
1108
|
+
if (owner == null || owner == doc || owner.ComponentFileIds.Count == 0)
|
|
1109
|
+
{
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
var index = owner.ComponentFileIds.IndexOf(doc.FileId);
|
|
1114
|
+
return index < 0 ? null : $"上から {index + 1} 番目 / 全 {owner.ComponentFileIds.Count} 個";
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
private static string GetComponentOrdinal(Component component)
|
|
1118
|
+
{
|
|
1119
|
+
var components = component.gameObject.GetComponents<Component>();
|
|
1120
|
+
var index = Array.IndexOf(components, component);
|
|
1121
|
+
return index < 0 ? null : $"上から {index + 1} 番目 / 全 {components.Length} 個";
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
private static string GetFieldTypeName(string scriptGuid, string key)
|
|
1125
|
+
{
|
|
1126
|
+
if (string.IsNullOrEmpty(scriptGuid))
|
|
1127
|
+
{
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
var scriptPath = AssetDatabase.GUIDToAssetPath(scriptGuid);
|
|
1132
|
+
if (string.IsNullOrEmpty(scriptPath))
|
|
1133
|
+
{
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
var script = AssetDatabase.LoadAssetAtPath<MonoScript>(scriptPath);
|
|
1138
|
+
var scriptClass = script != null ? script.GetClass() : null;
|
|
1139
|
+
if (scriptClass == null)
|
|
1140
|
+
{
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// "_entries[1]._material" のようなパスをフィールド定義に沿って辿る
|
|
1145
|
+
Type currentType = scriptClass;
|
|
1146
|
+
foreach (var segment in ArrayDataRegex.Replace(key, "[").Split('.'))
|
|
1147
|
+
{
|
|
1148
|
+
var fieldName = ArrayIndexRegex.Replace(segment, string.Empty);
|
|
1149
|
+
var field = FindField(currentType, fieldName);
|
|
1150
|
+
if (field == null)
|
|
1151
|
+
{
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
currentType = GetElementType(field.FieldType);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
return currentType?.Name;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
private static FieldInfo FindField(Type type, string fieldName)
|
|
1162
|
+
{
|
|
1163
|
+
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
|
|
1164
|
+
for (; type != null && type != typeof(object); type = type.BaseType)
|
|
1165
|
+
{
|
|
1166
|
+
var field = type.GetField(fieldName, flags);
|
|
1167
|
+
if (field != null)
|
|
1168
|
+
{
|
|
1169
|
+
return field;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
private static Type GetElementType(Type type)
|
|
1177
|
+
{
|
|
1178
|
+
if (type.IsArray)
|
|
1179
|
+
{
|
|
1180
|
+
return type.GetElementType();
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
|
|
1184
|
+
{
|
|
1185
|
+
return type.GetGenericArguments()[0];
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
return type;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
private static string GetSerializedPropertyTypeName(Component component, string propertyPath)
|
|
1192
|
+
{
|
|
1193
|
+
try
|
|
1194
|
+
{
|
|
1195
|
+
using var serializedObject = new SerializedObject(component);
|
|
1196
|
+
var property = serializedObject.FindProperty(propertyPath);
|
|
1197
|
+
if (property == null || property.propertyType != SerializedPropertyType.ObjectReference)
|
|
1198
|
+
{
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
var typeName = property.type;
|
|
1203
|
+
// "PPtr<$Transform>" のような表記から型名だけを取り出す
|
|
1204
|
+
if (typeName.StartsWith("PPtr<$") && typeName.EndsWith(">"))
|
|
1205
|
+
{
|
|
1206
|
+
typeName = typeName.Substring(6, typeName.Length - 7);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
return typeName;
|
|
1210
|
+
}
|
|
1211
|
+
catch (Exception)
|
|
1212
|
+
{
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/// <summary>"_entries[1]._material" → "Entries [1] > Material" のように Inspector の見た目に近づける。</summary>
|
|
1218
|
+
private static string NicifyPropertyPath(string propertyPath)
|
|
1219
|
+
{
|
|
1220
|
+
var builder = new StringBuilder();
|
|
1221
|
+
foreach (var segment in ArrayDataRegex.Replace(propertyPath, "[").Split('.'))
|
|
1222
|
+
{
|
|
1223
|
+
if (builder.Length > 0)
|
|
1224
|
+
{
|
|
1225
|
+
builder.Append(" > ");
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
var indexMatch = ArrayIndexRegex.Match(segment);
|
|
1229
|
+
var name = ArrayIndexRegex.Replace(segment, string.Empty);
|
|
1230
|
+
builder.Append(ObjectNames.NicifyVariableName(name));
|
|
1231
|
+
if (indexMatch.Success)
|
|
1232
|
+
{
|
|
1233
|
+
builder.Append(' ').Append(indexMatch.Value);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
return builder.ToString();
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// ---- 階層パス ----
|
|
1241
|
+
|
|
1242
|
+
/// <summary>GameObject ドキュメントの階層パス ("Root/Child/Leaf") を返す。</summary>
|
|
1243
|
+
private string GetGameObjectPath(DocumentInfo gameObject, int depth)
|
|
1244
|
+
{
|
|
1245
|
+
if (gameObject == null || depth > MaxPathDepth)
|
|
1246
|
+
{
|
|
1247
|
+
return "?";
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (gameObject.IsStripped)
|
|
1251
|
+
{
|
|
1252
|
+
return GetStrippedObjectPath(gameObject, depth);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
var name = string.IsNullOrEmpty(gameObject.Name) ? $"GameObject &{gameObject.FileId}" : gameObject.Name;
|
|
1256
|
+
if (!_transformByGameObject.TryGetValue(gameObject.FileId, out var transform) || transform.FatherFileId == 0)
|
|
1257
|
+
{
|
|
1258
|
+
return name;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
var parentPath = GetTransformOwnerPath(transform.FatherFileId, depth + 1);
|
|
1262
|
+
return parentPath == null ? name : $"{parentPath}/{name}";
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/// <summary>Transform ドキュメントの fileID から、その Transform を持つ GameObject の階層パスを返す。</summary>
|
|
1266
|
+
private string GetTransformOwnerPath(long transformFileId, int depth)
|
|
1267
|
+
{
|
|
1268
|
+
if (depth > MaxPathDepth || !_documentsById.TryGetValue(transformFileId, out var transform))
|
|
1269
|
+
{
|
|
1270
|
+
return null;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
if (transform.IsStripped)
|
|
1274
|
+
{
|
|
1275
|
+
return GetStrippedObjectPath(transform, depth);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
return _documentsById.TryGetValue(transform.GameObjectFileId, out var gameObject)
|
|
1279
|
+
? GetGameObjectPath(gameObject, depth + 1)
|
|
1280
|
+
: null;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/// <summary>Prefab インスタンス内の (stripped) GameObject / Transform ドキュメントの階層パスを返す。</summary>
|
|
1284
|
+
private string GetStrippedObjectPath(DocumentInfo stripped, int depth)
|
|
1285
|
+
{
|
|
1286
|
+
_documentsById.TryGetValue(stripped.PrefabInstanceFileId, out var instance);
|
|
1287
|
+
if (instance == null)
|
|
1288
|
+
{
|
|
1289
|
+
return $"(Prefab インスタンス内) &{stripped.FileId}";
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
return GetPrefabInstanceObjectPath(instance, stripped.CorrespondingSourceFileId, depth + 1);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/// <summary>
|
|
1296
|
+
/// PrefabInstance の元 Prefab 内 fileID (<paramref name="sourceLocalId"/>, 0 はルート) で示されるオブジェクトの、
|
|
1297
|
+
/// このファイル内での階層パスを返す。親側のパスはファイル内の Transform を辿り、Prefab 内のパスは元 Prefab を読んで解決する。
|
|
1298
|
+
/// </summary>
|
|
1299
|
+
private string GetPrefabInstanceObjectPath(DocumentInfo instance, long sourceLocalId, int depth)
|
|
1300
|
+
{
|
|
1301
|
+
string parentPath = null;
|
|
1302
|
+
if (instance.TransformParentFileId != 0 && depth <= MaxPathDepth)
|
|
1303
|
+
{
|
|
1304
|
+
parentPath = GetTransformOwnerPath(instance.TransformParentFileId, depth + 1);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
var sourceObject = PrefabAssetCache.GetGameObject(instance.SourcePrefabGuid, sourceLocalId);
|
|
1308
|
+
var segments = PrefabAssetCache.GetPathSegments(sourceObject);
|
|
1309
|
+
|
|
1310
|
+
string innerPath;
|
|
1311
|
+
if (segments == null)
|
|
1312
|
+
{
|
|
1313
|
+
// 元 Prefab を読めない (Missing など)。分かる範囲で名前を出す
|
|
1314
|
+
var rootName = !string.IsNullOrEmpty(instance.Name) ? instance.Name
|
|
1315
|
+
: instance.NameOverrides.Count > 0 ? GetFirstNameOverride(instance)
|
|
1316
|
+
: $"PrefabInstance &{instance.FileId}";
|
|
1317
|
+
innerPath = sourceLocalId == 0 ? rootName : $"{rootName}/(&{sourceLocalId})";
|
|
1318
|
+
}
|
|
1319
|
+
else
|
|
1320
|
+
{
|
|
1321
|
+
var builder = new StringBuilder();
|
|
1322
|
+
foreach (var (id, name) in segments)
|
|
1323
|
+
{
|
|
1324
|
+
if (builder.Length > 0)
|
|
1325
|
+
{
|
|
1326
|
+
builder.Append('/');
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
builder.Append(instance.NameOverrides.TryGetValue(id, out var overridden) ? overridden : name);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
innerPath = builder.ToString();
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
return parentPath == null ? innerPath : $"{parentPath}/{innerPath}";
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
private static string GetFirstNameOverride(DocumentInfo instance)
|
|
1339
|
+
{
|
|
1340
|
+
foreach (var pair in instance.NameOverrides)
|
|
1341
|
+
{
|
|
1342
|
+
return pair.Value;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
return null;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
private string GetSourcePrefabPath(DocumentInfo stripped)
|
|
1349
|
+
{
|
|
1350
|
+
if (_documentsById.TryGetValue(stripped.PrefabInstanceFileId, out var instance))
|
|
1351
|
+
{
|
|
1352
|
+
var path = PrefabAssetCache.GetPath(instance.SourcePrefabGuid);
|
|
1353
|
+
if (!string.IsNullOrEmpty(path))
|
|
1354
|
+
{
|
|
1355
|
+
return path;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
return "パス不明";
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// ---- 実オブジェクト解決 (Console クリック時のフォーカス用) ----
|
|
1363
|
+
|
|
1364
|
+
/// <summary>ドキュメントに対応する実オブジェクトを返す。シーンは開かれている場合のみ、Prefab は常に解決を試みる。</summary>
|
|
1365
|
+
private Object ResolveObject(DocumentInfo doc)
|
|
1366
|
+
{
|
|
1367
|
+
if (_isScene)
|
|
1368
|
+
{
|
|
1369
|
+
if (!_isSceneLoaded)
|
|
1370
|
+
{
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
return doc.IsStripped
|
|
1375
|
+
? ResolveSceneObject(doc.CorrespondingSourceFileId, doc.PrefabInstanceFileId)
|
|
1376
|
+
: ResolveSceneObject(doc.FileId, 0);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
return PrefabAssetCache.GetObject(_fileGuid, doc.FileId);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
private Object ResolveSceneObject(long targetId, long prefabInstanceId)
|
|
1383
|
+
{
|
|
1384
|
+
if (!GlobalObjectId.TryParse($"GlobalObjectId_V1-2-{_fileGuid}-{targetId}-{prefabInstanceId}", out var id))
|
|
1385
|
+
{
|
|
1386
|
+
return null;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
return GlobalObjectId.GlobalObjectIdentifierToObjectSlow(id);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
public Object GetContextObject(DocumentInfo doc, MissingEntry entry)
|
|
1393
|
+
{
|
|
1394
|
+
Object resolved = null;
|
|
1395
|
+
if (doc.ClassId == PrefabInstanceClassId)
|
|
1396
|
+
{
|
|
1397
|
+
if (_isScene && _isSceneLoaded && entry.TargetFileId != 0)
|
|
1398
|
+
{
|
|
1399
|
+
resolved = ResolveSceneObject(entry.TargetFileId, doc.FileId);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
else
|
|
1403
|
+
{
|
|
1404
|
+
resolved = ResolveObject(doc);
|
|
1405
|
+
if (resolved == null && doc.GameObjectFileId != 0 && _documentsById.TryGetValue(doc.GameObjectFileId, out var owner))
|
|
1406
|
+
{
|
|
1407
|
+
resolved = ResolveObject(owner);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
switch (resolved)
|
|
1412
|
+
{
|
|
1413
|
+
case GameObject go:
|
|
1414
|
+
return go;
|
|
1415
|
+
case Component component:
|
|
1416
|
+
return component.gameObject;
|
|
1417
|
+
default:
|
|
1418
|
+
return _mainAsset;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|