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
}
}