using System.IO; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; namespace Ubisoft { public class HtAssetDatabase { private static readonly string EXTENSION_META = ".meta"; private static readonly string EXTENSION_SCENE = ".unity"; private static readonly string ASSETS_FOLDER = "Assets"; // Directory separator used by Unity is "/" regardless the platform private static readonly char UNITY_PATH_DIRECTORY_SEPARATOR = '/'; private static HashSet s_assetPathsToImport; public static bool NeedsToSaveAssets { get; set; } = false; public static bool IsAMetaFile(string unityPath) { return Path.GetExtension(unityPath) == EXTENSION_META; } public static bool IsASceneFile(string unityPath) { return Path.GetExtension(unityPath) == EXTENSION_SCENE; } public static string GetMetaFilePath(string unityPath) { return string.IsNullOrEmpty(unityPath) ? unityPath : unityPath + EXTENSION_META; } public static string UnityPathCombine(string unityPath1, string unityPath2) { return unityPath1 + UNITY_PATH_DIRECTORY_SEPARATOR + unityPath2; } public static string PlatformPathCombine(string platformPath1, string platformPath2) { return platformPath1 + Path.DirectorySeparatorChar + platformPath2; } /// /// Translates a Unity path to its corresponding platform path. /// /// Unity path to translate, which means '/' is used as a directory separator char. /// When true project root path will be added to the returned platform path. /// Returns the platform path (directory separator char depends on platform) corresponding to the Unity path passed in. public static string UnityPathToPlatformPath(string unityPath, bool includeProjectRoot = false) { string returnValue = string.IsNullOrEmpty(unityPath) ? unityPath : unityPath.Replace(UNITY_PATH_DIRECTORY_SEPARATOR, Path.DirectorySeparatorChar); if (includeProjectRoot) { returnValue = Path.GetFullPath(returnValue); } return returnValue; } /// /// Translates a platform path to its corresponding Unity path. /// /// Platform path to translate, which means that directory separator char depends on the platform. /// When true the returned project root path will be removed from the returned unity path /// so it will start with 'Assets'. /// Returns the Unity path ('/' is used as a directory separator char) corresponding to the platform path passed in. public static string PlatformPathToUnityPath(string platformPath, bool removeProjectRoot = false) { string returnValue = string.IsNullOrEmpty(platformPath) ? platformPath : platformPath.Replace(Path.DirectorySeparatorChar, UNITY_PATH_DIRECTORY_SEPARATOR); if (removeProjectRoot) { returnValue = returnValue.Replace(Application.dataPath, $"{ASSETS_FOLDER}"); } return returnValue; } public static string UnityPathGetDirectoryName(string unityPath) { string returnValue = unityPath; if (!string.IsNullOrEmpty(unityPath)) { returnValue = Path.GetDirectoryName(UnityPathToPlatformPath(unityPath)); returnValue = PlatformPathToUnityPath(returnValue); } return returnValue; } /// /// Returns <true if a meta file for the file or folder passed as an argument exists /// /// Unity path to a file or folder that we want to know if it has a meta file public static bool ExistsMeta(string unityPath) { string platformPath = UnityPathToPlatformPath(unityPath); return HtSystemIO.File_Exists(GetMetaFilePath(platformPath)); } public static bool ExistsFile(string unityPath) { string platformPath = UnityPathToPlatformPath(unityPath); return HtSystemIO.File_Exists(platformPath); } public static bool ExistsFolder(string unityPath) { string platformPath = UnityPathToPlatformPath(unityPath); return Directory.Exists(platformPath); } public static string[] DirectoryGetFiles(string unityPath) { string[] returnValue = Directory.GetFiles(UnityPathToPlatformPath(unityPath)); int count = returnValue.Length; for (int i = 0; i < count; i++) { returnValue[i] = PlatformPathToUnityPath(returnValue[i]); } return returnValue; } public static string[] DirectoryGetDirectories(string unityPath) { string[] returnValue; if (ExistsFolder(unityPath)) { returnValue = Directory.GetDirectories(UnityPathToPlatformPath(unityPath)); int count = returnValue.Length; for (int i = 0; i < count; i++) { returnValue[i] = PlatformPathToUnityPath(returnValue[i]); } } else { returnValue = new string[0]; } return returnValue; } public static string UnityPathGetFileName(string unityPath) { return Path.GetFileName(UnityPathToPlatformPath(unityPath)); } public static string UnityPathGetFileNameWithoutExtension(string unityPath) { return Path.GetFileNameWithoutExtension(UnityPathToPlatformPath(unityPath)); } /// /// Returns the last entry of a unity path /// Example: /// GetPathLastEntry("dir_01/dir_02/filename") returns "fileName" /// GetPathLastEntry("dir_01/dir_02") returns "dir_02" /// GetPathLastEntry("") returns "" /// /// Source unity path /// Returns the last entry of a path public static string GetUnityPathLastEntry(string unityPath) { return string.IsNullOrEmpty(unityPath) ? unityPath : unityPath.Split(UNITY_PATH_DIRECTORY_SEPARATOR).Last(); } /// /// Returns the part of path that goes after directory. /// Example: /// GetUnityPathFromDirectory("dir_01/dir_02/dir_03/filename, "dir_02", true) returns dir_02/dir_03/filename /// GetUnityPathFromDirectory("dir_01/dir_02/dir_03/filename, "dir_02", false) returns dir_03/filename /// GetUnityPathFromDirectory("dir_01/dir_02/dir_03/filename, "dir_xx", false) returns null /// /// Source path /// Directory to search for /// Whether or not directory should be part of the returned value /// Returns the part of path that goes after directory. It returns null if path doesn't /// contain directory public static string GetUnityPathFromDirectory(string unityPath, string directory, bool includeDirectory) { string returnValue = null; if (!string.IsNullOrEmpty(unityPath) && !string.IsNullOrEmpty(directory)) { returnValue = HtSystemIO.GetPathFromDirectory(UnityPathToPlatformPath(unityPath), directory, includeDirectory); returnValue = PlatformPathToUnityPath(returnValue); } return returnValue; } public static void CreateFolder(string unityPath) { // Make sure unityPath is valid and it doesn't already exist if (!string.IsNullOrEmpty(unityPath) && !ExistsFolder(unityPath)) { string[] folders = unityPath.Split(UNITY_PATH_DIRECTORY_SEPARATOR); string parentPath; string tempPath; int count = folders.Length; if (count > 1) { parentPath = folders[0]; if (parentPath == ASSETS_FOLDER) { for (int i = 1; i < count; i++) { tempPath = UnityPathCombine(parentPath, folders[i]); if (!ExistsFolder(tempPath)) { _ = AssetDatabase.CreateFolder(parentPath, folders[i]); AssetDatabase.Refresh(); } parentPath = tempPath; } } else { Debug.EditorLogError("Path " + Debug.FormatTextInCodeContext(unityPath) + " must start by " + Debug.FormatTextInCodeContext(ASSETS_FOLDER)); } } } } public static void CopyFolder(string srcUnityPath, string dstUnityPath) { if (ExistsFolder(srcUnityPath)) { if (!ExistsFolder(dstUnityPath)) { CreateFolder(dstUnityPath); } // Copy files string newPath; string[] files = DirectoryGetFiles(srcUnityPath); int count = files.Length; for (int i = 0; i < count; i++) { // Metas are not copied because we are not moving the assets, we are copying them if (!IsAMetaFile(files[i])) { newPath = UnityPathCombine(dstUnityPath, Path.GetFileName(files[i])); _ = AssetDatabase.CopyAsset(files[i], newPath); } } // Copy subfolders string lastEntry; string[] directories = DirectoryGetDirectories(srcUnityPath); count = directories.Length; for (int i = 0; i < count; i++) { lastEntry = GetUnityPathLastEntry(directories[i]); newPath = UnityPathCombine(dstUnityPath, lastEntry); CopyFolder(directories[i], newPath); } AssetDatabase.Refresh(); } } /// /// Deletes the folder in unityPath and its meta /// /// Unity path to the folder to delete /// When true AssetDatabase.Refresh() is called. This is the default value /// Use false when you know you will call AssetDatabase.Refresh() manually after a batch of operations /// that modify AssetDatabase public static void DeleteFolder(string unityPath, bool refreshDB = true) { bool needsToRefresh = false; if (ExistsFolder(unityPath)) { string path = UnityPathToPlatformPath(unityPath); DeletePlatformFolder(path, true); needsToRefresh = true; } if (ExistsMeta(unityPath)) { string metaFilePath = GetMetaFilePath(unityPath); File.Delete(UnityPathToPlatformPath(metaFilePath)); needsToRefresh = true; } if (needsToRefresh && refreshDB) { AssetDatabase.Refresh(); } } public static void DeletePlatformFolder(string platformPath, bool recursive) { string directoryPlatformPath = Path.GetFullPath(platformPath); if (HtSymlinkUtility.IsASymlink(directoryPlatformPath)) { HtSymlinkUtility.DeleteSymlink(directoryPlatformPath); } else { if (recursive) { string[] paths = Directory.GetDirectories(platformPath); for (int i = paths.Length - 1; i > -1; i--) { DeletePlatformFolder(paths[i], recursive); } } Directory.Delete(platformPath, recursive); } } /// /// Copy the file in srcUnityPath to dstUnityPath /// /// Unity path to the file to copy /// Unity path where to copy the file to /// When true AssetDatabase.Refresh() is called. This is the default value /// Use false when you know you will call AssetDatabase.Refresh() manually after a batch of operations /// that modify AssetDatabase public static void CopyFile(string srcUnityPath, string dstUnityPath, bool refreshDB = true) { bool needsToRefresh = false; if (ExistsFile(srcUnityPath)) { HtSystemIO.CopyFile(UnityPathToPlatformPath(srcUnityPath), UnityPathToPlatformPath(dstUnityPath)); needsToRefresh = true; } if (needsToRefresh && refreshDB) { AssetDatabase.Refresh(); } } /// /// Deletes the file in unityPath and its meta /// /// Unity path to the file to delete /// When true AssetDatabase.Refresh() is called. This is the default value /// Use false when you know you will call AssetDatabase.Refresh() manually after a batch of operations /// that modify AssetDatabase public static void DeleteFile(string unityPath, bool refreshDB = true) { bool needsToRefresh = false; if (ExistsFile(unityPath)) { File.Delete(UnityPathToPlatformPath(unityPath)); needsToRefresh = true; } if (ExistsMeta(unityPath)) { string metaFilePath = GetMetaFilePath(unityPath); File.Delete(UnityPathToPlatformPath(metaFilePath)); needsToRefresh = true; } if (needsToRefresh && refreshDB) { AssetDatabase.Refresh(); } } public static void DeleteAsset(string unityPath, bool refreshDB = true) { if (AssetDatabase.IsValidFolder(unityPath)) { DeleteFolder(unityPath, refreshDB); } else { DeleteFile(unityPath, refreshDB); } } public static void RefreshObject(Object value) { if (value != null) { // Make sure packageSuite is saved, otherwise its properties can get lost after exiting play mode EditorUtility.SetDirty(value); AssetDatabase.Refresh(); // Import the asset so new objects added to this SO are shown on editor AddAssetToImport(value); } } public static void AddObjectToAsset(Object child, Object parent) { if (child != null && parent != null) { string path = AssetDatabase.GetAssetPath(parent.GetInstanceID()); AssetDatabase.AddObjectToAsset(child, path); RefreshObject(parent); } } public static void ClearAssetsInsideScriptableObject(Object parent) { if (parent != null) { string path = AssetDatabase.GetAssetPath(parent.GetInstanceID()); Object[] assets = AssetDatabase.LoadAllAssetsAtPath(path); int count = 0; for (int i = 0; i < assets.Length; i++) { Object asset = assets[i]; if (asset == null || AssetDatabase.IsMainAsset(asset) || asset is GameObject || asset is Component) { continue; } else { AssetDatabase.RemoveObjectFromAsset(asset); // Do not save assets since we'll call this function only once when we're doing with destroying DestroyImmediate(asset, true, false); count++; } } if (count > 0) { AssetDatabase.SaveAssets(); RefreshObject(parent); } } } public static void DestroyImmediate(Object obj, bool allowDestroying, bool saveAssets = true) { Object.DestroyImmediate(obj, allowDestroying); // Unity 2020 requires to call SaveAssets after destroying an object that is added as an asset to a ScriptableObject, // otherwise the object will still be part of the ScriptableObject if (saveAssets) { AssetDatabase.SaveAssets(); } } private static void AddAssetToImport(Object value) { if (value != null) { if (s_assetPathsToImport == null) { s_assetPathsToImport = new HashSet(); } string path = AssetDatabase.GetAssetPath(value.GetInstanceID()); if (!string.IsNullOrEmpty(path)) { NeedsToSaveAssets = true; _ = s_assetPathsToImport.Add(path); } } } public static void Update() { // We save assets in Update() so we only do it once and because Unity 2020 complains if we do it in the middle of some stuff if (NeedsToSaveAssets) { AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); NeedsToSaveAssets = false; } if (s_assetPathsToImport != null) { foreach (string path in s_assetPathsToImport) { AssetDatabase.ImportAsset(path); } s_assetPathsToImport.Clear(); } } } }