Unity-WebSocket/Assets/FishNet/Runtime/Editor/PrefabCollectionGenerator/Generator.cs
2025-06-28 11:28:54 +03:30

697 lines
28 KiB
C#

#if UNITY_EDITOR
using FishNet.Configuring;
using FishNet.Managing;
using FishNet.Managing.Object;
using FishNet.Object;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityDebug = UnityEngine.Debug;
namespace FishNet.Editing.PrefabCollectionGenerator
{
internal sealed class Generator : AssetPostprocessor
{
public Generator()
{
if (!_subscribed)
{
_subscribed = true;
EditorApplication.update += OnEditorUpdate;
}
}
~Generator()
{
if (_subscribed)
{
_subscribed = false;
EditorApplication.update -= OnEditorUpdate;
}
}
#region Types.
internal readonly struct SpecifiedFolder
{
public readonly string Path;
public readonly bool Recursive;
public SpecifiedFolder(string path, bool recursive)
{
Path = path;
Recursive = recursive;
}
}
#endregion
#region Public.
/// <summary>
/// True to ignore post process changes.
/// </summary>
public static bool IgnorePostProcess = false;
#endregion
#region Private.
/// <summary>
/// Last asset to import when there was only one imported asset and no other changes.
/// </summary>
private static string _lastSingleImportedAsset = string.Empty;
/// <summary>
/// Cached DefaultPrefabObjects reference.
/// </summary>
private static DefaultPrefabObjects _cachedDefaultPrefabs;
/// <summary>
/// True to refresh prefabs next update.
/// </summary>
private static bool _retryRefreshDefaultPrefabs;
/// <summary>
/// True if already subscribed to EditorApplication.Update.
/// </summary>
private static bool _subscribed;
/// <summary>
/// True if ran once since editor started.
/// </summary>
[System.NonSerialized]
private static bool _ranOnce;
/// <summary>
/// Last paths of updated nobs during a changed update.
/// </summary>
[System.NonSerialized]
private static List<string> _lastUpdatedNamePaths = new();
/// <summary>
/// Last frame changed was updated.
/// </summary>
[System.NonSerialized]
private static int _lastUpdatedFrame = -1;
/// <summary>
/// Length of assets strings during the last update.
/// </summary>
[System.NonSerialized]
private static int _lastUpdatedLengths = -1;
#endregion
internal static string[] GetProjectFiles(string startingPath, string fileExtension, List<string> excludedPaths, bool recursive)
{
//Opportunity to exit early if there are no excluded paths.
if (excludedPaths.Count == 0)
{
string[] strResults = Directory.GetFiles(startingPath, $"*{fileExtension}", SearchOption.AllDirectories);
return strResults;
}
//starting path is excluded.
if (excludedPaths.Contains(startingPath))
return new string[0];
//Folders remaining to be iterated.
List<string> enumeratedCollection = new() { startingPath };
//Only check other directories if recursive.
if (recursive)
{
//Find all folders which aren't excluded.
for (int i = 0; i < enumeratedCollection.Count; i++)
{
string[] allFolders = Directory.GetDirectories(enumeratedCollection[i], "*", SearchOption.TopDirectoryOnly);
for (int z = 0; z < allFolders.Length; z++)
{
string current = allFolders[z];
//Not excluded.
if (!excludedPaths.Contains(current))
enumeratedCollection.Add(current);
}
}
}
//Valid prefab files.
List<string> results = new();
//Build files from folders.
int count = enumeratedCollection.Count;
for (int i = 0; i < count; i++)
{
string[] r = Directory.GetFiles(enumeratedCollection[i], "*.prefab", SearchOption.TopDirectoryOnly);
results.AddRange(r);
}
return results.ToArray();
}
/// <summary>
/// Returns a message to attach to logs if objects were dirtied.
/// </summary>
private static string GetDirtiedMessage(PrefabGeneratorConfigurations settings, bool dirtied)
{
if (!settings.SaveChanges && dirtied)
return " One or more NetworkObjects were dirtied. Please save your project.";
else
return string.Empty;
}
/// <summary>
/// Updates prefabs by using only changed information.
/// </summary>
public static void GenerateChanged(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths, PrefabGeneratorConfigurations settings = null)
{
#if PARRELSYNC
if (ParrelSync.ClonesManager.IsClone() && ParrelSync.Preferences.AssetModPref.Value)
{
UnityDebug.Log("Skipping prefab generation in ParrelSync clone");
return;
}
#endif
//Do not run if the reserializer is currently running.
if (ReserializeNetworkObjectsEditor.IsRunning)
return;
if (settings == null)
settings = Configuration.Configurations.PrefabGenerator;
if (!settings.Enabled)
return;
bool log = settings.LogToConsole;
Stopwatch sw = (log) ? Stopwatch.StartNew() : null;
DefaultPrefabObjects prefabCollection = GetDefaultPrefabObjects(settings);
//No need to error if nto found, GetDefaultPrefabObjects will.
if (prefabCollection == null)
return;
int assetsLength = (importedAssets.Length + deletedAssets.Length + movedAssets.Length + movedFromAssetPaths.Length);
List<string> changedNobPaths = new();
System.Type goType = typeof(UnityEngine.GameObject);
IterateAssetCollection(importedAssets);
IterateAssetCollection(movedAssets);
//True if dirtied by changes.
bool dirtied;
//First remove null entries.
int startCount = prefabCollection.GetObjectCount();
prefabCollection.RemoveNull();
dirtied = (prefabCollection.GetObjectCount() != startCount);
//First index which new objects will be added to.
int firstAddIndex = (prefabCollection.GetObjectCount() - 1);
//Iterates strings adding prefabs to collection.
void IterateAssetCollection(string[] c)
{
foreach (string item in c)
{
System.Type assetType = AssetDatabase.GetMainAssetTypeAtPath(item);
if (assetType != goType)
continue;
NetworkObject nob = AssetDatabase.LoadAssetAtPath<NetworkObject>(item);
if (CanAddNetworkObject(nob, settings))
{
changedNobPaths.Add(item);
prefabCollection.AddObject(nob, true);
dirtied = true;
}
}
}
//To prevent out of range.
if (firstAddIndex < 0 || firstAddIndex >= prefabCollection.GetObjectCount())
firstAddIndex = 0;
dirtied |= prefabCollection.SetAssetPathHashes(firstAddIndex);
if (log && dirtied)
UnityDebug.Log($"Default prefab generator updated prefabs in {sw.ElapsedMilliseconds}ms.{GetDirtiedMessage(settings, dirtied)}");
//Check for redundancy.
int frameCount = Time.frameCount;
int changedCount = changedNobPaths.Count;
if (frameCount == _lastUpdatedFrame && assetsLength == _lastUpdatedLengths && (changedCount == _lastUpdatedNamePaths.Count) && changedCount > 0)
{
bool allMatch = true;
for (int i = 0; i < changedCount; i++)
{
if (changedNobPaths[i] != _lastUpdatedNamePaths[i])
{
allMatch = false;
break;
}
}
/* If the import results are the same as the last attempt, on the same frame
* then there is likely an issue saving the assets. */
if (allMatch)
{
//Unset dirtied to prevent a save.
dirtied = false;
//Log this no matter what, it's critical.
UnityDebug.LogError($"Default prefab generator had a problem saving one or more assets. " + $"This usually occurs when the assets cannot be saved due to missing scripts or serialization errors. " + $"Please see above any prefabs which could not save any make corrections.");
}
}
//Set last values.
_lastUpdatedFrame = Time.frameCount;
_lastUpdatedNamePaths = changedNobPaths;
_lastUpdatedLengths = assetsLength;
EditorUtility.SetDirty(prefabCollection);
if (dirtied && settings.SaveChanges)
AssetDatabase.SaveAssets();
}
/// <summary>
/// Gets NetworkObjects from folders using settings.
/// </summary>
internal static List<NetworkObject> GetNetworkObjects(PrefabGeneratorConfigurations settings = null)
{
List<SpecifiedFolder> folders = GetSpecifiedFolders(settings);
if (folders == null)
return new();
return GetNetworkObjects(folders, settings);
}
/// <summary>
/// Gets specified folders to check for prefab generation. This may include excluded paths. Null is returned on error.
/// </summary>
internal static List<SpecifiedFolder> GetSpecifiedFolders(PrefabGeneratorConfigurations settings = null)
{
settings = GetSettingsIfNull(settings);
List<string> folderStrs;
if (settings.SearchScope == (int)SearchScopeType.EntireProject)
{
folderStrs = new List<string>();
folderStrs.Add("Assets*");
}
else if (settings.SearchScope == (int)SearchScopeType.SpecificFolders)
{
folderStrs = settings.IncludedFolders.ToList();
}
else
{
UnityDebug.LogError($"{settings.SearchScope} is not handled; folder paths cannot be found.");
return null;
}
return GetSpecifiedFolders(folderStrs);
}
/// <summary>
/// Gets all NetworkObjects in specified folders while ignoring any excluded paths.
/// </summary>
internal static List<NetworkObject> GetNetworkObjects(SpecifiedFolder specifiedFolder, PrefabGeneratorConfigurations settings = null)
{
List<string> excludedPaths = GetSettingsIfNull(settings).ExcludedFolders;
List<NetworkObject> foundNobs = new();
foreach (string path in GetProjectFiles(specifiedFolder.Path, "prefab", excludedPaths, specifiedFolder.Recursive))
{
NetworkObject nob = AssetDatabase.LoadAssetAtPath<NetworkObject>(path);
if (CanAddNetworkObject(nob, settings))
foundNobs.Add(nob);
}
return foundNobs;
}
/// <summary>
/// Gets all NetworkObjects in specified folders while ignoring any excluded paths.
/// </summary>
internal static List<NetworkObject> GetNetworkObjects(List<SpecifiedFolder> specifiedFolders, PrefabGeneratorConfigurations settings = null)
{
List<NetworkObject> foundNobs = new();
foreach (SpecifiedFolder sf in specifiedFolders)
foundNobs.AddRange(GetNetworkObjects(sf, settings));
return foundNobs;
}
/// <summary>
/// Generates prefabs by iterating all files within settings parameters.
/// </summary>
public static void GenerateFull(PrefabGeneratorConfigurations settings = null, bool forced = false, bool initializeAdded = true)
{
#if PARRELSYNC
if (ParrelSync.ClonesManager.IsClone() && ParrelSync.Preferences.AssetModPref.Value)
{
UnityDebug.Log("Skipping prefab generation in ParrelSync clone");
return;
}
#endif
//Do not run if the reserializer is currently running.
if (ReserializeNetworkObjectsEditor.IsRunning)
{
UnityDebug.LogError($"Cannot generate default prefabs when ReserializeNetworkObjectsEditor is running");
return;
}
settings = GetSettingsIfNull(settings);
if (!forced && !settings.Enabled)
return;
bool log = settings.LogToConsole;
Stopwatch sw = (log) ? Stopwatch.StartNew() : null;
DefaultPrefabObjects prefabCollection = GetDefaultPrefabObjects(settings);
//No need to error if not found, GetDefaultPrefabObjects will throw.
if (prefabCollection == null)
return;
List<NetworkObject> foundNobs = GetNetworkObjects(settings);
//Clear and add built list.
prefabCollection.Clear();
prefabCollection.AddObjects(foundNobs, checkForDuplicates: false, initializeAdded);
bool dirtied = prefabCollection.SetAssetPathHashes(0);
int newCount = prefabCollection.GetObjectCount();
if (log)
{
string dirtiedMessage = (newCount > 0) ? GetDirtiedMessage(settings, dirtied) : string.Empty;
UnityDebug.Log($"Default prefab generator found {newCount} prefabs in {sw.ElapsedMilliseconds}ms.{dirtiedMessage}");
}
//Only set dirty if and save if prefabs were found.
if (newCount > 0)
{
EditorUtility.SetDirty(prefabCollection);
if (settings.SaveChanges)
AssetDatabase.SaveAssets();
}
}
/// <summary>
/// Returns settings parameter if not null, otherwise returns settings from configuration.
/// </summary>
internal static PrefabGeneratorConfigurations GetSettingsIfNull(PrefabGeneratorConfigurations settings) => (settings == null) ? Configuration.Configurations.PrefabGenerator : settings;
/// <summary>
/// Iterates folders building them into SpecifiedFolders.
/// </summary>
internal static List<SpecifiedFolder> GetSpecifiedFolders(List<string> strFolders)
{
List<SpecifiedFolder> results = new();
//Remove astericks.
foreach (string path in strFolders)
{
int pLength = path.Length;
if (pLength == 0)
continue;
bool recursive;
string p;
//If the last character indicates resursive.
if (path.Substring(pLength - 1, 1) == "*")
{
p = path.Substring(0, pLength - 1);
recursive = true;
}
else
{
p = path;
recursive = false;
}
p = GetPlatformPath(p);
//Path does not exist.
if (!Directory.Exists(p))
continue;
results.Add(new(p, recursive));
}
RemoveOverlappingFolders(results);
return results;
//Removes paths which may overlap each other, such as sub directories.
static void RemoveOverlappingFolders(List<SpecifiedFolder> specifiedFolders)
{
for (int z = 0; z < specifiedFolders.Count; z++)
{
for (int i = 0; i < specifiedFolders.Count; i++)
{
//Do not check against self.
if (i == z)
continue;
//Duplicate.
if (specifiedFolders[z].Path.Equals(specifiedFolders[i].Path, System.StringComparison.OrdinalIgnoreCase))
{
UnityDebug.LogError($"The same path is specified multiple times in the DefaultPrefabGenerator settings. Remove the duplicate to clear this error.");
specifiedFolders.RemoveAt(i);
break;
}
/* We are checking if i can be within
* z. This is only possible if i is longer
* than z. */
if (specifiedFolders[i].Path.Length < specifiedFolders[z].Path.Length)
continue;
/* Do not need to check if not recursive.
* Only recursive needs to be checked because
* a shorter recursive path could contain
* a longer path. */
if (!specifiedFolders[z].Recursive)
continue;
//Compare paths.
string zPath = GetPathWithSeparator(specifiedFolders[z].Path);
string iPath = zPath.Substring(0, zPath.Length);
//If paths match.
if (iPath.Equals(zPath, System.StringComparison.OrdinalIgnoreCase))
{
UnityDebug.LogError($"Path {specifiedFolders[i].Path} is included within recursive path {specifiedFolders[z].Path}. Remove path {specifiedFolders[i].Path} to clear this error.");
specifiedFolders.RemoveAt(i);
break;
}
}
}
string GetPathWithSeparator(string txt)
{
return txt.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
}
}
}
internal static string GetPlatformPath(string path)
{
if (string.IsNullOrEmpty(path))
return path;
path = path.Replace(@"\"[0], Path.DirectorySeparatorChar);
path = path.Replace(@"/"[0], Path.DirectorySeparatorChar);
return path;
}
/// <summary>
/// Returns the DefaultPrefabObjects file.
/// </summary>
internal static DefaultPrefabObjects GetDefaultPrefabObjects(PrefabGeneratorConfigurations settings = null)
{
if (settings == null)
settings = Configuration.Configurations.PrefabGenerator;
//If not using default prefabs then exit early.
if (!settings.Enabled)
return null;
//Load the prefab collection
string defaultPrefabsPath = settings.DefaultPrefabObjectsPath_Platform;
string fullDefaultPrefabsPath = (defaultPrefabsPath.Length > 0) ? Path.GetFullPath(defaultPrefabsPath) : string.Empty;
//If cached prefabs is not the same path as assetPath.
if (_cachedDefaultPrefabs != null)
{
string unityAssetPath = AssetDatabase.GetAssetPath(_cachedDefaultPrefabs);
string fullCachedPath = (unityAssetPath.Length > 0) ? Path.GetFullPath(unityAssetPath) : string.Empty;
if (fullCachedPath != fullDefaultPrefabsPath)
_cachedDefaultPrefabs = null;
}
//If cached is null try to get it.
if (_cachedDefaultPrefabs == null)
{
//Only try to load it if file exist.
if (File.Exists(fullDefaultPrefabsPath))
{
_cachedDefaultPrefabs = AssetDatabase.LoadAssetAtPath<DefaultPrefabObjects>(defaultPrefabsPath);
if (_cachedDefaultPrefabs == null)
{
//If already retried then throw an error.
if (_retryRefreshDefaultPrefabs)
{
UnityDebug.LogError("DefaultPrefabObjects file exists but it could not be loaded by Unity. Use the Fish-Networking menu -> Utility -> Refresh Default Prefabs to refresh prefabs.");
}
else
{
UnityDebug.Log("DefaultPrefabObjects file exists but it could not be loaded by Unity. Trying to reload the file next frame.");
_retryRefreshDefaultPrefabs = true;
}
return null;
}
}
}
#if PARRELSYNC
if (!ParrelSync.ClonesManager.IsClone() && ParrelSync.Preferences.AssetModPref.Value)
{
#endif
if (_cachedDefaultPrefabs == null)
{
string fullPath = Path.GetFullPath(defaultPrefabsPath);
UnityDebug.Log($"Creating a new DefaultPrefabsObject at {fullPath}.");
string directory = Path.GetDirectoryName(fullPath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
AssetDatabase.Refresh();
}
_cachedDefaultPrefabs = ScriptableObject.CreateInstance<DefaultPrefabObjects>();
AssetDatabase.CreateAsset(_cachedDefaultPrefabs, defaultPrefabsPath);
AssetDatabase.SaveAssets();
}
#if PARRELSYNC
}
#endif
if (_cachedDefaultPrefabs != null && _retryRefreshDefaultPrefabs)
UnityDebug.Log("DefaultPrefabObjects found on the second iteration.");
return _cachedDefaultPrefabs;
}
/// <summary>
/// Called every frame the editor updates.
/// </summary>
private static void OnEditorUpdate()
{
if (!_retryRefreshDefaultPrefabs)
return;
GenerateFull();
_retryRefreshDefaultPrefabs = false;
}
/// <summary>
/// Called by Unity when assets are modified.
/// </summary>
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
if (Application.isPlaying)
return;
//If retrying next frame don't bother updating, next frame will do a full refresh.
if (_retryRefreshDefaultPrefabs)
return;
//Post process is being ignored. Could be temporary or user has disabled this feature.
if (IgnorePostProcess)
return;
/* Don't iterate if updating or compiling as that could cause an infinite loop
* due to the prefabs being generated during an update, which causes the update
* to start over, which causes the generator to run again, which... you get the idea. */
if (EditorApplication.isCompiling)
return;
DefaultPrefabObjects prefabCollection = GetDefaultPrefabObjects();
if (prefabCollection == null)
return;
PrefabGeneratorConfigurations settings = Configuration.Configurations.PrefabGenerator;
if (prefabCollection.GetObjectCount() == 0)
{
//If there are no prefabs then do a full rebuild. Odds of there being none are pretty much nill.
GenerateFull(settings);
}
else
{
int totalChanges = importedAssets.Length + deletedAssets.Length + movedAssets.Length + movedFromAssetPaths.Length;
//Nothing has changed. This shouldn't occur but unity is funny so we're going to check anyway.
if (totalChanges == 0)
return;
//Normalizes path.
string dpoPath = Path.GetFullPath(settings.DefaultPrefabObjectsPath_Platform);
//If total changes is 1 and the only changed file is the default prefab collection then do nothing.
if (totalChanges == 1)
{
//Do not need to check movedFromAssetPaths because that's not possible for this check.
if ((importedAssets.Length == 1 && Path.GetFullPath(importedAssets[0]) == dpoPath) || (deletedAssets.Length == 1 && Path.GetFullPath(deletedAssets[0]) == dpoPath) || (movedAssets.Length == 1 && Path.GetFullPath(movedAssets[0]) == dpoPath))
return;
/* If the only change is an import then check if the imported file
* is the same as the last, and if so check into returning early.
* For some reason occasionally when files are saved unity runs postprocess
* multiple times on the same file. */
string imported = (importedAssets.Length == 1) ? importedAssets[0] : null;
if (imported != null && imported == _lastSingleImportedAsset)
{
//If here then the file is the same. Make sure it's already in the collection before returning.
System.Type assetType = AssetDatabase.GetMainAssetTypeAtPath(imported);
//Not a gameObject, no reason to continue.
if (assetType != typeof(GameObject))
return;
NetworkObject nob = AssetDatabase.LoadAssetAtPath<NetworkObject>(imported);
//If is a networked object.
if (CanAddNetworkObject(nob, settings))
{
//Already added!
if (prefabCollection.Prefabs.Contains(nob))
return;
}
}
else if (imported != null)
{
_lastSingleImportedAsset = imported;
}
}
bool fullRebuild = settings.FullRebuild;
/* If updating FN. This needs to be done a better way.
* Parsing the actual version file would be better.
* I'll get to it next release. */
if (!_ranOnce)
{
_ranOnce = true;
fullRebuild = true;
}
//Other conditions which a full rebuild may be required.
else if (!fullRebuild)
{
const string fishnetVersionSave = "fishnet_version";
string savedVersion = EditorPrefs.GetString(fishnetVersionSave, string.Empty);
if (savedVersion != NetworkManager.FISHNET_VERSION)
{
fullRebuild = true;
EditorPrefs.SetString(fishnetVersionSave, NetworkManager.FISHNET_VERSION);
}
}
if (fullRebuild)
GenerateFull(settings);
else
GenerateChanged(importedAssets, deletedAssets, movedAssets, movedFromAssetPaths, settings);
}
}
/// <summary>
/// Returns true if a NetworkObject can be added to DefaultPrefabs.
/// </summary>
private static bool CanAddNetworkObject(NetworkObject networkObject, PrefabGeneratorConfigurations settings)
{
settings = GetSettingsIfNull(settings);
return networkObject != null && (networkObject.GetIsSpawnable() || !settings.SpawnableOnly);
}
}
}
#endif