using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
namespace Ubisoft.Hotel.Package.Editor
{
///
/// Class responsible for performing some basic tasks required to install Hotel packages even though Hotel PackageManager package is not
/// installed which may be convenient while developing Hotel.
///
public class PackageManagerHelper : IPreprocessBuildWithReport
{
private static readonly string PACKAGE_LINK_XML_FILENAME = "link.xml";
private static readonly string PACKAGE_PLUGINS_FOLDER = "Plugins";
private static readonly string PACKAGE_TOOLS_FOLDER = "Tools";
///
/// The Verbatim folder is meant to store stuff that needs to be copied as it is over to app space exactly. It typically contains meta files.
///
private static readonly string PACKAGE_VERBATIM_FOLDER = "Verbatim";
private static readonly string HOTEL = "Hotel";
public static readonly string ASSETS_HOTEL_ROOT_PATH = $"Assets/{HOTEL}";
public static readonly string ASSETS_HOTEL_EDITOR_ROOT_PATH = $"{ASSETS_HOTEL_ROOT_PATH}/Editor";
public static readonly string ASSETS_HOTEL_RUNTIME_ROOT_PATH = $"{ASSETS_HOTEL_ROOT_PATH}/Runtime";
public static readonly string ASSETS_HOTEL_STREAMING_ASSETS_ROOT_PATH = $"Assets/StreamingAssets/{HOTEL}";
public static readonly string ASSETS_HOTEL_EDITOR_ASSETS_PATH = $"{ASSETS_HOTEL_EDITOR_ROOT_PATH}/Assets";
public static readonly string ASSETS_HOTEL_EDITOR_SCRIPTS_PATH = $"{ASSETS_HOTEL_EDITOR_ROOT_PATH}/Scripts";
///
/// Unity path to the temporary folder where all packages link.xml files are stored before building. These files are only required before building to
/// make sure that code that is not supposed to be stripped out is included in the build
///
private static readonly string ASSETS_HOTEL_EDITOR_PACKAGE_LINKS_PATH = $"{ASSETS_HOTEL_EDITOR_ROOT_PATH}/PackageLinks";
public static readonly string ASSETS_HOTEL_PLUGINS_PATH = $"{ASSETS_HOTEL_ROOT_PATH}/{PACKAGE_PLUGINS_FOLDER}";
public static readonly string ASSETS_HOTEL_EDITOR_TOOLS_PATH = $"{ASSETS_HOTEL_EDITOR_ROOT_PATH}/{PACKAGE_TOOLS_FOLDER}";
public static readonly string ASSETS_HOTEL_EDITOR_VERBATIM_PATH = $"{ASSETS_HOTEL_EDITOR_ROOT_PATH}/{PACKAGE_VERBATIM_FOLDER}";
public static readonly string ASSETS_HOTEL_RUNTIME_VERBATIM_PATH = $"{ASSETS_HOTEL_RUNTIME_ROOT_PATH}/{PACKAGE_VERBATIM_FOLDER}";
private static readonly string PACKAGE_EDITOR_ROOT_PATH = "Editor";
private static readonly string PACKAGE_RUNTIME_ROOT_PATH = "Runtime";
///
/// Path inside the package to link.xml file
///
private static readonly string PACKAGE_LINK_XML_PATH = $"{PACKAGE_EDITOR_ROOT_PATH}/{PACKAGE_LINK_XML_FILENAME}";
///
/// Path inside the package to Plugins folder
///
private static readonly string PACKAGE_PLUGINS_PATH = $"{PACKAGE_EDITOR_ROOT_PATH}/{PACKAGE_PLUGINS_FOLDER}";
///
/// Path inside the package to Tools folder
///
private static readonly string PACKAGE_TOOLS_PATH = $"{PACKAGE_EDITOR_ROOT_PATH}/{PACKAGE_TOOLS_FOLDER}";
private static readonly string RESOURCES_DIRECTORYNAME = "Resources";
///
/// Directory where to put files that this package needs to move to Hotel/Resources in App space
///
private static readonly string RESOURCES_PATH = "Editor/" + RESOURCES_DIRECTORYNAME;
private static ListRequest PackagesRequest { get; set; }
private static List>> OnProcessPackagesDoneCallbacks { get; set; }
#if !HT_PACKAGE_PACKAGEMANAGER_PROD
[MenuItem("Hotel/Core/Package/Update app space with packages configuration")]
internal static void UpdatePackages()
{
Patch(true);
}
#endif
[InitializeOnLoadMethod]
#pragma warning disable IDE0051 // Called by Unity
static void OnLoad()
#pragma warning restore IDE0051
{
#if !HT_PACKAGE_PACKAGEMANAGER_PROD
Init();
const string projectOpenedKey = "ProjectOpened";
if (!SessionState.GetBool(projectOpenedKey, false) && EditorApplication.isPlayingOrWillChangePlaymode == false)
{
SessionState.SetBool(projectOpenedKey, true);
Patch(true);
}
#endif
}
public static void Init()
{
}
public static void ProcessPackages(Action> onDone)
{
if (onDone != null)
{
if (OnProcessPackagesDoneCallbacks == null)
{
OnProcessPackagesDoneCallbacks = new List>>();
}
OnProcessPackagesDoneCallbacks.Add(onDone);
}
if (PackagesRequest == null)
{
PackagesRequest = Client.List(); // List packages installed for the project
EditorApplication.update += UpdatePackagesRequest;
}
}
private static void UpdatePackagesRequest()
{
if (PackagesRequest.IsCompleted)
{
EditorApplication.update -= UpdatePackagesRequest;
HashSet packageNames = new HashSet();
if (PackagesRequest.Status == StatusCode.Success)
{
foreach (var package in PackagesRequest.Result)
{
_ = packageNames.Add(package.name);
foreach (var dependency in package.resolvedDependencies)
{
_ = packageNames.Add(dependency.name);
}
}
DoProcessPackages(packageNames, GetHotelPackages());
}
else if (PackagesRequest.Status >= StatusCode.Failure)
{
Debug.EditorLogError("Error when retrieving packages: " + PackagesRequest.Error.message);
}
PackagesRequest = null;
if (OnProcessPackagesDoneCallbacks != null)
{
foreach (Action> callback in OnProcessPackagesDoneCallbacks)
{
callback(packageNames);
}
OnProcessPackagesDoneCallbacks.Clear();
}
}
}
public static List GetHotelPackages()
{
List returnValue = new List();
Type packageType = typeof(Package);
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (Type t in asm.GetTypes())
{
if (packageType != t && packageType.IsAssignableFrom(t))
{
returnValue.Add((Package)Activator.CreateInstance(t));
}
}
}
return returnValue;
}
public static void PrunePackages()
{
string unityPath = ASSETS_HOTEL_EDITOR_PACKAGE_LINKS_PATH;
if (HtAssetDatabase.ExistsFolder(unityPath))
{
HtAssetDatabase.DeleteFolder(unityPath);
}
// Plugins
unityPath = ASSETS_HOTEL_PLUGINS_PATH;
if (HtAssetDatabase.ExistsFolder(unityPath))
{
HtAssetDatabase.DeleteFolder(unityPath);
}
// Editor Verbatim
unityPath = ASSETS_HOTEL_EDITOR_VERBATIM_PATH;
if (HtAssetDatabase.ExistsFolder(unityPath))
{
HtAssetDatabase.DeleteFolder(unityPath);
}
HtAssetDatabase.CreateFolder(unityPath);
// Runtime Verbatim
unityPath = ASSETS_HOTEL_RUNTIME_VERBATIM_PATH;
if (HtAssetDatabase.ExistsFolder(unityPath))
{
HtAssetDatabase.DeleteFolder(unityPath);
}
HtAssetDatabase.CreateFolder(unityPath);
// Tools
unityPath = ASSETS_HOTEL_EDITOR_TOOLS_PATH;
if (HtAssetDatabase.ExistsFolder(unityPath))
{
HtAssetDatabase.DeleteFolder(unityPath);
}
}
private static void DoProcessPackages(HashSet packageNames, List hotelPackages)
{
PrunePackages();
HtAssetDatabase.CreateFolder(ASSETS_HOTEL_EDITOR_PACKAGE_LINKS_PATH);
HtAssetDatabase.CreateFolder(ASSETS_HOTEL_PLUGINS_PATH);
HtAssetDatabase.CreateFolder(ASSETS_HOTEL_EDITOR_VERBATIM_PATH);
HtAssetDatabase.CreateFolder(ASSETS_HOTEL_RUNTIME_VERBATIM_PATH);
HtAssetDatabase.CreateFolder(ASSETS_HOTEL_EDITOR_TOOLS_PATH);
// Store some information that might be required by some tools, namely:
// -)Unity Editor path
// -)Project path
string editorPath = EditorApplication.applicationPath;
#if UNITY_EDITOR_WIN
// Use quotes just in case the path contains a whitespace
editorPath = $"\"{editorPath}\"";
#else
editorPath = $"{editorPath}/Contents/MacOS/Unity";
#endif
WriteSettingsFile(ASSETS_HOTEL_EDITOR_TOOLS_PATH, "unity_editor_path.txt", editorPath);
// Application.dataPath contains "Asset" folder so we need to take that into account
WriteSettingsFile(ASSETS_HOTEL_EDITOR_TOOLS_PATH, "project_root_path.txt", HtAssetDatabase.PlatformPathCombine(UnityEngine.Application.dataPath, ".."));
if (packageNames != null)
{
// Packages link.xml files are copied into this package right before building, this way we make sure that only packages that are included in
// the build are taken into consideration
string platformDstLinkPath = HtAssetDatabase.UnityPathToPlatformPath(ASSETS_HOTEL_EDITOR_PACKAGE_LINKS_PATH, true);
string unityResourcesPath = AssetManager.HOTEL_ASSETS_RESOURCES_PATH;
string platformDstResourcesPath = HtAssetDatabase.UnityPathToPlatformPath(unityResourcesPath, true);
// Loop through all dependencies and copy link.xml file, if exists, into the Hotel folder dedicated to store all packages link.xml files
foreach (var packageName in packageNames)
{
if (UnityBuildProperty.IsAHotelPackage(packageName))
{
// Package link.xml
ProcessPackageLinkXml(packageName, platformDstLinkPath);
// Package Resources
ProcessPackageResources(packageName, platformDstResourcesPath);
// Package Plugins
ProcessPackagePlugins(packageName, ASSETS_HOTEL_PLUGINS_PATH);
// Package Tools
ProcessPackageTools(packageName, ASSETS_HOTEL_EDITOR_TOOLS_PATH);
// Package Editor Verbatim folder
ProcessPackageVerbatim(packageName, ASSETS_HOTEL_EDITOR_VERBATIM_PATH, true);
// Package Runtime Verbatim folder
ProcessPackageVerbatim(packageName, ASSETS_HOTEL_RUNTIME_VERBATIM_PATH, false);
}
}
}
// Every hotel packages implementing Package class has its chance to apply its settings
foreach (Package package in hotelPackages)
{
// Make sure package name is in packageNames
if (packageNames.Contains(package.Name))
{
package.OnInstallOrUpdate();
}
}
}
private static void WriteSettingsFile(string platformFolder, string fileName, string value)
{
string platformPath = HtAssetDatabase.PlatformPathCombine(platformFolder, fileName);
StreamWriter sw = File.CreateText(platformPath);
sw.WriteLine(value);
sw.Close();
}
private static void ProcessPackageLinkXml(string packageName, string platformRootDestPath)
{
string unityAssetPath = HtAssetUtility.GetAssetInPackagePath(packageName, PACKAGE_LINK_XML_PATH);
string platformPath = HtAssetDatabase.UnityPathToPlatformPath(unityAssetPath, true);
if (File.Exists(platformPath))
{
string platformDestPath = $"{platformRootDestPath}{Path.DirectorySeparatorChar}{packageName}";
if (!Directory.Exists(platformDestPath))
{
_ = Directory.CreateDirectory(platformDestPath);
}
platformDestPath += Path.DirectorySeparatorChar + PACKAGE_LINK_XML_FILENAME;
File.Copy(platformPath, platformDestPath);
}
}
private static void ProcessPackageResources(string packageName, string platformRootDestPath)
{
// Check if the dst folder already contains the files required by this package. If so then no files need to be moved into app space
// because those files may have been changed by the consumer
string platformDestPath = $"{platformRootDestPath}{Path.DirectorySeparatorChar}{packageName}";
string unityAssetPath = HtAssetUtility.GetAssetInPackagePath(packageName, RESOURCES_PATH);
string platformPath = HtAssetDatabase.UnityPathToPlatformPath(unityAssetPath, true);
if (Directory.Exists(platformPath))
{
IEnumerable appFilePaths = Directory.Exists(platformDestPath) ? HtSystemIO.GetAllFiles(platformDestPath, true) : new string[0];
IEnumerable packageFilePaths = HtSystemIO.GetAllFiles(platformPath, true);
HashSet filesToCreate = new HashSet(packageFilePaths);
HashSet filesToDelete = new HashSet();
// Loops through all files in app space:
// -)Those files in package space that also exist in app space mustn't be created again because the consumer might have changed them
// -)Those files in app space that don't exist in package space must be deleted because the package don't support them anymore
string filePath;
foreach (string path in appFilePaths)
{
filePath = path.Replace(platformDestPath, platformPath);
// If the file already exists in app space then the one in app space has precedence
// If it's actually a directory then it shouldn't be copied because we're copying files one by one
if (filesToCreate.Contains(filePath) || HtSystemIO.Directory_Exists(filePath))
{
_ = filesToCreate.Remove(filePath);
}
else
{
_ = filesToDelete.Add(path);
}
}
if (filesToCreate.Count > 0)
{
if (!Directory.Exists(platformDestPath))
{
_ = Directory.CreateDirectory(platformDestPath);
}
string newPath;
string platformFileDestPath;
foreach (string path in filesToCreate)
{
newPath = path.Replace(platformPath, "");
platformFileDestPath = platformDestPath;
if (!newPath.StartsWith("" + Path.DirectorySeparatorChar))
{
platformFileDestPath += Path.DirectorySeparatorChar;
}
platformFileDestPath += newPath;
// A directory is not copied because we're copying files one by one and if we copied
// the directory then its files would be copied twice
//
// A meta file is not copied either to avoid duplicates. Their meta files will be created by Unity when they're copied
// inside Assets folder
if (HtSystemIO.Directory_Exists(path))
{
_ = Directory.CreateDirectory(platformFileDestPath);
}
else if (!HtAssetDatabase.IsAMetaFile(HtAssetDatabase.PlatformPathToUnityPath(path)))
{
File.Copy(path, platformFileDestPath);
}
}
}
if (filesToDelete.Count > 0)
{
foreach (string path in filesToDelete)
{
if (HtSystemIO.Directory_Exists(path))
{
Directory.Delete(path, true);
}
else if (File.Exists(path))
{
File.Delete(path);
}
}
if (HtSystemIO.Directory_FileCount(platformPath) == 0)
{
Directory.Delete(platformPath);
}
}
}
}
private static void ProcessPackagePlugins(string packageName, string rootDstUnityPath)
{
string srcUnityPath = HtAssetUtility.GetAssetInPackagePath(packageName, PACKAGE_PLUGINS_PATH);
if (HtAssetDatabase.ExistsFolder(srcUnityPath))
{
// Android
string thisSrcUnityPath = $"{srcUnityPath}/Android";
if (HtAssetDatabase.ExistsFolder(thisSrcUnityPath))
{
string dstUnityPath = $"{rootDstUnityPath}/Android/{packageName}.androidlib";
if (!HtAssetDatabase.ExistsFolder(dstUnityPath))
{
HtAssetDatabase.CreateFolder(dstUnityPath);
}
HtAssetDatabase.CopyFolder(thisSrcUnityPath, dstUnityPath);
}
// iOS
thisSrcUnityPath = $"{srcUnityPath}/iOS";
if (HtAssetDatabase.ExistsFolder(thisSrcUnityPath))
{
string dstUnityPath = $"{rootDstUnityPath}/iOS/{packageName}";
if (!HtAssetDatabase.ExistsFolder(dstUnityPath))
{
HtAssetDatabase.CreateFolder(dstUnityPath);
}
HtAssetDatabase.CopyFolder(thisSrcUnityPath, dstUnityPath);
}
}
}
private static void ProcessPackageTools(string packageName, string rootDstUnityPath)
{
string srcUnityPath = HtAssetUtility.GetAssetInPackagePath(packageName, PACKAGE_TOOLS_PATH);
if (HtAssetDatabase.ExistsFolder(srcUnityPath))
{
string dstUnityPath = $"{rootDstUnityPath}/{packageName}";
if (!HtAssetDatabase.ExistsFolder(dstUnityPath))
{
HtAssetDatabase.CreateFolder(dstUnityPath);
}
HtAssetDatabase.CopyFolder(srcUnityPath, dstUnityPath);
}
}
///
/// Copy the content of Verbatim folder over to app space. Add stuff to this folder when you need that stuff to
/// by copied verbatim, typically when you need metas to preserve.
///
/// Name of the package which verbatim content needs to be copied to app space.
/// Unity path to the destination folder.
private static void ProcessPackageVerbatim(string packageName, string rootDstUnityPath, bool isEditor)
{
string srcUnityPath = HtAssetUtility.GetAssetInPackagePath(packageName, "");
if (!srcUnityPath.EndsWith("/"))
{
srcUnityPath += "/";
}
string srcPlatformPath = HtAssetDatabase.UnityPathToPlatformPath(srcUnityPath, true);
string srcPlatformFolder = isEditor ? PACKAGE_EDITOR_ROOT_PATH : PACKAGE_RUNTIME_ROOT_PATH;
string token = HtAssetDatabase.PlatformPathCombine(srcPlatformFolder, $"{PACKAGE_VERBATIM_FOLDER}~");
// Loop through all frist level subdirectories because a package might have some modules inside. Each submodule
// could have a Verbatim~ folder.
string subdirectoryName;
string candidatePlatformPath;
string[] directories = Directory.GetDirectories(srcPlatformPath);
int count = directories.Length;
for (int i = 0; i < count; i++)
{
subdirectoryName = directories[i].Replace($"{srcPlatformPath}", "");
if (subdirectoryName == PACKAGE_EDITOR_ROOT_PATH || subdirectoryName == PACKAGE_RUNTIME_ROOT_PATH)
{
candidatePlatformPath = subdirectoryName == srcPlatformFolder ? srcPlatformPath : null;
subdirectoryName = null;
}
else
{
candidatePlatformPath = directories[i];
}
if (!string.IsNullOrEmpty(candidatePlatformPath))
{
if (!candidatePlatformPath.EndsWith("" + Path.DirectorySeparatorChar))
{
candidatePlatformPath += Path.DirectorySeparatorChar;
}
candidatePlatformPath += token;
if (Directory.Exists(candidatePlatformPath))
{
CopyPackageVerbatim(candidatePlatformPath, rootDstUnityPath, packageName, subdirectoryName);
}
}
}
}
private static void CopyPackageVerbatim(string srcPlatformPath, string rootDstUnityPath, string packageName, string subdirectory)
{
if (Directory.Exists(srcPlatformPath))
{
string dstUnityPath = $"{rootDstUnityPath}/{packageName}";
if (!string.IsNullOrEmpty(subdirectory))
{
dstUnityPath += $"/{subdirectory}";
}
string dstPlatformPath = HtAssetDatabase.UnityPathToPlatformPath(dstUnityPath);
HtSystemIO.CopyDirectory(srcPlatformPath, dstPlatformPath);
}
}
public static void PrunePackagesResources(Dictionary packagesHandledByConsumer, Dependencies dependencies, string platformDstResourcesPath)
{
if (Directory.Exists(platformDstResourcesPath))
{
string[] directories = Directory.GetDirectories(platformDstResourcesPath);
string pathToReplace = $"{platformDstResourcesPath}{Path.DirectorySeparatorChar}";
string packageName;
foreach (string path in directories)
{
packageName = path.Replace(pathToReplace, "");
// If package name is not handled by the consumer anymore (it's not in the manifest or in the list of packages handled by the consumer) then
// it means that the consumer doesn't need that package anymore and its resources are deleted from the app space
if (!dependencies.ContainsDependency(packageName) &&
(packagesHandledByConsumer == null || !packagesHandledByConsumer.ContainsKey(packageName)))
{
Directory.Delete(path, true);
}
}
}
}
static void Patch(bool debugEnabled)
{
// if HT_PACKAGE_PACKAGEMANAGER_PROD is disabled then the consumer is not using the built-in system to deal enable/disable logging based on
// consumer's configuration. This situation typically happens when the consumer is a contributor developing a Hotel package.
// In this situation we want logging to be enabled
#if !HT_PACKAGE_PACKAGEMANAGER_PROD
Manifest manifest = GetManifest();
if (!manifest.Dependencies.ContainsDependency("com.ubisoft.hotel.packagemanager"))
{
ProcessPackages(null);
PatchDefineSymbols(true);
}
#endif
}
#if !HT_PACKAGE_PACKAGEMANAGER_PROD
private static Manifest GetManifest()
{
string json = UpmBuildProperty.ReadManifestJson();
return Manifest.Parse(json);
}
static void PatchDefineSymbols(bool debugEnabled)
{
BuildTargetGroup buildTargetGroup = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget);
HashSet symbols = HtPlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup);
// Make sure Logger works if Unity development is enabled
if (debugEnabled)
{
// Add Logger symbols so Debug is enabled
foreach (string symbol in Debug.HT_LOGGER_SYMBOLS)
{
_ = symbols.Add(symbol);
}
}
_ = symbols.Add("HT_PACKAGE_CORE_PROD");
HtPlayerSettings.SetScriptingDefineSymbolsForGroup(buildTargetGroup, symbols);
}
#endif
// It needs to be higher than Hotel PackagaManager.BuildProcessor's one to give it the chance to define HT_PACKAGE_PACKAGEMANAGER_PROD,
public int callbackOrder { get { return 1; } }
public void OnPreprocessBuild(BuildReport report)
{
Patch(EditorUserBuildSettings.development);
}
}
}