-
Notifications
You must be signed in to change notification settings - Fork 46
/
Copy pathAssetDatabase.Core.cs
611 lines (527 loc) · 22.4 KB
/
AssetDatabase.Core.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
// This file is part of the Prowl Game Engine
// Licensed under the MIT License. See the LICENSE file in the project root for details.
using System.Diagnostics;
using Prowl.Runtime;
using Prowl.Runtime.Utils;
using Prowl.Echo;
using Debug = Prowl.Runtime.Debug;
namespace Prowl.Editor.Assets;
[FilePath("Library/LastWriteTimes.cache", FilePathAttribute.Location.Data)]
public class LastWriteTimesCache : ScriptableSingleton<LastWriteTimesCache>
{
public readonly Dictionary<string, DateTime> fileLastWriteTimes = [];
}
public static partial class AssetDatabase
{
#region Properties
public static Guid LastLoadedAssetID { get; private set; } = Guid.Empty;
public static string TempAssetDirectory => Path.Combine(Project.Active.ProjectPath, "Library/AssetDatabase");
#endregion
#region Events
public static event Action? AssetCacheUpdated;
public static event Action<Guid, FileInfo>? AssetRemoved;
public static event Action<FileInfo>? Pinged;
#endregion
#region Private Fields
static readonly List<(DirectoryInfo, AssetDirectoryCache)> rootFolders = [];
static double RefreshTimer;
static bool lastFocused;
static readonly Dictionary<string, MetaFile> assetPathToMeta = new(StringComparer.OrdinalIgnoreCase);
static readonly Dictionary<Guid, MetaFile> assetGuidToMeta = [];
static readonly Dictionary<Guid, SerializedAsset> guidToAssetData = [];
static int updateLock = 0;
#endregion
#region Public Methods
internal static void InternalUpdate()
{
if (Screen.IsFocused)
{
// This could probably be improved to process more asyncronously
RefreshTimer += Time.deltaTime;
if (!lastFocused || RefreshTimer > 5f)
Update();
}
lastFocused = Screen.IsFocused;
}
/// <summary>
/// Gets the root folder cache at the specified index.
/// </summary>
public static AssetDirectoryCache GetRootFolderCache(int index)
{
if (index < 0 || index >= rootFolders.Count)
throw new IndexOutOfRangeException("Index out of range");
return rootFolders[index].Item2;
}
/// <summary>
/// Gets the list of root folders in the AssetDatabase.
/// </summary>
/// <returns>List of root folders.</returns>
public static List<DirectoryInfo> GetRootFolders() => rootFolders.Select(x => x.Item1).ToList();
/// <summary>
/// Adds a root folder to the AssetDatabase.
/// </summary>
/// <param name="rootFolder">The path to the root folder.</param>
public static void AddRootFolder(string rootFolder)
{
ArgumentException.ThrowIfNullOrEmpty(rootFolder);
var rootPath = Path.Combine(Project.Active.ProjectPath, rootFolder);
var info = new DirectoryInfo(rootPath);
if (!info.Exists)
info.Create();
if (rootFolders.Any(x => x.Item1.FullName.Equals(info.FullName, StringComparison.OrdinalIgnoreCase)))
throw new ArgumentException("Root Folder already exists in the Asset Database");
rootFolders.Add((info, new AssetDirectoryCache(info)));
}
/// <summary>
/// Prevents the AssetDatabase from updating.
/// </summary>
public static void LockUpdate() => updateLock++;
/// <summary>
/// Allows the AssetDatabase to update if it was previously locked.
/// </summary>
public static void UnlockUpdate()
{
bool wasLocked = updateLock > 0;
updateLock = Math.Max(0, updateLock - 1);
if (wasLocked && updateLock == 0)
Update(true, true);
}
/// <summary>
/// Checks for changes in the AssetDatabase.
/// Call manually when you make changes to the asset files to ensure the changes are loaded
/// </summary>
public static void Update(bool doUnload = true, bool forceCacheUpdate = false)
{
if (!Project.HasProject) return;
if (updateLock > 0) return;
RefreshTimer = 0f;
HashSet<string> currentFiles = [];
List<string> toReimport = [];
bool cacheModified = false;
foreach (var root in rootFolders)
{
var files = Directory.GetFiles(root.Item1.FullName, "*", SearchOption.AllDirectories)
.Where(file => !file.EndsWith(".meta"));
foreach (var file in files)
{
// Only process files that are supported by an importer, the rest are ignored
if (ImporterAttribute.SupportsExtension(Path.GetExtension(file)))
{
currentFiles.Add(file);
if (!LastWriteTimesCache.Instance.fileLastWriteTimes.TryGetValue(file, out var lastWriteTime)
|| !MetaFile.HasMeta(new FileInfo(file)))
{
// New file
Debug.Log("Asset Added: " + file);
lastWriteTime = File.GetLastWriteTime(file);
LastWriteTimesCache.Instance.fileLastWriteTimes[file] = lastWriteTime;
cacheModified = true;
if (ProcessFile(file, out _))
toReimport.Add(file);
}
else if (File.GetLastWriteTime(file) != lastWriteTime)
{
// File modified
Debug.Log("Asset Updated: " + file);
lastWriteTime = File.GetLastWriteTime(file);
LastWriteTimesCache.Instance.fileLastWriteTimes[file] = lastWriteTime;
cacheModified = true;
if (ProcessFile(file, out _))
toReimport.Add(file);
}
else if (!assetPathToMeta.TryGetValue(file, out var meta))
{
// File hasent changed but we dont have it in the cache, process it but dont reimport
Debug.Log("Asset Found: " + file);
ProcessFile(file, out var metaOutdated);
if (metaOutdated)
toReimport.Add(file);
}
}
}
}
// Defer the Reimports untill after all Meta files are loaded/updated
foreach (var file in toReimport)
{
Reimport(new(file));
}
if (doUnload)
{
// Check for missing paths
var missingPaths = LastWriteTimesCache.Instance.fileLastWriteTimes.Keys.Except(currentFiles).ToList();
foreach (var file in missingPaths)
{
LastWriteTimesCache.Instance.fileLastWriteTimes.Remove(file);
cacheModified = true;
bool hasMeta = assetPathToMeta.TryGetValue(file, out var meta);
if (hasMeta)
{
assetPathToMeta.Remove(file);
// The asset could have moved, in which case that's all we need todo
// But, if the guid leads to a meta file which has THIS asset path, then no new asset exists
// As it would have been updated from the code above and ProcessFile()
// Which means it didn't just move somewhere else, its gone
if (assetGuidToMeta.TryGetValue(meta.guid, out var existingMeta))
{
if (existingMeta.AssetPath.FullName.Equals(file, StringComparison.OrdinalIgnoreCase))
{
assetGuidToMeta.Remove(meta.guid);
DestroyStoredAsset(meta.guid);
Debug.Log("Asset Deleted: " + file);
AssetRemoved?.Invoke(meta.guid, new FileInfo(file));
}
}
}
}
}
// If anything changed update DirectoryCaches for Editor UI
if (forceCacheUpdate || cacheModified || toReimport.Count > 0)
{
foreach (var root in rootFolders)
root.Item2.Refresh();
AssetCacheUpdated?.Invoke();
}
if (cacheModified)
LastWriteTimesCache.Instance.Save();
}
/// <summary>
/// Process a File Change
/// </summary>
/// <param name="file"></param>
/// <returns>True if a reimport is needed</returns>
static bool ProcessFile(string file, out bool metaOutdated)
{
ArgumentException.ThrowIfNullOrEmpty(file);
var fileInfo = new FileInfo(file);
metaOutdated = false;
var meta = MetaFile.Load(fileInfo);
if (meta != null)
{
// Update the cache to record this new Meta file, Guid and Path
bool hasMetaAlready = assetGuidToMeta.ContainsKey(meta.guid);
assetGuidToMeta[meta.guid] = meta;
assetPathToMeta[fileInfo.FullName] = meta;
if (meta.version != MetaFile.MetaVersion)
{
metaOutdated = true;
return true; // Meta version is outdated
}
if (hasMetaAlready)
{
if (meta.lastModified != fileInfo.LastWriteTimeUtc)
{
// File modified, reimport
return true;
}
}
else
{
// New file with meta, import
return false;
}
}
else
{
// No meta file, create and import
var newMeta = new MetaFile(fileInfo);
if (newMeta.importer == null)
{
Debug.LogError($"No importer found for file:\n{fileInfo.FullName}");
return false;
}
newMeta.Save();
assetGuidToMeta[newMeta.guid] = newMeta;
assetPathToMeta[fileInfo.FullName] = newMeta;
return true;
}
return false; // No need to reimport, nothing changed
}
/// <summary>
/// Tries to get the GUID of a file.
/// </summary>
/// <param name="file">The file to get the GUID for.</param>
/// <param name="guid">The GUID of the file.</param>
/// <returns>True if the GUID was found, false otherwise.</returns>
public static bool TryGetGuid(FileInfo file, out Guid guid)
{
guid = Guid.Empty;
ArgumentNullException.ThrowIfNull(file);
if (!File.Exists(file.FullName)) return false;
if (assetPathToMeta.TryGetValue(file.FullName, out var meta))
{
guid = meta.guid;
return true;
}
return false;
}
/// <summary>
/// Tries to get the file with the specified GUID.
/// </summary>
/// <param name="guid">The GUID of the file.</param>
/// <param name="file">The file with the specified GUID.</param>
/// <returns>True if the file was found, false otherwise.</returns>
public static bool TryGetFile(Guid guid, out FileInfo? file)
{
file = null;
if (guid == Guid.Empty) throw new ArgumentException("Asset Guid cannot be empty", nameof(guid));
if (assetGuidToMeta.TryGetValue(guid, out var meta))
{
file = new FileInfo(meta.AssetPath.FullName);
return true;
}
return false;
}
/// <summary>
/// Reimports all assets in the AssetDatabase.
/// </summary>
public static void ReimportAll()
{
foreach (var root in rootFolders)
ReimportFolder(root.Item1);
}
/// <summary>
/// Reimports all assets in the specified directory.
/// </summary>
/// <param name="directory">The directory to reimport assets from.</param>
public static void ReimportFolder(DirectoryInfo directory)
{
ArgumentNullException.ThrowIfNull(directory);
var files = directory.GetFiles("*", SearchOption.AllDirectories)
.Where(file => !file.FullName.EndsWith(".meta"));
foreach (var file in files)
if (ImporterAttribute.SupportsExtension(file.Extension))
Reimport(file);
}
/// <summary>
/// Reimports an asset from the specified file.
/// </summary>
/// <param name="assetFile">The asset file to reimport.</param>
/// <param name="disposeExisting">Whether to dispose the existing asset in memory before reimporting.</param>
/// <returns>True if the asset was reimported successfully, false otherwise.</returns>
public static bool Reimport(FileInfo assetFile, bool disposeExisting = true)
{
Stopwatch sw = new();
sw.Start();
ArgumentNullException.ThrowIfNull(assetFile);
// Dispose if we already have it
if (disposeExisting)
if (TryGetGuid(assetFile, out var assetGuid))
DestroyStoredAsset(assetGuid);
// make sure path exists
if (!File.Exists(assetFile.FullName))
{
Debug.LogError($"Failed to import {ToRelativePath(assetFile)}. Asset does not exist. Took {sw.ElapsedMilliseconds}ms");
return false;
}
var meta = MetaFile.Load(assetFile);
if (meta == null)
{
Debug.LogError($"No valid meta file found for asset: {ToRelativePath(assetFile)} Took {sw.ElapsedMilliseconds}ms");
return false;
}
if (meta.importer == null)
{
Debug.LogError($"No valid importer found for asset: {ToRelativePath(assetFile)} Took {sw.ElapsedMilliseconds}ms");
return false;
}
// Import the asset
SerializedAsset ctx = new(meta.guid);
try
{
meta.importer.Import(ctx, assetFile);
}
catch (Exception e)
{
Debug.LogException(new Exception($"Failed to import {ToRelativePath(assetFile)} Took {sw.ElapsedMilliseconds}ms", e));
return false; // Import failed
}
if (!ctx.HasMain)
{
Debug.LogException(new Exception($"Failed to import {ToRelativePath(assetFile)}. No main object found. Took {sw.ElapsedMilliseconds}ms"));
return false; // Import failed no Main Object
}
// Delete the old imported asset if it exists
var serialized = GetSerializedFile(meta.guid);
if (File.Exists(serialized.FullName))
serialized.Delete();
// Save the asset
ctx.SaveToFile(serialized, out var dependencies);
// Update the meta file (LastModified is set by MetaFile.Load)
meta.assetTypes = new string[ctx.SubAssets.Count + 1];
meta.assetTypes[0] = ctx.Main.GetType().FullName!;
for (int i = 0; i < ctx.SubAssets.Count; i++)
meta.assetTypes[i + 1] = ctx.SubAssets[i].GetType().FullName!;
meta.assetNames = new string[ctx.SubAssets.Count + 1];
meta.assetNames[0] = ctx.Main.Name;
for (int i = 0; i < ctx.SubAssets.Count; i++)
meta.assetNames[i + 1] = ctx.SubAssets[i].Name;
meta.dependencies = dependencies.ToList();
meta.Save();
Debug.Log($"Reimported {Path.GetRelativePath(Project.Active.ProjectPath, assetFile.FullName)}. Took {sw.ElapsedMilliseconds}ms");
return true;
}
/// <summary>
/// Loads an asset of the specified type from the specified file path and file ID.
/// </summary>
/// <typeparam name="T">The type of the asset to load.</typeparam>
/// <param name="assetPath">The file path of the asset to load.</param>
/// <param name="fileID">The file ID of the asset to load.</param>
/// <returns>The loaded asset, or null if the asset could not be loaded.</returns>
public static T? LoadAsset<T>(FileInfo assetPath, ushort fileID) where T : EngineObject
{
ArgumentNullException.ThrowIfNull(assetPath);
if (TryGetGuid(assetPath, out var guid))
return LoadAsset<T>(guid, fileID);
return null;
}
/// <summary>
/// Loads an asset of the specified type from the specified GUID and file ID.
/// </summary>
/// <typeparam name="T">The type of the asset to load.</typeparam>
/// <param name="assetGuid">The GUID of the asset to load.</param>
/// <param name="fileID">The file ID of the asset to load.</param>
/// <returns>The loaded asset, or null if the asset could not be loaded.</returns>
public static T? LoadAsset<T>(Guid assetGuid, ushort fileID) where T : EngineObject
{
if (assetGuid == Guid.Empty) throw new ArgumentException("Asset Guid cannot be empty", nameof(assetGuid));
try
{
var serialized = LoadAsset(assetGuid);
if (serialized == null) return null;
T? asset = null;
if (fileID == 0)
{
// Main Asset
if (serialized.Main is not T) return null;
asset = (T)serialized.Main;
}
else
{
// Sub Asset
if (serialized.SubAssets[fileID - 1] is not T) return null;
asset = (T)serialized.SubAssets[fileID - 1];
}
asset.AssetID = assetGuid;
asset.FileID = fileID;
return asset;
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
throw new InvalidCastException($"Something went wrong loading asset.");
}
}
/// <summary>
/// Loads a serialized asset from the specified file path.
/// </summary>
/// <param name="assetPath">The file path of the serialized asset to load.</param>
/// <returns>The loaded serialized asset, or null if the asset could not be loaded.</returns>
public static SerializedAsset? LoadAsset(FileInfo assetPath)
{
ArgumentNullException.ThrowIfNull(assetPath);
if (TryGetGuid(assetPath, out var guid))
return LoadAsset(guid);
return null;
}
/// <summary>
/// Loads a serialized asset from the specified GUID.
/// </summary>
/// <param name="assetGuid">The GUID of the serialized asset to load.</param>
/// <returns>The loaded serialized asset, or null if the asset could not be loaded.</returns>
public static SerializedAsset? LoadAsset(Guid assetGuid)
{
if (assetGuid == Guid.Empty) throw new ArgumentException("Asset Guid cannot be empty", nameof(assetGuid));
if (guidToAssetData.TryGetValue(assetGuid, out SerializedAsset? value))
{
// if Main or any sub asset is Null or Destroyed, Destroy it from cache
// Destroying a Sub Asset destroys the Main Asset as well since they act as a single entity
if (value.Main == null || value.Main.IsDestroyed)
{
DestroyStoredAsset(assetGuid);
}
else if (value.SubAssets.Any(x => x == null || x.IsDestroyed))
{
DestroyStoredAsset(assetGuid);
}
else
{
return value;
}
}
if (!TryGetFile(assetGuid, out var asset))
return null;
if (!File.Exists(asset!.FullName)) throw new FileNotFoundException("Asset file does not exist.", asset.FullName);
FileInfo serializedAssetPath = GetSerializedFile(assetGuid);
if (!File.Exists(serializedAssetPath.FullName))
if (!Reimport(asset))
{
Debug.LogError($"Failed to import {serializedAssetPath.FullName}.");
throw new Exception($"Failed to import {serializedAssetPath.FullName}.");
}
try
{
var serializedAsset = SerializedAsset.FromSerializedAsset(serializedAssetPath.FullName);
if (serializedAsset == null || object.ReferenceEquals(serializedAsset.Main, null))
{
Debug.LogError($"Failed to load serialized asset {serializedAssetPath.FullName}, Asset was Null.");
return null;
}
serializedAsset.Guid = assetGuid;
serializedAsset.Main.AssetID = assetGuid;
serializedAsset.Main.FileID = 0;
for (int i = 0; i < serializedAsset.SubAssets.Count; i++)
{
serializedAsset.SubAssets[i].AssetID = assetGuid;
serializedAsset.SubAssets[i].FileID = (ushort)(i + 1);
}
guidToAssetData[assetGuid] = serializedAsset;
return serializedAsset;
}
catch (Exception e)
{
Debug.LogException(new Exception($"Failed to load serialized asset {serializedAssetPath.FullName}.", e));
return null; // Failed file might be in use?
}
}
/// <summary>
/// Saves the specified asset instance to its Asset file.
/// Keep in mind this Serializes and saves the asset instance to the original asset file.
/// This will overwrite the asset file with the new data, and may not behave how you expect.
/// For example if you call this on a Mesh asset, it will overwrite the mesh file which may be .obj or .fbx with a String Tag serialized format.
/// This would break the Mesh Asset!
/// Only use it on Assets where the Original asset file Is itself a string tag, like a Scene, Material, Prefab or ScriptableObject.
/// </summary>
/// <param name="assetInstance">The Asset Instance, Needs to be properly linked to the asset and have the AssetID Assigned!</param>
/// <param name="pingAsset">Whether to ping the asset after saving.</param>
public static void SaveAsset(EngineObject assetInstance, bool pingAsset = true)
{
ArgumentNullException.ThrowIfNull(assetInstance);
if (AssetDatabase.TryGetFile(assetInstance.AssetID, out FileInfo? fileInfo))
{
try
{
EchoObject serialized = Serializer.Serialize(assetInstance);
serialized.WriteToString(fileInfo);
if(pingAsset)
AssetDatabase.Ping(fileInfo);
// All we did was update the file on disk to perfectly match the already in memory asset
// So no need to reimport or update the cache's
}
catch (Exception e)
{
Debug.LogException(new Exception($"Failed to save asset {assetInstance.Name}.", e));
}
}
}
#endregion
#region Private Methods
private static void DestroyStoredAsset(Guid guid)
{
if (guidToAssetData.TryGetValue(guid, out SerializedAsset? value))
{
value.Destroy();
guidToAssetData.Remove(guid);
}
}
#endregion
}