1325 lines
44 KiB
C#
1325 lines
44 KiB
C#
using System.Linq;
|
|
using UnityEngine;
|
|
|
|
namespace Crosstales.Common.Util
|
|
{
|
|
/// <summary>Base for various helper functions.</summary>
|
|
#if UNITY_EDITOR
|
|
[UnityEditor.InitializeOnLoad]
|
|
#endif
|
|
public abstract class BaseHelper
|
|
{
|
|
#region Variables
|
|
|
|
public static readonly System.Globalization.CultureInfo BaseCulture = new System.Globalization.CultureInfo("en-US"); //TODO set with current user locale?
|
|
|
|
protected static readonly System.Text.RegularExpressions.Regex lineEndingsRegex = new System.Text.RegularExpressions.Regex(@"\r\n|\r|\n");
|
|
|
|
//protected static readonly Regex cleanStringRegex = new Regex(@"([^a-zA-Z0-9 ]|[ ]{2,})");
|
|
protected static readonly System.Text.RegularExpressions.Regex cleanSpacesRegex = new System.Text.RegularExpressions.Regex(@"\s+");
|
|
|
|
protected static readonly System.Text.RegularExpressions.Regex cleanTagsRegex = new System.Text.RegularExpressions.Regex(@"<.*?>");
|
|
//protected static readonly System.Text.RegularExpressions.Regex asciiOnlyRegex = new System.Text.RegularExpressions.Regex(@"[^\u0000-\u00FF]+");
|
|
|
|
protected static readonly System.Random rnd = new System.Random();
|
|
|
|
protected const string file_prefix = "file://";
|
|
|
|
public static bool ApplicationIsPlaying = Application.isPlaying;
|
|
private static string applicationDataPath = Application.dataPath;
|
|
|
|
#endregion
|
|
|
|
|
|
#region Properties
|
|
|
|
/// <summary>Checks if an Internet connection is available.</summary>
|
|
/// <returns>True if an Internet connection is available.</returns>
|
|
public static bool isInternetAvailable
|
|
{
|
|
get
|
|
{
|
|
#if CT_OC
|
|
if (OnlineCheck.OnlineCheck.Instance == null)
|
|
{
|
|
return Application.internetReachability != NetworkReachability.NotReachable;
|
|
}
|
|
else
|
|
{
|
|
return OnlineCheck.OnlineCheck.Instance.isInternetAvailable;
|
|
}
|
|
#else
|
|
return Application.internetReachability != NetworkReachability.NotReachable;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if the current platform is Windows.</summary>
|
|
/// <returns>True if the current platform is Windows.</returns>
|
|
public static bool isWindowsPlatform
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_STANDALONE_WIN
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if the current platform is OSX.</summary>
|
|
/// <returns>True if the current platform is OSX.</returns>
|
|
public static bool isMacOSPlatform
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_STANDALONE_OSX
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if the current platform is Linux.</summary>
|
|
/// <returns>True if the current platform is Linux.</returns>
|
|
public static bool isLinuxPlatform
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_STANDALONE_LINUX
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if the current platform is standalone (Windows, macOS or Linux).</summary>
|
|
/// <returns>True if the current platform is standalone (Windows, macOS or Linux).</returns>
|
|
public static bool isStandalonePlatform => isWindowsPlatform || isMacOSPlatform || isLinuxPlatform;
|
|
|
|
/// <summary>Checks if the current platform is Android.</summary>
|
|
/// <returns>True if the current platform is Android.</returns>
|
|
public static bool isAndroidPlatform
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_ANDROID
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if the current platform is iOS.</summary>
|
|
/// <returns>True if the current platform is iOS.</returns>
|
|
public static bool isIOSPlatform
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_IOS
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if the current platform is tvOS.</summary>
|
|
/// <returns>True if the current platform is tvOS.</returns>
|
|
public static bool isTvOSPlatform
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_TVOS
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if the current platform is WSA.</summary>
|
|
/// <returns>True if the current platform is WSA.</returns>
|
|
public static bool isWSAPlatform
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_WSA
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if the current platform is XboxOne.</summary>
|
|
/// <returns>True if the current platform is XboxOne.</returns>
|
|
public static bool isXboxOnePlatform
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_XBOXONE
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if the current platform is PS4.</summary>
|
|
/// <returns>True if the current platform is PS4.</returns>
|
|
public static bool isPS4Platform
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_PS4
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if the current platform is WebGL.</summary>
|
|
/// <returns>True if the current platform is WebGL.</returns>
|
|
public static bool isWebGLPlatform
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_WEBGL
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if the current platform is Web (WebPlayer or WebGL).</summary>
|
|
/// <returns>True if the current platform is Web (WebPlayer or WebGL).</returns>
|
|
public static bool isWebPlatform => isWebGLPlatform;
|
|
|
|
/// <summary>Checks if the current platform is Windows-based (Windows standalone, WSA or XboxOne).</summary>
|
|
/// <returns>True if the current platform is Windows-based (Windows standalone, WSA or XboxOne).</returns>
|
|
public static bool isWindowsBasedPlatform => isWindowsPlatform || isWSAPlatform || isXboxOnePlatform;
|
|
|
|
/// <summary>Checks if the current platform is WSA-based (WSA or XboxOne).</summary>
|
|
/// <returns>True if the current platform is WSA-based (WSA or XboxOne).</returns>
|
|
public static bool isWSABasedPlatform => isWSAPlatform || isXboxOnePlatform;
|
|
|
|
/// <summary>Checks if the current platform is Apple-based (macOS standalone, iOS or tvOS).</summary>
|
|
/// <returns>True if the current platform is Apple-based (macOS standalone, iOS or tvOS).</returns>
|
|
public static bool isAppleBasedPlatform => isMacOSPlatform || isIOSPlatform || isTvOSPlatform;
|
|
|
|
/// <summary>Checks if the current platform is iOS-based (iOS or tvOS).</summary>
|
|
/// <returns>True if the current platform is iOS-based (iOS or tvOS).</returns>
|
|
public static bool isIOSBasedPlatform => isIOSPlatform || isTvOSPlatform;
|
|
|
|
/// <summary>Checks if the current platform is mobile (Android and iOS).</summary>
|
|
/// <returns>True if the current platform is mobile (Android and iOS).</returns>
|
|
public static bool isMobilePlatform => isAndroidPlatform || isIOSBasedPlatform;
|
|
|
|
/// <summary>Checks if we are inside the Editor.</summary>
|
|
/// <returns>True if we are inside the Editor.</returns>
|
|
public static bool isEditor => isWindowsEditor || isMacOSEditor || isLinuxEditor;
|
|
|
|
/// <summary>Checks if we are inside the Windows Editor.</summary>
|
|
/// <returns>True if we are inside the Windows Editor.</returns>
|
|
public static bool isWindowsEditor
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_EDITOR_WIN
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if we are inside the macOS Editor.</summary>
|
|
/// <returns>True if we are inside the macOS Editor.</returns>
|
|
public static bool isMacOSEditor
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_EDITOR_OSX
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if we are inside the Linux Editor.</summary>
|
|
/// <returns>True if we are inside the Linux Editor.</returns>
|
|
public static bool isLinuxEditor
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_EDITOR_LINUX
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if we are in Editor mode.</summary>
|
|
/// <returns>True if in Editor mode.</returns>
|
|
public static bool isEditorMode => isEditor && !ApplicationIsPlaying;
|
|
|
|
/// <summary>Checks if the current build target uses IL2CPP.</summary>
|
|
/// <returns>True if the current build target uses IL2CPP.</returns>
|
|
public static bool isIL2CPP
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_EDITOR
|
|
UnityEditor.BuildTarget target = UnityEditor.EditorUserBuildSettings.activeBuildTarget;
|
|
UnityEditor.BuildTargetGroup group = UnityEditor.BuildPipeline.GetBuildTargetGroup(target);
|
|
|
|
return UnityEditor.PlayerSettings.GetScriptingBackend(group) == UnityEditor.ScriptingImplementation.IL2CPP;
|
|
#else
|
|
#if ENABLE_IL2CPP
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>Returns the current platform.</summary>
|
|
/// <returns>The current platform.</returns>
|
|
public static Model.Enum.Platform CurrentPlatform
|
|
{
|
|
get
|
|
{
|
|
if (isWindowsPlatform)
|
|
return Model.Enum.Platform.Windows;
|
|
|
|
if (isMacOSPlatform)
|
|
return Model.Enum.Platform.OSX;
|
|
|
|
if (isLinuxPlatform)
|
|
return Model.Enum.Platform.Linux;
|
|
|
|
if (isAndroidPlatform)
|
|
return Model.Enum.Platform.Android;
|
|
|
|
if (isIOSBasedPlatform)
|
|
return Model.Enum.Platform.IOS;
|
|
|
|
if (isWSABasedPlatform)
|
|
return Model.Enum.Platform.WSA;
|
|
|
|
return isWebPlatform ? Model.Enum.Platform.Web : Model.Enum.Platform.Unsupported;
|
|
}
|
|
}
|
|
|
|
/// <summary>Returns the path to the the "Streaming Assets".</summary>
|
|
/// <returns>The path to the the "Streaming Assets".</returns>
|
|
public static string StreamingAssetsPath
|
|
{
|
|
get
|
|
{
|
|
if (isAndroidPlatform && !isEditor)
|
|
return $"jar:file://{applicationDataPath}!/assets/";
|
|
|
|
if (isIOSBasedPlatform && !isEditor)
|
|
return $"{applicationDataPath}/Raw/";
|
|
|
|
return $"{applicationDataPath}/StreamingAssets/";
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Static block
|
|
|
|
static BaseHelper()
|
|
{
|
|
//Debug.Log("Static block");
|
|
initialize();
|
|
}
|
|
|
|
[RuntimeInitializeOnLoadMethod]
|
|
private static void initialize()
|
|
{
|
|
//Debug.Log("initialize");
|
|
ApplicationIsPlaying = Application.isPlaying;
|
|
applicationDataPath = Application.dataPath;
|
|
|
|
if (!isEditorMode)
|
|
{
|
|
GameObject go = new GameObject("_HelperCT");
|
|
go.AddComponent<HelperCT>();
|
|
GameObject.DontDestroyOnLoad(go);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Public methods
|
|
|
|
/// <summary>Opens the given URL with the file explorer or browser.</summary>
|
|
/// <param name="url">URL to open</param>
|
|
/// <returns>True uf the URL was valid.</returns>
|
|
public static bool OpenURL(string url)
|
|
{
|
|
if (isValidURL(url))
|
|
{
|
|
Application.OpenURL(url);
|
|
|
|
return true;
|
|
}
|
|
|
|
Debug.LogWarning($"URL was invalid: {url}");
|
|
return false;
|
|
}
|
|
|
|
/// <summary>Creates a string of characters with a given length.</summary>
|
|
/// <param name="replaceChars">Characters to generate the string (if more than one character is used, the generated string will be a randomized result of all characters)</param>
|
|
/// <param name="stringLength">Length of the generated string</param>
|
|
/// <returns>Generated string</returns>
|
|
public static string CreateString(string replaceChars, int stringLength)
|
|
{
|
|
if (replaceChars != null)
|
|
{
|
|
if (replaceChars.Length > 1)
|
|
{
|
|
char[] chars = new char[stringLength];
|
|
|
|
for (int ii = 0; ii < stringLength; ii++)
|
|
{
|
|
chars[ii] = replaceChars[rnd.Next(0, replaceChars.Length)];
|
|
}
|
|
|
|
return new string(chars);
|
|
}
|
|
|
|
return replaceChars.Length == 1 ? new string(replaceChars[0], stringLength) : string.Empty;
|
|
}
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
/// <summary>Determines if an AudioSource has an active clip.</summary>
|
|
/// <param name="source">AudioSource to check.</param>
|
|
/// <returns>True if the AudioSource has an active clip.</returns>
|
|
public static bool hasActiveClip(AudioSource source)
|
|
{
|
|
int timeSamples = source.timeSamples;
|
|
return source != null && source.clip != null &&
|
|
(!source.loop && timeSamples > 0 && timeSamples < source.clip.samples - 256 ||
|
|
source.loop ||
|
|
source.isPlaying);
|
|
}
|
|
|
|
#if !UNITY_WSA || UNITY_EDITOR
|
|
/// <summary>HTTPS-certification callback.</summary>
|
|
public static bool RemoteCertificateValidationCallback(object sender,
|
|
System.Security.Cryptography.X509Certificates.X509Certificate certificate,
|
|
System.Security.Cryptography.X509Certificates.X509Chain chain,
|
|
System.Net.Security.SslPolicyErrors sslPolicyErrors)
|
|
{
|
|
bool isOk = true;
|
|
|
|
// If there are errors in the certificate chain, look at each error to determine the cause.
|
|
if (sslPolicyErrors != System.Net.Security.SslPolicyErrors.None)
|
|
{
|
|
foreach (System.Security.Cryptography.X509Certificates.X509ChainStatus t in chain.ChainStatus.Where(t =>
|
|
t.Status != System.Security.Cryptography.X509Certificates.X509ChainStatusFlags
|
|
.RevocationStatusUnknown))
|
|
{
|
|
chain.ChainPolicy.RevocationFlag = System.Security.Cryptography.X509Certificates.X509RevocationFlag.EntireChain;
|
|
chain.ChainPolicy.RevocationMode = System.Security.Cryptography.X509Certificates.X509RevocationMode.Online;
|
|
chain.ChainPolicy.UrlRetrievalTimeout = new System.TimeSpan(0, 1, 0);
|
|
chain.ChainPolicy.VerificationFlags = System.Security.Cryptography.X509Certificates.X509VerificationFlags.AllFlags;
|
|
|
|
isOk = chain.Build((System.Security.Cryptography.X509Certificates.X509Certificate2)certificate);
|
|
}
|
|
}
|
|
|
|
return isOk;
|
|
}
|
|
#endif
|
|
|
|
/// <summary>Validates a given path and add missing slash.</summary>
|
|
/// <param name="path">Path to validate</param>
|
|
/// <param name="addEndDelimiter">Add delimiter at the end of the path (optional, default: true)</param>
|
|
/// <returns>Valid path</returns>
|
|
public static string ValidatePath(string path, bool addEndDelimiter = true)
|
|
{
|
|
if (!string.IsNullOrEmpty(path))
|
|
{
|
|
string pathTemp = path.Trim();
|
|
string result;
|
|
|
|
if ((isWindowsBasedPlatform || isWindowsEditor) && !isMacOSEditor && !isLinuxEditor)
|
|
{
|
|
result = pathTemp.Replace('/', '\\');
|
|
|
|
if (addEndDelimiter)
|
|
{
|
|
if (!result.CTEndsWith(BaseConstants.PATH_DELIMITER_WINDOWS))
|
|
{
|
|
result += BaseConstants.PATH_DELIMITER_WINDOWS;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = pathTemp.Replace('\\', '/');
|
|
|
|
if (addEndDelimiter)
|
|
{
|
|
if (!result.CTEndsWith(BaseConstants.PATH_DELIMITER_UNIX))
|
|
{
|
|
result += BaseConstants.PATH_DELIMITER_UNIX;
|
|
}
|
|
}
|
|
}
|
|
|
|
return string.Join(string.Empty, result.Split(System.IO.Path.GetInvalidPathChars()));
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
/// <summary>Validates a given file.</summary>
|
|
/// <param name="path">File to validate</param>
|
|
/// <returns>Valid file path</returns>
|
|
public static string ValidateFile(string path)
|
|
{
|
|
if (!string.IsNullOrEmpty(path))
|
|
{
|
|
string result = ValidatePath(path);
|
|
|
|
if (result.CTEndsWith(BaseConstants.PATH_DELIMITER_WINDOWS) ||
|
|
result.CTEndsWith(BaseConstants.PATH_DELIMITER_UNIX))
|
|
{
|
|
result = result.Substring(0, result.Length - 1);
|
|
}
|
|
|
|
string fileName;
|
|
if ((isWindowsBasedPlatform || isWindowsEditor) && !isMacOSEditor && !isLinuxEditor)
|
|
{
|
|
fileName = result.Substring(result.CTLastIndexOf(BaseConstants.PATH_DELIMITER_WINDOWS) + 1);
|
|
}
|
|
else
|
|
{
|
|
fileName = result.Substring(result.CTLastIndexOf(BaseConstants.PATH_DELIMITER_UNIX) + 1);
|
|
}
|
|
|
|
string newName =
|
|
string.Join(string.Empty,
|
|
fileName.Split(System.IO.Path
|
|
.GetInvalidFileNameChars())); //.Replace(BaseConstants.PATH_DELIMITER_WINDOWS, string.Empty).Replace(BaseConstants.PATH_DELIMITER_UNIX, string.Empty);
|
|
|
|
return result.Substring(0, result.Length - fileName.Length) + newName;
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find files inside a path.
|
|
/// </summary>
|
|
/// <param name="path">Path to find the files</param>
|
|
/// <param name="isRecursive">Recursive search (default: false, optional)</param>
|
|
/// <param name="extensions">Extensions for the file search, e.g. "png" (optional)</param>
|
|
/// <returns>Returns array of the found files inside the path (alphabetically ordered). Zero length array when an error occured.</returns>
|
|
public static string[] GetFiles(string path, bool isRecursive = false, params string[] extensions)
|
|
{
|
|
if (isWebPlatform && !isEditor)
|
|
{
|
|
Debug.LogWarning("'GetFiles' is not supported for the current platform!");
|
|
}
|
|
else if (isWSABasedPlatform && !isEditor)
|
|
{
|
|
#if CT_FB_PRO
|
|
#if UNITY_WSA && !UNITY_EDITOR
|
|
Crosstales.FB.FileBrowserWSAImpl fbWsa = new Crosstales.FB.FileBrowserWSAImpl();
|
|
fbWsa.isBusy = true;
|
|
UnityEngine.WSA.Application.InvokeOnUIThread(() => { fbWsa.GetFiles(path, isRecursive, extensions); }, false);
|
|
|
|
do
|
|
{
|
|
//wait
|
|
} while (fbWsa.isBusy);
|
|
|
|
return fbWsa.Selection.ToArray();
|
|
#endif
|
|
#else
|
|
Debug.LogWarning($"'GetFiles' under UWP (WSA) is supported in combination with 'File Browser PRO'. For more, please see: {BaseConstants.ASSET_FB}");
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
if (!string.IsNullOrEmpty(path))
|
|
{
|
|
try
|
|
{
|
|
string _path = ValidatePath(path);
|
|
|
|
if (extensions == null || extensions.Length == 0 || extensions.Any(extension => extension.Equals("*") || extension.Equals("*.*")))
|
|
{
|
|
#if NET_4_6 || NET_STANDARD_2_0
|
|
return System.IO.Directory.EnumerateFiles(_path, "*", isRecursive
|
|
? System.IO.SearchOption.AllDirectories
|
|
: System.IO.SearchOption.TopDirectoryOnly).ToArray();
|
|
#else
|
|
return System.IO.Directory.GetFiles(_path, "*",
|
|
isRecursive
|
|
? System.IO.SearchOption.AllDirectories
|
|
: System.IO.SearchOption.TopDirectoryOnly);
|
|
#endif
|
|
}
|
|
|
|
System.Collections.Generic.List<string> files = new System.Collections.Generic.List<string>();
|
|
|
|
foreach (string extension in extensions)
|
|
{
|
|
files.AddRange(System.IO.Directory.EnumerateFiles(_path, $"*.{extension}", isRecursive
|
|
? System.IO.SearchOption.AllDirectories
|
|
: System.IO.SearchOption.TopDirectoryOnly));
|
|
}
|
|
|
|
return files.OrderBy(q => q).ToArray();
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogWarning($"Could not scan the path for files: {ex}");
|
|
}
|
|
}
|
|
}
|
|
|
|
return new string[0];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find directories inside.
|
|
/// </summary>
|
|
/// <param name="path">Path to find the directories</param>
|
|
/// <param name="isRecursive">Recursive search (default: false, optional)</param>
|
|
/// <returns>Returns array of the found directories inside the path. Zero length array when an error occured.</returns>
|
|
public static string[] GetDirectories(string path, bool isRecursive = false)
|
|
{
|
|
if (isWebPlatform && !isEditor)
|
|
{
|
|
Debug.LogWarning("'GetDirectories' is not supported for the current platform!");
|
|
}
|
|
else if (isWSABasedPlatform && !isEditor)
|
|
{
|
|
#if CT_FB_PRO
|
|
#if UNITY_WSA && !UNITY_EDITOR
|
|
Crosstales.FB.FileBrowserWSAImpl fbWsa = new Crosstales.FB.FileBrowserWSAImpl();
|
|
fbWsa.isBusy = true;
|
|
UnityEngine.WSA.Application.InvokeOnUIThread(() => { fbWsa.GetDirectories(path, isRecursive); }, false);
|
|
|
|
do
|
|
{
|
|
//wait
|
|
} while (fbWsa.isBusy);
|
|
|
|
return fbWsa.Selection.ToArray();
|
|
#endif
|
|
#else
|
|
Debug.LogWarning($"'GetDirectories' under UWP (WSA) is supported in combination with 'File Browser PRO'. For more, please see: {BaseConstants.ASSET_FB}");
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
if (!string.IsNullOrEmpty(path))
|
|
{
|
|
try
|
|
{
|
|
string _path = ValidatePath(path);
|
|
#if NET_4_6 || NET_STANDARD_2_0
|
|
return System.IO.Directory.EnumerateDirectories(_path, "*", isRecursive
|
|
? System.IO.SearchOption.AllDirectories
|
|
: System.IO.SearchOption.TopDirectoryOnly).ToArray();
|
|
#else
|
|
return System.IO.Directory.GetDirectories(_path, "*",
|
|
isRecursive
|
|
? System.IO.SearchOption.AllDirectories
|
|
: System.IO.SearchOption.TopDirectoryOnly);
|
|
#endif
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogWarning($"Could not scan the path for directories: {ex}");
|
|
}
|
|
}
|
|
}
|
|
|
|
return new string[0];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find all logical drives.
|
|
/// </summary>
|
|
/// <returns>Returns array of the found drives. Zero length array when an error occured.</returns>
|
|
public static string[] GetDrives() //TODO replace with "Util.Helper.GetDrives" in the next version
|
|
{
|
|
if (isWebPlatform && !isEditor)
|
|
{
|
|
Debug.LogWarning("'GetDrives' is not supported for the current platform!");
|
|
}
|
|
else if (isWSABasedPlatform && !isEditor)
|
|
{
|
|
#if CT_FB_PRO
|
|
#if UNITY_WSA && !UNITY_EDITOR && ENABLE_WINMD_SUPPORT
|
|
Crosstales.FB.FileBrowserWSAImpl fbWsa = new Crosstales.FB.FileBrowserWSAImpl();
|
|
fbWsa.isBusy = true;
|
|
UnityEngine.WSA.Application.InvokeOnUIThread(() => { fbWsa.GetDrives(); }, false);
|
|
|
|
do
|
|
{
|
|
//wait
|
|
} while (fbWsa.isBusy);
|
|
|
|
return fbWsa.Selection.ToArray();
|
|
#endif
|
|
#else
|
|
Debug.LogWarning($"'GetDrives' under UWP (WSA) is supported in combination with 'File Browser PRO'. For more, please see: {BaseConstants.ASSET_FB}");
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
#if !UNITY_WSA || UNITY_EDITOR
|
|
try
|
|
{
|
|
return System.IO.Directory.GetLogicalDrives();
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogWarning($"Could not scan the path for directories: {ex}");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
return new string[0];
|
|
}
|
|
|
|
/*
|
|
/// <summary>Validates a given path and add missing slash.</summary>
|
|
/// <param name="path">Path to validate</param>
|
|
/// <returns>Valid path</returns>
|
|
public static string ValidPath(string path)
|
|
{
|
|
if (!string.IsNullOrEmpty(path))
|
|
{
|
|
string pathTemp = path.Trim();
|
|
string result = null;
|
|
|
|
if (isWindowsPlatform)
|
|
{
|
|
result = pathTemp.Replace('/', '\\');
|
|
|
|
if (!result.EndsWith(BaseConstants.PATH_DELIMITER_WINDOWS))
|
|
{
|
|
result += BaseConstants.PATH_DELIMITER_WINDOWS;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = pathTemp.Replace('\\', '/');
|
|
|
|
if (!result.EndsWith(BaseConstants.PATH_DELIMITER_UNIX))
|
|
{
|
|
result += BaseConstants.PATH_DELIMITER_UNIX;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
/// <summary>Validates a given file.</summary>
|
|
/// <param name="path">File to validate</param>
|
|
/// <returns>Valid file path</returns>
|
|
public static string ValidFilePath(string path)
|
|
{
|
|
if (!string.IsNullOrEmpty(path))
|
|
{
|
|
|
|
string result = ValidPath(path);
|
|
|
|
if (result.EndsWith(BaseConstants.PATH_DELIMITER_WINDOWS) || result.EndsWith(BaseConstants.PATH_DELIMITER_UNIX))
|
|
{
|
|
result = result.Substring(0, result.Length - 1);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
return path;
|
|
}
|
|
*/
|
|
|
|
/// <summary>Validates a given file.</summary>
|
|
/// <param name="path">File to validate</param>
|
|
/// <returns>Valid file path</returns>
|
|
public static string ValidURLFromFilePath(string path)
|
|
{
|
|
if (!string.IsNullOrEmpty(path))
|
|
{
|
|
if (!isValidURL(path))
|
|
return BaseConstants.PREFIX_FILE + System.Uri.EscapeUriString(ValidateFile(path).Replace('\\', '/'));
|
|
|
|
return System.Uri.EscapeUriString(ValidateFile(path).Replace('\\', '/'));
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
/// <summary>Cleans a given URL.</summary>
|
|
/// <param name="url">URL to clean</param>
|
|
/// <param name="removeProtocol">Remove the protocol, e.g. http:// (default: true, optional).</param>
|
|
/// <param name="removeWWW">Remove www (default: true, optional).</param>
|
|
/// <param name="removeSlash">Remove slash at the end (default: true, optional)</param>
|
|
/// <returns>Clean URL</returns>
|
|
public static string CleanUrl(string url, bool removeProtocol = true, bool removeWWW = true,
|
|
bool removeSlash = true)
|
|
{
|
|
string result = url?.Trim();
|
|
|
|
if (!string.IsNullOrEmpty(url))
|
|
{
|
|
if (removeProtocol)
|
|
{
|
|
result = result.Substring(result.CTIndexOf("//") + 2);
|
|
}
|
|
|
|
if (removeWWW)
|
|
{
|
|
result = result.CTReplace("www.", string.Empty);
|
|
}
|
|
|
|
if (removeSlash && result.CTEndsWith(BaseConstants.PATH_DELIMITER_UNIX))
|
|
{
|
|
result = result.Substring(0, result.Length - 1);
|
|
}
|
|
|
|
/*
|
|
if (urlTemp.StartsWith("http://"))
|
|
{
|
|
result = urlTemp.Substring(7);
|
|
}
|
|
else if (urlTemp.StartsWith("https://"))
|
|
{
|
|
result = urlTemp.Substring(8);
|
|
}
|
|
else
|
|
{
|
|
result = urlTemp;
|
|
}
|
|
|
|
if (result.StartsWith("www."))
|
|
{
|
|
result = result.Substring(4);
|
|
}
|
|
*/
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>Cleans a given text from tags.</summary>
|
|
/// <param name="text">Text to clean.</param>
|
|
/// <returns>Clean text without tags.</returns>
|
|
public static string ClearTags(string text)
|
|
{
|
|
return text != null ? cleanTagsRegex.Replace(text, string.Empty).Trim() : null;
|
|
}
|
|
|
|
/// <summary>Cleans a given text from multiple spaces.</summary>
|
|
/// <param name="text">Text to clean.</param>
|
|
/// <returns>Clean text without multiple spaces.</returns>
|
|
public static string ClearSpaces(string text)
|
|
{
|
|
return text != null ? cleanSpacesRegex.Replace(text, " ").Trim() : null;
|
|
}
|
|
|
|
/// <summary>Cleans a given text from line endings.</summary>
|
|
/// <param name="text">Text to clean.</param>
|
|
/// <returns>Clean text without line endings.</returns>
|
|
public static string ClearLineEndings(string text)
|
|
{
|
|
return text != null ? lineEndingsRegex.Replace(text, string.Empty).Trim() : null;
|
|
}
|
|
|
|
/// <summary>Split the given text to lines and return it as list.</summary>
|
|
/// <param name="text">Complete text fragment</param>
|
|
/// <param name="ignoreCommentedLines">Ignore commente lines (default: true, optional)</param>
|
|
/// <param name="skipHeaderLines">Number of skipped header lines (default: 0, optional)</param>
|
|
/// <param name="skipFooterLines">Number of skipped footer lines (default: 0, optional)</param>
|
|
/// <returns>Splitted lines as array</returns>
|
|
public static System.Collections.Generic.List<string> SplitStringToLines(string text,
|
|
bool ignoreCommentedLines = true, int skipHeaderLines = 0, int skipFooterLines = 0)
|
|
{
|
|
System.Collections.Generic.List<string> result = new System.Collections.Generic.List<string>(100);
|
|
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
Debug.LogWarning("Parameter 'text' is null or empty => 'SplitStringToLines()' will return an empty string list.");
|
|
}
|
|
else
|
|
{
|
|
string[] lines = lineEndingsRegex.Split(text);
|
|
|
|
for (int ii = 0; ii < lines.Length; ii++)
|
|
{
|
|
if (ii + 1 > skipHeaderLines && ii < lines.Length - skipFooterLines)
|
|
{
|
|
if (!string.IsNullOrEmpty(lines[ii]))
|
|
{
|
|
if (ignoreCommentedLines)
|
|
{
|
|
if (!lines[ii].CTStartsWith("#")) //valid and not disabled line?
|
|
result.Add(lines[ii]);
|
|
}
|
|
else
|
|
{
|
|
result.Add(lines[ii]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>Format byte-value to Human-Readable-Form.</summary>
|
|
/// <returns>Formatted byte-value in Human-Readable-Form.</returns>
|
|
public static string FormatBytesToHRF(long bytes)
|
|
{
|
|
string[] sizes = {"B", "KB", "MB", "GB", "TB"};
|
|
double len = bytes;
|
|
int order = 0;
|
|
while (len >= 1024 && order < sizes.Length - 1)
|
|
{
|
|
order++;
|
|
len /= 1024;
|
|
}
|
|
|
|
// Adjust the format string to your preferences.
|
|
return $"{len:0.##} {sizes[order]}";
|
|
}
|
|
|
|
/// <summary>Format seconds to Human-Readable-Form.</summary>
|
|
/// <returns>Formatted seconds in Human-Readable-Form.</returns>
|
|
public static string FormatSecondsToHourMinSec(double seconds)
|
|
{
|
|
int totalSeconds = (int)seconds;
|
|
int calcSeconds = totalSeconds % 60;
|
|
|
|
if (seconds >= 86400)
|
|
{
|
|
int calcDays = totalSeconds / 86400;
|
|
int calcHours = (totalSeconds -= calcDays * 86400) / 3600;
|
|
int calcMinutes = (totalSeconds - calcHours * 3600) / 60;
|
|
|
|
return $"{calcDays}d {calcHours}:{addLeadingZero(calcMinutes)}:{addLeadingZero(calcSeconds)}";
|
|
}
|
|
|
|
if (seconds >= 3600)
|
|
{
|
|
int calcHours = totalSeconds / 3600;
|
|
int calcMinutes = (totalSeconds - calcHours * 3600) / 60;
|
|
|
|
return $"{calcHours}:{addLeadingZero(calcMinutes)}:{addLeadingZero(calcSeconds)}";
|
|
}
|
|
else
|
|
{
|
|
int calcMinutes = totalSeconds / 60;
|
|
|
|
return $"{calcMinutes}:{addLeadingZero(calcSeconds)}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate nice HSV colors.
|
|
/// Based on https://gist.github.com/rje/6206099
|
|
/// </summary>
|
|
/// <param name="h">Hue</param>
|
|
/// <param name="s">Saturation</param>
|
|
/// <param name="v">Value</param>
|
|
/// <param name="a">Alpha (optional)</param>
|
|
/// <returns>True if the current platform is supported.</returns>
|
|
public static Color HSVToRGB(float h, float s, float v, float a = 1f)
|
|
{
|
|
if (Mathf.Abs(s) < BaseConstants.FLOAT_TOLERANCE)
|
|
return new Color(v, v, v, a);
|
|
|
|
float _h = h / 60f;
|
|
int sector = Mathf.FloorToInt(_h);
|
|
float fact = _h - sector;
|
|
float p = v * (1f - s);
|
|
float q = v * (1f - s * fact);
|
|
float t = v * (1f - s * (1f - fact));
|
|
|
|
switch (sector)
|
|
{
|
|
case 0:
|
|
return new Color(v, t, p, a);
|
|
case 1:
|
|
return new Color(q, v, p, a);
|
|
case 2:
|
|
return new Color(p, v, t, a);
|
|
case 3:
|
|
return new Color(p, q, v, a);
|
|
case 4:
|
|
return new Color(t, p, v, a);
|
|
default:
|
|
return new Color(v, p, q, a);
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if the URL is valid.</summary>
|
|
/// <param name="url">URL to check</param>
|
|
/// <returns>True if the URL is valid.</returns>
|
|
public static bool isValidURL(string url)
|
|
{
|
|
return !string.IsNullOrEmpty(url) &&
|
|
(url.StartsWith(file_prefix, System.StringComparison.OrdinalIgnoreCase) ||
|
|
url.StartsWith(BaseConstants.PREFIX_HTTP, System.StringComparison.OrdinalIgnoreCase) ||
|
|
url.StartsWith(BaseConstants.PREFIX_HTTPS, System.StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
/// <summary>Copy or move a file.</summary>
|
|
/// <param name="inputFile">Input file path</param>
|
|
/// <param name="outputFile">Output file path</param>
|
|
/// <param name="move">Move file instead of copy (default: false, optional)</param>
|
|
public static void FileCopy(string inputFile, string outputFile, bool move = false)
|
|
{
|
|
if ((isWSABasedPlatform || isWebPlatform) && !isEditor)
|
|
{
|
|
Debug.LogWarning("'FileCopy' is not supported for the current platform!");
|
|
}
|
|
else
|
|
{
|
|
if (!string.IsNullOrEmpty(outputFile))
|
|
{
|
|
try
|
|
{
|
|
if (!System.IO.File.Exists(inputFile))
|
|
{
|
|
Debug.LogError($"Input file does not exists: {inputFile}");
|
|
}
|
|
else
|
|
{
|
|
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(outputFile));
|
|
|
|
if (System.IO.File.Exists(outputFile))
|
|
{
|
|
if (BaseConstants.DEV_DEBUG)
|
|
Debug.LogWarning($"Overwrite output file: {outputFile}");
|
|
|
|
System.IO.File.Delete(outputFile);
|
|
}
|
|
|
|
if (move)
|
|
{
|
|
#if UNITY_STANDALONE || UNITY_EDITOR
|
|
System.IO.File.Move(inputFile, outputFile);
|
|
#else
|
|
System.IO.File.Copy(inputFile, outputFile);
|
|
System.IO.File.Delete(inputFile);
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
System.IO.File.Copy(inputFile, outputFile);
|
|
}
|
|
}
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogError($"Could not copy file: {ex}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows the location of a path or file in OS file explorer.
|
|
/// NOTE: only works for standalone platforms
|
|
/// </summary>
|
|
/// <param name="file">File path</param>
|
|
public static void ShowFileLocation(string file)
|
|
{
|
|
if (isStandalonePlatform || isEditor)
|
|
{
|
|
#if UNITY_STANDALONE || UNITY_EDITOR
|
|
string path;
|
|
|
|
if (string.IsNullOrEmpty(file) || file.Equals("."))
|
|
{
|
|
path = ".";
|
|
}
|
|
else if ((isWindowsPlatform || isWindowsEditor) && file.Length < 4)
|
|
{
|
|
path = file; //root directory
|
|
}
|
|
else
|
|
{
|
|
path = ValidatePath(System.IO.Path.GetDirectoryName(file));
|
|
}
|
|
|
|
try
|
|
{
|
|
if (System.IO.Directory.Exists(path))
|
|
{
|
|
#if ENABLE_IL2CPP
|
|
using (CTProcess process = new CTProcess())
|
|
{
|
|
process.StartInfo.Arguments = $"\"{path}\"";
|
|
|
|
if (isWindowsPlatform || isWindowsEditor)
|
|
{
|
|
process.StartInfo.FileName = "explorer.exe";
|
|
process.StartInfo.UseCmdExecute = true;
|
|
process.StartInfo.CreateNoWindow = true;
|
|
}
|
|
else if (isMacOSPlatform || isMacOSEditor)
|
|
{
|
|
process.StartInfo.FileName = "open";
|
|
}
|
|
else
|
|
{
|
|
process.StartInfo.FileName = "xdg-open";
|
|
}
|
|
|
|
process.Start();
|
|
}
|
|
#else
|
|
System.Diagnostics.Process.Start(path);
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"Path to file doesn't exist: {path}");
|
|
}
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogError($"Could not show file location: {ex}");
|
|
}
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("'ShowFileLocation' is not supported on the current platform!");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Opens a file with the OS default application.
|
|
/// NOTE: only works for standalone platforms
|
|
/// </summary>
|
|
/// <param name="file">File path</param>
|
|
public static void OpenFile(string file)
|
|
{
|
|
if (isStandalonePlatform || isEditor)
|
|
{
|
|
try
|
|
{
|
|
#if UNITY_STANDALONE || UNITY_EDITOR
|
|
if (System.IO.File.Exists(file))
|
|
{
|
|
#if ENABLE_IL2CPP
|
|
using (CTProcess process = new CTProcess())
|
|
{
|
|
process.StartInfo.Arguments = $"\"{file}\"";
|
|
|
|
if (isWindowsPlatform || isWindowsEditor)
|
|
{
|
|
process.StartInfo.FileName = "explorer.exe";
|
|
process.StartInfo.UseCmdExecute = true;
|
|
process.StartInfo.CreateNoWindow = true;
|
|
}
|
|
else if (isMacOSPlatform || isMacOSEditor)
|
|
{
|
|
process.StartInfo.FileName = "open";
|
|
}
|
|
else
|
|
{
|
|
process.StartInfo.FileName = "xdg-open";
|
|
}
|
|
|
|
process.Start();
|
|
}
|
|
#else
|
|
using (System.Diagnostics.Process process = new System.Diagnostics.Process())
|
|
{
|
|
if (isMacOSPlatform || isMacOSEditor)
|
|
{
|
|
process.StartInfo.FileName = "open";
|
|
process.StartInfo.WorkingDirectory =
|
|
System.IO.Path.GetDirectoryName(file) + BaseConstants.PATH_DELIMITER_UNIX;
|
|
process.StartInfo.Arguments = $"-t \"{System.IO.Path.GetFileName(file)}\"";
|
|
}
|
|
else if (isLinuxPlatform || isLinuxEditor)
|
|
{
|
|
process.StartInfo.FileName = "xdg-open";
|
|
process.StartInfo.WorkingDirectory =
|
|
System.IO.Path.GetDirectoryName(file) + BaseConstants.PATH_DELIMITER_UNIX;
|
|
process.StartInfo.Arguments = System.IO.Path.GetFileName(file);
|
|
}
|
|
else
|
|
{
|
|
process.StartInfo.FileName = file;
|
|
}
|
|
|
|
process.Start();
|
|
}
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"File doesn't exist: {file}");
|
|
}
|
|
#endif
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogError($"Could not open file: {ex}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("'OpenFile' is not supported on the current platform!");
|
|
}
|
|
}
|
|
|
|
/// <summary>Returns the IP of a given host name.</summary>
|
|
/// <param name="host">Host name</param>
|
|
/// <returns>IP of a given host name.</returns>
|
|
public static string getIP(string host)
|
|
{
|
|
if (!string.IsNullOrEmpty(host))
|
|
{
|
|
#if !UNITY_WSA && !UNITY_WEBGL
|
|
try
|
|
{
|
|
return System.Net.Dns.GetHostAddresses(host)[0].ToString();
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogWarning($"Could not resolve host '{host}': {ex}");
|
|
}
|
|
#else
|
|
Debug.LogWarning("'getIP' doesn't work in WebGL or WSA! Returning original string.");
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("Host name is null or empty - can't resolve to IP!");
|
|
}
|
|
|
|
return host;
|
|
}
|
|
|
|
private static string addLeadingZero(int value)
|
|
{
|
|
return value < 10 ? "0" + value : value.ToString();
|
|
}
|
|
|
|
// StringHelper
|
|
/*
|
|
public static byte[] GetBytesFromText(string text) {
|
|
return new UnicodeEncoding().GetBytes(text);
|
|
}
|
|
|
|
public static string GetTextFromBytes(byte[] bytes) {
|
|
return new UnicodeEncoding().GetString(bytes, 0, bytes.Length);
|
|
}
|
|
|
|
public static byte[] GetBytesFromBase64(string text) {
|
|
return Convert.FromBase64String(text);
|
|
}
|
|
|
|
public static string GetBase64FromBytes(byte[] bytes) {
|
|
return Convert.ToBase64String(bytes);
|
|
}
|
|
*/
|
|
|
|
|
|
// MathHelper
|
|
/*
|
|
public static bool IsInRange(float actValue, float refValue, float range) {
|
|
|
|
return (actValue >= refValue-range) && (actValue <= refValue+range);
|
|
}
|
|
|
|
|
|
public static bool IsInRange(int actValue, int refValue, int range) {
|
|
|
|
return (actValue >= refValue-range) && (actValue <= refValue+range);
|
|
}
|
|
*/
|
|
|
|
|
|
// Add Math3dHelper?
|
|
|
|
|
|
// Color Helper
|
|
/*
|
|
public static string ColorToHex(Color32 color)
|
|
{
|
|
// if (color == null)
|
|
// throw new ArgumentNullException("color");
|
|
|
|
string hex = color.r.ToString("X2") + color.g.ToString("X2") + color.b.ToString("X2");
|
|
return hex;
|
|
}
|
|
|
|
public static Color HexToColor(string hex)
|
|
{
|
|
if (string.IsNullOrEmpty(hex))
|
|
throw new ArgumentNullException("hex");
|
|
|
|
byte r = byte.Parse(hex.Substring(0, 2), System.Globalization.NumberStyles.HexNumber);
|
|
byte g = byte.Parse(hex.Substring(2, 2), System.Globalization.NumberStyles.HexNumber);
|
|
byte b = byte.Parse(hex.Substring(4, 2), System.Globalization.NumberStyles.HexNumber);
|
|
return new Color32(r, g, b, 255);
|
|
}
|
|
*/
|
|
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>Helper to reset the necessary settings.</summary>
|
|
public class HelperCT : MonoBehaviour
|
|
{
|
|
private void OnApplicationQuit()
|
|
{
|
|
BaseHelper.ApplicationIsPlaying = false;
|
|
}
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
[UnityEditor.CustomEditor(typeof(HelperCT))]
|
|
public class HelperCTEditor : UnityEditor.Editor
|
|
{
|
|
public override void OnInspectorGUI()
|
|
{
|
|
UnityEditor.EditorGUILayout.HelpBox("This helper ensures the flawless working of the assets from 'crosstales LLC'.\nPlease do not delete it from the hierarchy.", UnityEditor.MessageType.Info);
|
|
}
|
|
}
|
|
|
|
#endif
|
|
}
|
|
// © 2015-2020 crosstales LLC (https://www.crosstales.com) |