using System; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; namespace Ubisoft.Hotel.Package.Editor { public class AssetManager { public static readonly string HOTEL_ROOT_PATH = "Hotel"; public static readonly string ASSETS_FOLDER_NAME = "Assets"; public static readonly string ROOT_PATH = $"{HOTEL_ROOT_PATH}/{ASSETS_FOLDER_NAME}"; public static readonly string HOTEL_ASSETS_RESOURCES_PATH = $"{ROOT_PATH}/Resources"; private static readonly string REGISTRY_PATH = HOTEL_ROOT_PATH; private static readonly string REGISTRY_FULL_PATH = Path.GetFullPath(REGISTRY_PATH); private static readonly string REGISTRY_FILE_PATH = $"{REGISTRY_PATH}/assetsRegistry.json"; public static readonly string HOTEL_GIT_IGNORED_PATH = $"{HOTEL_ROOT_PATH}/Editor/GitIgnored"; public static readonly string ASSETS_HOTEL_GIT_IGNORED_PATH = GetDstUnityPathWithAssets($"{HOTEL_GIT_IGNORED_PATH}"); [Serializable] internal class FileOperationRecord { internal enum FileOperation { CopyFile, CreateDirectory, SymLink }; [SerializeField] #pragma warning disable IDE1006 // These fields names need to match the attribute names used in the json internal string path; [SerializeField] internal FileOperation fileOperation; #pragma warning restore IDE1006 // Naming Styles internal FileOperationRecord(string path, FileOperation fileOperation) { this.path = path; this.fileOperation = fileOperation; } } [Serializable] internal class FileOperationRegistry { [SerializeField] private List m_fileOperationRecords = new List(); public void Clear() { m_fileOperationRecords.Clear(); } public void AddFileOperation(string path, FileOperationRecord.FileOperation fileOperation) { FileOperationRecord record = new FileOperationRecord(path, fileOperation); m_fileOperationRecords.Add(record); } public void Unapply() { if (m_fileOperationRecords.Count > 0) { // Sort file operations so CreateDirectory ones are at the end because we need the directories to be // empty before deleting them m_fileOperationRecords.Sort(delegate (FileOperationRecord x, FileOperationRecord y) { int xOp = (int)x.fileOperation; int yOp = (int)y.fileOperation; if (xOp < yOp) { return -1; } else if (xOp > yOp) { return 1; } else { return 0; } }); string path; string unityPath; while (m_fileOperationRecords.Count > 0) { path = m_fileOperationRecords[0].path; unityPath = HtAssetDatabase.PlatformPathToUnityPath(path); switch (m_fileOperationRecords[0].fileOperation) { case FileOperationRecord.FileOperation.CopyFile: HtAssetDatabase.DeleteFile(unityPath, false); break; case FileOperationRecord.FileOperation.CreateDirectory: case FileOperationRecord.FileOperation.SymLink: HtAssetDatabase.DeleteFolder(unityPath, false); break; } m_fileOperationRecords.RemoveAt(0); } AssetDatabase.Refresh(); } } } [Serializable] public class OrderItem { private static readonly char[] CharsToTrim = new char[] { '/' }; [SerializeField] private string m_srcUnityPath; [SerializeField] private string m_dstUnityPath; [SerializeField] private bool m_useSymLink; [SerializeField] private bool m_useHotelAssetsRootPath; public OrderItem(string srcUnityPath, string dstUnityPath, bool useSymLink, bool useHotelAssetsRootPath = true) { SrcUnityPath = TrimPath(srcUnityPath); DstUnityPath = TrimPath(dstUnityPath); UseSymLink = useSymLink; UseHotelAssetsRootPath = useHotelAssetsRootPath; } public string SrcUnityPath { get { return m_srcUnityPath; } private set { m_srcUnityPath = value; } } public string DstUnityPath { get { return m_dstUnityPath; } private set { m_dstUnityPath = value; } } public bool UseSymLink { get { return m_useSymLink; } private set { m_useSymLink = value; } } public bool UseHotelAssetsRootPath { get { return m_useHotelAssetsRootPath; } private set { m_useHotelAssetsRootPath = value; } } private string TrimPath(string value) { string returnValue = value; if (!string.IsNullOrEmpty(value)) { returnValue = value.Trim(); returnValue = returnValue.TrimEnd(CharsToTrim); } return returnValue; } } private static FileOperationRegistry s_fileRegistry; /// /// Dictionary used when moving assets from Hotel to Assets folder /// key: relative path of every file to move /// value: full path of every file to move /// private static readonly Dictionary FilesToMove = new Dictionary(); private static FileOperationRegistry FileRegistry { get { if (s_fileRegistry == null) { string fullPath = Path.GetFullPath(REGISTRY_FILE_PATH); if (File.Exists(fullPath)) { string json = File.ReadAllText(fullPath); s_fileRegistry = JsonUtility.FromJson(json); } else { s_fileRegistry = new FileOperationRegistry(); } } return s_fileRegistry; } } public static string GetUnityPathInGitIgnored(string relativePath) { return $"{HOTEL_GIT_IGNORED_PATH}/{relativePath}"; } public static void Apply(List order, EPlatform platform) { FileOperationRegistry fileRegistry = FileRegistry; fileRegistry.Clear(); FilesToMove.Clear(); if (order != null && order.Count > 0) { foreach (OrderItem orderItem in order) { if (orderItem.UseSymLink) { ApplySymLink(fileRegistry, orderItem); } else { AddFilesToMoveByPlatform(orderItem.SrcUnityPath, orderItem.DstUnityPath, platform, orderItem.UseHotelAssetsRootPath); } } FileInfo dstFileInfo; string dstFile; foreach (KeyValuePair pair in FilesToMove) { if ((File.GetAttributes(pair.Key) & FileAttributes.Directory) != FileAttributes.Directory) { dstFile = pair.Value; dstFileInfo = new FileInfo(dstFile); if (!dstFileInfo.Directory.Exists) { fileRegistry.AddFileOperation(dstFileInfo.Directory.FullName, FileOperationRecord.FileOperation.CreateDirectory); dstFileInfo.Directory.Create(); } fileRegistry.AddFileOperation(dstFile, FileOperationRecord.FileOperation.CopyFile); File.Copy(pair.Key, dstFile, true); } } FilesToMove.Clear(); SaveRegistryToFile(); AssetDatabase.Refresh(); } } private static void ApplySymLink(FileOperationRegistry fileRegistry, OrderItem orderItem) { string srcUnityPath = orderItem.SrcUnityPath; if (orderItem.UseHotelAssetsRootPath) { srcUnityPath = $"{ROOT_PATH}/{srcUnityPath}"; } string dstUnityPath = GetDstUnityPathWithAssets(orderItem.DstUnityPath); string fullSrcPath = HtAssetDatabase.UnityPathToPlatformPath(srcUnityPath, true); string fullDstPath = HtAssetDatabase.UnityPathToPlatformPath(dstUnityPath, true); string dstLastEntry = HtSystemIO.GetPathLastEntry(fullDstPath); dstUnityPath = dstUnityPath.Replace($"/{dstLastEntry}", ""); if (!HtAssetDatabase.ExistsFolder(dstUnityPath)) { HtAssetDatabase.CreateFolder(dstUnityPath); } HtSymlinkUtility.Symlink(fullSrcPath, fullDstPath, false); fileRegistry.AddFileOperation(fullDstPath, FileOperationRecord.FileOperation.SymLink); } public static void AddOrderToGitIgnore(List order, HashSet pathsToIgnore) { if (order != null) { string fullSrcPath; string dstPath; foreach (OrderItem item in order) { dstPath = item.DstUnityPath; // If dstPath is empty and srcPath is a directory then all files in srcPath directory will be copied into Assets directory if (string.IsNullOrEmpty(dstPath)) { fullSrcPath = GetFullSrcPath(item.SrcUnityPath); if (HtSystemIO.Directory_Exists(fullSrcPath)) { IEnumerable files = HtSystemIO.GetAllFiles(fullSrcPath); string relativeFile; foreach (string file in files) { relativeFile = file.Replace($"{fullSrcPath}/", ""); if (IsAValidRelativeFile(relativeFile)) { InternalAddPathsToGitIgnore(pathsToIgnore, relativeFile); } } } else if (HtSystemIO.File_Exists(fullSrcPath)) { if (IsAValidRelativeFile(item.SrcUnityPath)) { InternalAddPathsToGitIgnore(pathsToIgnore, item.SrcUnityPath); } } } else if (IsAValidRelativeFile(dstPath)) { InternalAddPathsToGitIgnore(pathsToIgnore, dstPath); } } } } private static bool IsAValidRelativeFile(string value) { // .DS_Store macOS files are ignored return !string.IsNullOrEmpty(value) && !value.Contains(".DS_Store"); } public static void AddPathsToGitIgnore(HashSet pathsToIgnore) { _ = pathsToIgnore.Add(REGISTRY_FILE_PATH); _ = pathsToIgnore.Add(ASSETS_HOTEL_GIT_IGNORED_PATH); _ = pathsToIgnore.Add($"{ASSETS_HOTEL_GIT_IGNORED_PATH}/*"); } private static void InternalAddPathsToGitIgnore(HashSet pathsToIgnore, string relativePath) { if (pathsToIgnore != null && !string.IsNullOrEmpty(relativePath) && !relativePath.StartsWith(HOTEL_GIT_IGNORED_PATH)) { _ = pathsToIgnore.Add(GetDstUnityPathWithAssets(relativePath)); } } private static string GetDstUnityPathWithAssets(string unityPath) { return $"Assets/{unityPath}"; } private static void AddToFilesToMove(string srcUnityPath, string dstUnityPath) { string fullSrcPath = Path.GetFullPath(HtAssetDatabase.UnityPathToPlatformPath(srcUnityPath)); string fullDstPath = Path.GetFullPath(HtAssetDatabase.UnityPathToPlatformPath(GetDstUnityPathWithAssets(dstUnityPath))); string relativeFile; if (HtSystemIO.Directory_Exists(fullSrcPath)) { IEnumerable files = HtSystemIO.GetAllFiles(fullSrcPath); fullSrcPath += Path.DirectorySeparatorChar; foreach (string file in files) { relativeFile = file.Replace(fullSrcPath, ""); // If the relative file has already been moved then file is ignored since files are added to filesToMove respecting precedence if (!FilesToMove.ContainsKey(relativeFile) && IsAValidRelativeFile(relativeFile)) { FilesToMove.Add(file, $"{fullDstPath}{Path.DirectorySeparatorChar}{relativeFile}"); } } } else if (HtSystemIO.File_Exists(fullSrcPath)) { FilesToMove.Add(fullSrcPath, fullDstPath); } else { Debug.EditorLogError($"ERROR: {Debug.FormatTextInUserContext(srcUnityPath)} can't be moved because it doesn't exist"); } } private static string GetFullSrcPath(string srcUnityPath) { string returnValue = null; // Paths related to platforms have precedence string candidateSrcPath; string platformPath; string fullPath; List platformKeys = PlatformUtils.EPlatformKeys; for (int i = 0; i < platformKeys.Count && returnValue == null; ++i) { candidateSrcPath = $"{ROOT_PATH}/{platformKeys[i]}/{srcUnityPath}"; platformPath = HtAssetDatabase.UnityPathToPlatformPath(candidateSrcPath); fullPath = Path.GetFullPath(platformPath); if (HtSystemIO.Directory_Exists(fullPath) || HtSystemIO.File_Exists(fullPath)) { returnValue = fullPath; } } // If no files/directory was found in build target paths then we use the path as it is if (returnValue == null) { candidateSrcPath = $"{ROOT_PATH}/{srcUnityPath}"; platformPath = HtAssetDatabase.UnityPathToPlatformPath(candidateSrcPath); returnValue = Path.GetFullPath(platformPath); } return returnValue; } private static void AddFilesToMoveByPlatform(string srcUnityPath, string dstUnityPath, EPlatform platform, bool useHotelAssetsRootPath) { string candidateSrcPath = srcUnityPath; string platformPath; string fullPath; if (useHotelAssetsRootPath) { // currentPlaytformKey needs to be passed first because files of current platform have priority over DEFAULT_PLATFORM // A file is taken from DEFAULT_PLATFORM only if that file doesn't exist in currentPlatformKey folder string platformKey = PlatformUtils.EPlatformToKey(platform); if (!string.IsNullOrEmpty(platformKey)) { candidateSrcPath = $"{ROOT_PATH}/{platformKey}/{srcUnityPath}"; platformPath = HtAssetDatabase.UnityPathToPlatformPath(candidateSrcPath); fullPath = Path.GetFullPath(platformPath); if (!HtSystemIO.Directory_Exists(fullPath) && !HtSystemIO.File_Exists(fullPath)) { candidateSrcPath = $"{ROOT_PATH}/{srcUnityPath}"; } } } platformPath = HtAssetDatabase.UnityPathToPlatformPath(candidateSrcPath); fullPath = Path.GetFullPath(platformPath); // If dstUnityPath is empty and srcUnityPath is a file then we just use srcUnityPath as the destination file name if (HtSystemIO.File_Exists(fullPath) && string.IsNullOrEmpty(dstUnityPath)) { dstUnityPath = srcUnityPath; } AddToFilesToMove(candidateSrcPath, dstUnityPath); } public static void Unapply() { FileRegistry.Unapply(); SaveRegistryToFile(); } public static void SaveRegistryToFile() { if (s_fileRegistry != null) { string json = JsonUtility.ToJson(FileRegistry); if (!Directory.Exists(REGISTRY_FULL_PATH)) { _ = Directory.CreateDirectory(REGISTRY_FULL_PATH); } string fullPath = Path.GetFullPath(REGISTRY_FILE_PATH); File.WriteAllText(fullPath, json); } } } }