using System; using System.Collections.Generic; using System.IO; using Ubisoft.Hotel.Package.Editor; using UnityEditor; using UnityEngine; namespace Ubisoft.Hotel.PackageManager.Editor { public class PackageManager : ScriptableObject { public static readonly string PACKAGE_NAME = "com.ubisoft.hotel.packagemanager"; private static readonly string SETTINGS_FILENAME_NO_EXT = "hotelSettings"; public static readonly string SETTINGS_FILENAME = SETTINGS_FILENAME_NO_EXT + ".asset"; public static readonly string CONSUMER_BUILD_SUITE_PROVIDER_FILENAME_NO_EXT = AppSpaceBuildSuiteProvider.CONSUMER_PACKAGE_SUITE_PROVIDER_FILENAME_NO_EXT; private static readonly string CONSUMER_ASMDEF_FILENAME_NO_EXT = AppSpaceBuildSuiteProvider.CONSUMER_ASMDEF_FILENAME_NO_EXT; public static readonly string CONSUMER_BUILD_SUITE_PROVIDER_FILENAME = AppSpaceBuildSuiteProvider.CONSUMER_PACKAGE_SUITE_PROVIDER_FILENAME; public static readonly string PRECOMMIT_GIT_HOOK_HOTEL = "pre-commit_hotel"; public static readonly string ASSETS_HOTEL_EDITOR_ROOT_PATH = PackageManagerHelper.ASSETS_HOTEL_EDITOR_ROOT_PATH; public static readonly string ASSETS_HOTEL_EDITOR_HOTEL_ASSETS_PATH = PackageManagerHelper.ASSETS_HOTEL_EDITOR_ASSETS_PATH; private static readonly string ASSETS_HOTEL_EDITOR_SCRIPTS_PATH = $"{ASSETS_HOTEL_EDITOR_ROOT_PATH}/Scripts"; private static readonly string ASSETS_HOTEL_EDITOR_SCRIPTS_PACKAGE_MANAGER_PATH = $"{ASSETS_HOTEL_EDITOR_SCRIPTS_PATH}/PackageManager"; private static readonly string ASSETS_HOTEL_EDITOR_PACKAGE_MANAGER_PATH = $"{ASSETS_HOTEL_EDITOR_HOTEL_ASSETS_PATH}/{SETTINGS_FILENAME}"; private static readonly string ASSETS_HOTEL_EDITOR_CONSUMER_BUILD_SUITE_PROVIDER_PATH = $"{ASSETS_HOTEL_EDITOR_SCRIPTS_PACKAGE_MANAGER_PATH}/{CONSUMER_BUILD_SUITE_PROVIDER_FILENAME_NO_EXT }.cs"; private static readonly string ASSETS_HOTEL_EDITOR_CONSUMER_ASMDEF_PATH = $"{ASSETS_HOTEL_EDITOR_SCRIPTS_PACKAGE_MANAGER_PATH}/{CONSUMER_ASMDEF_FILENAME_NO_EXT}.asmdef"; public static readonly string ASSETS_HOTEL_EDITOR_PLUGGABLE_PACKAGES = $"{ASSETS_HOTEL_EDITOR_HOTEL_ASSETS_PATH}/pluggable_packages.txt"; private static readonly string ASSETS_HOTEL_ROOT_PATH = PackageManagerHelper.ASSETS_HOTEL_ROOT_PATH; private static readonly string ASSETS_HOTEL_RUNTIME_ROOT_PATH = PackageManagerHelper.ASSETS_HOTEL_RUNTIME_ROOT_PATH; private static readonly string ASSETS_HOTEL_PLUGINS_PATH = PackageManagerHelper.ASSETS_HOTEL_PLUGINS_PATH; private static readonly string ASSETS_HOTEL_EDITOR_TOOLS_PATH = PackageManagerHelper.ASSETS_HOTEL_EDITOR_TOOLS_PATH; private static readonly string ASSETS_HOTEL_EDITOR_VERBATIM_PATH = PackageManagerHelper.ASSETS_HOTEL_EDITOR_VERBATIM_PATH; private static readonly string ASSETS_HOTEL_STREAMING_ASSETS_ROOT_PATH = PackageManagerHelper.ASSETS_HOTEL_STREAMING_ASSETS_ROOT_PATH; internal static readonly string ASSETS_HOTEL_RESOURCES_ROOT_PATH = $"{ASSETS_HOTEL_ROOT_PATH}/Resources"; /// /// 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"; private static readonly string NEW_LINE = "\n"; public static readonly string ATT_APPLY_CHANGES_AUTOMATICALLY = "m_applyChangesAutomatically"; public static readonly string ATT_ASK_CONFIRMATION_BEFORE_SYNC = "m_askConfirmationBeforeSync"; private static readonly string PREFS_OPTIMISED_MODE_KEY = "HOTEL.PACKAGE_MANAGER.IS_OPTIMISED_MODE"; internal static bool NeedsToReloadPackageManager { get; set; } public static PackageManager LoadPackageManager(bool forceGenerate = false) { if (NeedsToReloadPackageManager) { forceGenerate = true; NeedsToReloadPackageManager = false; } PackageManager returnValue = AssetDatabase.LoadAssetAtPath(ASSETS_HOTEL_EDITOR_PACKAGE_MANAGER_PATH, typeof(PackageManager)) as PackageManager; if (returnValue == null || forceGenerate) { returnValue = CreatePackageManager(); } else { bool crcHasChanged = returnValue.CalculateCrc(); if (crcHasChanged) { Log("PackageManger: Recreating suites..."); bool generatePackageSuites = true; if (returnValue.AskConfirmationBeforeSync) { string msg = "Generated package suites are out of sync with code.\n\n"; msg += "It is recommended to keep them in sync, but be aware that syncing will destroy any changes that you may have made to current package suites.\n\n"; msg += $"If you choose 'No' remember that you can sync manually by hitting '{PackageManagerEditor.BUTTON_GENERATE_PACKAGE_SUITES}' "; msg += $"button on {SETTINGS_FILENAME_NO_EXT} menu.\n\n"; msg += $"Do you want to sync with {AppSpaceBuildSuiteProvider.CONSUMER_PACKAGE_SUITE_PROVIDER_FILENAME}?"; generatePackageSuites = EditorUtility.DisplayDialog("Hotel PackageManager", msg, "Yes", "No"); } if (generatePackageSuites) { HandleGitHooks(); returnValue.Generate(); } } } return returnValue; } private static PackageManager CreatePackageManager() { // Create Hotel/Assets directory string name = SETTINGS_FILENAME_NO_EXT; string assetsUnityPath = ASSETS_HOTEL_EDITOR_HOTEL_ASSETS_PATH; HtAssetDatabase.CreateFolder(assetsUnityPath); // Recreate PackageManager SO string unityPath = HtAssetDatabase.UnityPathCombine(assetsUnityPath, $"{name}.asset"); if (HtAssetDatabase.ExistsFile(unityPath)) { HtAssetDatabase.DeleteFile(unityPath); } PackageManager returnValue = HtUnityEditorFactory.CreateScriptableObject(typeof(PackageManager), assetsUnityPath, name) as PackageManager; returnValue.name = name; string srcDirectoryUnityPath = GetEditorAssetsUnityPath(); // Create Scripts folder HtAssetDatabase.CreateFolder(ASSETS_HOTEL_EDITOR_SCRIPTS_PACKAGE_MANAGER_PATH); // Copy ConsumerBuildSuiteProvider.cs into app space string dstUnityPath = ASSETS_HOTEL_EDITOR_CONSUMER_BUILD_SUITE_PROVIDER_PATH; if (!HtAssetDatabase.ExistsFile(dstUnityPath)) { string srcUnityPath = HtAssetDatabase.UnityPathCombine(srcDirectoryUnityPath, CONSUMER_BUILD_SUITE_PROVIDER_FILENAME_NO_EXT + ".txt"); HtAssetDatabase.CopyFile(srcUnityPath, dstUnityPath, false); } // Copy the assembly definition into app space dstUnityPath = ASSETS_HOTEL_EDITOR_CONSUMER_ASMDEF_PATH; if (!HtAssetDatabase.ExistsFile(dstUnityPath)) { string srcUnityPath = HtAssetDatabase.UnityPathCombine(srcDirectoryUnityPath, CONSUMER_ASMDEF_FILENAME_NO_EXT + ".txt"); HtAssetDatabase.CopyFile(srcUnityPath, dstUnityPath, false); } // Handle git hooks HandleGitHooks(); // We need to regenerate it in order to make sure that it will reflect the scripted configuration returnValue.Generate(); EditorUtility.SetDirty(returnValue); AssetDatabase.Refresh(); return returnValue; } private static string GetEditorAssetsUnityPath() { string assetsDirectoryUnityPath = "Editor/Assets"; #if HT_PACKAGE_DEV return $"Assets/Hotel/PackageManager/{assetsDirectoryUnityPath}"; #else return HtAssetUtility.GetAssetInPackagePath("com.ubisoft.hotel.packagemanager", assetsDirectoryUnityPath); #endif } private static void HandleGitHooks() { string root = ".git/hooks"; string rootPlatformPath = HtAssetDatabase.UnityPathToPlatformPath(root); if (HtSystemIO.Directory_Exists(rootPlatformPath)) { // Copy git hooks string precommitGitHookHotelPath = $"{root}/{PRECOMMIT_GIT_HOOK_HOTEL}"; string dstPlatformPath = HtAssetDatabase.UnityPathToPlatformPath(precommitGitHookHotelPath); if (HtSystemIO.File_Exists(dstPlatformPath)) { File.Delete(dstPlatformPath); } Log("Patching git hooks ..."); // Copy hotel pre-commit git hook string srcUnityPath = HtAssetDatabase.UnityPathCombine(GetEditorAssetsUnityPath(), PRECOMMIT_GIT_HOOK_HOTEL); HtAssetDatabase.CopyFile(srcUnityPath, dstPlatformPath, false); // Check if the consumer has a pre-commit file dstPlatformPath = HtAssetDatabase.UnityPathToPlatformPath($".git/hooks/pre-commit"); string content = precommitGitHookHotelPath; bool existsFile = HtSystemIO.File_Exists(dstPlatformPath); string prefix = existsFile ? "" : "#!/bin/bash"; PatchHotelStuffInFile(dstPlatformPath, content, prefix); #if !UNITY_EDITOR_WIN Log($"Granting execution permissions for {dstPlatformPath}"); string command = $"chmod u+x {dstPlatformPath}"; HtOSUtility.ExecuteCommand(command); #endif } } private static long CalculateCrc(string txt) { return HtByte.Crc32(System.Text.Encoding.ASCII.GetBytes(txt)); } private static long CalculateScriptCrc() { long returnValue = 0; string txt; string fileFullPath; string consumerAssetsEditorScriptsPath = ASSETS_HOTEL_EDITOR_SCRIPTS_PACKAGE_MANAGER_PATH; string fullPath = HtAssetDatabase.UnityPathToPlatformPath(consumerAssetsEditorScriptsPath, true); IEnumerable files = HtSystemIO.GetAllFiles(fullPath, true); IEnumerator enumerator = files.GetEnumerator(); while (enumerator.MoveNext()) { fileFullPath = enumerator.Current; if (fileFullPath.EndsWith(".cs")) { txt = File.ReadAllText(fileFullPath); returnValue += CalculateCrc(txt); } } return returnValue; } public static void DeleteHotelSettings() { HtAssetDatabase.DeleteFile(ASSETS_HOTEL_EDITOR_PACKAGE_MANAGER_PATH); } [SerializeField] private bool m_applyChangesAutomatically = true; [SerializeField] private bool m_askConfirmationBeforeSync = false; [SerializeField] private AppSpaceBuildSuiteProvider m_appSpaceBuildSuiteProvider; private PackageSpaceBuildSuiteProvider m_packageSpaceBuildSuiteProvider; [SerializeField] private BuildSuite m_appSpaceBuildSuite; [SerializeField] private BuildSuite m_packageSpaceBuildSuite; [SerializeField] private BuildSuite m_combinedSpacesBuildSuite; [SerializeField] private int m_buildTargetIndex = 0; private string[] m_buildTargetOptions; private bool NeedsToApply { get; set; } public void Update() { Build_Update(); // Build batch needs to be resumed only if it was interrupted while performing because of a change in define symbols made // Unity recompile. If that's the case then we need to apply the build suite again to finish the job. Upon applying it again won't // make Unity recompile again because the define symbols won't change if (Build_NeedsToResumeBatch()) { NeedsToApply = false; Build_ResumeBatch(); } if (NeedsToApply) { NeedsToApply = false; Apply(false); } } public bool ApplyChangesAutomatically { get { return m_applyChangesAutomatically; } set { if (m_applyChangesAutomatically != value) { m_applyChangesAutomatically = value; RefreshObject(); } } } public bool AskConfirmationBeforeSync { get { return m_askConfirmationBeforeSync; } set { if (m_askConfirmationBeforeSync != value) { m_askConfirmationBeforeSync = value; RefreshObject(); } } } internal bool IsOptimisedMode { get { // Optimised mode is meant to be used by consumers to speed up suite application. // Contributors can decide to go optimised or non-optimised mode. // When working on a particular Hotel package contributors might need to make sure the whole suite is applied all the way. // It's not used when building in batch mode (cicd) for security return !Build_IsBatchMode && EditorPrefs.GetBool(PREFS_OPTIMISED_MODE_KEY, true); } } internal void SwitchIsOptimiseMode() { bool value = EditorPrefs.GetBool(PREFS_OPTIMISED_MODE_KEY, true); EditorPrefs.SetBool(PREFS_OPTIMISED_MODE_KEY, !value); } [SerializeField] private long m_crcManifest = 0; [SerializeField] private long m_crcScripts = 0; [SerializeField] private long m_crcSymbols = 0; public bool HasCrcBeenCalculatedBefore() { return m_crcSymbols != 0 || m_crcManifest != 0 || m_crcScripts != 0; } /// /// Calculate Crc of ConsumerBuildSuiteProvider.cs /// /// Returns true if Crc has changed, which means that package suite ScriptableObjects are obsolete or /// manifest.json has changed. /// public bool CalculateCrc() { long crcScripts = CalculateScriptCrc(); // Manifest.json is also taken into consideration just in case some player stuff needs to be moved to app space. string manifesJson = UpmBuildProperty.ReadManifestJson(); long crcManifest = CalculateCrc(manifesJson); // Symbols should be taken into account only on Unity Editor when no batch is being performed in order to detect when symbols have // been changed from outside Unity Editor. bool needsToCalculateCrcSymbols = Build_BatchMode == EBuildBatchMode.None && !Application.isBatchMode; // Debug symbols from PackageSettings BuildTargetGroup buildTargetGroup = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget); HashSet symbols = HtPlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup, false, true); string symbolsStr = HtPlayerSettings.SymbolsToStr(symbols); long crcSymbols = CalculateCrc(symbolsStr); bool returnValue = m_crcManifest != crcManifest || m_crcScripts != crcScripts; // Symbols are checked only when Editor is open just to recreate suites if consumers have changed symbols, // typically because thye have discarded ProjectSettings, that's why they don't need to be checked while // performing a batch. if (!returnValue && needsToCalculateCrcSymbols) { returnValue = m_crcSymbols != crcSymbols; } if (returnValue) { Log("[PackageManger] calculating crc: oldCrcManifest: " + m_crcManifest + " newCrcManifest: " + crcManifest + " oldCrcScript: " + m_crcScripts + " newCrcScript: " + crcScripts + " oldCrcSymbols: " + m_crcSymbols + " newCrcSymbols: " + crcSymbols + " needsToCalculateCrcSymbols: " + needsToCalculateCrcSymbols); } m_crcManifest = crcManifest; m_crcScripts = crcScripts; m_crcSymbols = crcSymbols; return returnValue; } public int BuildTargetIndex { get { return m_buildTargetIndex; } set { m_buildTargetIndex = value; } } public string[] BuildTargetOptions { get { if (m_buildTargetOptions == null || m_buildTargetOptions.Length == 0) { List platformKeys = PlatformUtils.EPlatformKeys; int count = platformKeys.Count; m_buildTargetOptions = new string[count + 1]; m_buildTargetOptions[0] = "UseBuildTarget"; for (int i = 0; i < count; ++i) { if ((EPlatform)i != EPlatform.None) { m_buildTargetOptions[i + 1] = platformKeys[i]; } } } return m_buildTargetOptions; } } public AppSpaceBuildSuiteProvider AppSpaceBuildSuiteProvider { get { if (m_appSpaceBuildSuiteProvider == null) { m_appSpaceBuildSuiteProvider = CreateInstance(); m_appSpaceBuildSuiteProvider.name = HtTypes.GetTypeShortName(m_appSpaceBuildSuiteProvider.GetType()); AddObjectToAsset(m_appSpaceBuildSuiteProvider); } return m_appSpaceBuildSuiteProvider; } } public PackageSpaceBuildSuiteProvider PackageSpaceBuildSuiteProvider { get { if (m_packageSpaceBuildSuiteProvider == null) { m_packageSpaceBuildSuiteProvider = new PackageSpaceBuildSuiteProvider(this); } return m_packageSpaceBuildSuiteProvider; } } internal BuildSuite AppSpaceBuildSuite { get { if (m_appSpaceBuildSuite == null) { m_appSpaceBuildSuite = BuildSuite.CreateInstanceWithParams("BuildSuite_AppSpace"); m_appSpaceBuildSuite.Serialize(this); RefreshObject(); } return m_appSpaceBuildSuite; } } internal BuildSuite PackageSpaceBuildSuite { get { if (m_packageSpaceBuildSuite == null) { m_packageSpaceBuildSuite = BuildSuite.CreateInstanceWithParams("BuildSuite_PackageSpace"); m_packageSpaceBuildSuite.Serialize(this); RefreshObject(); } return m_packageSpaceBuildSuite; } } internal BuildSuite CombinedSpaceBuildSuite { get { if (m_combinedSpacesBuildSuite == null) { m_combinedSpacesBuildSuite = BuildSuite.CreateInstanceWithParams("BuildSuite_CombinedSpace"); m_combinedSpacesBuildSuite.Serialize(this); RefreshObject(); } return m_combinedSpacesBuildSuite; } } public EPlatform GetPlatform() { EPlatform returnValue = EPlatform.None; if (BuildTargetIndex > 0) { int index = BuildTargetIndex - 1; List platformKeys = PlatformUtils.EPlatformKeys; if (index < platformKeys.Count && PlatformUtils.IsPlatformKeySupported(platformKeys[index])) { returnValue = (EPlatform)index; } } else { returnValue = PlatformUtils.GetActivePlatform(); } return returnValue; } public void Generate() { if (AppSpaceBuildSuiteProvider.IsEnabled()) { // We need to store currentPackageSuiteName so it can be restored after regenerating AppSpaceBuildSuiteProvider string currentPackageSuiteName = AppSpaceBuildSuiteProvider.CurrentPackageSuiteName; HtAssetDatabase.ClearAssetsInsideScriptableObject(this); m_appSpaceBuildSuiteProvider = null; EPlatform platform = GetPlatform(); AppSpaceBuildSuiteProvider.Generate(platform, currentPackageSuiteName); // Add destination paths of files/directories that any suite may copy into Assets to .gitignore HashSet paths = new HashSet(); AppSpaceBuildSuiteProvider.AddPathsToGitIgnore(paths); // Add the folder where the packages link.xml files are copied to from package space before building to .gitignore _ = paths.Add(ASSETS_HOTEL_EDITOR_PACKAGE_LINKS_PATH); // Add a pattern to add all Orion's Plugins/Android stuff _ = paths.Add("Assets/Plugins/Android/com.ubisoft.orion.*"); // Add Hotel/Plugins _ = paths.Add(ASSETS_HOTEL_PLUGINS_PATH); // Add Hotel/Editor/Tools _ = paths.Add(ASSETS_HOTEL_EDITOR_TOOLS_PATH); // Add Hotel/Editor/Verbatim _ = paths.Add(ASSETS_HOTEL_EDITOR_VERBATIM_PATH); // Add Hotel/Runtime _ = paths.Add(ASSETS_HOTEL_RUNTIME_ROOT_PATH); // Add StreamingAssets/Hotel _ = paths.Add(ASSETS_HOTEL_STREAMING_ASSETS_ROOT_PATH); // Add Addressables remote asset bundles _ = paths.Add(Addressables.Core.Editor.BuildProcessorConstants.REMOTE_ASSET_BUNDLES_PATH); PatchGitIgnore(paths); NeedsToApply = true; } } public void Apply(bool isManualBuild) { EPlatform platform = GetPlatform(); if (platform == EPlatform.None) { if (BuildTargetIndex > 0) { LogError($"Selected build target {Debug.FormatTextInCodeContext(BuildTargetOptions[BuildTargetIndex])} is not supported"); } } else { Build_PerformApplyBuildSuiteBatch(platform, null, isManualBuild, false, false); } } private void AddObjectToAsset(ScriptableObject so) { HtAssetDatabase.AddObjectToAsset(so, this); } internal void RefreshObject() { // Make sure packageSuite is saved, otherwise its properties can get lost after exiting play mode HtAssetDatabase.RefreshObject(this); } public void ExportSettings(string path) { if (path.Length != 0) { Log("Exporting setup: " + path); // Make sure assets are stored before copying them AssetDatabase.SaveAssets(); if (Directory.Exists(path)) { Directory.Delete(path, true); } string fullDstPath; // Copy setup string fullSrcPath = HtAssetDatabase.UnityPathToPlatformPath(ASSETS_HOTEL_EDITOR_ROOT_PATH, true); if (Directory.Exists(fullSrcPath)) { fullDstPath = Path.Combine(path, ASSETS_HOTEL_EDITOR_ROOT_PATH.Replace("Assets/", "")); HtSystemIO.CopyDirectory(fullSrcPath, fullDstPath); } // Copy assets fullSrcPath = HtAssetDatabase.UnityPathToPlatformPath(AssetManager.ROOT_PATH, true); if (Directory.Exists(fullSrcPath)) { fullDstPath = Path.Combine(path, AssetManager.ROOT_PATH); HtSystemIO.CopyDirectory(fullSrcPath, fullDstPath); } // Copy packages (manifest.json) string target = "manifest.json"; fullSrcPath = HtAssetDatabase.UnityPathToPlatformPath($"Packages/{target}", true); fullDstPath = Path.Combine(path, target); File.Copy(fullSrcPath, fullDstPath); // Copy ProjectSettings target = "ProjectSettings"; fullSrcPath = HtAssetDatabase.UnityPathToPlatformPath(target, true); fullDstPath = Path.Combine(path, target); HtSystemIO.CopyDirectory(fullSrcPath, fullDstPath); } } public void ImportSettings(string path) { if (path.Length != 0) { FileInfo fileInfo = new FileInfo(path); path = fileInfo.DirectoryName; string fullDstPath; // Copy setup string relativePath = ASSETS_HOTEL_EDITOR_ROOT_PATH.Replace("Assets/", ""); string fullSrcPath = Path.Combine(path, HtAssetDatabase.UnityPathToPlatformPath(relativePath)); if (Directory.Exists(fullSrcPath)) { fullDstPath = Path.GetFullPath(ASSETS_HOTEL_EDITOR_ROOT_PATH); if (Directory.Exists(fullDstPath)) { Directory.Delete(fullDstPath, true); } HtSystemIO.CopyDirectory(fullSrcPath, fullDstPath); } // Copy assets string platformRoothPath = HtAssetDatabase.UnityPathToPlatformPath(AssetManager.ROOT_PATH); fullSrcPath = Path.Combine(path, platformRoothPath); if (Directory.Exists(fullSrcPath)) { fullDstPath = Path.GetFullPath("."); fullDstPath = Path.Combine(fullDstPath, platformRoothPath); HtSystemIO.CopyDirectory(fullSrcPath, fullDstPath); } // Copy manifest.json string target = "manifest.json"; fullSrcPath = Path.Combine(path, target); fullDstPath = Path.GetFullPath(Path.Combine("Packages", target)); File.Copy(fullSrcPath, fullDstPath, true); // Copy ProjectSettings target = "ProjectSettings"; fullSrcPath = Path.GetFullPath(Path.Combine(path, target)); fullDstPath = Path.GetFullPath("."); fullDstPath = Path.Combine(fullDstPath, target); if (Directory.Exists(fullDstPath)) { Directory.Delete(fullDstPath, true); } HtSystemIO.CopyDirectory(fullSrcPath, fullDstPath); AssetDatabase.Refresh(); // Order to load the configuration that has just been imported // We don't do it right away because it may not be ready yet NeedsToReloadPackageManager = true; } } #region git_ignore private void PatchGitIgnore(HashSet pathsToGitIgnore) { Log("Patching .gitignore ..."); string path = Application.dataPath + "/../" + ".gitignore"; string content = ""; // Assets moved automatically by Hotel depending on the selected package suite AssetManager.AddPathsToGitIgnore(pathsToGitIgnore); // Files generated by Firebase FirebaseBuildProperty.AddPathsToGitIgnore(pathsToGitIgnore); content = AddPathListToGitIgnoreContent(content, pathsToGitIgnore); // Hotel Resources folder where Hotel stores the files that it generates for the player content = AddPathToGitIgnoreContent(content, ASSETS_HOTEL_RESOURCES_ROOT_PATH); // hotelSettings.asset is used only to handle settings temporarily content = AddPathToGitIgnoreContent(content, ASSETS_HOTEL_EDITOR_PACKAGE_MANAGER_PATH); // buildSettings.asset is used only to handle build settings temporarily content = AddPathToGitIgnoreContent(content, AppSpaceBuildSuiteProvider.GetBuildSettingsPath()); // Addressables build folder which content is generated upon applying a build suite or building addressables content = AddPathToGitIgnoreContent(content, Addressables.Core.Editor.AddressablesBuildSettings.RootDirectory); PatchHotelStuffInFile(path, content); } private static void PatchHotelStuffInFile(string path, string value, string prefix = null) { const string HOTEL_BEGIN = "# Hotel BEGIN"; const string HOTEL_END = "# Hotel END"; string content = ""; int lastLineCharCount = 0; if (File.Exists(path)) { string[] lines = File.ReadAllLines(path); // Delete Hotel stuff bool canWrite = true; int count = lines.Length; for (int i = 0; i < count; i++) { if (lines[i].Contains(HOTEL_BEGIN)) { canWrite = false; } if (canWrite) { lastLineCharCount = lines[i].Length; content += lines[i] + NEW_LINE; } if (lines[i].Contains(HOTEL_END)) { canWrite = true; } } } if (lastLineCharCount > 0) { content += NEW_LINE; } if (!string.IsNullOrEmpty(prefix)) { content += prefix + NEW_LINE; } content += HOTEL_BEGIN + NEW_LINE; content += value; content += NEW_LINE + HOTEL_END; File.WriteAllText(path, content); } private static string AddPathToGitIgnoreContent(string content, string path) { if (!string.IsNullOrEmpty(content)) { content += NEW_LINE; } content += path; if (path.StartsWith("Assets/")) { content += NEW_LINE + path + ".meta"; } return content; } private static string AddPathListToGitIgnoreContent(string content, HashSet paths) { if (paths != null) { foreach (string path in paths) { content = AddPathToGitIgnoreContent(content, path); } } return content; } #endregion #region build private const char BUILD_BATCH_LABEL_SEPARATOR = ','; private const char BUILD_BATCH_TIME_SEPARATOR = ':'; internal enum EBuildBatchMode { None, ApplyBuildSuite, BuildAddressables, BuildPlayer, Full, }; private string Build_CurrentStepName { get; set; } private EBuildBatchMode Build_BatchMode { get; set; } private EPlatform Build_Platform { get; set; } private string[] Build_Arguments { get; set; } private bool Build_IsManualBuild { get; set; } private bool Build_IsBatchMode { get; set; } internal bool BuildBatch_ManifestHasChanged { get; set; } internal long BuildBatch_TicksAtBatchBegin { get; set; } internal long BuildBatch_TicksAtStepBegin { get; set; } internal double BuildBatch_TimeSpent { get { DateTime timeBegin = new DateTime(BuildBatch_TicksAtBatchBegin); TimeSpan timeSpan = DateTime.Now - timeBegin; return timeSpan.TotalSeconds; } } internal string BuildBatch_TimeSpentAsString => $"{BuildBatch_TimeSpent:0.00} s"; internal double BuildBatch_TimeSpentAtStep { get { DateTime timeBegin = new DateTime(BuildBatch_TicksAtStepBegin); TimeSpan timeSpan = DateTime.Now - timeBegin; return timeSpan.TotalSeconds; } } internal string BuildBatch_TimeSpentAtStepAsString => $"{BuildBatch_TimeSpentAtStep:0.00} s"; private string BuildBatch_TimeDetail { get; set; } internal void BuildBatch_AddTimeDetail(string label) { if (!string.IsNullOrEmpty(BuildBatch_TimeDetail)) { BuildBatch_TimeDetail += BUILD_BATCH_LABEL_SEPARATOR; } BuildBatch_TimeDetail += $"{label}{BUILD_BATCH_TIME_SEPARATOR}{DateTime.Now.Ticks}"; } internal string BuildBatch_FormatTimeDetail() { string returnValue = ""; if (!string.IsNullOrEmpty(BuildBatch_TimeDetail)) { DateTime now = DateTime.Now; string[] labels = BuildBatch_TimeDetail.Split(BUILD_BATCH_LABEL_SEPARATOR); long ticks; DateTime labelTime; TimeSpan timeSpan; string[] tokens; string labelText; for (int i = labels.Length - 1; i > -1; --i) { tokens = labels[i].Split(BUILD_BATCH_TIME_SEPARATOR); ticks = HtLong.Parse(tokens[1]); labelTime = new DateTime(ticks); timeSpan = now - labelTime; labelText = string.Format("{0,0}: {1, 0} s", tokens[0], timeSpan.TotalSeconds.ToString("0.00")); returnValue = $"{labelText}\n{returnValue}"; now = labelTime; } } return $"Profiling times:\n{returnValue}"; } private BuildBatch m_buildBatch; private BuildBatch Build_Batch { get { if (m_buildBatch == null) { Build_CreateBatch(); } return m_buildBatch; } } private void Build_CreateBatch() { if (m_buildBatch == null) { m_buildBatch = new BuildBatch(Build_OnBatchStepPerformed); } } private void Build_Update() { BuildBatch batch = Build_Batch; if (Build_BatchMode != EBuildBatchMode.None) { if (batch != null) { try { batch.Update(); if (batch.IsDone) { Build_EndBatch(true); } } catch (Exception e) { Build_OnAbortBatch(e); } } } } private void Build_ResetBatch() { Build_BatchMode = EBuildBatchMode.None; Build_Arguments = null; Build_IsManualBuild = false; Build_CurrentStepName = null; Build_IsBatchMode = false; BuildBatch_ManifestHasChanged = false; BuildBatch_TicksAtBatchBegin = 0; BuildBatch_TicksAtStepBegin = 0; if (!string.IsNullOrEmpty(BuildBatch_TimeDetail)) { BuildBatch_TimeDetail = ""; } } private bool Build_BeginBatch(EBuildBatchMode mode, EPlatform platform, string[] arguments, bool isManualBuild) { bool returnValue = Build_BatchMode == EBuildBatchMode.None; if (AppSpaceBuildSuiteProvider.CanPerform()) { if (returnValue) { Build_BatchMode = mode; Build_Platform = platform; Build_Arguments = arguments; Build_IsManualBuild = isManualBuild; Build_IsBatchMode = Application.isBatchMode; Build_Batch.Reset(); } /*else { LogError("Can't start a new build batch because it's already performing one."); }*/ } else { LogError($"ERROR: Enable Hotel package suites and define some suites in {CONSUMER_BUILD_SUITE_PROVIDER_FILENAME} before attempting to apply a suite."); } Log($"Build_BeginBatch allowed: {returnValue} IsBatchMode: {Build_IsBatchMode}"); return returnValue; } private void Build_EndBatch(bool success) { // Batch mode is no executed with -quit option because changes in manifest.json are not processed by Unity 2020 // until the next tick. This means that we need to quit once the job is done when executing in batch mode if (Build_IsBatchMode) { Log($"Build_EndBatch Success: {success}"); EditorApplication.Exit(success ? 0 : 1); } else { Build_ResetBatch(); } } private void Build_OnBatchStepPerformed(BuildStep buildStep) { Build_CurrentStepName = buildStep.Name; } private bool Build_NeedsToResumeBatch() { // Build batch needs to be resumed only if it was interrupted while performing because of a change in define symbols made // Unity recompile. If that's the case then we need to apply build suite again to finish the job. Upon applying it again won't // make Unity recompile again because the define symbols won't change return Build_BatchMode != EBuildBatchMode.None && !Build_Batch.IsPerforming; } private void Build_ResumeBatch() { // We can only resume a batch if it was interrupted when it was performing ApplyBuildSuite or ReloadAssemblies steps, otherwise we need to reset it so // the user can run it agan if (Build_CurrentStepName == BuildStep_ApplyBuildSuite.StepName || Build_CurrentStepName == BuildStep_ReloadAssemblies.StepName) { EBuildBatchMode mode = Build_BatchMode; Build_BatchMode = EBuildBatchMode.None; string argumentsString = Build_Arguments == null ? "null" : string.Join(", ", Build_Arguments); Log($"Resuming build batch mode: {mode} arguments: {argumentsString} currentStepName: {Build_CurrentStepName} time spent: {BuildBatch_TimeSpentAtStepAsString}"); BuildBatch_AddTimeDetail("Reloading manifest.json"); Build_PerformBatchMode(mode, Build_Platform, Build_Arguments, Build_IsManualBuild, true); } else { // Print the message that lets consumers know that the batch is done if (Build_Batch != null) { Build_Batch.OnDone(); } Build_EndBatch(true); } } private void Build_SetupApplyBuildSuiteBatch(EPlatform platform, string[] arguments, bool isManualBuild, bool deleteBuildFolder) { BuildStep step; if (string.IsNullOrEmpty(Build_CurrentStepName)) { step = new BuildStep_ApplyBuildSuite(platform, arguments, isManualBuild, deleteBuildFolder, IsOptimisedMode); Build_Batch.EnqueueStep(step); } if (string.IsNullOrEmpty(Build_CurrentStepName) || Build_CurrentStepName == BuildStep_ApplyBuildSuite.StepName || Build_CurrentStepName == BuildStep_ReloadAssemblies.StepName) { step = new BuildStep_ReloadAssemblies(); Build_Batch.EnqueueStep(step); step = new BuildStep_PreparePlayer(platform); Build_Batch.EnqueueStep(step); } } #pragma warning disable IDE0060 // Remove unused parameters, kept for compatibility with the other build batch modes private void Build_SetupBuildPlayerBatch(EPlatform platform, string[] arguments) #pragma warning restore IDE0060 // Remove unused parameter { BuildStep step = new BuildStep_BuildPlayer(); Build_Batch.EnqueueStep(step); } #pragma warning disable IDE0060 // Remove unused parameters, kept for compatibility with the other build batch modes private void Build_SetupBuildAddressablesBatch(EPlatform platform, string[] arguments) #pragma warning restore IDE0060 // Remove unused parameter { BuildStep step = new BuildStep_BuildAddressables(); Build_Batch.EnqueueStep(step); } private void Build_SetupFullBatch(EPlatform platform, string[] arguments, bool isManualBuild) { // Apply build suite to make sure that properties that need to be applied on postprocess are applied Build_SetupApplyBuildSuiteBatch(platform, arguments, isManualBuild, true); Build_SetupBuildAddressablesBatch(platform, arguments); Build_SetupBuildPlayerBatch(platform, arguments); } internal void Build_PerformBatchMode(EBuildBatchMode mode, EPlatform platform, string[] arguments, bool isManualBuild, bool isResuming) { Build_CreateBatch(); switch (mode) { case EBuildBatchMode.ApplyBuildSuite: Build_PerformApplyBuildSuiteBatch(platform, arguments, isManualBuild, true, isResuming); break; case EBuildBatchMode.BuildAddressables: Build_PerformBuildAddressablesBatch(platform, arguments, isManualBuild, isResuming); break; case EBuildBatchMode.BuildPlayer: Build_PerformBuildPlayerBatch(platform, arguments, isManualBuild, isResuming); break; case EBuildBatchMode.Full: Build_PerformFullBatch(platform, arguments, isManualBuild, isResuming); break; } } /// /// Perform build batch to just apply a build suite, typically when the consumer hits 'Apply changes' button on Hotel settings screen /// internal void Build_PerformApplyBuildSuiteBatch(EPlatform platform, string[] arguments, bool isManualBuild, bool deleteBuildFolder, bool isResuming) { if (Build_BeginBatch(EBuildBatchMode.ApplyBuildSuite, platform, arguments, isManualBuild)) { Build_SetupApplyBuildSuiteBatch(platform, arguments, isManualBuild, deleteBuildFolder); Build_PerformBatch(isResuming); } } internal void Build_PerformBuildPlayerBatch(EPlatform platform, string[] arguments, bool isManualBuild, bool isResuming) { if (Build_BeginBatch(EBuildBatchMode.BuildPlayer, platform, arguments, isManualBuild)) { Build_SetupBuildPlayerBatch(platform, arguments); Build_PerformBatch(isResuming, BuildBatch.EWhenOnDone.OnPostprocessDone); } } internal void Build_PerformBuildAddressablesBatch(EPlatform platform, string[] arguments, bool isManualBuild, bool isResuming) { if (Build_BeginBatch(EBuildBatchMode.BuildAddressables, platform, arguments, isManualBuild)) { Build_SetupBuildAddressablesBatch(platform, arguments); Build_PerformBatch(isResuming); } } internal void Build_PerformFullBatch(EPlatform platform, string[] arguments, bool isManualBuild, bool isResuming) { if (Build_BeginBatch(EBuildBatchMode.Full, platform, arguments, isManualBuild)) { Build_SetupFullBatch(platform, arguments, isManualBuild); Build_PerformBatch(isResuming, BuildBatch.EWhenOnDone.OnPostprocessDone); } } private void Build_PerformBatch(bool isResuming, BuildBatch.EWhenOnDone whenOnDone = BuildBatch.EWhenOnDone.AllStepsDone) { if (!isResuming) { BuildBatch_TicksAtBatchBegin = DateTime.Now.Ticks; } if (Build_IsBatchMode) { // Exceptions are not caught in batch mode so compilation errors are reported to the pipeline to stop it Build_Batch.Perform(whenOnDone); } else { // Exceptions are caught to abort the batch so users can perform another batch try { Build_Batch.Perform(whenOnDone); } catch (Exception e) { Build_OnAbortBatch(e); } } } private void Build_OnAbortBatch(Exception e) { LogError($"BuildBatch: Aborted because the following exception was thrown while performing {Build_CurrentStepName} step: {e}"); if (Build_Batch != null) { Build_Batch.OnAborted(); } Build_EndBatch(false); throw e; } internal void Build_OnPreprocess(BuildTarget buildTarget, string pathToBuiltProject) { Log("Build_OnPreprocess for target " + Debug.FormatTextInUserContext(buildTarget.ToString()) + " at path " + Debug.FormatTextInUserContext(pathToBuiltProject)); } internal void Build_OnPostprocess(BuildTarget buildTarget, string pathToBuiltProject) { Log("Build_OnPostprocess for target " + Debug.FormatTextInUserContext(buildTarget.ToString()) + " at path " + Debug.FormatTextInUserContext(pathToBuiltProject)); if (AppSpaceBuildSuiteProvider.CanPerform()) { if (CombinedSpaceBuildSuite == null) { LogError("ERROR: PostProcessBuild aborted because the BuildSuite object to apply is missing"); } else { CombinedSpaceBuildSuite.PostBuild(buildTarget, pathToBuiltProject); } Build_Batch.OnPostprocessBuild(); } else { LogError("ERROR: PostProcessBuild aborted because AppSpaceBuildSuiteProvider can't perform"); } } #endregion #region release public string Release_GetDefaultTag(bool bumpVersion) { VersionProperty versionProperty = AppSpaceBuildSuiteProvider.GameVersionProperty; string versionNumber = bumpVersion ? versionProperty.GetVersionAsString(versionProperty.Version.Major, versionProperty.Version.Minor, versionProperty.Version.Patch + 1) : versionProperty.VersionAsString; return $"release/{versionNumber}"; } public void Release_Perform(bool bumpVersion, bool tag, string tagName = null) { if (AppSpaceBuildSuiteProvider.CanPerform()) { VersionProperty versionProperty = AppSpaceBuildSuiteProvider.GameVersionProperty; if (bumpVersion) { versionProperty.BumpPatch(); HtAssetDatabase.RefreshObject(versionProperty); // Save asset immediately so git notices the change AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Log($"Release: Bumping version to {versionProperty.VersionAsString} ..."); Log($"Release: Committing {AppSpaceBuildSuiteProvider.GetGameVersionPath()} ..."); // Commit the change _ = HtOSUtility.ExecuteGitCommand($"add {AppSpaceBuildSuiteProvider.GetGameVersionPath()}"); string message = $"\"Bump version to {versionProperty.VersionAsString}\""; _ = HtOSUtility.ExecuteGitCommand($"commit -m {message}"); Log($"Release: Pushing new game version to repository ..."); _ = HtOSUtility.ExecuteGitCommand($"push"); } if (tag) { if (string.IsNullOrEmpty(tagName)) { // Version has already been bumped tagName = Release_GetDefaultTag(false); } Log($"Release: Creating {tagName} tag ..."); _ = HtOSUtility.ExecuteGitCommand($"tag {tagName}"); Log($"Release: Pushing new tag to repository ..."); _ = HtOSUtility.ExecuteGitCommand($"push origin {tagName}"); } Log($"Release: done!"); } else { LogError($"Release: AppSpaceBuildSuiteProvider is not ready"); } } #endregion #region log public static void Log(string msg) { Debug.EditorLog(msg); } public static void LogWarning(string msg) { Debug.EditorLogWarning(msg); } public static void LogError(string msg) { Debug.EditorLogError(msg); } #endregion } }