using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine.SceneManagement; using UnityEngine.UIElements; namespace Unity.Play.Publisher.Editor { /// /// Provides a collection of utility methods. These methods are used by the WebGL Publisher and by other tools that need information about existing builds. /// public static class PublisherUtils { /// /// The max number of builds that can be displayed in the UI at the same time /// public const int MaxDisplayedBuilds = 10; /// /// the default name of every uploaded game /// public const string DefaultGameName = "Untitled"; const string ProjectVersionRegex = "^\\d{4}\\.\\d{1}\\Z"; /// /// Retrieves a list of build directories. /// /// /// The list always contains MaxDisplayedBuilds elements. /// If not enough build paths are retrieved, the missing elements will just contain empty paths. /// /// /// /// /// Returns a list of build directories public static List GetAllBuildsDirectories() { List result = Enumerable.Repeat(string.Empty, MaxDisplayedBuilds).ToList(); string path = GetEditorPreference("buildOutputDirList"); if (string.IsNullOrEmpty(path)) { return result; } List existingPaths = path.Split(';').ToList(); for (int i = 0; i < existingPaths.Count; i++) { result[i] = existingPaths[i]; } return result; } /// /// Adds a directory to the list of tracked build directories /// /// The absolute path to a build /// /// if there are already MaxDisplayedBuilds elements in the list, the oldest element will be removed /// to make room for the new build directory. /// /// /// /// public static void AddBuildDirectory(string buildPath) { string path = GetEditorPreference("buildOutputDirList"); List buildPaths = path.Split(';').ToList(); if (buildPaths.Contains(buildPath)) { return; } while (buildPaths.Count < MaxDisplayedBuilds) { buildPaths.Add(string.Empty); } //Left Shift for (int i = MaxDisplayedBuilds - 1; i > 0; i--) { buildPaths[i] = buildPaths[i - 1]; } buildPaths[0] = buildPath; SetEditorPreference("buildOutputDirList", string.Join(";", buildPaths)); } /// /// Removes a directory from the list of tracked builds /// /// The absolute path to a build /// /// The removed element will be replaced with an empty string. /// /// /// /// public static void RemoveBuildDirectory(string buildPath) { List buildPaths = GetEditorPreference("buildOutputDirList").Split(';').ToList(); buildPaths.Remove(buildPath); while (buildPaths.Count < MaxDisplayedBuilds) { buildPaths.Add(string.Empty); } SetEditorPreference("buildOutputDirList", string.Join(";", buildPaths)); } /// /// Determines whether a valid build is tracked /// /// /// A build is considered valid if its folders contain all files that are typically created for WebGL builds, specific to its Unity version. /// /// Returns True if a non-corrupted build is tracked public static bool ValidBuildExists() => !string.IsNullOrEmpty(GetFirstValidBuildPath()); /// /// Returns the first valid build path among all builds tracked /// /// Returns the first valid build path among all builds tracked, if any. Otherwise, returns an empty string /// /// /// public static string GetFirstValidBuildPath() => GetAllBuildsDirectories().FirstOrDefault(BuildIsValid); /// /// Determines whether a build is valid or not /// /// /// A build is considered valid if its folders contain all files that are typically created for WebGL builds, specific to its Unity version. /// /// The path to a build /// Returns True if the build follows the standard for a supported Unity version, otherwise returns false public static bool BuildIsValid(string buildPath) { if (string.IsNullOrEmpty(buildPath)) { return false; } string unityVersionOfBuild = GetUnityVersionOfBuild(buildPath); //UnityEngine.Debug.Log("unity version: " + unityVersionOfBuild); if (string.IsNullOrEmpty(unityVersionOfBuild)) { return false; } string descriptorFileName = buildPath.Split('/').Last(); switch (unityVersionOfBuild) { case "2019.3": return BuildIsCompatibleFor2019_3(buildPath, descriptorFileName); case "2020.2": return BuildIsCompatibleFor2020_2(buildPath, descriptorFileName); default: return true; //if we don't know the exact build structure for other unity versions, we assume the build is valid } } /// /// Determines whether a build is valid or not, according to Unity 2019.3 WebGL build standard output /// /// The path to a build /// /// Returns True if the build follows the standard for a supported Unity version, otherwise returns false public static bool BuildIsCompatibleFor2019_3(string buildPath, string descriptorFileName) { return File.Exists(Path.Combine(buildPath, string.Format("Build/{0}.data.unityweb", descriptorFileName))) && File.Exists(Path.Combine(buildPath, string.Format("Build/{0}.wasm.code.unityweb", descriptorFileName))) && File.Exists(Path.Combine(buildPath, string.Format("Build/{0}.wasm.framework.unityweb", descriptorFileName))) && File.Exists(Path.Combine(buildPath, string.Format("Build/{0}.json", descriptorFileName))) && File.Exists(Path.Combine(buildPath, string.Format("Build/UnityLoader.js", descriptorFileName))); } /// /// Determines whether a build is valid or not, according to Unity 2020.2 WebGL build standard output /// /// The path to a build /// /// Returns True if the build follows the standard for a supported Unity version, otherwise returns false public static bool BuildIsCompatibleFor2020_2(string buildPath, string descriptorFileName) { string buildFilesPath = Path.Combine(buildPath, "Build/"); return Directory.GetFiles(buildFilesPath, string.Format("{0}.data.*", descriptorFileName)).Length > 0 && Directory.GetFiles(buildFilesPath, string.Format("{0}.framework.js.*", descriptorFileName)).Length > 0 && File.Exists(Path.Combine(buildPath, string.Format("Build/{0}.loader.js", descriptorFileName))) && Directory.GetFiles(buildFilesPath, string.Format("{0}.wasm.*", descriptorFileName)).Length > 0; } /// /// Gets the Unity version with which a WebGL build was made /// /// The path to a build /// Returns the Unity version with which a WebGL build was made, if the build contains that information. Otherwise, returns an empty string public static string GetUnityVersionOfBuild(string buildPath) { if (string.IsNullOrEmpty(buildPath)) { return string.Empty; } string versionFile = Path.Combine(buildPath, "ProjectVersion.txt"); if (!File.Exists(versionFile)) { return string.Empty; } string version = File.ReadAllLines(versionFile)[0].Split(' ')[1].Substring(0, 6); //The row is something like: m_EditorVersion: 2019.3.4f1, so it will return 2019.3 return Regex.IsMatch(version, ProjectVersionRegex) ? version : string.Empty; } internal static bool AddCurrentSceneToBuildSettings() { string currentScenePath = SceneManager.GetActiveScene().path; if (string.IsNullOrEmpty(currentScenePath)) { return false; } List editorBuildSettingsScenes = new List(); string[] currentScenesList = EditorBuildSettingsScene.GetActiveSceneList(EditorBuildSettings.scenes); foreach (var scenePath in currentScenesList) { editorBuildSettingsScenes.Add(new EditorBuildSettingsScene(scenePath, true)); } editorBuildSettingsScenes.Add(new EditorBuildSettingsScene(currentScenePath, true)); EditorBuildSettings.scenes = editorBuildSettingsScenes.ToArray(); return true; } /// /// Sets an editor preference for the project, using the PublisherSettingsManager /// /// ID of the preference /// New value public static void SetEditorPreference(string key, string value) { PublisherSettingsManager.instance.Set(key, value, SettingsScope.Project); } /// /// Gets an editor preference for the project, using the PublisherSettingsManager /// /// ID of the preference /// Returns the value stored for that preference ID public static string GetEditorPreference(string key) { string result = PublisherSettingsManager.instance.Get(key, SettingsScope.Project); if (result == null) { result = string.Empty; SetEditorPreference(key, result); } return result; } /// /// Filters the name of the game, ensuring it contains something more than just spaces. /// /// The original name of the game /// /// /// /// Returns the name of the game if it contains something different than just spaces, or a default name in case the original one only contains spaces. public static string GetFilteredGameTitle(string currentGameTitle) { if (string.IsNullOrEmpty(currentGameTitle?.Trim())) { return DefaultGameName; } return currentGameTitle; } /// /// Formats an amount of bytes so it is represented in one of its multiples. Supports GB, MB, KB, or B /// /// The amount of bytes to represent /// /// /// /// Returns a string representing multiples of Bytes with two decimals and Bytes with zero decimals public static string FormatBytes(ulong bytes) { double gb = bytes / (1024.0 * 1024.0 * 1024.0); double mb = bytes / (1024.0 * 1024.0); double kb = bytes / 1024.0; // Use :#.000 to specify further precision if wanted if (mb >= 1000) return $"{gb:#.00} GB"; if (kb >= 1000) return $"{mb:#.00} MB"; if (kb >= 1) return $"{kb:#.00} KB"; return $"{bytes} B"; } /// /// Gets the size of a folder, in bytes /// /// The folder to analyze /// Returns the size of the folder, in bytes public static ulong GetFolderSize(string folder) { ulong size = 0; DirectoryInfo directoryInfo = new DirectoryInfo(folder); foreach (FileInfo fileInfo in directoryInfo.GetFiles("*", SearchOption.AllDirectories)) { size += (ulong)fileInfo.Length; } return size; } /// /// Gets the current PublisherState of a PublisherWindow instance /// /// /// Use this to detect a change in the current step and react accordingly /// (I.E: use it to understand if the user started the publishing process) /// /// /// /// /// The instance of an open PublisherWindow /// Returns the current PublisherState of a PublisherWindow instance public static PublisherState GetCurrentPublisherState(PublisherWindow instance) { return instance.Store.state.step; } /// /// Gets the URL of the last build that was published during the current session. /// Only valid as long as the PublisherWindow stays open. /// /// /// If this is empty, no build has been published. /// /// /// /// /// The instance of an open PublisherWindow /// Returns the URL of the last build that was published during the current session. public static string GetUrlOfLastPublishedBuild(PublisherWindow instance) { return instance.Store.state.url; } /// /// Represents a MouseManipulator that allows a visual element to react when left clicked /// public class LeftClickManipulator : MouseManipulator { Action OnClick; bool active; /// /// Initializes and returns an instance of LeftClickManipulator. /// /// The default callback that will be triggered when the element is clicked public LeftClickManipulator(Action OnClick) { activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse }); this.OnClick = OnClick; } /// /// Registers the callbacks on the target /// protected override void RegisterCallbacksOnTarget() { target.RegisterCallback(OnMouseDown); target.RegisterCallback(OnMouseUp); } /// /// Unregisters the callbacks on the target /// protected override void UnregisterCallbacksFromTarget() { target.UnregisterCallback(OnMouseUp); target.UnregisterCallback(OnMouseDown); } /// /// Called when the mouse is clicked on the target, when the user starts pressing the button /// /// protected void OnMouseDown(MouseDownEvent e) { if (active) { e.StopImmediatePropagation(); return; } if (CanStartManipulation(e)) { active = true; target.CaptureMouse(); e.StopPropagation(); } } /// /// Called when the mouse is clicked on the target, when the user stops pressing the button /// /// protected void OnMouseUp(MouseUpEvent e) { if (!active || !target.HasMouseCapture() || !CanStopManipulation(e)) { return; } active = false; target.ReleaseMouse(); e.StopPropagation(); if (OnClick == null) { return; } OnClick.Invoke(target); } } } }