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