diff --git a/.github/Asset Store Tools/Api.meta b/.github/Asset Store Tools/Api.meta new file mode 100644 index 00000000000..0cf8e3fbed4 --- /dev/null +++ b/.github/Asset Store Tools/Api.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d48a2325d352e7a4cae56d3f8eeaab2d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Abstractions.meta b/.github/Asset Store Tools/Api/Abstractions.meta new file mode 100644 index 00000000000..80ccfb13c73 --- /dev/null +++ b/.github/Asset Store Tools/Api/Abstractions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 25799fb31cd475347af7f5442c231797 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Abstractions/AuthenticationBase.cs b/.github/Asset Store Tools/Api/Abstractions/AuthenticationBase.cs new file mode 100644 index 00000000000..84d1fa83e40 --- /dev/null +++ b/.github/Asset Store Tools/Api/Abstractions/AuthenticationBase.cs @@ -0,0 +1,48 @@ +using AssetStoreTools.Api.Responses; +using AssetStoreTools.Utility; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; + +namespace AssetStoreTools.Api +{ + internal abstract class AuthenticationBase : IAuthenticationType + { + protected Uri LoginUrl = ApiUtility.CreateUri(Constants.Api.AuthenticateUrl, true); + protected FormUrlEncodedContent AuthenticationContent; + + protected FormUrlEncodedContent GetAuthenticationContent(params KeyValuePair[] content) + { + var baseContent = Constants.Api.DefaultAssetStoreQuery(); + + try { baseContent.Add("license_hash", ApiUtility.GetLicenseHash()); } catch { ASDebug.LogWarning("Could not retrieve license hash"); } + try { baseContent.Add("hardware_hash", ApiUtility.GetHardwareHash()); } catch { ASDebug.LogWarning("Could not retrieve hardware hash"); } + + foreach (var extraContent in content) + { + baseContent.Add(extraContent.Key, extraContent.Value); + } + + return new FormUrlEncodedContent(baseContent); + } + + protected AuthenticationResponse ParseResponse(HttpResponseMessage response) + { + try + { + response.EnsureSuccessStatusCode(); + var responseString = response.Content.ReadAsStringAsync().Result; + return new AuthenticationResponse(responseString); + } + catch (HttpRequestException e) + { + return new AuthenticationResponse(response.StatusCode, e) { Success = false }; + } + } + + public abstract Task Authenticate(IAssetStoreClient client, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Abstractions/AuthenticationBase.cs.meta b/.github/Asset Store Tools/Api/Abstractions/AuthenticationBase.cs.meta new file mode 100644 index 00000000000..6b0cd8f745e --- /dev/null +++ b/.github/Asset Store Tools/Api/Abstractions/AuthenticationBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f677e03f1be1048439a1fa5e7a0a37b6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Abstractions/IAssetStoreApi.cs b/.github/Asset Store Tools/Api/Abstractions/IAssetStoreApi.cs new file mode 100644 index 00000000000..86db5cdb388 --- /dev/null +++ b/.github/Asset Store Tools/Api/Abstractions/IAssetStoreApi.cs @@ -0,0 +1,21 @@ +using AssetStoreTools.Api.Models; +using AssetStoreTools.Api.Responses; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace AssetStoreTools.Api +{ + internal interface IAssetStoreApi + { + Task GetLatestAssetStoreToolsVersion(CancellationToken cancellationToken = default); + Task Authenticate(IAuthenticationType authenticationType, CancellationToken cancellationToken = default); + void Deauthenticate(); + Task GetPackages(CancellationToken cancellationToken = default); + Task GetCategories(CancellationToken cancellationToken = default); + Task GetPackageThumbnail(Package package, CancellationToken cancellationToken = default); + Task RefreshPackageMetadata(Package package, CancellationToken cancellationToken = default); + Task GetPackageUploadedVersions(Package package, CancellationToken cancellationToken = default); + Task UploadPackage(IPackageUploader uploader, IProgress progress = null, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Abstractions/IAssetStoreApi.cs.meta b/.github/Asset Store Tools/Api/Abstractions/IAssetStoreApi.cs.meta new file mode 100644 index 00000000000..91ac8510699 --- /dev/null +++ b/.github/Asset Store Tools/Api/Abstractions/IAssetStoreApi.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e616488c25d278741bb0d08168219309 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Abstractions/IAssetStoreClient.cs b/.github/Asset Store Tools/Api/Abstractions/IAssetStoreClient.cs new file mode 100644 index 00000000000..f6a6d3e198d --- /dev/null +++ b/.github/Asset Store Tools/Api/Abstractions/IAssetStoreClient.cs @@ -0,0 +1,18 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace AssetStoreTools.Api +{ + internal interface IAssetStoreClient + { + void SetSessionId(string sessionId); + void ClearSessionId(); + + Task Get(Uri uri, CancellationToken cancellationToken = default); + Task Post(Uri uri, HttpContent content, CancellationToken cancellationToken = default); + Task Put(Uri uri, HttpContent content, CancellationToken cancellationToken = default); + Task Send(HttpRequestMessage request, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Abstractions/IAssetStoreClient.cs.meta b/.github/Asset Store Tools/Api/Abstractions/IAssetStoreClient.cs.meta new file mode 100644 index 00000000000..6671aaf60d9 --- /dev/null +++ b/.github/Asset Store Tools/Api/Abstractions/IAssetStoreClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b2bbadec62178cc4189e605367b219e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Abstractions/IAuthenticationType.cs b/.github/Asset Store Tools/Api/Abstractions/IAuthenticationType.cs new file mode 100644 index 00000000000..8fa5c78e808 --- /dev/null +++ b/.github/Asset Store Tools/Api/Abstractions/IAuthenticationType.cs @@ -0,0 +1,11 @@ +using AssetStoreTools.Api.Responses; +using System.Threading; +using System.Threading.Tasks; + +namespace AssetStoreTools.Api +{ + internal interface IAuthenticationType + { + Task Authenticate(IAssetStoreClient client, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Abstractions/IAuthenticationType.cs.meta b/.github/Asset Store Tools/Api/Abstractions/IAuthenticationType.cs.meta new file mode 100644 index 00000000000..916782fd84f --- /dev/null +++ b/.github/Asset Store Tools/Api/Abstractions/IAuthenticationType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0000dcd6975bc8e4abc546a19f194040 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Abstractions/IPackageUploader.cs b/.github/Asset Store Tools/Api/Abstractions/IPackageUploader.cs new file mode 100644 index 00000000000..ef7c89f9d3f --- /dev/null +++ b/.github/Asset Store Tools/Api/Abstractions/IPackageUploader.cs @@ -0,0 +1,12 @@ +using AssetStoreTools.Api.Responses; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace AssetStoreTools.Api +{ + internal interface IPackageUploader + { + Task Upload(IAssetStoreClient client, IProgress progress, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Abstractions/IPackageUploader.cs.meta b/.github/Asset Store Tools/Api/Abstractions/IPackageUploader.cs.meta new file mode 100644 index 00000000000..b4eb3db02aa --- /dev/null +++ b/.github/Asset Store Tools/Api/Abstractions/IPackageUploader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0fc6c47b1c0a65540a40efbf1491193b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Abstractions/PackageUploaderBase.cs b/.github/Asset Store Tools/Api/Abstractions/PackageUploaderBase.cs new file mode 100644 index 00000000000..e26bfc18cfb --- /dev/null +++ b/.github/Asset Store Tools/Api/Abstractions/PackageUploaderBase.cs @@ -0,0 +1,59 @@ +using AssetStoreTools.Api.Responses; +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace AssetStoreTools.Api +{ + internal abstract class PackageUploaderBase : IPackageUploader + { + protected const int UploadChunkSizeBytes = 32768; + protected const int UploadResponseTimeoutMs = 10000; + + protected abstract void ValidateSettings(); + public abstract Task Upload(IAssetStoreClient client, IProgress progress = null, CancellationToken cancellationToken = default); + + protected void EnsureSuccessResponse(HttpResponseMessage response) + { + try + { + response.EnsureSuccessStatusCode(); + } + catch + { + throw new Exception(response.Content.ReadAsStringAsync().Result); + } + } + + protected void WaitForUploadCompletion(Task response, FileStream requestFileStream, IProgress progress, CancellationToken cancellationToken) + { + // Progress tracking + int updateIntervalMs = 100; + bool allBytesSent = false; + DateTime timeOfCompletion = default; + + while (!response.IsCompleted) + { + float uploadProgress = (float)requestFileStream.Position / requestFileStream.Length * 100; + progress?.Report(uploadProgress); + Thread.Sleep(updateIntervalMs); + + // A timeout for rare cases, when package uploading reaches 100%, but Put task IsComplete value remains 'False' + if (requestFileStream.Position == requestFileStream.Length) + { + if (!allBytesSent) + { + allBytesSent = true; + timeOfCompletion = DateTime.UtcNow; + } + else if (DateTime.UtcNow.Subtract(timeOfCompletion).TotalMilliseconds > UploadResponseTimeoutMs) + { + throw new TimeoutException(); + } + } + } + } + } +} diff --git a/.github/Asset Store Tools/Api/Abstractions/PackageUploaderBase.cs.meta b/.github/Asset Store Tools/Api/Abstractions/PackageUploaderBase.cs.meta new file mode 100644 index 00000000000..4764456055a --- /dev/null +++ b/.github/Asset Store Tools/Api/Abstractions/PackageUploaderBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2718ddd16e425ba4a82ab973724bcff7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/ApiUtility.cs b/.github/Asset Store Tools/Api/ApiUtility.cs new file mode 100644 index 00000000000..545d6d47605 --- /dev/null +++ b/.github/Asset Store Tools/Api/ApiUtility.cs @@ -0,0 +1,76 @@ +using AssetStoreTools.Api.Models; +using AssetStoreTools.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditorInternal; + +namespace AssetStoreTools.Api +{ + internal class ApiUtility + { + public static Uri CreateUri(string url, bool includeDefaultAssetStoreQuery) => CreateUri(url, null, includeDefaultAssetStoreQuery); + public static Uri CreateUri(string url, IDictionary queryParameters, bool includeDefaultAssetStoreQuery) + { + IDictionary fullQueryParameters = includeDefaultAssetStoreQuery ? + Constants.Api.DefaultAssetStoreQuery() : new Dictionary(); + + if (queryParameters != null && queryParameters.Count > 0) + { + foreach (var kvp in queryParameters) + fullQueryParameters.Add(kvp); + } + + var builder = new UriBuilder(url); + if (fullQueryParameters.Count == 0) + return builder.Uri; + + var fullQueryParameterString = string.Empty; + foreach (var queryParam in fullQueryParameters) + { + var escapedValue = queryParam.Value != null ? Uri.EscapeDataString(queryParam.Value) : string.Empty; + fullQueryParameterString += $"{queryParam.Key}={escapedValue}&"; + } + fullQueryParameterString = fullQueryParameterString.Remove(fullQueryParameterString.Length - 1); + + builder.Query = fullQueryParameterString; + return builder.Uri; + } + + public static List CombinePackageData(List mainPackageData, List extraPackageData, List categoryData) + { + foreach (var package in mainPackageData) + { + var extraData = extraPackageData.FirstOrDefault(x => package.PackageId == x.PackageId); + + if (extraData == null) + { + ASDebug.LogWarning($"Could not find extra data for Package {package.PackageId}"); + continue; + } + + var categoryId = extraData.CategoryId; + var category = categoryData.FirstOrDefault(x => x.Id.ToString() == categoryId); + if (category != null) + package.Category = category.Name; + else + package.Category = "Unknown"; + + package.Modified = extraData.Modified; + package.Size = extraData.Size; + } + + return mainPackageData; + } + + public static string GetLicenseHash() + { + return InternalEditorUtility.GetAuthToken().Substring(0, 40); + } + + public static string GetHardwareHash() + { + return InternalEditorUtility.GetAuthToken().Substring(40, 40); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/ApiUtility.cs.meta b/.github/Asset Store Tools/Api/ApiUtility.cs.meta new file mode 100644 index 00000000000..52919583f19 --- /dev/null +++ b/.github/Asset Store Tools/Api/ApiUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5becec0b3c0ba274fb0b01544e63b6c4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/AssetStoreApi.cs b/.github/Asset Store Tools/Api/AssetStoreApi.cs new file mode 100644 index 00000000000..a8530dcca81 --- /dev/null +++ b/.github/Asset Store Tools/Api/AssetStoreApi.cs @@ -0,0 +1,268 @@ +using AssetStoreTools.Api.Models; +using AssetStoreTools.Api.Responses; +using Newtonsoft.Json.Linq; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace AssetStoreTools.Api +{ + internal class AssetStoreApi : IAssetStoreApi + { + private IAssetStoreClient _client; + + public AssetStoreApi(IAssetStoreClient client) + { + _client = client; + } + + public async Task GetLatestAssetStoreToolsVersion(CancellationToken cancellationToken = default) + { + try + { + var uri = ApiUtility.CreateUri(Constants.Api.AssetStoreToolsLatestVersionUrl, false); + var response = await _client.Get(uri, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + response.EnsureSuccessStatusCode(); + var responseStr = response.Content.ReadAsStringAsync().Result; + return new AssetStoreToolsVersionResponse(responseStr); + } + catch (OperationCanceledException e) + { + return new AssetStoreToolsVersionResponse() { Success = false, Cancelled = true, Exception = e }; + } + catch (Exception e) + { + return new AssetStoreToolsVersionResponse() { Success = false, Exception = e }; + } + } + + public async Task Authenticate(IAuthenticationType authenticationType, CancellationToken cancellationToken = default) + { + try + { + var loginResponse = await authenticationType.Authenticate(_client, cancellationToken); + if (loginResponse.Success) + { + _client.SetSessionId(loginResponse.User.SessionId); + } + + return loginResponse; + } + catch (OperationCanceledException e) + { + return new AuthenticationResponse() { Success = false, Cancelled = true, Exception = e }; + } + catch (Exception e) + { + return new AuthenticationResponse() { Success = false, Exception = e }; + } + } + + public void Deauthenticate() + { + _client.ClearSessionId(); + } + + public async Task GetPackages(CancellationToken cancellationToken = default) + { + try + { + var mainDataResponse = await GetPackageDataMain(cancellationToken); + if (!mainDataResponse.Success) + throw mainDataResponse.Exception; + var additionalDataResponse = await GetPackageDataExtra(cancellationToken); + if (!additionalDataResponse.Success) + throw additionalDataResponse.Exception; + var categoryDataResponse = await GetCategories(cancellationToken); + if (!categoryDataResponse.Success) + throw categoryDataResponse.Exception; + + var joinedData = ApiUtility.CombinePackageData(mainDataResponse.Packages, additionalDataResponse.Packages, categoryDataResponse.Categories); + return new PackagesDataResponse() { Success = true, Packages = joinedData }; + } + catch (OperationCanceledException e) + { + return new PackagesDataResponse() { Success = false, Cancelled = true, Exception = e }; + } + catch (Exception e) + { + return new PackagesDataResponse() { Success = false, Exception = e }; + } + } + + private async Task GetPackageDataMain(CancellationToken cancellationToken) + { + try + { + var uri = ApiUtility.CreateUri(Constants.Api.GetPackagesUrl, true); + var response = await _client.Get(uri, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + response.EnsureSuccessStatusCode(); + + var responseStr = response.Content.ReadAsStringAsync().Result; + return new PackagesDataResponse(responseStr); + } + catch (OperationCanceledException e) + { + return new PackagesDataResponse() { Success = false, Cancelled = true, Exception = e }; + } + catch (Exception e) + { + return new PackagesDataResponse() { Success = false, Exception = e }; + } + } + + private async Task GetPackageDataExtra(CancellationToken cancellationToken) + { + try + { + var uri = ApiUtility.CreateUri(Constants.Api.GetPackagesAdditionalDataUrl, true); + var response = await _client.Get(uri, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + response.EnsureSuccessStatusCode(); + + var responseStr = response.Content.ReadAsStringAsync().Result; + return new PackagesAdditionalDataResponse(responseStr); + } + catch (OperationCanceledException e) + { + return new PackagesAdditionalDataResponse() { Success = false, Cancelled = true, Exception = e }; + } + catch (Exception e) + { + return new PackagesAdditionalDataResponse() { Success = false, Exception = e }; + } + } + + public async Task GetCategories(CancellationToken cancellationToken) + { + try + { + var uri = ApiUtility.CreateUri(Constants.Api.GetCategoriesUrl, true); + var response = await _client.Get(uri, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + response.EnsureSuccessStatusCode(); + + var responseStr = response.Content.ReadAsStringAsync().Result; + return new CategoryDataResponse(responseStr); + } + catch (OperationCanceledException e) + { + return new CategoryDataResponse() { Success = false, Cancelled = true, Exception = e }; + } + catch (Exception e) + { + return new CategoryDataResponse() { Success = false, Exception = e }; + } + } + + public async Task GetPackageThumbnail(Package package, CancellationToken cancellationToken = default) + { + try + { + if (string.IsNullOrEmpty(package.IconUrl)) + throw new Exception($"Could not retrieve thumbnail for package {package.PackageId} - icon url is null"); + + var response = await _client.Get(new Uri(package.IconUrl), cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + response.EnsureSuccessStatusCode(); + + var responseBytes = response.Content.ReadAsByteArrayAsync().Result; + return new PackageThumbnailResponse(responseBytes); + } + catch (OperationCanceledException e) + { + return new PackageThumbnailResponse() { Success = false, Cancelled = true, Exception = e }; + } + catch (Exception e) + { + return new PackageThumbnailResponse() { Success = false, Exception = e }; + } + } + + public async Task RefreshPackageMetadata(Package package, CancellationToken cancellationToken = default) + { + try + { + var refreshedPackage = JObject.FromObject(package).DeepClone().ToObject(); + + var packagesResponse = await GetPackageDataExtra(cancellationToken); + if (!packagesResponse.Success) + throw packagesResponse.Exception; + + // Find the updated package data in the latest data json + var packageRefreshSource = packagesResponse.Packages.FirstOrDefault(x => x.PackageId == refreshedPackage.PackageId); + if (packageRefreshSource == null) + return new RefreshedPackageDataResponse() { Success = false, Exception = new MissingMemberException($"Unable to find downloaded package data for package id {package.PackageId}") }; + + // Retrieve the category map + var categoryData = await GetCategories(cancellationToken); + if (!categoryData.Success) + return new RefreshedPackageDataResponse() { Success = false, Exception = packagesResponse.Exception }; + + // Update the package data + refreshedPackage.Name = packageRefreshSource.Name; + refreshedPackage.Status = packageRefreshSource.Status; + var newCategory = categoryData.Categories.FirstOrDefault(x => x.Id.ToString() == packageRefreshSource.CategoryId); + refreshedPackage.Category = newCategory != null ? newCategory.Name : "Unknown"; + refreshedPackage.Modified = packageRefreshSource.Modified; + refreshedPackage.Size = packageRefreshSource.Size; + + return new RefreshedPackageDataResponse() { Success = true, Package = refreshedPackage }; + } + catch (OperationCanceledException) + { + return new RefreshedPackageDataResponse() { Success = false, Cancelled = true }; + } + catch (Exception e) + { + return new RefreshedPackageDataResponse() { Success = false, Exception = e }; + } + } + + public async Task GetPackageUploadedVersions(Package package, CancellationToken cancellationToken = default) + { + try + { + var uri = ApiUtility.CreateUri(Constants.Api.GetPackageUploadedVersionsUrl(package.PackageId, package.VersionId), true); + var response = await _client.Get(uri, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + response.EnsureSuccessStatusCode(); + + var responseStr = response.Content.ReadAsStringAsync().Result; + return new PackageUploadedUnityVersionDataResponse(responseStr); + } + catch (OperationCanceledException e) + { + return new PackageUploadedUnityVersionDataResponse() { Success = false, Cancelled = true, Exception = e }; + } + catch (Exception e) + { + return new PackageUploadedUnityVersionDataResponse() { Success = false, Exception = e }; + } + } + + public async Task UploadPackage(IPackageUploader uploader, IProgress progress = null, CancellationToken cancellationToken = default) + { + try + { + return await uploader.Upload(_client, progress, cancellationToken); + } + catch (OperationCanceledException e) + { + return new PackageUploadResponse() { Success = false, Cancelled = true, Exception = e }; + } + catch (Exception e) + { + return new PackageUploadResponse() { Success = false, Exception = e }; + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/AssetStoreApi.cs.meta b/.github/Asset Store Tools/Api/AssetStoreApi.cs.meta new file mode 100644 index 00000000000..d248bfef2ec --- /dev/null +++ b/.github/Asset Store Tools/Api/AssetStoreApi.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 684fca3fffd79d944a32d9b3adbfc007 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/AssetStoreClient.cs b/.github/Asset Store Tools/Api/AssetStoreClient.cs new file mode 100644 index 00000000000..f71bff81820 --- /dev/null +++ b/.github/Asset Store Tools/Api/AssetStoreClient.cs @@ -0,0 +1,55 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace AssetStoreTools.Api +{ + internal class AssetStoreClient : IAssetStoreClient + { + private HttpClient _httpClient; + + public AssetStoreClient() + { + ServicePointManager.DefaultConnectionLimit = 500; + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.ConnectionClose = false; + _httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); + _httpClient.Timeout = TimeSpan.FromMinutes(1320); + } + + public void SetSessionId(string sessionId) + { + ClearSessionId(); + + if (!string.IsNullOrEmpty(sessionId)) + _httpClient.DefaultRequestHeaders.Add("X-Unity-Session", sessionId); + } + + public void ClearSessionId() + { + _httpClient.DefaultRequestHeaders.Remove("X-Unity-Session"); + } + + public Task Get(Uri uri, CancellationToken cancellationToken = default) + { + return _httpClient.GetAsync(uri, cancellationToken); + } + + public Task Post(Uri uri, HttpContent content, CancellationToken cancellationToken = default) + { + return _httpClient.PostAsync(uri, content, cancellationToken); + } + + public Task Put(Uri uri, HttpContent content, CancellationToken cancellationToken = default) + { + return _httpClient.PutAsync(uri, content, cancellationToken); + } + + public Task Send(HttpRequestMessage request, CancellationToken cancellationToken = default) + { + return _httpClient.SendAsync(request, cancellationToken); + } + } +} diff --git a/.github/Asset Store Tools/Api/AssetStoreClient.cs.meta b/.github/Asset Store Tools/Api/AssetStoreClient.cs.meta new file mode 100644 index 00000000000..097e6a3dc3b --- /dev/null +++ b/.github/Asset Store Tools/Api/AssetStoreClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 80b4527c908161a4b9f06dc393b502f9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/CloudTokenAuthentication.cs b/.github/Asset Store Tools/Api/CloudTokenAuthentication.cs new file mode 100644 index 00000000000..5a1bd5e7413 --- /dev/null +++ b/.github/Asset Store Tools/Api/CloudTokenAuthentication.cs @@ -0,0 +1,25 @@ +using AssetStoreTools.Api.Responses; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AssetStoreTools.Api +{ + internal class CloudTokenAuthentication : AuthenticationBase + { + public CloudTokenAuthentication(string cloudToken) + { + AuthenticationContent = GetAuthenticationContent( + new KeyValuePair("user_access_token", cloudToken) + ); + } + + public override async Task Authenticate(IAssetStoreClient client, CancellationToken cancellationToken) + { + var result = await client.Post(LoginUrl, AuthenticationContent, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + return ParseResponse(result); + } + } +} diff --git a/.github/Asset Store Tools/Api/CloudTokenAuthentication.cs.meta b/.github/Asset Store Tools/Api/CloudTokenAuthentication.cs.meta new file mode 100644 index 00000000000..4d1495ae72d --- /dev/null +++ b/.github/Asset Store Tools/Api/CloudTokenAuthentication.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 99f1baec74f26a34bb972b19c92d523f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/CredentialsAuthentication.cs b/.github/Asset Store Tools/Api/CredentialsAuthentication.cs new file mode 100644 index 00000000000..2c515059ed6 --- /dev/null +++ b/.github/Asset Store Tools/Api/CredentialsAuthentication.cs @@ -0,0 +1,26 @@ +using AssetStoreTools.Api.Responses; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AssetStoreTools.Api +{ + internal class CredentialsAuthentication : AuthenticationBase + { + public CredentialsAuthentication(string email, string password) + { + AuthenticationContent = GetAuthenticationContent( + new KeyValuePair("user", email), + new KeyValuePair("pass", password) + ); + } + + public override async Task Authenticate(IAssetStoreClient client, CancellationToken cancellationToken) + { + var result = await client.Post(LoginUrl, AuthenticationContent, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + return ParseResponse(result); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/CredentialsAuthentication.cs.meta b/.github/Asset Store Tools/Api/CredentialsAuthentication.cs.meta new file mode 100644 index 00000000000..35404d1cc6f --- /dev/null +++ b/.github/Asset Store Tools/Api/CredentialsAuthentication.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 353e556b63fd441428f387bc85aa612c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Models.meta b/.github/Asset Store Tools/Api/Models.meta new file mode 100644 index 00000000000..bfcab0b6186 --- /dev/null +++ b/.github/Asset Store Tools/Api/Models.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1f83e4b5507886f4b873c22c146b8f6a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Models/Category.cs b/.github/Asset Store Tools/Api/Models/Category.cs new file mode 100644 index 00000000000..2cb1298ebbf --- /dev/null +++ b/.github/Asset Store Tools/Api/Models/Category.cs @@ -0,0 +1,58 @@ +using Newtonsoft.Json.Serialization; +using System.Collections.Generic; + +namespace AssetStoreTools.Api.Models +{ + internal class Category + { + public int Id { get; set; } + public string Name { get; set; } + public string Status { get; set; } + + public class AssetStoreCategoryResolver : DefaultContractResolver + { + private Dictionary _propertyConversions; + + public AssetStoreCategoryResolver() + { + _propertyConversions = new Dictionary() + { + { nameof(Category.Name), "assetstore_name" } + }; + } + + protected override string ResolvePropertyName(string propertyName) + { + if (_propertyConversions.ContainsKey(propertyName)) + return _propertyConversions[propertyName]; + + return base.ResolvePropertyName(propertyName); + } + } + + public class CachedCategoryResolver : DefaultContractResolver + { + private static CachedCategoryResolver _instance; + public static CachedCategoryResolver Instance => _instance ?? (_instance = new CachedCategoryResolver()); + + private Dictionary _propertyConversion; + + private CachedCategoryResolver() + { + this.NamingStrategy = new SnakeCaseNamingStrategy(); + _propertyConversion = new Dictionary() + { + { nameof(Category.Name), "name" } + }; + } + + protected override string ResolvePropertyName(string propertyName) + { + if (_propertyConversion.ContainsKey(propertyName)) + return _propertyConversion[propertyName]; + + return base.ResolvePropertyName(propertyName); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Models/Category.cs.meta b/.github/Asset Store Tools/Api/Models/Category.cs.meta new file mode 100644 index 00000000000..cbdb96e24d0 --- /dev/null +++ b/.github/Asset Store Tools/Api/Models/Category.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5897866bc65f5834dab1f17371daada7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Models/Package.cs b/.github/Asset Store Tools/Api/Models/Package.cs new file mode 100644 index 00000000000..442c919d640 --- /dev/null +++ b/.github/Asset Store Tools/Api/Models/Package.cs @@ -0,0 +1,80 @@ +using Newtonsoft.Json.Serialization; +using System.Collections.Generic; + +namespace AssetStoreTools.Api.Models +{ + internal class Package + { + public string PackageId { get; set; } + public string VersionId { get; set; } + public string Name { get; set; } + public string Status { get; set; } + public string Category { get; set; } + public bool IsCompleteProject { get; set; } + public string RootGuid { get; set; } + public string RootPath { get; set; } + public string ProjectPath { get; set; } + public string Modified { get; set; } + public string Size { get; set; } + public string IconUrl { get; set; } + + public class AssetStorePackageResolver : DefaultContractResolver + { + private static AssetStorePackageResolver _instance; + public static AssetStorePackageResolver Instance => _instance ?? (_instance = new AssetStorePackageResolver()); + + private Dictionary _propertyConversions; + + private AssetStorePackageResolver() + { + _propertyConversions = new Dictionary() + { + { nameof(Package.VersionId), "id" }, + { nameof(Package.IsCompleteProject), "is_complete_project" }, + { nameof(Package.RootGuid), "root_guid" }, + { nameof(Package.RootPath), "root_path" }, + { nameof(Package.ProjectPath), "project_path" }, + { nameof(Package.IconUrl), "icon_url" } + }; + } + + protected override string ResolvePropertyName(string propertyName) + { + if (_propertyConversions.ContainsKey(propertyName)) + return _propertyConversions[propertyName]; + + return base.ResolvePropertyName(propertyName); + } + } + + public class CachedPackageResolver : DefaultContractResolver + { + private static CachedPackageResolver _instance; + public static CachedPackageResolver Instance => _instance ?? (_instance = new CachedPackageResolver()); + + private Dictionary _propertyConversion; + + private CachedPackageResolver() + { + this.NamingStrategy = new SnakeCaseNamingStrategy(); + _propertyConversion = new Dictionary() + { + { nameof(Package.PackageId), "package_id" }, + { nameof(Package.VersionId), "version_id" }, + { nameof(Package.IsCompleteProject), "is_complete_project" }, + { nameof(Package.RootGuid), "root_guid" }, + { nameof(Package.RootPath), "root_path" }, + { nameof(Package.IconUrl), "icon_url" } + }; + } + + protected override string ResolvePropertyName(string propertyName) + { + if (_propertyConversion.ContainsKey(propertyName)) + return _propertyConversion[propertyName]; + + return base.ResolvePropertyName(propertyName); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Models/Package.cs.meta b/.github/Asset Store Tools/Api/Models/Package.cs.meta new file mode 100644 index 00000000000..6e345c3f6e6 --- /dev/null +++ b/.github/Asset Store Tools/Api/Models/Package.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7e9f0b99820061b49abf6e8cf544a727 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Models/PackageAdditionalData.cs b/.github/Asset Store Tools/Api/Models/PackageAdditionalData.cs new file mode 100644 index 00000000000..8686ccc1751 --- /dev/null +++ b/.github/Asset Store Tools/Api/Models/PackageAdditionalData.cs @@ -0,0 +1,41 @@ +using Newtonsoft.Json.Serialization; +using System.Collections.Generic; + +namespace AssetStoreTools.Api.Models +{ + internal class PackageAdditionalData + { + public string Name { get; set; } + public string Status { get; set; } + public string PackageId { get; set; } + public string VersionId { get; set; } + public string CategoryId { get; set; } + public string Modified { get; set; } + public string Size { get; set; } + + public class AssetStorePackageResolver : DefaultContractResolver + { + private static AssetStorePackageResolver _instance; + public static AssetStorePackageResolver Instance => _instance ?? (_instance = new AssetStorePackageResolver()); + + private Dictionary _propertyConversions; + + private AssetStorePackageResolver() + { + _propertyConversions = new Dictionary() + { + { nameof(PackageAdditionalData.PackageId), "id" }, + { nameof(PackageAdditionalData.CategoryId), "category_id" } + }; + } + + protected override string ResolvePropertyName(string propertyName) + { + if (_propertyConversions.ContainsKey(propertyName)) + return _propertyConversions[propertyName]; + + return base.ResolvePropertyName(propertyName); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Models/PackageAdditionalData.cs.meta b/.github/Asset Store Tools/Api/Models/PackageAdditionalData.cs.meta new file mode 100644 index 00000000000..75113a2039a --- /dev/null +++ b/.github/Asset Store Tools/Api/Models/PackageAdditionalData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0663f29f3fcd0e34ab77338d1bdbb528 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Models/User.cs b/.github/Asset Store Tools/Api/Models/User.cs new file mode 100644 index 00000000000..c80a50678a8 --- /dev/null +++ b/.github/Asset Store Tools/Api/Models/User.cs @@ -0,0 +1,51 @@ +using Newtonsoft.Json.Serialization; +using System.Collections.Generic; + +namespace AssetStoreTools.Api.Models +{ + internal class User + { + public string Id { get; set; } + public string SessionId { get; set; } + public string Name { get; set; } + public string Username { get; set; } + public string PublisherId { get; set; } + public bool IsPublisher => !string.IsNullOrEmpty(PublisherId); + + public override string ToString() + { + return + $"{nameof(Id)}: {Id}\n" + + $"{nameof(Name)}: {Name}\n" + + $"{nameof(Username)}: {Username}\n" + + $"{nameof(PublisherId)}: {PublisherId}\n" + + $"{nameof(IsPublisher)}: {IsPublisher}\n" + + $"{nameof(SessionId)}: [HIDDEN]"; + } + + public class AssetStoreUserResolver : DefaultContractResolver + { + private static AssetStoreUserResolver _instance; + public static AssetStoreUserResolver Instance => _instance ?? (_instance = new AssetStoreUserResolver()); + + private Dictionary _propertyConversions; + + private AssetStoreUserResolver() + { + _propertyConversions = new Dictionary() + { + { nameof(User.SessionId), "xunitysession" }, + { nameof(User.PublisherId), "publisher" } + }; + } + + protected override string ResolvePropertyName(string propertyName) + { + if (_propertyConversions.ContainsKey(propertyName)) + return _propertyConversions[propertyName]; + + return base.ResolvePropertyName(propertyName); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Models/User.cs.meta b/.github/Asset Store Tools/Api/Models/User.cs.meta new file mode 100644 index 00000000000..bf60282b8c6 --- /dev/null +++ b/.github/Asset Store Tools/Api/Models/User.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: caf38df5cd685a345a1ebec8f7651c93 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Responses.meta b/.github/Asset Store Tools/Api/Responses.meta new file mode 100644 index 00000000000..534c529ffe6 --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 49581213e7b6ca645955cce8ce23ac4b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Responses/AssetStoreResponse.cs b/.github/Asset Store Tools/Api/Responses/AssetStoreResponse.cs new file mode 100644 index 00000000000..2e7a6b24297 --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/AssetStoreResponse.cs @@ -0,0 +1,45 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; + +namespace AssetStoreTools.Api.Responses +{ + /// + /// A structure used to return the success outcome and the result of Asset Store API calls + /// + internal class AssetStoreResponse + { + public bool Success { get; set; } = false; + public bool Cancelled { get; set; } = false; + public Exception Exception { get; set; } + + public AssetStoreResponse() { } + + public AssetStoreResponse(Exception e) : this() + { + Exception = e; + } + + protected void ValidateAssetStoreResponse(string json) + { + var dict = JsonConvert.DeserializeObject(json); + if (dict == null) + throw new Exception("Response is empty"); + + // Some json responses return an error field on error + if (dict.ContainsKey("error")) + { + // Server side error message + // Do not write to console since this is an error that + // is "expected" ie. can be handled by the gui. + throw new Exception(dict.GetValue("error").ToString()); + } + // Some json responses return status+message fields instead of an error field. Go figure. + else if (dict.ContainsKey("status") && dict.GetValue("status").ToString() != "ok" + && dict.ContainsKey("message")) + { + throw new Exception(dict.GetValue("message").ToString()); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Responses/AssetStoreResponse.cs.meta b/.github/Asset Store Tools/Api/Responses/AssetStoreResponse.cs.meta new file mode 100644 index 00000000000..3169e74e559 --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/AssetStoreResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ee338db031a0cfb459f7cac7f41a5d75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Responses/AssetStoreToolsVersionResponse.cs b/.github/Asset Store Tools/Api/Responses/AssetStoreToolsVersionResponse.cs new file mode 100644 index 00000000000..ec3bb87d0d6 --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/AssetStoreToolsVersionResponse.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; + +namespace AssetStoreTools.Api.Responses +{ + internal class AssetStoreToolsVersionResponse : AssetStoreResponse + { + public string Version { get; set; } + + public AssetStoreToolsVersionResponse() : base() { } + public AssetStoreToolsVersionResponse(Exception e) : base(e) { } + + public AssetStoreToolsVersionResponse(string json) + { + try + { + ValidateAssetStoreResponse(json); + ParseVersion(json); + Success = true; + } + catch (Exception e) + { + Success = false; + Exception = e; + } + } + + private void ParseVersion(string json) + { + var dict = JsonConvert.DeserializeObject(json); + if (!dict.ContainsKey("version")) + throw new Exception("Version was not found"); + + Version = dict.GetValue("version").ToString(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Responses/AssetStoreToolsVersionResponse.cs.meta b/.github/Asset Store Tools/Api/Responses/AssetStoreToolsVersionResponse.cs.meta new file mode 100644 index 00000000000..cdcdfd56a05 --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/AssetStoreToolsVersionResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 40558675926f913478a654350149209e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Responses/AuthenticationResponse.cs b/.github/Asset Store Tools/Api/Responses/AuthenticationResponse.cs new file mode 100644 index 00000000000..09c786c2f1f --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/AuthenticationResponse.cs @@ -0,0 +1,74 @@ +using AssetStoreTools.Api.Models; +using Newtonsoft.Json; +using System; +using System.Net; +using System.Net.Http; + +namespace AssetStoreTools.Api.Responses +{ + internal class AuthenticationResponse : AssetStoreResponse + { + public User User { get; set; } + + public AuthenticationResponse() : base() { } + + public AuthenticationResponse(Exception e) : base(e) { } + + public AuthenticationResponse(HttpStatusCode statusCode, HttpRequestException fallbackException) + { + string message; + switch (statusCode) + { + case HttpStatusCode.Unauthorized: + message = "Incorrect email and/or password. Please try again."; + break; + case HttpStatusCode.InternalServerError: + message = "Authentication request failed\nIf you were logging in with your Unity Cloud account, please make sure you are still logged in.\n" + + "This might also be caused by too many invalid login attempts - if that is the case, please try again later."; + break; + default: + Exception = fallbackException; + return; + } + + Exception = new Exception(message); + } + + public AuthenticationResponse(string json) + { + try + { + ValidateAssetStoreResponse(json); + var serializerSettings = new JsonSerializerSettings() + { + ContractResolver = User.AssetStoreUserResolver.Instance + }; + User = JsonConvert.DeserializeObject(json, serializerSettings); + ValidateLoginData(); + ValidatePublisher(); + Success = true; + } + catch (Exception e) + { + Success = false; + Exception = e; + } + } + + private void ValidateLoginData() + { + if (string.IsNullOrEmpty(User.Id) + || string.IsNullOrEmpty(User.SessionId) + || string.IsNullOrEmpty(User.Name) + || string.IsNullOrEmpty(User.Username)) + throw new Exception("Could not parse the necessary publisher information from the response."); + } + + private void ValidatePublisher() + { + if (!User.IsPublisher) + throw new Exception($"Your Unity ID {User.Name} is not currently connected to a publisher account. " + + $"Please create a publisher profile."); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Responses/AuthenticationResponse.cs.meta b/.github/Asset Store Tools/Api/Responses/AuthenticationResponse.cs.meta new file mode 100644 index 00000000000..8a0544c5a2a --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/AuthenticationResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ec3a5cb59a7e78646b07f800d317874d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Responses/CategoryDataResponse.cs b/.github/Asset Store Tools/Api/Responses/CategoryDataResponse.cs new file mode 100644 index 00000000000..4ff740bc090 --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/CategoryDataResponse.cs @@ -0,0 +1,43 @@ +using AssetStoreTools.Api.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; + +namespace AssetStoreTools.Api.Responses +{ + internal class CategoryDataResponse : AssetStoreResponse + { + public List Categories { get; set; } + + public CategoryDataResponse() : base() { } + public CategoryDataResponse(Exception e) : base(e) { } + + public CategoryDataResponse(string json) + { + try + { + var categoryArray = JsonConvert.DeserializeObject(json); + + Categories = new List(); + var serializer = new JsonSerializer() + { + ContractResolver = new Category.AssetStoreCategoryResolver() + }; + + foreach (var categoryData in categoryArray) + { + var category = categoryData.ToObject(serializer); + Categories.Add(category); + } + + Success = true; + } + catch (Exception e) + { + Success = false; + Exception = e; + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Responses/CategoryDataResponse.cs.meta b/.github/Asset Store Tools/Api/Responses/CategoryDataResponse.cs.meta new file mode 100644 index 00000000000..9c79529c72d --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/CategoryDataResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e3789323453f1604286b436f77bdca97 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Responses/PackageThumbnailResponse.cs b/.github/Asset Store Tools/Api/Responses/PackageThumbnailResponse.cs new file mode 100644 index 00000000000..f4e8ac9edf1 --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/PackageThumbnailResponse.cs @@ -0,0 +1,31 @@ +using System; +using UnityEngine; + +namespace AssetStoreTools.Api.Responses +{ + internal class PackageThumbnailResponse : AssetStoreResponse + { + public Texture2D Thumbnail { get; set; } + public PackageThumbnailResponse() : base() { } + public PackageThumbnailResponse(Exception e) : base(e) { } + + public PackageThumbnailResponse(byte[] textureBytes) + { + try + { + var tex = new Texture2D(1, 1, TextureFormat.RGBA32, false); + var success = tex.LoadImage(textureBytes); + if (!success) + throw new Exception("Could not retrieve image from the provided texture bytes"); + + Thumbnail = tex; + Success = true; + } + catch (Exception e) + { + Success = false; + Exception = e; + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Responses/PackageThumbnailResponse.cs.meta b/.github/Asset Store Tools/Api/Responses/PackageThumbnailResponse.cs.meta new file mode 100644 index 00000000000..4187474468a --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/PackageThumbnailResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dacfba636b3757e408514b850d715e18 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Responses/PackageUploadedUnityVersionDataResponse.cs b/.github/Asset Store Tools/Api/Responses/PackageUploadedUnityVersionDataResponse.cs new file mode 100644 index 00000000000..4f57d6454ad --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/PackageUploadedUnityVersionDataResponse.cs @@ -0,0 +1,44 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; + +namespace AssetStoreTools.Api.Responses +{ + internal class PackageUploadedUnityVersionDataResponse : AssetStoreResponse + { + public List UnityVersions { get; set; } + + public PackageUploadedUnityVersionDataResponse() : base() { } + public PackageUploadedUnityVersionDataResponse(Exception e) : base(e) { } + + public PackageUploadedUnityVersionDataResponse(string json) + { + try + { + ValidateAssetStoreResponse(json); + ParseVersionData(json); + Success = true; + } + catch (Exception e) + { + Success = false; + Exception = e; + } + } + + private void ParseVersionData(string json) + { + var data = JsonConvert.DeserializeObject(json); + try + { + var content = data.GetValue("content").ToObject(); + UnityVersions = content.GetValue("unity_versions").ToObject>(); + } + catch + { + throw new Exception("Could not parse the unity versions array"); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Responses/PackageUploadedUnityVersionDataResponse.cs.meta b/.github/Asset Store Tools/Api/Responses/PackageUploadedUnityVersionDataResponse.cs.meta new file mode 100644 index 00000000000..d34cef1ffbf --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/PackageUploadedUnityVersionDataResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2552f659a600e124aa952f3ba760ddf3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Responses/PackagesAdditionalDataResponse.cs b/.github/Asset Store Tools/Api/Responses/PackagesAdditionalDataResponse.cs new file mode 100644 index 00000000000..df01e70d4f1 --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/PackagesAdditionalDataResponse.cs @@ -0,0 +1,59 @@ +using AssetStoreTools.Api.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; + +namespace AssetStoreTools.Api.Responses +{ + internal class PackagesAdditionalDataResponse : AssetStoreResponse + { + public List Packages { get; set; } + + public PackagesAdditionalDataResponse() : base() { } + public PackagesAdditionalDataResponse(Exception e) : base(e) { } + + public PackagesAdditionalDataResponse(string json) + { + try + { + ValidateAssetStoreResponse(json); + ParseExtraData(json); + Success = true; + } + catch (Exception e) + { + Success = false; + Exception = e; + } + } + + private void ParseExtraData(string json) + { + var packageDict = JsonConvert.DeserializeObject(json); + if (!packageDict.ContainsKey("packages")) + throw new Exception("Response did not not contain the list of packages"); + + Packages = new List(); + var serializer = new JsonSerializer() + { + ContractResolver = PackageAdditionalData.AssetStorePackageResolver.Instance + }; + + var packageArray = packageDict.GetValue("packages").ToObject(); + foreach (var packageData in packageArray) + { + var package = packageData.ToObject(serializer); + + // Some fields are based on the latest version in the json + var latestVersion = packageData["versions"].ToObject().Last; + + package.VersionId = latestVersion["id"].ToString(); + package.Modified = latestVersion["modified"].ToString(); + package.Size = latestVersion["size"].ToString(); + + Packages.Add(package); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Responses/PackagesAdditionalDataResponse.cs.meta b/.github/Asset Store Tools/Api/Responses/PackagesAdditionalDataResponse.cs.meta new file mode 100644 index 00000000000..c5e3704f78d --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/PackagesAdditionalDataResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 88d58ad5e0eea6345b5c83f30ee8ebd5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Responses/PackagesDataResponse.cs b/.github/Asset Store Tools/Api/Responses/PackagesDataResponse.cs new file mode 100644 index 00000000000..d9fb47c1dd0 --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/PackagesDataResponse.cs @@ -0,0 +1,59 @@ +using AssetStoreTools.Api.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; + +namespace AssetStoreTools.Api.Responses +{ + internal class PackagesDataResponse : AssetStoreResponse + { + public List Packages { get; set; } + + public PackagesDataResponse() : base() { } + public PackagesDataResponse(Exception e) : base(e) { } + + public PackagesDataResponse(string json) + { + try + { + ValidateAssetStoreResponse(json); + ParseMainData(json); + Success = true; + } + catch (Exception e) + { + Success = false; + Exception = e; + } + } + + private void ParseMainData(string json) + { + var packageDict = JsonConvert.DeserializeObject(json); + if (!packageDict.ContainsKey("packages")) + throw new Exception("Response did not not contain the list of packages"); + + Packages = new List(); + var serializer = new JsonSerializer() + { + ContractResolver = Package.AssetStorePackageResolver.Instance + }; + + foreach (var packageToken in packageDict["packages"]) + { + var property = (JProperty)packageToken; + var packageData = property.Value.ToObject(serializer); + + // Package Id is the key of the package object + packageData.PackageId = property.Name; + + // Package Icon Url is returned without the https: prefix + if (!string.IsNullOrEmpty(packageData.IconUrl)) + packageData.IconUrl = $"https:{packageData.IconUrl}"; + + Packages.Add(packageData); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Responses/PackagesDataResponse.cs.meta b/.github/Asset Store Tools/Api/Responses/PackagesDataResponse.cs.meta new file mode 100644 index 00000000000..c47ff0bc399 --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/PackagesDataResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 705ec748e689148479f54666993cd79d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Responses/RefreshedPackageDataResponse.cs b/.github/Asset Store Tools/Api/Responses/RefreshedPackageDataResponse.cs new file mode 100644 index 00000000000..a0479a03507 --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/RefreshedPackageDataResponse.cs @@ -0,0 +1,12 @@ +using AssetStoreTools.Api.Models; +using System; + +namespace AssetStoreTools.Api.Responses +{ + internal class RefreshedPackageDataResponse : AssetStoreResponse + { + public Package Package { get; set; } + public RefreshedPackageDataResponse() { } + public RefreshedPackageDataResponse(Exception e) : base(e) { } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Responses/RefreshedPackageDataResponse.cs.meta b/.github/Asset Store Tools/Api/Responses/RefreshedPackageDataResponse.cs.meta new file mode 100644 index 00000000000..2b7a974ea42 --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/RefreshedPackageDataResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 20f710024d5ed514db02672f12ac361c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/Responses/UploadResponse.cs b/.github/Asset Store Tools/Api/Responses/UploadResponse.cs new file mode 100644 index 00000000000..3fa21600292 --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/UploadResponse.cs @@ -0,0 +1,28 @@ +using System; + +namespace AssetStoreTools.Api.Responses +{ + internal class PackageUploadResponse : AssetStoreResponse + { + public UploadStatus Status { get; set; } + + public PackageUploadResponse() : base() { } + public PackageUploadResponse(Exception e) : base(e) { } + + public PackageUploadResponse(string json) + { + try + { + ValidateAssetStoreResponse(json); + Status = UploadStatus.Success; + Success = true; + } + catch (Exception e) + { + Success = false; + Status = UploadStatus.Fail; + Exception = e; + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/Responses/UploadResponse.cs.meta b/.github/Asset Store Tools/Api/Responses/UploadResponse.cs.meta new file mode 100644 index 00000000000..6ac626b3ef3 --- /dev/null +++ b/.github/Asset Store Tools/Api/Responses/UploadResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f1758cfa8119cf49a61b010a04352e4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/SessionAuthentication.cs b/.github/Asset Store Tools/Api/SessionAuthentication.cs new file mode 100644 index 00000000000..eb5e54cbb13 --- /dev/null +++ b/.github/Asset Store Tools/Api/SessionAuthentication.cs @@ -0,0 +1,25 @@ +using AssetStoreTools.Api.Responses; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AssetStoreTools.Api +{ + internal class SessionAuthentication : AuthenticationBase + { + public SessionAuthentication(string sessionId) + { + AuthenticationContent = GetAuthenticationContent( + new KeyValuePair("reuse_session", sessionId) + ); + } + + public override async Task Authenticate(IAssetStoreClient client, CancellationToken cancellationToken) + { + var result = await client.Post(LoginUrl, AuthenticationContent, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + return ParseResponse(result); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/SessionAuthentication.cs.meta b/.github/Asset Store Tools/Api/SessionAuthentication.cs.meta new file mode 100644 index 00000000000..f11fc25c0ff --- /dev/null +++ b/.github/Asset Store Tools/Api/SessionAuthentication.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 02e970f660088cc4b89003608d59cd06 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/UnityPackageUploader.cs b/.github/Asset Store Tools/Api/UnityPackageUploader.cs new file mode 100644 index 00000000000..17c765f270f --- /dev/null +++ b/.github/Asset Store Tools/Api/UnityPackageUploader.cs @@ -0,0 +1,99 @@ +using AssetStoreTools.Api.Responses; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace AssetStoreTools.Api +{ + internal class UnityPackageUploadSettings + { + public string VersionId { get; set; } + public string UnityPackagePath { get; set; } + public string RootGuid { get; set; } + public string RootPath { get; set; } + public string ProjectPath { get; set; } + } + + internal class UnityPackageUploader : PackageUploaderBase + { + private UnityPackageUploadSettings _settings; + private Uri _uploadUri; + + public UnityPackageUploader(UnityPackageUploadSettings settings) + { + _settings = settings; + } + + protected override void ValidateSettings() + { + if (string.IsNullOrEmpty(_settings.VersionId)) + throw new Exception("Version Id is unset"); + + if (string.IsNullOrEmpty(_settings.UnityPackagePath) + || !File.Exists(_settings.UnityPackagePath)) + throw new Exception("Package file could not be found"); + + if (!_settings.UnityPackagePath.EndsWith(".unitypackage")) + throw new Exception("Provided package file is not .unitypackage"); + } + + public override async Task Upload(IAssetStoreClient client, IProgress progress = null, CancellationToken cancellationToken = default) + { + try + { + ValidateSettings(); + + var endpoint = Constants.Api.UploadUnityPackageUrl(_settings.VersionId); + var query = new Dictionary() + { + { "root_guid", _settings.RootGuid }, + { "root_path", _settings.RootPath }, + { "project_path", _settings.ProjectPath } + }; + + _uploadUri = ApiUtility.CreateUri(endpoint, query, true); + } + catch (Exception e) + { + return new PackageUploadResponse() { Success = false, Status = UploadStatus.Fail, Exception = e }; + } + + return await Task.Run(() => UploadTask(client, progress, cancellationToken)); + } + + private PackageUploadResponse UploadTask(IAssetStoreClient client, IProgress progress, CancellationToken cancellationToken) + { + try + { + using (FileStream requestFileStream = new FileStream(_settings.UnityPackagePath, FileMode.Open, FileAccess.Read)) + { + var content = new StreamContent(requestFileStream, UploadChunkSizeBytes); + + var response = client.Put(_uploadUri, content, cancellationToken); + WaitForUploadCompletion(response, requestFileStream, progress, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + EnsureSuccessResponse(response.Result); + + var responseStr = response.Result.Content.ReadAsStringAsync().Result; + return new PackageUploadResponse(responseStr); + } + } + catch (OperationCanceledException e) + { + return new PackageUploadResponse() { Success = false, Cancelled = true, Status = UploadStatus.Cancelled, Exception = e }; + } + catch (TimeoutException e) + { + return new PackageUploadResponse() { Success = true, Status = UploadStatus.ResponseTimeout, Exception = e }; + } + catch (Exception e) + { + return new PackageUploadResponse() { Success = false, Exception = e, Status = UploadStatus.Fail }; + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/UnityPackageUploader.cs.meta b/.github/Asset Store Tools/Api/UnityPackageUploader.cs.meta new file mode 100644 index 00000000000..cbff3f37657 --- /dev/null +++ b/.github/Asset Store Tools/Api/UnityPackageUploader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 95bd0284375397f41a2065e07d4ba526 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Api/UploadStatus.cs b/.github/Asset Store Tools/Api/UploadStatus.cs new file mode 100644 index 00000000000..cc3adf44e6a --- /dev/null +++ b/.github/Asset Store Tools/Api/UploadStatus.cs @@ -0,0 +1,11 @@ +namespace AssetStoreTools.Api +{ + internal enum UploadStatus + { + Default = 0, + Success = 1, + Fail = 2, + Cancelled = 3, + ResponseTimeout = 4 + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Api/UploadStatus.cs.meta b/.github/Asset Store Tools/Api/UploadStatus.cs.meta new file mode 100644 index 00000000000..3c252053dd4 --- /dev/null +++ b/.github/Asset Store Tools/Api/UploadStatus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0a121831a941cb64a8a9d21ca6fda9c3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/AssemblyInfo.cs b/.github/Asset Store Tools/AssemblyInfo.cs new file mode 100644 index 00000000000..81fbd086509 --- /dev/null +++ b/.github/Asset Store Tools/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("AssetStoreTools.Tests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +[assembly: InternalsVisibleTo("ab-builder")] +[assembly: InternalsVisibleTo("Inspector-Editor")] diff --git a/.github/Asset Store Tools/AssemblyInfo.cs.meta b/.github/Asset Store Tools/AssemblyInfo.cs.meta new file mode 100644 index 00000000000..972ae52b3f8 --- /dev/null +++ b/.github/Asset Store Tools/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ccfd7faf75ab3c74a98015e772288d86 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/AssetStoreTools.cs b/.github/Asset Store Tools/AssetStoreTools.cs new file mode 100644 index 00000000000..373ac32914a --- /dev/null +++ b/.github/Asset Store Tools/AssetStoreTools.cs @@ -0,0 +1,82 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Previews.UI; +using AssetStoreTools.Uploader; +using AssetStoreTools.Utility; +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.UI; +using System; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools +{ + internal static class AssetStoreTools + { + [MenuItem("Tools/Asset Store/Uploader", false, 0)] + public static void ShowAssetStoreToolsUploader() + { + Type inspectorType = Type.GetType("UnityEditor.InspectorWindow,UnityEditor.dll"); + var wnd = EditorWindow.GetWindow(inspectorType); + wnd.Show(); + } + + [MenuItem("Tools/Asset Store/Validator", false, 1)] + public static void ShowAssetStoreToolsValidator() + { + Type inspectorType = Type.GetType("UnityEditor.InspectorWindow,UnityEditor.dll"); + var wnd = EditorWindow.GetWindow(typeof(UploaderWindow), inspectorType); + wnd.Show(); + } + + public static void ShowAssetStoreToolsValidator(ValidationSettings settings, ValidationResult result) + { + ShowAssetStoreToolsValidator(); + EditorWindow.GetWindow().Load(settings, result); + } + + [MenuItem("Tools/Asset Store/Preview Generator", false, 2)] + public static void ShowAssetStoreToolsPreviewGenerator() + { + Type inspectorType = Type.GetType("UnityEditor.InspectorWindow,UnityEditor.dll"); + var wnd = EditorWindow.GetWindow(inspectorType); + wnd.Show(); + } + + public static void ShowAssetStoreToolsPreviewGenerator(PreviewGenerationSettings settings) + { + ShowAssetStoreToolsPreviewGenerator(); + EditorWindow.GetWindow().Load(settings); + } + + [MenuItem("Tools/Asset Store/Publisher Portal", false, 20)] + public static void OpenPublisherPortal() + { + Application.OpenURL("https://publisher.unity.com/"); + } + + [MenuItem("Tools/Asset Store/Submission Guidelines", false, 21)] + public static void OpenSubmissionGuidelines() + { + Application.OpenURL("https://assetstore.unity.com/publishing/submission-guidelines/"); + } + + [MenuItem("Tools/Asset Store/Provide Feedback", false, 22)] + public static void OpenFeedback() + { + Application.OpenURL("https://forum.unity.com/threads/new-asset-store-tools-version-coming-july-20th-2022.1310939/"); + } + + [MenuItem("Tools/Asset Store/Check for Updates", false, 45)] + public static void OpenUpdateChecker() + { + var wnd = EditorWindow.GetWindowWithRect(new Rect(Screen.width / 2, Screen.height / 2, 400, 150), true); + wnd.Show(); + } + + [MenuItem("Tools/Asset Store/Settings", false, 50)] + public static void OpenSettings() + { + ASToolsPreferencesProvider.OpenSettings(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/AssetStoreTools.cs.meta b/.github/Asset Store Tools/AssetStoreTools.cs.meta new file mode 100644 index 00000000000..9452bb05933 --- /dev/null +++ b/.github/Asset Store Tools/AssetStoreTools.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6060eef206afc844caaa1732538e8890 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/AssetStoreToolsWindow.cs b/.github/Asset Store Tools/AssetStoreToolsWindow.cs new file mode 100644 index 00000000000..43f3c117b1b --- /dev/null +++ b/.github/Asset Store Tools/AssetStoreToolsWindow.cs @@ -0,0 +1,23 @@ +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools +{ + internal abstract class AssetStoreToolsWindow : EditorWindow + { + protected abstract string WindowTitle { get; } + + private void DefaultInit() + { + titleContent = new GUIContent(WindowTitle); + Init(); + } + + protected abstract void Init(); + + private void OnEnable() + { + DefaultInit(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/AssetStoreToolsWindow.cs.meta b/.github/Asset Store Tools/AssetStoreToolsWindow.cs.meta new file mode 100644 index 00000000000..2fe87e5787b --- /dev/null +++ b/.github/Asset Store Tools/AssetStoreToolsWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c1057a05baaa45942808573065c02a03 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Constants.cs b/.github/Asset Store Tools/Constants.cs new file mode 100644 index 00000000000..d2c777a71d6 --- /dev/null +++ b/.github/Asset Store Tools/Constants.cs @@ -0,0 +1,178 @@ +using AssetStoreTools.Previews.Data; +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using PackageInfo = UnityEditor.PackageManager.PackageInfo; + +namespace AssetStoreTools +{ + internal class Constants + { +#if UNITY_EDITOR_OSX + public static readonly string UnityPath = System.IO.Path.Combine(EditorApplication.applicationPath, "Contents", "MacOS", "Unity"); +#else + public static readonly string UnityPath = EditorApplication.applicationPath; +#endif + public static readonly string RootProjectPath = Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length); + + private static bool GetArgument(string argumentName, out string argumentValue) + { + argumentValue = string.Empty; + var args = Environment.GetCommandLineArgs(); + + for (int i = 0; i < args.Length; i++) + { + if (!args[i].Equals(argumentName, StringComparison.OrdinalIgnoreCase)) + continue; + + if (i + 1 >= args.Length) + return false; + + argumentValue = args[i + 1]; + break; + } + + return !string.IsNullOrEmpty(argumentValue); + } + + public class Api + { + public static readonly string ApiVersion = $"V{PackageInfo.FindForAssetPath("Packages/com.unity.asset-store-tools").version}"; + public const string AssetStoreToolsLatestVersionUrl = "https://api.assetstore.unity3d.com/package/latest-version/115"; + + private const string AssetStoreBaseUrlDefault = "https://kharma.unity3d.com"; + private const string AssetStoreBaseUrlOverrideArgument = "-assetStoreUrl"; + public static readonly string AssetStoreBaseUrl = !GetArgument(AssetStoreBaseUrlOverrideArgument, out var overriddenUrl) + ? AssetStoreBaseUrlDefault + : overriddenUrl; + + public static readonly string AuthenticateUrl = $"{AssetStoreBaseUrl}/login"; + public static readonly string GetPackagesUrl = $"{AssetStoreBaseUrl}/api/asset-store-tools/metadata/0.json"; + public static readonly string GetPackagesAdditionalDataUrl = $"{AssetStoreBaseUrl}/api/management/packages.json"; + public static readonly string GetCategoriesUrl = $"{AssetStoreBaseUrl}/api/management/categories.json"; + + public static string GetPackageUploadedVersionsUrl(string packageId, string versionId) => + $"{AssetStoreBaseUrl}/api/content/preview/{packageId}/{versionId}.json"; + public static string UploadUnityPackageUrl(string versionId) => + $"{AssetStoreBaseUrl}/api/asset-store-tools/package/{versionId}/unitypackage.json"; + + public static IDictionary DefaultAssetStoreQuery() + { + var dict = new Dictionary() + { + { "unityversion", Application.unityVersion }, + { "toolversion", ApiVersion } + }; + return dict; + } + } + + public class Updater + { + public const string AssetStoreToolsUrl = "https://assetstore.unity.com/packages/tools/utilities/asset-store-publishing-tools-115"; + } + + public class Cache + { + public const string SessionTokenKey = "kharma.sessionid"; + public const string TempCachePath = "Temp/AssetStoreToolsCache"; + public const string PersistentCachePath = "Library/AssetStoreToolsCache"; + + public const string PackageDataFileName = "PackageMetadata.json"; + public const string CategoryDataFile = "Categories.json"; + public const string ValidationResultFile = "ValidationStateData.asset"; + + public static string PackageThumbnailFileName(string packageId) => $"{packageId}.png"; + public static string WorkflowStateDataFileName(string packageId) => $"{packageId}-workflowStateData.asset"; + } + + public class Uploader + { + public const string MinRequiredUnitySupportVersion = "2021.3"; + public const long MaxPackageSizeBytes = 6576668672; // 6 GB + 128MB of headroom + public const string AccountRegistrationUrl = "https://publisher.unity.com/access"; + public const string AccountForgottenPasswordUrl = "https://id.unity.com/password/new"; + + public class Analytics + { + public const string VendorKey = "unity.assetStoreTools"; + public const int MaxEventsPerHour = 20; + public const int MaxNumberOfElements = 1000; + + public class AuthenticationAnalytics + { + public const string EventName = "assetStoreToolsLogin"; + public const int EventVersion = 1; + } + + public class PackageUploadAnalytics + { + public const string EventName = "assetStoreTools"; + public const int EventVersion = 3; + } + } + } + + public class Validator + { + public const string SubmissionGuidelinesUrl = "https://assetstore.unity.com/publishing/submission-guidelines#Overview"; + public const string SupportTicketUrl = "https://support.unity.com/hc/en-us/requests/new?ticket_form_id=65905"; + + public class Tests + { + public const string TestDefinitionsPath = "Packages/com.unity.asset-store-tools/Editor/Validator/Tests"; + public const string TestMethodsPath = "Packages/com.unity.asset-store-tools/Editor/Validator/Scripts/Test Methods"; + + public static readonly string GenericTestMethodsPath = $"{TestMethodsPath}/Generic"; + public static readonly string UnityPackageTestMethodsPath = $"{TestMethodsPath}/UnityPackage"; + } + } + + public class Previews + { + public const string PreviewDatabaseFile = "PreviewDatabase.json"; + + public static readonly string DefaultOutputPath = $"{Cache.TempCachePath}/AssetPreviews"; + public const FileNameFormat DefaultFileNameFormat = FileNameFormat.Guid; + + public class Native + { + public static readonly string DefaultOutputPath = $"{Previews.DefaultOutputPath}/Native"; + public const PreviewFormat DefaultFormat = PreviewFormat.PNG; + public const bool DefaultWaitForPreviews = true; + public const bool DefaultChunkedPreviewLoading = true; + public const int DefaultChunkSize = 100; + } + + public class Custom + { + public static readonly string DefaultOutputPath = $"{Previews.DefaultOutputPath}/Custom"; + public const PreviewFormat DefaultFormat = PreviewFormat.JPG; + public const int DefaultWidth = 300; + public const int DefaultHeight = 300; + public const int DefaultDepth = 32; + + public const int DefaultNativeWidth = 900; + public const int DefaultNativeHeight = 900; + + public static readonly Color DefaultAudioSampleColor = new Color(1f, 0.55f, 0); + public static readonly Color DefaultAudioBackgroundColor = new Color(0.32f, 0.32f, 0.32f); + } + } + + public class WindowStyles + { + public const string UploaderStylesPath = "Packages/com.unity.asset-store-tools/Editor/Uploader/Styles"; + public const string ValidatorStylesPath = "Packages/com.unity.asset-store-tools/Editor/Validator/Styles"; + public const string ValidatorIconsPath = "Packages/com.unity.asset-store-tools/Editor/Validator/Icons"; + public const string PreviewGeneratorStylesPath = "Packages/com.unity.asset-store-tools/Editor/Previews/Styles"; + public const string UpdaterStylesPath = "Packages/com.unity.asset-store-tools/Editor/Utility/Styles/Updater"; + } + + public class Debug + { + public const string DebugModeKey = "ASTDebugMode"; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Constants.cs.meta b/.github/Asset Store Tools/Constants.cs.meta new file mode 100644 index 00000000000..682cecba592 --- /dev/null +++ b/.github/Asset Store Tools/Constants.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a4ee1f78505bda748ae24b3dfd2606ca +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Exporter.meta b/.github/Asset Store Tools/Exporter.meta new file mode 100644 index 00000000000..4e1143b83a9 --- /dev/null +++ b/.github/Asset Store Tools/Exporter.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5f5ca981958937a43997a9f365759edf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Exporter/Abstractions.meta b/.github/Asset Store Tools/Exporter/Abstractions.meta new file mode 100644 index 00000000000..ac18e6c5f08 --- /dev/null +++ b/.github/Asset Store Tools/Exporter/Abstractions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2072d354c04b19c48b22593536b3ebcf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Exporter/Abstractions/IPackageExporter.cs b/.github/Asset Store Tools/Exporter/Abstractions/IPackageExporter.cs new file mode 100644 index 00000000000..0201a8ea6cd --- /dev/null +++ b/.github/Asset Store Tools/Exporter/Abstractions/IPackageExporter.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace AssetStoreTools.Exporter +{ + internal interface IPackageExporter + { + PackageExporterSettings Settings { get; } + + Task Export(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Exporter/Abstractions/IPackageExporter.cs.meta b/.github/Asset Store Tools/Exporter/Abstractions/IPackageExporter.cs.meta new file mode 100644 index 00000000000..91a3d965df3 --- /dev/null +++ b/.github/Asset Store Tools/Exporter/Abstractions/IPackageExporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 41bc3a111ed1fd64c8b9acef211d9e51 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Exporter/Abstractions/IPreviewInjector.cs b/.github/Asset Store Tools/Exporter/Abstractions/IPreviewInjector.cs new file mode 100644 index 00000000000..ba503d8d76f --- /dev/null +++ b/.github/Asset Store Tools/Exporter/Abstractions/IPreviewInjector.cs @@ -0,0 +1,7 @@ +namespace AssetStoreTools.Exporter +{ + internal interface IPreviewInjector + { + void Inject(string temporaryPackagePath); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Exporter/Abstractions/IPreviewInjector.cs.meta b/.github/Asset Store Tools/Exporter/Abstractions/IPreviewInjector.cs.meta new file mode 100644 index 00000000000..534a5dd1992 --- /dev/null +++ b/.github/Asset Store Tools/Exporter/Abstractions/IPreviewInjector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dcff58dc716351f43b2709cfacdfebba +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Exporter/Abstractions/PackageExporterBase.cs b/.github/Asset Store Tools/Exporter/Abstractions/PackageExporterBase.cs new file mode 100644 index 00000000000..885a188be46 --- /dev/null +++ b/.github/Asset Store Tools/Exporter/Abstractions/PackageExporterBase.cs @@ -0,0 +1,134 @@ +using AssetStoreTools.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using UnityEditor; + +namespace AssetStoreTools.Exporter +{ + internal abstract class PackageExporterBase : IPackageExporter + { + public PackageExporterSettings Settings { get; private set; } + + public const string ProgressBarTitle = "Exporting Package"; + public const string ProgressBarStepSavingAssets = "Saving Assets..."; + public const string ProgressBarStepGatheringFiles = "Gathering files..."; + public const string ProgressBarStepGeneratingPreviews = "Generating previews..."; + public const string ProgressBarStepCompressingPackage = "Compressing package..."; + + private static readonly string[] PluginFolderExtensions = { "androidlib", "bundle", "plugin", "framework", "xcframework" }; + + public PackageExporterBase(PackageExporterSettings settings) + { + Settings = settings; + } + + public async Task Export() + { + try + { + ValidateSettings(); + } + catch (Exception e) + { + return new PackageExporterResult() { Success = false, Exception = e }; + } + + return await ExportImpl(); + } + + protected virtual void ValidateSettings() + { + if (Settings == null) + throw new ArgumentException("Settings cannot be null"); + + if (string.IsNullOrEmpty(Settings.OutputFilename)) + throw new ArgumentException("Output path cannot be null"); + + if (Settings.OutputFilename.EndsWith("/") || Settings.OutputFilename.EndsWith("\\")) + throw new ArgumentException("Output path must be a valid filename and not end with a directory separator character"); + } + + protected abstract Task ExportImpl(); + + protected string[] GetAssetPaths(string rootPath) + { + // To-do: slight optimization is possible in the future by having a list of excluded folders/file extensions + List paths = new List(); + + // Add files within given directory + var filePaths = Directory.GetFiles(rootPath).Select(p => p.Replace('\\', '/')).ToArray(); + paths.AddRange(filePaths); + + // Add directories within given directory + var directoryPaths = Directory.GetDirectories(rootPath).Select(p => p.Replace('\\', '/')).ToArray(); + foreach (var nestedDirectory in directoryPaths) + paths.AddRange(GetAssetPaths(nestedDirectory)); + + // Add the given directory itself if it is not empty + if (filePaths.Length > 0 || directoryPaths.Length > 0) + paths.Add(rootPath); + + return paths.ToArray(); + } + + protected string GetAssetGuid(string assetPath, bool generateIfPlugin, bool scrapeFromMeta) + { + if (!FileUtility.ShouldHaveMeta(assetPath)) + return string.Empty; + + // Skip ProjectVersion.txt file specifically as it may introduce + // project compatibility issues when imported + if (string.Compare(assetPath, "ProjectSettings/ProjectVersion.txt", StringComparison.OrdinalIgnoreCase) == 0) + return string.Empty; + + // Attempt retrieving guid from the Asset Database first + var guid = AssetDatabase.AssetPathToGUID(assetPath); + if (guid != string.Empty) + return guid; + + // Some special folders (e.g. SomeName.framework) do not have meta files inside them. + // Their contents should be exported with any arbitrary GUID so that Unity Importer could pick them up + if (generateIfPlugin && PathBelongsToPlugin(assetPath)) + return GUID.Generate().ToString(); + + // Files in hidden folders (e.g. Samples~) are not part of the Asset Database, + // therefore GUIDs need to be scraped from the .meta file. + // Note: only do this for non-native exporter since the native exporter + // will not be able to retrieve the asset path from a hidden folder + if (scrapeFromMeta) + { + var metaPath = $"{assetPath}.meta"; + + if (!File.Exists(metaPath)) + return string.Empty; + + using (StreamReader reader = new StreamReader(metaPath)) + { + string line; + while ((line = reader.ReadLine()) != string.Empty) + { + if (!line.StartsWith("guid:")) + continue; + var metaGuid = line.Substring("guid:".Length).Trim(); + return metaGuid; + } + } + } + + return string.Empty; + } + + private bool PathBelongsToPlugin(string assetPath) + { + return PluginFolderExtensions.Any(extension => assetPath.ToLower().Contains($".{extension}/")); + } + + protected virtual void PostExportCleanup() + { + EditorUtility.ClearProgressBar(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Exporter/Abstractions/PackageExporterBase.cs.meta b/.github/Asset Store Tools/Exporter/Abstractions/PackageExporterBase.cs.meta new file mode 100644 index 00000000000..1c88d873bce --- /dev/null +++ b/.github/Asset Store Tools/Exporter/Abstractions/PackageExporterBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aab20a0b596e60b40b1f7f7e0f54858e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Exporter/Abstractions/PackageExporterSettings.cs b/.github/Asset Store Tools/Exporter/Abstractions/PackageExporterSettings.cs new file mode 100644 index 00000000000..6197ba568e6 --- /dev/null +++ b/.github/Asset Store Tools/Exporter/Abstractions/PackageExporterSettings.cs @@ -0,0 +1,7 @@ +namespace AssetStoreTools.Exporter +{ + internal abstract class PackageExporterSettings + { + public string OutputFilename; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Exporter/Abstractions/PackageExporterSettings.cs.meta b/.github/Asset Store Tools/Exporter/Abstractions/PackageExporterSettings.cs.meta new file mode 100644 index 00000000000..25ff4272abc --- /dev/null +++ b/.github/Asset Store Tools/Exporter/Abstractions/PackageExporterSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 82c350daaabdc784e95e09cdc8511e23 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Exporter/DefaultExporterSettings.cs b/.github/Asset Store Tools/Exporter/DefaultExporterSettings.cs new file mode 100644 index 00000000000..82a10ac1ccd --- /dev/null +++ b/.github/Asset Store Tools/Exporter/DefaultExporterSettings.cs @@ -0,0 +1,11 @@ +using AssetStoreTools.Previews.Generators; + +namespace AssetStoreTools.Exporter +{ + internal class DefaultExporterSettings : PackageExporterSettings + { + public string[] ExportPaths; + public string[] Dependencies; + public IPreviewGenerator PreviewGenerator; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Exporter/DefaultExporterSettings.cs.meta b/.github/Asset Store Tools/Exporter/DefaultExporterSettings.cs.meta new file mode 100644 index 00000000000..31644b1fec0 --- /dev/null +++ b/.github/Asset Store Tools/Exporter/DefaultExporterSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 92cbd0e60b4bb9049bcf1e9fd92ccae6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Exporter/DefaultPackageExporter.cs b/.github/Asset Store Tools/Exporter/DefaultPackageExporter.cs new file mode 100644 index 00000000000..c8d6c428b0a --- /dev/null +++ b/.github/Asset Store Tools/Exporter/DefaultPackageExporter.cs @@ -0,0 +1,304 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Utility; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using CacheConstants = AssetStoreTools.Constants.Cache; + +namespace AssetStoreTools.Exporter +{ + internal class DefaultPackageExporter : PackageExporterBase + { + private const string TemporaryExportPathName = "CustomExport"; + + private DefaultExporterSettings _defaultExportSettings; + + public DefaultPackageExporter(DefaultExporterSettings settings) : base(settings) + { + _defaultExportSettings = settings; + } + + protected override void ValidateSettings() + { + base.ValidateSettings(); + + if (_defaultExportSettings.ExportPaths == null || _defaultExportSettings.ExportPaths.Length == 0) + throw new ArgumentException("Export paths array cannot be empty"); + } + + protected override async Task ExportImpl() + { + return await this.Export(); + } + + private new async Task Export() + { + ASDebug.Log("Using custom package exporter"); + + // Save assets before exporting + EditorUtility.DisplayProgressBar(ProgressBarTitle, ProgressBarStepSavingAssets, 0.1f); + AssetDatabase.SaveAssets(); + + try + { + // Create a temporary export path + PostExportCleanup(); + var temporaryExportPath = GetTemporaryExportPath(); + if (!Directory.Exists(temporaryExportPath)) + Directory.CreateDirectory(temporaryExportPath); + + // Construct an unzipped package structure + CreateTempPackageStructure(temporaryExportPath); + + var previewGenerationResult = await GeneratePreviews(); + InjectPreviews(previewGenerationResult, temporaryExportPath); + + // Build a .unitypackage file from the temporary folder + CreateUnityPackage(temporaryExportPath, _defaultExportSettings.OutputFilename); + + EditorUtility.RevealInFinder(_defaultExportSettings.OutputFilename); + + ASDebug.Log($"Package file has been created at {_defaultExportSettings.OutputFilename}"); + return new PackageExporterResult() { Success = true, ExportedPath = _defaultExportSettings.OutputFilename, PreviewGenerationResult = previewGenerationResult }; + } + catch (Exception e) + { + return new PackageExporterResult() { Success = false, Exception = e }; + } + finally + { + PostExportCleanup(); + } + } + + private string GetTemporaryExportPath() + { + return $"{CacheConstants.TempCachePath}/{TemporaryExportPathName}"; + } + + private void CreateTempPackageStructure(string tempOutputPath) + { + EditorUtility.DisplayProgressBar(ProgressBarTitle, ProgressBarStepGatheringFiles, 0.4f); + var pathGuidPairs = GetPathGuidPairs(_defaultExportSettings.ExportPaths); + + foreach (var pair in pathGuidPairs) + { + var originalAssetPath = pair.Key; + var outputAssetPath = $"{tempOutputPath}/{pair.Value}"; + + if (Directory.Exists(outputAssetPath)) + { + var path1 = File.ReadAllText($"{outputAssetPath}/pathname"); + var path2 = originalAssetPath; + throw new InvalidOperationException($"Multiple assets with guid {pair.Value} have been detected " + + $"when exporting the package. Please resolve the guid conflicts and try again:\n{path1}\n{path2}"); + } + + Directory.CreateDirectory(outputAssetPath); + + // Every exported asset has a pathname file + using (StreamWriter writer = new StreamWriter($"{outputAssetPath}/pathname")) + writer.Write(originalAssetPath); + + // Only files (not folders) have an asset file + if (File.Exists(originalAssetPath)) + File.Copy(originalAssetPath, $"{outputAssetPath}/asset"); + + // Most files and folders have an asset.meta file (but ProjectSettings folder assets do not) + if (File.Exists($"{originalAssetPath}.meta")) + File.Copy($"{originalAssetPath}.meta", $"{outputAssetPath}/asset.meta"); + + // To-do: handle previews in hidden folders as they are not part of the AssetDatabase + var previewObject = AssetDatabase.LoadAssetAtPath(originalAssetPath); + if (previewObject == null) + continue; + } + + if (_defaultExportSettings.Dependencies == null || _defaultExportSettings.Dependencies.Length == 0) + return; + + var exportDependenciesDict = new JObject(); + var allRegistryPackages = PackageUtility.GetAllRegistryPackages(); + + foreach (var exportDependency in _defaultExportSettings.Dependencies) + { + var registryPackage = allRegistryPackages.FirstOrDefault(x => x.name == exportDependency); + if (registryPackage == null) + { + // Package is either not from a registry source or does not exist in the project + UnityEngine.Debug.LogWarning($"Found an unsupported Package Manager dependency: {exportDependency}.\n" + + "This dependency is not supported in the project's manifest.json and will be skipped."); + continue; + } + + exportDependenciesDict[registryPackage.name] = registryPackage.version; + } + + if (exportDependenciesDict.Count == 0) + return; + + var exportManifestJson = new JObject(); + exportManifestJson["dependencies"] = exportDependenciesDict; + + var tempManifestDirectoryPath = $"{tempOutputPath}/packagemanagermanifest"; + Directory.CreateDirectory(tempManifestDirectoryPath); + var tempManifestFilePath = $"{tempManifestDirectoryPath}/asset"; + + File.WriteAllText(tempManifestFilePath, exportManifestJson.ToString()); + } + + private Dictionary GetPathGuidPairs(string[] exportPaths) + { + var pathGuidPairs = new Dictionary(); + + foreach (var exportPath in exportPaths) + { + var assetPaths = GetAssetPaths(exportPath); + + foreach (var assetPath in assetPaths) + { + var guid = GetAssetGuid(assetPath, true, true); + if (string.IsNullOrEmpty(guid)) + continue; + + pathGuidPairs.Add(assetPath, guid); + } + } + + return pathGuidPairs; + } + + private async Task GeneratePreviews() + { + if (_defaultExportSettings.PreviewGenerator == null) + return null; + + void ReportProgress(float progress) + { + EditorUtility.DisplayProgressBar(ProgressBarTitle, ProgressBarStepGeneratingPreviews, progress); + } + + _defaultExportSettings.PreviewGenerator.OnProgressChanged += ReportProgress; + var result = await _defaultExportSettings.PreviewGenerator.Generate(); + _defaultExportSettings.PreviewGenerator.OnProgressChanged -= ReportProgress; + EditorUtility.ClearProgressBar(); + + if (!result.Success) + { + UnityEngine.Debug.LogWarning($"An error was encountered while generating previews. Exported package may be missing previews.\n{result.Exception}"); + } + + return result; + } + + private void InjectPreviews(PreviewGenerationResult result, string temporaryExportPath) + { + if (result == null || !result.Success) + return; + + var injector = new PreviewInjector(result); + injector.Inject(temporaryExportPath); + } + + private void CreateUnityPackage(string pathToArchive, string outputPath) + { + if (Directory.GetDirectories(pathToArchive).Length == 0) + throw new InvalidOperationException("Unable to export package. The specified path is empty"); + + EditorUtility.DisplayProgressBar(ProgressBarTitle, ProgressBarStepCompressingPackage, 0.8f); + + // Archiving process working path will be set to the + // temporary package path so adjust the output path accordingly + if (!Path.IsPathRooted(outputPath)) + outputPath = $"{Application.dataPath.Substring(0, Application.dataPath.Length - "/Assets".Length)}/{outputPath}"; + +#if UNITY_EDITOR_WIN + CreateUnityPackageUniversal(pathToArchive, outputPath); +#elif UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX + CreateUnityPackageOsxLinux(pathToArchive, outputPath); +#endif + } + + private void CreateUnityPackageUniversal(string pathToArchive, string outputPath) + { + var _7zPath = EditorApplication.applicationContentsPath; +#if UNITY_EDITOR_WIN + _7zPath = Path.Combine(_7zPath, "Tools", "7z.exe"); +#elif UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX + _7zPath = Path.Combine(_7zPath, "Tools", "7za"); +#endif + if (!File.Exists(_7zPath)) + throw new FileNotFoundException("Archiving utility was not found in your Unity installation directory"); + + var argumentsTar = $"a -r -ttar -y -bd archtemp.tar ."; + var result = StartProcess(_7zPath, argumentsTar, pathToArchive); + if (result != 0) + throw new Exception("Failed to compress the package"); + + // Create a GZIP archive + var argumentsGzip = $"a -tgzip -bd -y \"{outputPath}\" archtemp.tar"; + result = StartProcess(_7zPath, argumentsGzip, pathToArchive); + if (result != 0) + throw new Exception("Failed to compress the package"); + } + +#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX + private void CreateUnityPackageOsxLinux(string pathToArchive, string outputPath) + { + var tarPath = "/usr/bin/tar"; + + if (!File.Exists(tarPath)) + { + // Fallback to the universal export method + ASDebug.LogWarning("'/usr/bin/tar' executable not found. Falling back to 7za"); + CreateUnityPackageUniversal(pathToArchive, outputPath); + return; + } + + // Create a TAR archive + var arguments = $"-czpf \"{outputPath}\" ."; + var result = StartProcess(tarPath, arguments, pathToArchive); + if (result != 0) + throw new Exception("Failed to compress the package"); + } +#endif + + private int StartProcess(string processPath, string arguments, string workingDirectory) + { + var info = new ProcessStartInfo() + { + FileName = processPath, + Arguments = arguments, + WorkingDirectory = workingDirectory, + CreateNoWindow = true, + UseShellExecute = false + }; + +#if UNITY_EDITOR_OSX + // Prevent OSX-specific archive pollution + info.EnvironmentVariables.Add("COPYFILE_DISABLE", "1"); +#endif + + using (Process process = Process.Start(info)) + { + process.WaitForExit(); + return process.ExitCode; + } + } + + protected override void PostExportCleanup() + { + base.PostExportCleanup(); + + var tempExportPath = GetTemporaryExportPath(); + if (Directory.Exists(tempExportPath)) + Directory.Delete(tempExportPath, true); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Exporter/DefaultPackageExporter.cs.meta b/.github/Asset Store Tools/Exporter/DefaultPackageExporter.cs.meta new file mode 100644 index 00000000000..fa1bd636a82 --- /dev/null +++ b/.github/Asset Store Tools/Exporter/DefaultPackageExporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 32f50122a1b2bc2428cf8fba321e2414 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Exporter/LegacyExporterSettings.cs b/.github/Asset Store Tools/Exporter/LegacyExporterSettings.cs new file mode 100644 index 00000000000..52a7cadcc97 --- /dev/null +++ b/.github/Asset Store Tools/Exporter/LegacyExporterSettings.cs @@ -0,0 +1,8 @@ +namespace AssetStoreTools.Exporter +{ + internal class LegacyExporterSettings : PackageExporterSettings + { + public string[] ExportPaths; + public bool IncludeDependencies; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Exporter/LegacyExporterSettings.cs.meta b/.github/Asset Store Tools/Exporter/LegacyExporterSettings.cs.meta new file mode 100644 index 00000000000..dbade085152 --- /dev/null +++ b/.github/Asset Store Tools/Exporter/LegacyExporterSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c7dea1cfe45989e4eab6fc5fd18c1e10 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Exporter/LegacyPackageExporter.cs b/.github/Asset Store Tools/Exporter/LegacyPackageExporter.cs new file mode 100644 index 00000000000..172004397c0 --- /dev/null +++ b/.github/Asset Store Tools/Exporter/LegacyPackageExporter.cs @@ -0,0 +1,109 @@ +using AssetStoreTools.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace AssetStoreTools.Exporter +{ + internal class LegacyPackageExporter : PackageExporterBase + { + private const string ExportMethodWithoutDependencies = "UnityEditor.PackageUtility.ExportPackage"; + private const string ExportMethodWithDependencies = "UnityEditor.PackageUtility.ExportPackageAndPackageManagerManifest"; + + private LegacyExporterSettings _legacyExportSettings; + + public LegacyPackageExporter(LegacyExporterSettings settings) : base(settings) + { + _legacyExportSettings = settings; + } + + protected override void ValidateSettings() + { + base.ValidateSettings(); + + if (_legacyExportSettings.ExportPaths == null || _legacyExportSettings.ExportPaths.Length == 0) + throw new ArgumentException("Export paths array cannot be empty"); + } + + protected override async Task ExportImpl() + { + return await this.Export(); + } + + private async new Task Export() + { + ASDebug.Log("Using native package exporter"); + + try + { + var guids = GetGuids(_legacyExportSettings.ExportPaths, out bool onlyFolders); + + if (guids.Length == 0 || onlyFolders) + throw new ArgumentException("Package Exporting failed: provided export paths are empty or only contain empty folders"); + + string exportMethod = ExportMethodWithoutDependencies; + if (_legacyExportSettings.IncludeDependencies) + exportMethod = ExportMethodWithDependencies; + + var split = exportMethod.Split('.'); + var assembly = Assembly.Load(split[0]); // UnityEditor + var typeName = $"{split[0]}.{split[1]}"; // UnityEditor.PackageUtility + var methodName = split[2]; // ExportPackage or ExportPackageAndPackageManagerManifest + + var type = assembly.GetType(typeName); + var method = type.GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public, + null, new Type[] { typeof(string[]), typeof(string) }, null); + + ASDebug.Log("Invoking native export method"); + + method?.Invoke(null, new object[] { guids, _legacyExportSettings.OutputFilename }); + + // The internal exporter methods are asynchronous, therefore + // we need to wait for exporting to finish before returning + await Task.Run(() => + { + while (!File.Exists(_legacyExportSettings.OutputFilename)) + Thread.Sleep(100); + }); + + ASDebug.Log($"Package file has been created at {_legacyExportSettings.OutputFilename}"); + return new PackageExporterResult() { Success = true, ExportedPath = _legacyExportSettings.OutputFilename }; + } + catch (Exception e) + { + return new PackageExporterResult() { Success = false, Exception = e }; + } + finally + { + PostExportCleanup(); + } + } + + private string[] GetGuids(string[] exportPaths, out bool onlyFolders) + { + var guids = new List(); + onlyFolders = true; + + foreach (var exportPath in exportPaths) + { + var assetPaths = GetAssetPaths(exportPath); + + foreach (var assetPath in assetPaths) + { + var guid = GetAssetGuid(assetPath, false, false); + if (string.IsNullOrEmpty(guid)) + continue; + + guids.Add(guid); + if (onlyFolders == true && (File.Exists(assetPath))) + onlyFolders = false; + } + } + + return guids.ToArray(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Exporter/LegacyPackageExporter.cs.meta b/.github/Asset Store Tools/Exporter/LegacyPackageExporter.cs.meta new file mode 100644 index 00000000000..853e472fe6a --- /dev/null +++ b/.github/Asset Store Tools/Exporter/LegacyPackageExporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3200380dff2de104aa79620e4b41dc70 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Exporter/PackageExporterResult.cs b/.github/Asset Store Tools/Exporter/PackageExporterResult.cs new file mode 100644 index 00000000000..a0ae7a5d858 --- /dev/null +++ b/.github/Asset Store Tools/Exporter/PackageExporterResult.cs @@ -0,0 +1,13 @@ +using AssetStoreTools.Previews.Data; +using System; + +namespace AssetStoreTools.Exporter +{ + internal class PackageExporterResult + { + public bool Success; + public string ExportedPath; + public PreviewGenerationResult PreviewGenerationResult; + public Exception Exception; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Exporter/PackageExporterResult.cs.meta b/.github/Asset Store Tools/Exporter/PackageExporterResult.cs.meta new file mode 100644 index 00000000000..840bcf64dbd --- /dev/null +++ b/.github/Asset Store Tools/Exporter/PackageExporterResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e685b1c322eab4540919d4fc970e812d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Exporter/PreviewInjector.cs b/.github/Asset Store Tools/Exporter/PreviewInjector.cs new file mode 100644 index 00000000000..69e06da3725 --- /dev/null +++ b/.github/Asset Store Tools/Exporter/PreviewInjector.cs @@ -0,0 +1,41 @@ +using AssetStoreTools.Previews.Data; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace AssetStoreTools.Exporter +{ + internal class PreviewInjector : IPreviewInjector + { + private PreviewGenerationResult _result; + + public PreviewInjector(PreviewGenerationResult result) + { + _result = result; + } + + public void Inject(string temporaryPackagePath) + { + if (_result == null || !_result.Success) + return; + + var previews = _result.Previews.Where(x => x.Type == _result.GenerationType && x.Exists()); + InjectFilesIntoGuidFolders(previews, temporaryPackagePath); + } + + private void InjectFilesIntoGuidFolders(IEnumerable previews, string temporaryPackagePath) + { + foreach (var assetFolder in Directory.EnumerateDirectories(temporaryPackagePath)) + { + var guid = assetFolder.Replace("\\", "/").Split('/').Last(); + var generatedPreview = previews.FirstOrDefault(x => x.Guid.Equals(guid)); + + if (generatedPreview == null) + continue; + + // Note: Unity Importer and Asset Store only operate with .png extensions + File.Copy(generatedPreview.Path, $"{assetFolder}/preview.png", true); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Exporter/PreviewInjector.cs.meta b/.github/Asset Store Tools/Exporter/PreviewInjector.cs.meta new file mode 100644 index 00000000000..98e155a764d --- /dev/null +++ b/.github/Asset Store Tools/Exporter/PreviewInjector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 772db784128e32d4792bb680258c71df +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews.meta b/.github/Asset Store Tools/Previews.meta new file mode 100644 index 00000000000..747d5eb988a --- /dev/null +++ b/.github/Asset Store Tools/Previews.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 13e8cd63112e52d43a7e65949f0143a4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts.meta b/.github/Asset Store Tools/Previews/Scripts.meta new file mode 100644 index 00000000000..f95e49f043a --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cbe1aebea6551424997b361fab69f266 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Data.meta b/.github/Asset Store Tools/Previews/Scripts/Data.meta new file mode 100644 index 00000000000..89f1356f5d9 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ae99e2e3b5a83d1469110306c96f4c58 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/CustomPreviewGenerationSettings.cs b/.github/Asset Store Tools/Previews/Scripts/Data/CustomPreviewGenerationSettings.cs new file mode 100644 index 00000000000..27058752022 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/CustomPreviewGenerationSettings.cs @@ -0,0 +1,19 @@ +using UnityEngine; + +namespace AssetStoreTools.Previews.Data +{ + internal class CustomPreviewGenerationSettings : PreviewGenerationSettings + { + public override GenerationType GenerationType => GenerationType.Custom; + + public int Width; + public int Height; + public int Depth; + + public int NativeWidth; + public int NativeHeight; + + public Color AudioSampleColor; + public Color AudioBackgroundColor; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/CustomPreviewGenerationSettings.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Data/CustomPreviewGenerationSettings.cs.meta new file mode 100644 index 00000000000..38f9eba36cb --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/CustomPreviewGenerationSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2ccb1292c1c4ba94cb6f4022ecfdfa50 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/FileNameFormat.cs b/.github/Asset Store Tools/Previews/Scripts/Data/FileNameFormat.cs new file mode 100644 index 00000000000..cb841a86d13 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/FileNameFormat.cs @@ -0,0 +1,9 @@ +namespace AssetStoreTools.Previews.Data +{ + internal enum FileNameFormat + { + Guid = 0, + FullAssetPath = 1, + AssetName = 2, + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/FileNameFormat.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Data/FileNameFormat.cs.meta new file mode 100644 index 00000000000..9a6639fe42f --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/FileNameFormat.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 38a1babecfeaf524f98e8d67882acf48 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/GenerationType.cs b/.github/Asset Store Tools/Previews/Scripts/Data/GenerationType.cs new file mode 100644 index 00000000000..fef979f7066 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/GenerationType.cs @@ -0,0 +1,9 @@ +namespace AssetStoreTools.Previews.Data +{ + internal enum GenerationType + { + Unknown = 0, + Native = 1, + Custom = 2 + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/GenerationType.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Data/GenerationType.cs.meta new file mode 100644 index 00000000000..f1791234dc5 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/GenerationType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 66697a5d16404d948ba3191ddfc60bd9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/NativePreviewGenerationSettings.cs b/.github/Asset Store Tools/Previews/Scripts/Data/NativePreviewGenerationSettings.cs new file mode 100644 index 00000000000..fff204ee27e --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/NativePreviewGenerationSettings.cs @@ -0,0 +1,10 @@ +namespace AssetStoreTools.Previews.Data +{ + internal class NativePreviewGenerationSettings : PreviewGenerationSettings + { + public override GenerationType GenerationType => GenerationType.Native; + public bool WaitForPreviews; + public bool ChunkedPreviewLoading; + public int ChunkSize; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/NativePreviewGenerationSettings.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Data/NativePreviewGenerationSettings.cs.meta new file mode 100644 index 00000000000..e3e79403b1f --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/NativePreviewGenerationSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5b2ef019acae6fe43b5565858e15433a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/PreviewDatabase.cs b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewDatabase.cs new file mode 100644 index 00000000000..cabc56c1f45 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewDatabase.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace AssetStoreTools.Previews.Data +{ + internal class PreviewDatabase + { + public List Previews; + + public PreviewDatabase() + { + Previews = new List(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/PreviewDatabase.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewDatabase.cs.meta new file mode 100644 index 00000000000..fd55b05fa53 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewDatabase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cf8cef28a68324742a7e4b47efc87563 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/PreviewFormat.cs b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewFormat.cs new file mode 100644 index 00000000000..e68f29a41e5 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewFormat.cs @@ -0,0 +1,8 @@ +namespace AssetStoreTools.Previews.Data +{ + internal enum PreviewFormat + { + JPG = 0, + PNG = 1 + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/PreviewFormat.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewFormat.cs.meta new file mode 100644 index 00000000000..0d8c2148c34 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewFormat.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0500e4459ebfe8448a13194af49f89fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/PreviewGenerationResult.cs b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewGenerationResult.cs new file mode 100644 index 00000000000..6fb779111b8 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewGenerationResult.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace AssetStoreTools.Previews.Data +{ + internal class PreviewGenerationResult + { + public GenerationType GenerationType; + public bool Success; + public IEnumerable GeneratedPreviews; + public IEnumerable Previews; + public Exception Exception; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/PreviewGenerationResult.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewGenerationResult.cs.meta new file mode 100644 index 00000000000..7062382bfb0 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewGenerationResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e040f2cdf0177824dacb158b23a63374 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/PreviewGenerationSettings.cs b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewGenerationSettings.cs new file mode 100644 index 00000000000..920ac1395ad --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewGenerationSettings.cs @@ -0,0 +1,12 @@ +namespace AssetStoreTools.Previews.Data +{ + internal abstract class PreviewGenerationSettings + { + public abstract GenerationType GenerationType { get; } + public string[] InputPaths; + public string OutputPath; + public PreviewFormat Format; + public FileNameFormat PreviewFileNamingFormat; + public bool OverwriteExisting; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/PreviewGenerationSettings.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewGenerationSettings.cs.meta new file mode 100644 index 00000000000..38563dccfea --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewGenerationSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7e578ae6616505a4795da8f632d63229 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/PreviewMetadata.cs b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewMetadata.cs new file mode 100644 index 00000000000..a7ba227e1f0 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewMetadata.cs @@ -0,0 +1,17 @@ +using System.IO; + +namespace AssetStoreTools.Previews.Data +{ + internal class PreviewMetadata + { + public GenerationType Type; + public string Guid; + public string Name; + public string Path; + + public bool Exists() + { + return File.Exists(Path); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Data/PreviewMetadata.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewMetadata.cs.meta new file mode 100644 index 00000000000..2aed929054e --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Data/PreviewMetadata.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4ff6be4e277d8314e921baff52ea25bc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators.meta b/.github/Asset Store Tools/Previews/Scripts/Generators.meta new file mode 100644 index 00000000000..7019d5cb351 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ed651159a2004574789e97726da5090c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom.meta new file mode 100644 index 00000000000..cac3d999b12 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f675855c3d971694785806c0c7a463be +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/AudioChannel.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/AudioChannel.cs new file mode 100644 index 00000000000..f46e4062021 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/AudioChannel.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace AssetStoreTools.Previews.Generators.Custom +{ + internal class AudioChannel + { + private int _yMin; + private int _yMax; + + private int _yBaseline; + private int _yAmplitude; + + private List _samples; + + public AudioChannel(int minY, int maxY, List samples) + { + _yMin = minY; + _yMax = maxY; + + _yBaseline = (_yMin + _yMax) / 2; + _yAmplitude = _yMax - _yBaseline; + + _samples = samples; + } + + public IEnumerable GetCoordinateData(int desiredWidth) + { + var coordinates = new List(); + var step = Mathf.RoundToInt((float)_samples.Count / desiredWidth); + + for (int i = 0; i < desiredWidth; i++) + { + var startIndex = i * step; + var endIndex = (i + 1) * step; + var sampleChunk = CreateChunk(startIndex, endIndex); + + if (sampleChunk.Count() == 0) + break; + + DownsampleMax(sampleChunk, out var aboveBaseline, out var belowBaseline); + + var yAboveBaseline = SampleToCoordinate(aboveBaseline); + var yBelowBaseline = SampleToCoordinate(belowBaseline); + + coordinates.Add(new AudioChannelCoordinate(i, _yBaseline, yAboveBaseline, yBelowBaseline)); + } + + // If there weren't enough samples to complete the desired width - fill out the rest with zeroes + for (int i = coordinates.Count; i < desiredWidth; i++) + coordinates.Add(new AudioChannelCoordinate(i, _yBaseline, 0, 0)); + + return coordinates; + } + + private IEnumerable CreateChunk(int startIndex, int endIndex) + { + var chunk = new List(); + for (int i = startIndex; i < endIndex; i++) + { + if (i >= _samples.Count) + break; + + chunk.Add(_samples[i]); + } + + return chunk; + } + + private void DownsampleMax(IEnumerable samples, out float valueAboveBaseline, out float valueBelowBaseline) + { + valueAboveBaseline = 0; + valueBelowBaseline = 0; + + foreach (var sample in samples) + { + if (sample > 0 && sample > valueAboveBaseline) + { + valueAboveBaseline = sample; + continue; + } + + if (sample < 0 && sample < valueBelowBaseline) + { + valueBelowBaseline = sample; + continue; + } + } + } + + private int SampleToCoordinate(float sample) + { + return _yBaseline + (int)(sample * _yAmplitude); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/AudioChannel.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/AudioChannel.cs.meta new file mode 100644 index 00000000000..4a7d3fb73fa --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/AudioChannel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 82fab55b08a1be94cb2e18f3feae91ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/AudioChannelCoordinate.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/AudioChannelCoordinate.cs new file mode 100644 index 00000000000..1030eed227e --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/AudioChannelCoordinate.cs @@ -0,0 +1,18 @@ +namespace AssetStoreTools.Previews.Generators.Custom +{ + internal struct AudioChannelCoordinate + { + public int X { get; private set; } + public int YBaseline { get; private set; } + public int YAboveBaseline { get; private set; } + public int YBelowBaseline { get; private set; } + + public AudioChannelCoordinate(int x, int yBaseline, int yAboveBaseline, int yBelowBaseline) + { + X = x; + YBaseline = yBaseline; + YAboveBaseline = yAboveBaseline; + YBelowBaseline = yBelowBaseline; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/AudioChannelCoordinate.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/AudioChannelCoordinate.cs.meta new file mode 100644 index 00000000000..91425543e8e --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/AudioChannelCoordinate.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b54462f6af82a2644944d6e4bde23c9e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters.meta new file mode 100644 index 00000000000..91cfc9f9d74 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c07070deed666d54cb72a89a5fcd7ef7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/ISceneScreenshotter.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/ISceneScreenshotter.cs new file mode 100644 index 00000000000..ec7cd578593 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/ISceneScreenshotter.cs @@ -0,0 +1,12 @@ +using UnityEngine; + +namespace AssetStoreTools.Previews.Generators.Custom.Screenshotters +{ + internal interface ISceneScreenshotter + { + SceneScreenshotterSettings Settings { get; } + + string Screenshot(string outputPath); + string Screenshot(GameObject target, string outputPath); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/ISceneScreenshotter.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/ISceneScreenshotter.cs.meta new file mode 100644 index 00000000000..1c185498cb5 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/ISceneScreenshotter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 045ac265a792af243918af0849ee2ac8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/MaterialScreenshotter.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/MaterialScreenshotter.cs new file mode 100644 index 00000000000..a44620894f1 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/MaterialScreenshotter.cs @@ -0,0 +1,32 @@ +using UnityEngine; + +namespace AssetStoreTools.Previews.Generators.Custom.Screenshotters +{ + internal class MaterialScreenshotter : SceneScreenshotterBase + { + public MaterialScreenshotter(SceneScreenshotterSettings settings) : base(settings) { } + + public override void PositionCamera(GameObject target) + { + var renderers = target.GetComponentsInChildren(); + if (renderers == null || renderers.Length == 0) + return; + + var bounds = GetGlobalBounds(renderers); + + var materialSphereRadius = bounds.extents.y * 1.1f; + + var angle = Camera.fieldOfView / 2; + var sinAngle = Mathf.Sin(angle * Mathf.Deg2Rad); + var distance = materialSphereRadius / sinAngle; + + Camera.transform.position = new Vector3(bounds.center.x, bounds.center.y + distance, bounds.center.z); + Camera.transform.LookAt(bounds.center); + Camera.transform.RotateAround(bounds.center, Vector3.left, 60); + Camera.transform.RotateAround(bounds.center, Vector3.up, -45); + + Camera.nearClipPlane = 0.01f; + Camera.farClipPlane = 10000; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/MaterialScreenshotter.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/MaterialScreenshotter.cs.meta new file mode 100644 index 00000000000..9c075059e9e --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/MaterialScreenshotter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c2bd7b01b0cebeb43a6fbc53377f0ea6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/MeshScreenshotter.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/MeshScreenshotter.cs new file mode 100644 index 00000000000..6ce59053a3d --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/MeshScreenshotter.cs @@ -0,0 +1,33 @@ +using UnityEngine; + +namespace AssetStoreTools.Previews.Generators.Custom.Screenshotters +{ + internal class MeshScreenshotter : SceneScreenshotterBase + { + public MeshScreenshotter(SceneScreenshotterSettings settings) : base(settings) { } + + public override void PositionCamera(GameObject target) + { + var renderers = target.GetComponentsInChildren(); + if (renderers == null || renderers.Length == 0) + return; + + var bounds = GetGlobalBounds(renderers); + + var encapsulatingSphereDiameter = (bounds.max - bounds.min).magnitude; + var encapsulatingSphereRadius = encapsulatingSphereDiameter / 2; + + var angle = Camera.fieldOfView / 2; + var sinAngle = Mathf.Sin(angle * Mathf.Deg2Rad); + var distance = encapsulatingSphereRadius / sinAngle; + + Camera.transform.position = new Vector3(bounds.center.x, bounds.center.y + distance, bounds.center.z); + Camera.transform.LookAt(bounds.center); + Camera.transform.RotateAround(bounds.center, Vector3.left, 65); + Camera.transform.RotateAround(bounds.center, Vector3.up, 235); + + Camera.nearClipPlane = 0.01f; + Camera.farClipPlane = 10000; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/MeshScreenshotter.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/MeshScreenshotter.cs.meta new file mode 100644 index 00000000000..660fb065799 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/MeshScreenshotter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f0339d22d91b7c94ebc18b1de6f1e287 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/SceneScreenshotterBase.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/SceneScreenshotterBase.cs new file mode 100644 index 00000000000..fb289f42df2 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/SceneScreenshotterBase.cs @@ -0,0 +1,124 @@ +using AssetStoreTools.Previews.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; + +namespace AssetStoreTools.Previews.Generators.Custom.Screenshotters +{ + internal abstract class SceneScreenshotterBase : ISceneScreenshotter + { + public SceneScreenshotterSettings Settings { get; } + + protected Camera Camera => GetCamera(); + private Camera _camera; + + public SceneScreenshotterBase(SceneScreenshotterSettings settings) + { + Settings = settings; + } + + private Camera GetCamera() + { + if (_camera == null) + { +#if UNITY_2022_3_OR_NEWER + _camera = GameObject.FindFirstObjectByType(FindObjectsInactive.Include); +#else + _camera = GameObject.FindObjectOfType(); +#endif + } + + return _camera; + } + + public virtual void ValidateSettings() + { + if (Settings.Width <= 0) + throw new ArgumentException("Width should be larger than 0"); + + if (Settings.Height <= 0) + throw new ArgumentException("Height should be larger than 0"); + + if (Settings.Depth <= 0) + throw new ArgumentException("Depth should be larger than 0"); + + if (Settings.NativeWidth <= 0) + throw new ArgumentException("Native width should be larger than 0"); + + if (Settings.NativeHeight <= 0) + throw new ArgumentException("Native height should be larger than 0"); + } + + public abstract void PositionCamera(GameObject target); + + public string Screenshot(string outputPath) + { + ValidateSettings(); + + var texture = GraphicsUtility.GetTextureFromCamera(Camera, Settings.NativeWidth, Settings.NativeHeight, Settings.Depth); + + if (Settings.Width < Settings.NativeWidth || Settings.Height < Settings.NativeHeight) + texture = GraphicsUtility.ResizeTexture(texture, Settings.Width, Settings.Height); + + var extension = PreviewConvertUtility.ConvertExtension(Settings.Format); + var writtenPath = $"{outputPath}.{extension}"; + var bytes = PreviewConvertUtility.ConvertTexture(texture, Settings.Format); + File.WriteAllBytes(writtenPath, bytes); + + return writtenPath; + } + + public string Screenshot(GameObject target, string outputPath) + { + PositionCamera(target); + PositionLighting(target); + return Screenshot(outputPath); + } + + private void PositionLighting(GameObject target) + { +#if UNITY_2022_3_OR_NEWER + var light = GameObject.FindFirstObjectByType(FindObjectsInactive.Include); +#else + var light = GameObject.FindObjectOfType(); +#endif + light.transform.position = Camera.transform.position; + light.transform.LookAt(target.transform); + light.transform.RotateAround(target.transform.position, Vector3.forward, 60f); + } + + protected Bounds GetGlobalBounds(IEnumerable renderers) + { + var center = Vector3.zero; + + foreach (var renderer in renderers) + { + center += renderer.bounds.center; + } + center /= renderers.Count(); + + var globalBounds = new Bounds(center, Vector3.zero); + + foreach (var renderer in renderers) + { + globalBounds.Encapsulate(renderer.bounds); + } + + return globalBounds; + } + + protected Bounds GetNormalizedBounds(Bounds bounds) + { + var largestExtent = Mathf.Max(bounds.extents.x, bounds.extents.y, bounds.extents.z); + var normalizedBounds = new Bounds() + { + center = bounds.center, + extents = new Vector3(largestExtent, largestExtent, largestExtent) + }; + + return normalizedBounds; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/SceneScreenshotterBase.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/SceneScreenshotterBase.cs.meta new file mode 100644 index 00000000000..adab09fe572 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/SceneScreenshotterBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ce77aefdce8a37f498d17d73da53d0a4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/SceneScreenshotterSettings.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/SceneScreenshotterSettings.cs new file mode 100644 index 00000000000..82e0ec1db20 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/SceneScreenshotterSettings.cs @@ -0,0 +1,16 @@ +using AssetStoreTools.Previews.Data; + +namespace AssetStoreTools.Previews.Generators.Custom.Screenshotters +{ + internal class SceneScreenshotterSettings + { + public int Width; + public int Height; + public int Depth; + + public int NativeWidth; + public int NativeHeight; + + public PreviewFormat Format; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/SceneScreenshotterSettings.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/SceneScreenshotterSettings.cs.meta new file mode 100644 index 00000000000..c152ceff98b --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/Screenshotters/SceneScreenshotterSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aa34806e243bad949892d06dd47295e2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators.meta new file mode 100644 index 00000000000..e13d7264a1e --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b4bb2dd0960418d4a8d4efd34b92a418 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/AudioTypeGeneratorSettings.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/AudioTypeGeneratorSettings.cs new file mode 100644 index 00000000000..36a6b2154f8 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/AudioTypeGeneratorSettings.cs @@ -0,0 +1,15 @@ +using AssetStoreTools.Previews.Data; +using UnityEngine; + +namespace AssetStoreTools.Previews.Generators.Custom.TypeGenerators +{ + internal class AudioTypeGeneratorSettings : TypeGeneratorSettings + { + public int Width; + public int Height; + + public Color SampleColor; + public Color BackgroundColor; + public PreviewFormat Format; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/AudioTypeGeneratorSettings.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/AudioTypeGeneratorSettings.cs.meta new file mode 100644 index 00000000000..22aa253624a --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/AudioTypeGeneratorSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5b7ab1b072d95be4daf221ee23af1c80 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/AudioTypePreviewGenerator.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/AudioTypePreviewGenerator.cs new file mode 100644 index 00000000000..a0f15453dc9 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/AudioTypePreviewGenerator.cs @@ -0,0 +1,207 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Previews.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Previews.Generators.Custom.TypeGenerators +{ + internal class AudioTypePreviewGenerator : TypePreviewGeneratorBase + { + private AudioTypeGeneratorSettings _settings; + private Texture2D _texture; + + public override event Action OnAssetProcessed; + + public AudioTypePreviewGenerator(AudioTypeGeneratorSettings settings) : base(settings) + { + _settings = settings; + } + + public override void ValidateSettings() + { + base.ValidateSettings(); + + if (_settings.Width <= 0) + throw new ArgumentException("Width must be larger than 0"); + + if (_settings.Height <= 0) + throw new ArgumentException("Height must be larger than 0"); + } + + protected override IEnumerable CollectAssets() + { + var assets = new List(); + var materialGuids = AssetDatabase.FindAssets("t:audioclip", Settings.InputPaths); + foreach (var guid in materialGuids) + { + var audioClip = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); + + // Skip nested audio clips + if (!AssetDatabase.IsMainAsset(audioClip)) + continue; + + // Skip materials with an error shader + if (!IsLoadTypeSupported(audioClip)) + { + Debug.LogWarning($"Audio clip '{audioClip}' is using a load type which cannot retrieve sample data. Preview will not be generated."); + continue; + } + + assets.Add(audioClip); + } + + return assets; + } + + private bool IsLoadTypeSupported(AudioClip audioClip) + { + if (audioClip.loadType == AudioClipLoadType.DecompressOnLoad) + return true; + + return false; + } + + protected override async Task> GenerateImpl(IEnumerable assets) + { + var generatedPreviews = new List(); + var audioClips = assets.ToList(); + for (int i = 0; i < audioClips.Count; i++) + { + var audioClip = audioClips[i] as AudioClip; + if (audioClip != null) + { + var texture = GenerateAudioClipTexture(audioClip); + + var outputPath = GenerateOutputPathWithExtension(audioClip, _settings.PreviewFileNamingFormat, _settings.Format); + var bytes = PreviewConvertUtility.ConvertTexture(texture, _settings.Format); + File.WriteAllBytes(outputPath, bytes); + generatedPreviews.Add(ObjectToMetadata(audioClip, outputPath)); + } + + OnAssetProcessed?.Invoke(i, audioClips.Count); + await Task.Yield(); + } + + return generatedPreviews; + } + + private Texture2D GenerateAudioClipTexture(AudioClip audioClip) + { + if (!audioClip.LoadAudioData()) + throw new Exception("Could not load audio data"); + + try + { + if (_texture == null) + _texture = new Texture2D(_settings.Width, _settings.Height); + else +#if UNITY_2021_3_OR_NEWER || UNITY_2022_1_OR_NEWER || UNITY_2021_2_OR_NEWER + _texture.Reinitialize(_settings.Width, _settings.Height); +#else + _texture.Resize(_settings.Width, _settings.Height); +#endif + + FillTextureBackground(); + FillTextureForeground(audioClip); + + _texture.Apply(); + return _texture; + } + finally + { + audioClip.UnloadAudioData(); + } + } + + private void FillTextureBackground() + { + for (int i = 0; i < _texture.width; i++) + { + for (int j = 0; j < _texture.height; j++) + { + _texture.SetPixel(i, j, _settings.BackgroundColor); + } + } + } + + private void FillTextureForeground(AudioClip audioClip) + { + var channels = CreateChannels(audioClip); + + for (int i = 0; i < channels.Count; i++) + { + DrawChannel(channels[i]); + } + } + + private List CreateChannels(AudioClip audioClip) + { + var channelSamples = GetChannelSamples(audioClip); + var sectionSize = _texture.height / audioClip.channels; + + var channels = new List(); + + for (int i = 0; i < audioClip.channels; i++) + { + var channelMaxY = (_texture.height - 1) - i * sectionSize; + var channelMinY = _texture.height - (i + 1) * sectionSize; + var channel = new AudioChannel(channelMinY, channelMaxY, channelSamples[i]); + channels.Add(channel); + } + + return channels; + } + + private List> GetChannelSamples(AudioClip audioClip) + { + var channelSamples = new List>(); + var allSamples = new float[audioClip.samples * audioClip.channels]; + + if (!audioClip.GetData(allSamples, 0)) + throw new Exception("Could not retrieve audio samples"); + + for (int i = 0; i < audioClip.channels; i++) + { + var samples = new List(); + var sampleIndex = i; + while (sampleIndex < allSamples.Length) + { + samples.Add(allSamples[sampleIndex]); + sampleIndex += audioClip.channels; + } + + channelSamples.Add(samples); + } + + return channelSamples; + } + + private void DrawChannel(AudioChannel channel) + { + var sectionData = channel.GetCoordinateData(_texture.width); + + foreach (var data in sectionData) + { + DrawVerticalColumn(data.X, data.YBaseline, data.YAboveBaseline, data.YBelowBaseline, _settings.SampleColor); + } + } + + private void DrawVerticalColumn(int x, int yBaseline, int y1, int y2, Color color) + { + _texture.SetPixel(x, yBaseline, color); + + var startIndex = y1 < y2 ? y1 : y2; + var endIndex = y1 < y2 ? y2 : y1; + + for (int i = startIndex; i < endIndex; i++) + { + _texture.SetPixel(x, i, color); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/AudioTypePreviewGenerator.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/AudioTypePreviewGenerator.cs.meta new file mode 100644 index 00000000000..0511663fb82 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/AudioTypePreviewGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9ddf69bb2dca51a42aff247b3a471bb3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/ITypePreviewGenerator.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/ITypePreviewGenerator.cs new file mode 100644 index 00000000000..bdd5056bd5c --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/ITypePreviewGenerator.cs @@ -0,0 +1,16 @@ +using AssetStoreTools.Previews.Data; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AssetStoreTools.Previews.Generators.Custom.TypeGenerators +{ + internal interface ITypePreviewGenerator + { + TypeGeneratorSettings Settings { get; } + + event Action OnAssetProcessed; + + Task> Generate(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/ITypePreviewGenerator.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/ITypePreviewGenerator.cs.meta new file mode 100644 index 00000000000..a42ecd1e561 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/ITypePreviewGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7d9cd368dc73a23478390ee1332cb0be +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/MaterialTypePreviewGenerator.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/MaterialTypePreviewGenerator.cs new file mode 100644 index 00000000000..13b44ec534a --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/MaterialTypePreviewGenerator.cs @@ -0,0 +1,85 @@ +using AssetStoreTools.Previews.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Previews.Generators.Custom.TypeGenerators +{ + internal class MaterialTypePreviewGenerator : TypePreviewGeneratorFromScene + { + public override event Action OnAssetProcessed; + + public MaterialTypePreviewGenerator(TypePreviewGeneratorFromSceneSettings settings) : base(settings) { } + + protected override IEnumerable CollectAssets() + { + var assets = new List(); + var materialGuids = AssetDatabase.FindAssets("t:material", Settings.InputPaths); + foreach (var guid in materialGuids) + { + var mat = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); + + // Skip nested materials + if (!AssetDatabase.IsMainAsset(mat)) + continue; + + // Skip materials with an error shader + if (IsShaderInvalid(mat.shader)) + { + Debug.LogWarning($"Material '{mat}' is using an erroring shader. Preview will not be generated."); + continue; + } + + assets.Add(mat); + } + + return assets; + } + + protected override async Task> GeneratePreviewsInScene(IEnumerable assets) + { + var generatedPreviews = new List(); + var materials = assets.ToList(); + var sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); + + var hasMeshRenderer = sphere.TryGetComponent(out var meshRenderer); + if (!hasMeshRenderer) + throw new Exception($"Could not find a MeshRenderer for {sphere}"); + + for (int i = 0; i < materials.Count; i++) + { + ThrowIfSceneChanged(); + + var material = materials[i] as Material; + + if (material != null) + { + meshRenderer.sharedMaterial = material; + var previewPath = Settings.Screenshotter.Screenshot(sphere, GenerateOutputPathWithoutExtension(material, Settings.PreviewFileNamingFormat)); + if (!string.IsNullOrEmpty(previewPath)) + generatedPreviews.Add(ObjectToMetadata(material, previewPath)); + } + + OnAssetProcessed?.Invoke(i, materials.Count); + await Task.Yield(); + } + + UnityEngine.Object.DestroyImmediate(sphere); + return generatedPreviews; + } + + private bool IsShaderInvalid(Shader shader) + { + if (ShaderUtil.ShaderHasError(shader)) + return true; + + if (!shader.isSupported) + return true; + + return false; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/MaterialTypePreviewGenerator.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/MaterialTypePreviewGenerator.cs.meta new file mode 100644 index 00000000000..c692df7121c --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/MaterialTypePreviewGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a4e121bae63a199458e53a523dd18c8c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/ModelTypePreviewGenerator.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/ModelTypePreviewGenerator.cs new file mode 100644 index 00000000000..7d97607a4f6 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/ModelTypePreviewGenerator.cs @@ -0,0 +1,86 @@ +using AssetStoreTools.Previews.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Previews.Generators.Custom.TypeGenerators +{ + internal class ModelTypePreviewGenerator : TypePreviewGeneratorFromScene + { + public override event Action OnAssetProcessed; + + public ModelTypePreviewGenerator(TypePreviewGeneratorFromSceneSettings settings) : base(settings) { } + + protected override IEnumerable CollectAssets() + { + var models = new List(); + var modelGuids = AssetDatabase.FindAssets("t:model", Settings.InputPaths); + + foreach (var guid in modelGuids) + { + var model = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); + + // Skip nested models + if (!AssetDatabase.IsMainAsset(model)) + continue; + + // Skip models without renderers + if (model.GetComponentsInChildren().Length == 0) + continue; + + models.Add(model); + } + + return models; + } + + protected override async Task> GeneratePreviewsInScene(IEnumerable assets) + { + var generatedPreviews = new List(); + var models = assets.ToList(); + var referenceShader = GetDefaultObjectShader(); + + for (int i = 0; i < models.Count; i++) + { + ThrowIfSceneChanged(); + + var model = models[i] as GameObject; + + if (model != null) + { + var go = UnityEngine.Object.Instantiate(model, Vector3.zero, Quaternion.Euler(0, 0, 0)); + ReplaceShaders(go, referenceShader); + + var previewPath = Settings.Screenshotter.Screenshot(go, GenerateOutputPathWithoutExtension(model, Settings.PreviewFileNamingFormat)); + if (!string.IsNullOrEmpty(previewPath)) + generatedPreviews.Add(ObjectToMetadata(model, previewPath)); + + UnityEngine.Object.DestroyImmediate(go); + } + + OnAssetProcessed?.Invoke(i, models.Count); + await Task.Yield(); + } + + return generatedPreviews; + } + + private void ReplaceShaders(GameObject go, Shader shader) + { + var meshRenderers = go.GetComponentsInChildren(); + foreach (var mr in meshRenderers) + { + var materialArray = mr.sharedMaterials; + for (int i = 0; i < materialArray.Length; i++) + { + materialArray[i] = new Material(shader) { color = new Color(0.7f, 0.7f, 0.7f) }; + } + + mr.sharedMaterials = materialArray; + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/ModelTypePreviewGenerator.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/ModelTypePreviewGenerator.cs.meta new file mode 100644 index 00000000000..72c41d06372 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/ModelTypePreviewGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fca8a1fa8a211874cb84d3d811a0158c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/PrefabTypePreviewGenerator.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/PrefabTypePreviewGenerator.cs new file mode 100644 index 00000000000..f0eeb6b985f --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/PrefabTypePreviewGenerator.cs @@ -0,0 +1,113 @@ +using AssetStoreTools.Previews.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Previews.Generators.Custom.TypeGenerators +{ + internal class PrefabTypePreviewGenerator : TypePreviewGeneratorFromScene + { + public override event Action OnAssetProcessed; + + public PrefabTypePreviewGenerator(TypePreviewGeneratorFromSceneSettings settings) : base(settings) { } + + protected override IEnumerable CollectAssets() + { + var prefabs = new List(); + var prefabGuids = AssetDatabase.FindAssets("t:prefab", Settings.InputPaths); + + foreach (var guid in prefabGuids) + { + var prefab = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); + + // Skip nested prefabs + if (!AssetDatabase.IsMainAsset(prefab)) + continue; + + // Skip prefabs without renderers + if (prefab.GetComponentsInChildren().Length == 0) + continue; + + prefabs.Add(prefab); + } + + return prefabs; + } + + protected override async Task> GeneratePreviewsInScene(IEnumerable assets) + { + var generatedPreviews = new List(); + var prefabs = assets.ToList(); + var objectReferenceShader = GetDefaultObjectShader(); + var particleReferenceShader = GetDefaultParticleShader(); + + for (int i = 0; i < prefabs.Count; i++) + { + ThrowIfSceneChanged(); + + var prefab = prefabs[i] as GameObject; + if (prefab != null) + { + var go = UnityEngine.Object.Instantiate(prefab, Vector3.zero, Quaternion.Euler(0, 0, 0)); + + ReplaceMissingShaders(go, objectReferenceShader, particleReferenceShader); + + HandleParticleSystems(go); + + var previewPath = Settings.Screenshotter.Screenshot(go, GenerateOutputPathWithoutExtension(prefab, Settings.PreviewFileNamingFormat)); + if (!string.IsNullOrEmpty(previewPath)) + generatedPreviews.Add(ObjectToMetadata(prefab, previewPath)); + + UnityEngine.Object.DestroyImmediate(go); + } + + OnAssetProcessed?.Invoke(i, prefabs.Count); + await Task.Yield(); + } + + return generatedPreviews; + } + + private void ReplaceMissingShaders(GameObject go, Shader objectShader, Shader particleShader) + { + var meshRenderers = go.GetComponentsInChildren(); + foreach (var mr in meshRenderers) + { + var shaderToUse = mr is ParticleSystemRenderer ? particleShader : objectShader; + + var materialArray = mr.sharedMaterials; + for (int i = 0; i < materialArray.Length; i++) + { + if (materialArray[i] == null) + { + materialArray[i] = new Material(shaderToUse); + } + else if (!materialArray[i].shader.isSupported) + { + materialArray[i].shader = shaderToUse; + } + } + + mr.sharedMaterials = materialArray; + } + } + + private void HandleParticleSystems(GameObject go) + { + var particleSystems = go.GetComponentsInChildren(); + if (particleSystems.Length == 0) + return; + + foreach (var ps in particleSystems) + { + ps.Stop(); + ps.Clear(); + ps.randomSeed = 1; + ps.Simulate(10, false, true, false); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/PrefabTypePreviewGenerator.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/PrefabTypePreviewGenerator.cs.meta new file mode 100644 index 00000000000..61b31c14c23 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/PrefabTypePreviewGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 24b15b10bc361c84581f46cb6dd644cc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TextureTypeGeneratorSettings.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TextureTypeGeneratorSettings.cs new file mode 100644 index 00000000000..666b32dddeb --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TextureTypeGeneratorSettings.cs @@ -0,0 +1,11 @@ +using AssetStoreTools.Previews.Data; + +namespace AssetStoreTools.Previews.Generators.Custom.TypeGenerators +{ + internal class TextureTypeGeneratorSettings : TypeGeneratorSettings + { + public int MaxWidth; + public int MaxHeight; + public PreviewFormat Format; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TextureTypeGeneratorSettings.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TextureTypeGeneratorSettings.cs.meta new file mode 100644 index 00000000000..29385d121b3 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TextureTypeGeneratorSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 058d746982619b04eb5e200363003899 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TextureTypePreviewGenerator.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TextureTypePreviewGenerator.cs new file mode 100644 index 00000000000..24e1102ffc2 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TextureTypePreviewGenerator.cs @@ -0,0 +1,116 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Previews.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Previews.Generators.Custom.TypeGenerators +{ + internal class TextureTypePreviewGenerator : TypePreviewGeneratorBase + { + private TextureTypeGeneratorSettings _settings; + + public override event Action OnAssetProcessed; + + public TextureTypePreviewGenerator(TextureTypeGeneratorSettings settings) : base(settings) + { + _settings = settings; + } + + public override void ValidateSettings() + { + base.ValidateSettings(); + + if (_settings.MaxWidth <= 0) + throw new ArgumentException("Max width should be larger than 0"); + + if (_settings.MaxHeight <= 0) + throw new ArgumentException("Max height should be larger than 0"); + } + + protected override IEnumerable CollectAssets() + { + var textures = new List(); + var textureGuids = AssetDatabase.FindAssets("t:texture", Settings.InputPaths); + + foreach (var guid in textureGuids) + { + var texture = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); + + // Skip nested textures + if (!AssetDatabase.IsMainAsset(texture)) + continue; + + textures.Add(texture); + } + + return textures; + } + + protected override async Task> GenerateImpl(IEnumerable assets) + { + var generatedPreviews = new List(); + var textures = assets.ToList(); + + for (int i = 0; i < textures.Count; i++) + { + var texture = textures[i] as Texture2D; + + if (texture != null) + { + Texture2D resizedTexture; + CalculateTextureSize(texture, out var resizeWidth, out var resizeHeight); + + var importer = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(texture)) as TextureImporter; + if (importer != null && importer.textureType == TextureImporterType.NormalMap) + resizedTexture = GraphicsUtility.ResizeTextureNormalMap(texture, resizeWidth, resizeHeight); + else + resizedTexture = GraphicsUtility.ResizeTexture(texture, resizeWidth, resizeHeight); + + var previewPath = GenerateOutputPathWithExtension(texture, _settings.PreviewFileNamingFormat, _settings.Format); + + // Some textures may be transparent and need to be encoded as PNG to look correctly + var targetFormat = texture.alphaIsTransparency ? PreviewFormat.PNG : _settings.Format; + var bytes = PreviewConvertUtility.ConvertTexture(resizedTexture, targetFormat); + + File.WriteAllBytes(previewPath, bytes); + generatedPreviews.Add(ObjectToMetadata(texture, previewPath)); + } + + OnAssetProcessed?.Invoke(i, textures.Count); + await Task.Yield(); + } + + return generatedPreviews; + } + + private void CalculateTextureSize(Texture2D texture, out int width, out int height) + { + if (texture.width <= _settings.MaxWidth && texture.height <= _settings.MaxHeight) + { + width = texture.width; + height = texture.height; + return; + } + + var widthLongerThanHeight = texture.width > texture.height; + + if (widthLongerThanHeight) + { + var ratio = (float)texture.width / texture.height; + width = _settings.MaxWidth; + height = Mathf.RoundToInt(width / ratio); + } + else + { + var ratio = (float)texture.height / texture.width; + height = _settings.MaxHeight; + width = Mathf.RoundToInt(height / ratio); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TextureTypePreviewGenerator.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TextureTypePreviewGenerator.cs.meta new file mode 100644 index 00000000000..cbe7491ccc2 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TextureTypePreviewGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b04f55867ee575c489803356220feb31 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypeGeneratorSettings.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypeGeneratorSettings.cs new file mode 100644 index 00000000000..1aaaa42dca4 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypeGeneratorSettings.cs @@ -0,0 +1,12 @@ +using AssetStoreTools.Previews.Data; + +namespace AssetStoreTools.Previews.Generators.Custom.TypeGenerators +{ + internal abstract class TypeGeneratorSettings + { + public string[] InputPaths; + public string[] IgnoredGuids; + public string OutputPath; + public FileNameFormat PreviewFileNamingFormat; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypeGeneratorSettings.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypeGeneratorSettings.cs.meta new file mode 100644 index 00000000000..4c24cf5d734 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypeGeneratorSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a224a4b41a8c7cf4cb53dd77d6f2518b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorBase.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorBase.cs new file mode 100644 index 00000000000..a49d9686d10 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorBase.cs @@ -0,0 +1,126 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Previews.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using UnityEditor; + +namespace AssetStoreTools.Previews.Generators.Custom.TypeGenerators +{ + internal abstract class TypePreviewGeneratorBase : ITypePreviewGenerator + { + public TypeGeneratorSettings Settings { get; } + + public abstract event Action OnAssetProcessed; + + public TypePreviewGeneratorBase(TypeGeneratorSettings settings) + { + Settings = settings; + } + + public virtual void ValidateSettings() + { + if (Settings.InputPaths == null || Settings.InputPaths.Length == 0) + throw new ArgumentException("Input path cannot be null"); + + foreach (var path in Settings.InputPaths) + { + var inputPath = path.EndsWith("/") ? path.Remove(path.Length - 1) : path; + if (!AssetDatabase.IsValidFolder(inputPath)) + throw new ArgumentException($"Input path '{inputPath}' is not a valid ADB folder"); + } + + if (string.IsNullOrEmpty(Settings.OutputPath)) + throw new ArgumentException("Output path cannot be null"); + } + + public async Task> Generate() + { + var generatedPreviews = new List(); + ValidateSettings(); + + var assets = CollectAssets(); + assets = FilterIgnoredAssets(assets); + + if (assets.Count() == 0) + return generatedPreviews; + + return await GenerateImpl(assets); + } + + protected abstract IEnumerable CollectAssets(); + + private IEnumerable FilterIgnoredAssets(IEnumerable assets) + { + if (Settings.IgnoredGuids == null || Settings.IgnoredGuids.Length == 0) + return assets; + + var filteredAssets = new List(); + foreach (var asset in assets) + { + if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out var guid, out long _)) + continue; + + if (Settings.IgnoredGuids.Any(x => x == guid)) + continue; + + filteredAssets.Add(asset); + } + + return filteredAssets; + } + + protected abstract Task> GenerateImpl(IEnumerable assets); + + protected PreviewMetadata ObjectToMetadata(UnityEngine.Object obj, string previewPath) + { + if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out var guid, out long _)) + throw new Exception($"Could not retrieve guid for object {obj}"); + + return new PreviewMetadata() + { + Type = GenerationType.Custom, + Guid = guid, + Name = obj.name, + Path = previewPath + }; + } + + protected string GenerateOutputPathWithoutExtension(UnityEngine.Object asset, FileNameFormat fileNameFormat) + { + PrepareOutputFolder(Settings.OutputPath, false); + var directoryPath = Settings.OutputPath; + var fileName = PreviewConvertUtility.ConvertFilename(asset, fileNameFormat); + var fullPath = $"{directoryPath}/{fileName}"; + + return fullPath; + } + + protected string GenerateOutputPathWithExtension(UnityEngine.Object asset, FileNameFormat fileNameFormat, PreviewFormat previewFormat) + { + var partialOutputPath = GenerateOutputPathWithoutExtension(asset, fileNameFormat); + var extension = PreviewConvertUtility.ConvertExtension(previewFormat); + + return $"{partialOutputPath}.{extension}"; + } + + private void PrepareOutputFolder(string outputPath, bool cleanup) + { + var dir = new DirectoryInfo(outputPath); + + if (!dir.Exists) + { + dir.Create(); + return; + } + + if (!cleanup) + return; + + dir.Delete(true); + dir.Create(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorBase.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorBase.cs.meta new file mode 100644 index 00000000000..7e751c2cece --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c6fb2d639232bce4698338a252f47f3c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorFromScene.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorFromScene.cs new file mode 100644 index 00000000000..f9fe453e910 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorFromScene.cs @@ -0,0 +1,111 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Previews.Utility; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace AssetStoreTools.Previews.Generators.Custom.TypeGenerators +{ + internal abstract class TypePreviewGeneratorFromScene : TypePreviewGeneratorBase + { + protected new TypePreviewGeneratorFromSceneSettings Settings; + + private CancellationTokenSource _cancellationTokenSource; + + public TypePreviewGeneratorFromScene(TypePreviewGeneratorFromSceneSettings settings) : base(settings) + { + Settings = settings; + } + + public override void ValidateSettings() + { + base.ValidateSettings(); + + if (Settings.Screenshotter == null) + throw new ArgumentException("Screenshotter cannot be null"); + } + + protected sealed override async Task> GenerateImpl(IEnumerable assets) + { + var originalScenePath = EditorSceneManager.GetActiveScene().path; + await PreviewSceneUtility.OpenPreviewSceneForCurrentPipeline(); + + try + { + _cancellationTokenSource = new CancellationTokenSource(); + EditorSceneManager.sceneOpened += SceneOpenedDuringGeneration; + return await GeneratePreviewsInScene(assets); + } + finally + { + EditorSceneManager.sceneOpened -= SceneOpenedDuringGeneration; + _cancellationTokenSource.Dispose(); + if (!string.IsNullOrEmpty(originalScenePath)) + EditorSceneManager.OpenScene(originalScenePath); + } + } + + protected abstract Task> GeneratePreviewsInScene(IEnumerable assets); + + private void SceneOpenedDuringGeneration(Scene _, OpenSceneMode __) + { + if (!_cancellationTokenSource.IsCancellationRequested) + _cancellationTokenSource.Cancel(); + } + + protected void ThrowIfSceneChanged() + { + if (_cancellationTokenSource.Token.IsCancellationRequested) + throw new Exception("Preview generation was aborted due to a change of the scene"); + } + + protected Shader GetDefaultObjectShader() + { + switch (RenderPipelineUtility.GetCurrentPipeline()) + { + case RenderPipeline.BiRP: + return Shader.Find("Standard"); + case RenderPipeline.URP: + return Shader.Find("Universal Render Pipeline/Lit"); + case RenderPipeline.HDRP: + return Shader.Find("HDRP/Lit"); + default: + throw new NotImplementedException("Undefined Render Pipeline"); + } + } + + protected Shader GetDefaultParticleShader() + { + switch (RenderPipelineUtility.GetCurrentPipeline()) + { + case RenderPipeline.BiRP: + return Shader.Find("Particles/Standard Unlit"); + case RenderPipeline.URP: + return Shader.Find("Universal Render Pipeline/Particles/Unlit"); + case RenderPipeline.HDRP: + return Shader.Find("HDRP/Unlit"); + default: + throw new NotImplementedException("Undefined Render Pipeline"); + } + } + + protected Shader GetDefaultTextureShader() + { + switch (RenderPipelineUtility.GetCurrentPipeline()) + { + case RenderPipeline.BiRP: + return Shader.Find("Unlit/Texture"); + case RenderPipeline.URP: + return Shader.Find("Universal Render Pipeline/Unlit"); + case RenderPipeline.HDRP: + return Shader.Find("HDRP/Unlit"); + default: + throw new NotImplementedException("Undefined Render Pipeline"); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorFromScene.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorFromScene.cs.meta new file mode 100644 index 00000000000..fcc5e57d23d --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorFromScene.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1bf0eb8d7ef65f340be785dae96e4b73 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorFromSceneSettings.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorFromSceneSettings.cs new file mode 100644 index 00000000000..36a2e6e0a29 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorFromSceneSettings.cs @@ -0,0 +1,9 @@ +using AssetStoreTools.Previews.Generators.Custom.Screenshotters; + +namespace AssetStoreTools.Previews.Generators.Custom.TypeGenerators +{ + internal class TypePreviewGeneratorFromSceneSettings : TypeGeneratorSettings + { + public ISceneScreenshotter Screenshotter; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorFromSceneSettings.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorFromSceneSettings.cs.meta new file mode 100644 index 00000000000..ead93bd3860 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/Custom/TypeGenerators/TypePreviewGeneratorFromSceneSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a6b871798f99ad44d9fca46789239ec1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/CustomPreviewGenerator.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/CustomPreviewGenerator.cs new file mode 100644 index 00000000000..7a1ffebce02 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/CustomPreviewGenerator.cs @@ -0,0 +1,213 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Previews.Generators; +using AssetStoreTools.Previews.Generators.Custom.Screenshotters; +using AssetStoreTools.Previews.Generators.Custom.TypeGenerators; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UnityEditor; + +namespace AssetStoreTools.Previews +{ + internal class CustomPreviewGenerator : PreviewGeneratorBase + { + private CustomPreviewGenerationSettings _customSettings; + + public override event Action OnProgressChanged; + + public CustomPreviewGenerator(CustomPreviewGenerationSettings settings) + : base(settings) + { + _customSettings = settings; + } + + protected override void Validate() + { + base.Validate(); + + if (_customSettings.Width <= 0) + throw new ArgumentException("Width should be larger than 0"); + + if (_customSettings.Height <= 0) + throw new ArgumentException("Height should be larger than 0"); + + if (_customSettings.Depth <= 0) + throw new ArgumentException("Depth should be larger than 0"); + + if (_customSettings.NativeWidth <= 0) + throw new ArgumentException("Native width should be larger than 0"); + + if (_customSettings.NativeHeight <= 0) + throw new ArgumentException("Native height should be larger than 0"); + } + + protected override async Task GenerateImpl() + { + var result = new PreviewGenerationResult() + { + GenerationType = _customSettings.GenerationType + }; + + OnProgressChanged?.Invoke(0f); + + var generatedPreviews = new List(); + var existingPreviews = GetExistingPreviews(); + var generators = CreateGenerators(existingPreviews); + + var currentGenerator = 0; + Action generatorProgressCallback = null; + generatorProgressCallback = (currentAsset, totalAssets) => ReportProgress(currentGenerator, generators.Count(), currentAsset, totalAssets); + + try + { + foreach (var generator in generators) + { + generator.OnAssetProcessed += generatorProgressCallback; + var typeGeneratorPreviews = await generator.Generate(); + generatedPreviews.AddRange(typeGeneratorPreviews); + currentGenerator++; + } + + AssetDatabase.Refresh(); + + var allPreviews = new List(); + allPreviews.AddRange(generatedPreviews); + allPreviews.AddRange(existingPreviews); + + result.Success = true; + result.GeneratedPreviews = generatedPreviews; + result.Previews = allPreviews; + } + catch (Exception e) + { + result.Success = false; + result.Exception = e; + } + finally + { + foreach (var generator in generators) + generator.OnAssetProcessed -= generatorProgressCallback; + } + + return result; + } + + private IEnumerable GetExistingPreviews() + { + var existingPreviews = new List(); + + if (Settings.OverwriteExisting || !CachingService.GetCachedMetadata(out var database)) + return existingPreviews; + + var inputGuids = AssetDatabase.FindAssets("", _customSettings.InputPaths); + existingPreviews = database.Previews.Where(x => x.Type == GenerationType.Custom && x.Exists() && inputGuids.Any(y => y.Equals(x.Guid))).ToList(); + return existingPreviews; + } + + private IEnumerable CreateGenerators(IEnumerable existingPreviews) + { + var ignoredGuids = existingPreviews.Select(x => x.Guid).ToArray(); + + var generators = new ITypePreviewGenerator[] + { + CreateAudioPreviewGenerator(ignoredGuids), + CreateMaterialPreviewGenerator(ignoredGuids), + CreateModelPreviewGenerator(ignoredGuids), + CreatePrefabPreviewGenerator(ignoredGuids), + CreateTexturePreviewGenerator(ignoredGuids) + }; + + return generators; + } + + private ITypePreviewGenerator CreateAudioPreviewGenerator(string[] ignoredGuids) + { + var settings = new AudioTypeGeneratorSettings() + { + Width = _customSettings.Width, + Height = _customSettings.Height, + InputPaths = _customSettings.InputPaths, + OutputPath = _customSettings.OutputPath, + PreviewFileNamingFormat = _customSettings.PreviewFileNamingFormat, + Format = _customSettings.Format, + SampleColor = _customSettings.AudioSampleColor, + BackgroundColor = _customSettings.AudioBackgroundColor, + IgnoredGuids = ignoredGuids + }; + + return new AudioTypePreviewGenerator(settings); + } + + private ITypePreviewGenerator CreateMaterialPreviewGenerator(string[] ignoredGuids) + { + var settings = CreateSceneGeneratorSettings(new MaterialScreenshotter(CreateScreenshotterSettings()), ignoredGuids); + return new MaterialTypePreviewGenerator(settings); + } + + private ITypePreviewGenerator CreateModelPreviewGenerator(string[] ignoredGuids) + { + var settings = CreateSceneGeneratorSettings(new MeshScreenshotter(CreateScreenshotterSettings()), ignoredGuids); + return new ModelTypePreviewGenerator(settings); + } + + private ITypePreviewGenerator CreatePrefabPreviewGenerator(string[] ignoredGuids) + { + var settings = CreateSceneGeneratorSettings(new MeshScreenshotter(CreateScreenshotterSettings()), ignoredGuids); + return new PrefabTypePreviewGenerator(settings); + } + + private ITypePreviewGenerator CreateTexturePreviewGenerator(string[] ignoredGuids) + { + var settings = new TextureTypeGeneratorSettings() + { + MaxWidth = _customSettings.Width, + MaxHeight = _customSettings.Height, + InputPaths = _customSettings.InputPaths, + OutputPath = _customSettings.OutputPath, + Format = _customSettings.Format, + PreviewFileNamingFormat = _customSettings.PreviewFileNamingFormat, + IgnoredGuids = ignoredGuids + }; + + return new TextureTypePreviewGenerator(settings); + } + + private TypePreviewGeneratorFromSceneSettings CreateSceneGeneratorSettings(ISceneScreenshotter screenshotter, string[] ignoredGuids) + { + var settings = new TypePreviewGeneratorFromSceneSettings() + { + InputPaths = _customSettings.InputPaths, + OutputPath = _customSettings.OutputPath, + PreviewFileNamingFormat = _customSettings.PreviewFileNamingFormat, + Screenshotter = screenshotter, + IgnoredGuids = ignoredGuids + }; + + return settings; + } + + private SceneScreenshotterSettings CreateScreenshotterSettings() + { + var settings = new SceneScreenshotterSettings() + { + Width = _customSettings.Width, + Height = _customSettings.Height, + Depth = _customSettings.Depth, + Format = _customSettings.Format, + NativeWidth = _customSettings.NativeWidth, + NativeHeight = _customSettings.NativeHeight, + }; + + return settings; + } + + private void ReportProgress(int currentGenerator, int totalGenerators, int currentGeneratorAsset, int totalCurrentGeneratorAssets) + { + var completedGeneratorProgress = (float)currentGenerator / totalGenerators; + var currentGeneratorProgress = ((float)currentGeneratorAsset / totalCurrentGeneratorAssets) / totalGenerators; + var progressToReport = completedGeneratorProgress + currentGeneratorProgress; + OnProgressChanged?.Invoke(progressToReport); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/CustomPreviewGenerator.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/CustomPreviewGenerator.cs.meta new file mode 100644 index 00000000000..e10c56c7615 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/CustomPreviewGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e5906f8cb3e4eec489a2f7a82844bb3f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/IPreviewGenerator.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/IPreviewGenerator.cs new file mode 100644 index 00000000000..b548bafb318 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/IPreviewGenerator.cs @@ -0,0 +1,15 @@ +using AssetStoreTools.Previews.Data; +using System; +using System.Threading.Tasks; + +namespace AssetStoreTools.Previews.Generators +{ + internal interface IPreviewGenerator + { + PreviewGenerationSettings Settings { get; } + + event Action OnProgressChanged; + + Task Generate(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/IPreviewGenerator.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/IPreviewGenerator.cs.meta new file mode 100644 index 00000000000..e379870834e --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/IPreviewGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ebddaccb94ca6e34aa36b535d0a47249 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/NativePreviewGenerator.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/NativePreviewGenerator.cs new file mode 100644 index 00000000000..760e2d14dd4 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/NativePreviewGenerator.cs @@ -0,0 +1,362 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Previews.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using UnityEngine.Tilemaps; + +namespace AssetStoreTools.Previews.Generators +{ + internal class NativePreviewGenerator : PreviewGeneratorBase + { + private const double InitialPreviewLoadingTimeoutSeconds = 10; + + private NativePreviewGenerationSettings _nativeSettings; + + private RenderTexture _renderTexture; + + private int _generatedPreviewsCount; + private int _totalPreviewsCount; + + public override event Action OnProgressChanged; + + public NativePreviewGenerator(NativePreviewGenerationSettings settings) + : base(settings) + { + _nativeSettings = settings; + } + + protected override void Validate() + { + base.Validate(); + + if (_nativeSettings.ChunkSize <= 0) + throw new ArgumentException("Chunk size must be larger than 0"); + } + + protected override async Task GenerateImpl() + { + var result = new PreviewGenerationResult() + { + GenerationType = _nativeSettings.GenerationType + }; + + OnProgressChanged?.Invoke(0f); + + try + { + var objects = GetObjectsRequiringPreviews(_nativeSettings.InputPaths); + var filteredObjects = new List(); + var reusedPreviews = new List(); + FilterObjects(objects, filteredObjects, reusedPreviews); + + _generatedPreviewsCount = 0; + _totalPreviewsCount = objects.Count; + + Directory.CreateDirectory(_nativeSettings.OutputPath); + + var generatedPreviews = new List(); + if (!_nativeSettings.WaitForPreviews) + { + WritePreviewsWithoutWaiting(filteredObjects, out generatedPreviews); + } + else + { + if (_nativeSettings.ChunkedPreviewLoading) + { + await WaitAndWritePreviewsChunked(filteredObjects, generatedPreviews); + } + else + { + await WaitAndWritePreviews(filteredObjects, generatedPreviews); + } + } + + var allPreviews = new List(); + allPreviews.AddRange(generatedPreviews); + allPreviews.AddRange(reusedPreviews); + + result.Success = true; + result.GeneratedPreviews = generatedPreviews; + result.Previews = allPreviews; + } + catch (Exception e) + { + result.Success = false; + result.Exception = e; + } + + return result; + } + + private List GetObjectsRequiringPreviews(string[] inputPaths) + { + var objects = new List(); + var guids = AssetDatabase.FindAssets("", inputPaths); + + foreach (var guid in guids) + { + if (objects.Any(x => x.Guid == guid)) + continue; + + var assetPath = AssetDatabase.GUIDToAssetPath(guid); + var obj = AssetDatabase.LoadAssetAtPath(assetPath); + if (!ShouldHavePreview(obj)) + continue; + + objects.Add(new PreviewMetadata() { Type = GenerationType.Native, Guid = guid }); + } + + return objects; + } + + private void FilterObjects(List objects, List filteredObjects, List reusedPreviews) + { + if (Settings.OverwriteExisting || !CachingService.GetCachedMetadata(out var database)) + { + filteredObjects.AddRange(objects); + return; + } + + foreach (var obj in objects) + { + var matchingEntry = database.Previews.FirstOrDefault(x => + x.Guid == obj.Guid + && x.Type == GenerationType.Native + && x.Exists()); + + if (matchingEntry == null) + { + filteredObjects.Add(obj); + } + else + { + reusedPreviews.Add(matchingEntry); + } + } + } + + private bool ShouldHavePreview(UnityEngine.Object asset) + { + if (asset == null) + return false; + + if (!AssetDatabase.IsMainAsset(asset)) + return false; + + switch (asset) + { + case AudioClip _: + case Material _: + case Mesh _: + case TerrainLayer _: + case Texture _: + case Tile _: + return true; + case GameObject go: + var renderers = go.GetComponentsInChildren(); + return renderers != null && renderers.Length > 0; + default: + return false; + } + } + + private PreviewMetadata WritePreviewToDisk(PreviewMetadata metadata, Texture2D texture) + { + var asset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(metadata.Guid)); + var width = Mathf.Min(texture.width, 128); + var height = Mathf.Min(texture.height, 128); + var readableTexture = GraphicsUtility.ResizeTexture(texture, width, height); + var fileName = PreviewConvertUtility.ConvertFilenameWithExtension(asset, _nativeSettings.PreviewFileNamingFormat, _nativeSettings.Format); + var filePath = $"{_nativeSettings.OutputPath}/{fileName}"; + var bytes = PreviewConvertUtility.ConvertTexture(readableTexture, _nativeSettings.Format); + + File.WriteAllBytes(filePath, bytes); + + metadata.Type = GenerationType.Native; + metadata.Name = asset.name; + metadata.Path = filePath; + + return metadata; + } + + private void WritePreviewsWithoutWaiting(List objects, out List generatedPreviews) + { + generatedPreviews = new List(); + + foreach (var obj in objects) + { + var texture = GetAssetPreviewFromGuid(obj.Guid); + if (texture == null) + continue; + + var generatedPreview = WritePreviewToDisk(obj, texture); + generatedPreviews.Add(generatedPreview); + } + } + + private Texture2D GetAssetPreviewFromGuid(string guid) + { + var method = typeof(AssetPreview).GetMethod("GetAssetPreviewFromGUID", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, new[] { typeof(string) }, null); + var args = new object[] { guid }; + + return method?.Invoke(null, args) as Texture2D; + } + + private async Task WaitAndWritePreviewsChunked(List objects, List generatedPreviews) + { + var chunks = objects.Count / _nativeSettings.ChunkSize; + var remainder = objects.Count % _nativeSettings.ChunkSize; + if (remainder != 0) + chunks += 1; + + for (int i = 0; i < chunks; i++) + { + var chunkObjects = new List(); + + for (int j = 0; j < _nativeSettings.ChunkSize; j++) + { + var index = i * _nativeSettings.ChunkSize + j; + if (index == objects.Count) + break; + + chunkObjects.Add(objects[index]); + } + + var generatedPreviewsChunk = new List(); + await WaitAndWritePreviews(chunkObjects, generatedPreviewsChunk); + generatedPreviews.AddRange(generatedPreviewsChunk); + } + } + + private async Task WaitAndWritePreviews(List objects, List generatedPreviews) + { + var initialObjectCount = objects.Count(); + if (initialObjectCount == 0) + return; + + await WaitAndWritePreviewIteration(objects, generatedPreviews); + var remainingObjectCount = objects.Count; + + // First iteration may take longer to start loading objects + var firstIterationStartTime = EditorApplication.timeSinceStartup; + while (true) + { + if (remainingObjectCount < initialObjectCount) + break; + + if (EditorApplication.timeSinceStartup - firstIterationStartTime > InitialPreviewLoadingTimeoutSeconds) + throw new Exception("Preview loading timed out."); + + await WaitAndWritePreviewIteration(objects, generatedPreviews); + remainingObjectCount = objects.Count; + } + + if (remainingObjectCount == 0) + return; + + while (true) + { + await WaitForEndOfFrame(1); + await WaitAndWritePreviewIteration(objects, generatedPreviews); + + // If no more previews are being loaded, try one more time before quitting + if (objects.Count == remainingObjectCount) + { + await WaitForEndOfFrame(1); + await WaitAndWritePreviewIteration(objects, generatedPreviews); + + if (objects.Count == remainingObjectCount) + { + var missingObjects = string.Join("\n", objects.Select(x => AssetDatabase.GUIDToAssetPath(x.Guid))); + Debug.LogWarning($"Unity Editor failed to fetch previews for {objects.Count} objects:\n{missingObjects}"); + break; + } + } + + remainingObjectCount = objects.Count; + + // Exit when all previews are loaded + if (remainingObjectCount == 0) + break; + } + } + + private async Task WaitAndWritePreviewIteration(List objects, List generatedPreviews) + { + var cacheSize = Mathf.Max(_nativeSettings.ChunkSize * 2, objects.Count() + _nativeSettings.ChunkSize); + AssetPreview.SetPreviewTextureCacheSize(cacheSize); + + // Initial queueing + foreach (var obj in objects) + { + var asset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(obj.Guid)); + AssetPreview.GetAssetPreview(asset); + } + + await WaitForEndOfFrame(); + + // Waiting (NOTE: works inconsistently across Unity streams) + foreach (var obj in objects) + { + var asset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(obj.Guid)); + if (AssetPreview.IsLoadingAssetPreview(asset.GetInstanceID())) + { + await WaitForEndOfFrame(); + } + } + + // Writing + for (int i = 0; i < objects.Count; i++) + { + var obj = objects[i]; + + var asset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(obj.Guid)); + var texture = AssetPreview.GetAssetPreview(asset); + if (texture == null) + continue; + + WritePreviewToDisk(obj, texture); + generatedPreviews.Add(obj); + _generatedPreviewsCount++; + OnProgressChanged?.Invoke((float)_generatedPreviewsCount / _totalPreviewsCount); + } + + // Removing written objects from the list + for (int i = objects.Count - 1; i >= 0; i--) + { + if (objects[i].Exists()) + objects.RemoveAt(i); + } + } + + private async Task WaitForEndOfFrame(double atLeastSeconds) + { + var startTime = EditorApplication.timeSinceStartup; + while (EditorApplication.timeSinceStartup - startTime <= atLeastSeconds) + { + await WaitForEndOfFrame(); + } + } + + private async Task WaitForEndOfFrame() + { + var isNextFrame = false; + EditorApplication.CallbackFunction callback = null; + callback = () => + { + EditorApplication.update -= callback; + isNextFrame = true; + }; + + EditorApplication.update += callback; + while (!isNextFrame) + await Task.Yield(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/NativePreviewGenerator.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/NativePreviewGenerator.cs.meta new file mode 100644 index 00000000000..5a59e849447 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/NativePreviewGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dd6eeb2f97a2ed34db51ab5ac0b3ffa1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/PreviewGeneratorBase.cs b/.github/Asset Store Tools/Previews/Scripts/Generators/PreviewGeneratorBase.cs new file mode 100644 index 00000000000..02d779d33f8 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/PreviewGeneratorBase.cs @@ -0,0 +1,45 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Previews.Services; +using System; +using System.Threading.Tasks; + +namespace AssetStoreTools.Previews.Generators +{ + internal abstract class PreviewGeneratorBase : IPreviewGenerator + { + public PreviewGenerationSettings Settings { get; } + protected ICachingService CachingService; + + public abstract event Action OnProgressChanged; + + public PreviewGeneratorBase(PreviewGenerationSettings settings) + { + Settings = settings; + CachingService = PreviewServiceProvider.Instance.GetService(); + } + + public async Task Generate() + { + Validate(); + + var result = await GenerateImpl(); + if (result.Success) + { + CachingService.CacheMetadata(result.GeneratedPreviews); + } + + return result; + } + + protected virtual void Validate() + { + if (Settings.InputPaths == null || Settings.InputPaths.Length == 0) + throw new ArgumentException("Input paths cannot be null"); + + if (string.IsNullOrEmpty(Settings.OutputPath)) + throw new ArgumentException("Output path cannot be null"); + } + + protected abstract Task GenerateImpl(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Generators/PreviewGeneratorBase.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Generators/PreviewGeneratorBase.cs.meta new file mode 100644 index 00000000000..0369b2b3db9 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Generators/PreviewGeneratorBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cedf01448e0edcc4fb55f19f2e92b740 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Services.meta b/.github/Asset Store Tools/Previews/Scripts/Services.meta new file mode 100644 index 00000000000..55ebe0c7410 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Services.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: aa18c820f185bfc4d8cd59e3418e2c4e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Services/Caching.meta b/.github/Asset Store Tools/Previews/Scripts/Services/Caching.meta new file mode 100644 index 00000000000..449835ad0b1 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Services/Caching.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eb61a60f2ff91a448a7808ef2a25f871 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Services/Caching/CachingService.cs b/.github/Asset Store Tools/Previews/Scripts/Services/Caching/CachingService.cs new file mode 100644 index 00000000000..10bb448e395 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Services/Caching/CachingService.cs @@ -0,0 +1,87 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Utility; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace AssetStoreTools.Previews.Services +{ + internal class CachingService : ICachingService + { + public void CacheMetadata(IEnumerable previews) + { + var updatedDatabase = UpdatePreviewDatabase(previews); + + var serializerSettings = new JsonSerializerSettings() + { + ContractResolver = PreviewDatabaseContractResolver.Instance, + Converters = new List() { new StringEnumConverter() }, + Formatting = Formatting.Indented + }; + + CacheUtil.CreateFileInTempCache(Constants.Previews.PreviewDatabaseFile, JsonConvert.SerializeObject(updatedDatabase, serializerSettings), true); + } + + public bool GetCachedMetadata(out PreviewDatabase previewDatabase) + { + previewDatabase = null; + if (!CacheUtil.GetFileFromTempCache(Constants.Previews.PreviewDatabaseFile, out string filePath)) + return false; + + try + { + var serializerSettings = new JsonSerializerSettings() + { + ContractResolver = PreviewDatabaseContractResolver.Instance, + Converters = new List() { new StringEnumConverter() } + }; + + previewDatabase = JsonConvert.DeserializeObject(File.ReadAllText(filePath, Encoding.UTF8), serializerSettings); + return true; + } + catch + { + return false; + } + } + + private PreviewDatabase UpdatePreviewDatabase(IEnumerable previews) + { + PreviewDatabase database; + if (!GetCachedMetadata(out database)) + database = new PreviewDatabase(); + + // Delete missing previews + for (int i = database.Previews.Count - 1; i >= 0; i--) + { + if (database.Previews[i].Exists()) + continue; + + database.Previews.RemoveAt(i); + } + + // Append new previews & Replace existing previews + foreach (var preview in previews) + { + var matchingPreviews = database.Previews.Where(x => x.Guid == preview.Guid).ToList(); + foreach (var matchingPreview in matchingPreviews) + { + // Delete previously generated preview of the same type + if (matchingPreview.Type == preview.Type) + database.Previews.Remove(matchingPreview); + // Delete previously generated preview of a different type if the path matches + else if (matchingPreview.Path.Equals(preview.Path)) + database.Previews.Remove(matchingPreview); + } + + database.Previews.Add(preview); + } + + database.Previews = database.Previews.OrderBy(x => x.Guid).ThenBy(x => x.Type).ToList(); + return database; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Services/Caching/CachingService.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Services/Caching/CachingService.cs.meta new file mode 100644 index 00000000000..02a153e742a --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Services/Caching/CachingService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e0b6cf909c8798b4590744959571a21f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Services/Caching/ICachingService.cs b/.github/Asset Store Tools/Previews/Scripts/Services/Caching/ICachingService.cs new file mode 100644 index 00000000000..67130008a75 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Services/Caching/ICachingService.cs @@ -0,0 +1,11 @@ +using AssetStoreTools.Previews.Data; +using System.Collections.Generic; + +namespace AssetStoreTools.Previews.Services +{ + internal interface ICachingService : IPreviewService + { + void CacheMetadata(IEnumerable previews); + bool GetCachedMetadata(out PreviewDatabase previewDatabase); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Services/Caching/ICachingService.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Services/Caching/ICachingService.cs.meta new file mode 100644 index 00000000000..7dd11953528 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Services/Caching/ICachingService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eeaeae010299dcd489adb00dbf51b274 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Services/IPreviewService.cs b/.github/Asset Store Tools/Previews/Scripts/Services/IPreviewService.cs new file mode 100644 index 00000000000..0bc56aeb886 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Services/IPreviewService.cs @@ -0,0 +1,4 @@ +namespace AssetStoreTools.Previews.Services +{ + public interface IPreviewService { } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Services/IPreviewService.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Services/IPreviewService.cs.meta new file mode 100644 index 00000000000..478011447d0 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Services/IPreviewService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8c2761fe05638644d8e3a265865beef8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Services/PreviewServiceProvider.cs b/.github/Asset Store Tools/Previews/Scripts/Services/PreviewServiceProvider.cs new file mode 100644 index 00000000000..7b2754bed7c --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Services/PreviewServiceProvider.cs @@ -0,0 +1,17 @@ +using AssetStoreTools.Utility; + +namespace AssetStoreTools.Previews.Services +{ + internal class PreviewServiceProvider : ServiceProvider + { + public static PreviewServiceProvider Instance => _instance ?? (_instance = new PreviewServiceProvider()); + private static PreviewServiceProvider _instance; + + private PreviewServiceProvider() { } + + protected override void RegisterServices() + { + Register(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Services/PreviewServiceProvider.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Services/PreviewServiceProvider.cs.meta new file mode 100644 index 00000000000..3dd46316310 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Services/PreviewServiceProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a19bf5a4e3e441047bbc1b894e2a1149 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI.meta b/.github/Asset Store Tools/Previews/Scripts/UI.meta new file mode 100644 index 00000000000..67d04e3cd6f --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4738f3648c8368244a968bc840c1152b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Data.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Data.meta new file mode 100644 index 00000000000..b302df52dbc --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 23a2f4eadd444194a91ff4ce509e4798 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Data/AssetPreview.cs b/.github/Asset Store Tools/Previews/Scripts/UI/Data/AssetPreview.cs new file mode 100644 index 00000000000..1b2fee975f9 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Data/AssetPreview.cs @@ -0,0 +1,56 @@ +using AssetStoreTools.Previews.Data; +using System; +using System.IO; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Previews.UI.Data +{ + internal class AssetPreview : IAssetPreview + { + private PreviewMetadata _metadata; + + private UnityEngine.Object _cachedAsset; + private string _cachedAssetPath; + private Texture2D _cachedTexture; + + public UnityEngine.Object Asset => _cachedAsset ?? (_cachedAsset = AssetDatabase.LoadAssetAtPath(AssetPath)); + public string AssetPath => _cachedAssetPath ?? (_cachedAssetPath = AssetDatabase.GUIDToAssetPath(_metadata.Guid)); + + public AssetPreview(PreviewMetadata metadata) + { + _metadata = metadata; + } + + public string GetAssetPath() + { + var assetPath = AssetDatabase.GUIDToAssetPath(_metadata.Guid); + return assetPath; + } + + public async Task LoadImage(Action onSuccess) + { + if (_cachedTexture == null) + { + if (!_metadata.Exists()) + return; + + await Task.Yield(); + + try + { + _cachedTexture = new Texture2D(1, 1); + _cachedTexture.LoadImage(File.ReadAllBytes(_metadata.Path)); + } + catch (Exception e) + { + Debug.LogException(e); + return; + } + } + + onSuccess?.Invoke(_cachedTexture); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Data/AssetPreview.cs.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Data/AssetPreview.cs.meta new file mode 100644 index 00000000000..580a1b189e4 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Data/AssetPreview.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 739cf05c689204f4089fd0a6bddb8c3b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Data/AssetPreviewCollection.cs b/.github/Asset Store Tools/Previews/Scripts/UI/Data/AssetPreviewCollection.cs new file mode 100644 index 00000000000..40db2b622fd --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Data/AssetPreviewCollection.cs @@ -0,0 +1,46 @@ +using AssetStoreTools.Previews.Data; +using System; +using System.Collections.Generic; + +namespace AssetStoreTools.Previews.UI.Data +{ + internal class AssetPreviewCollection : IAssetPreviewCollection + { + private GenerationType _generationType; + private List _images; + + public event Action OnCollectionChanged; + + public AssetPreviewCollection() + { + _images = new List(); + } + + public GenerationType GetGenerationType() + { + return _generationType; + } + + public IEnumerable GetPreviews() + { + return _images; + } + + public void Refresh(GenerationType generationType, IEnumerable previews) + { + _images.Clear(); + + _generationType = generationType; + + foreach (var entry in previews) + { + if (!entry.Exists()) + continue; + + _images.Add(new AssetPreview(entry)); + } + + OnCollectionChanged?.Invoke(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Data/AssetPreviewCollection.cs.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Data/AssetPreviewCollection.cs.meta new file mode 100644 index 00000000000..6977abd3e67 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Data/AssetPreviewCollection.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9b1a0db8710933048b49dcca463fb8fd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Data/IAssetPreview.cs b/.github/Asset Store Tools/Previews/Scripts/UI/Data/IAssetPreview.cs new file mode 100644 index 00000000000..f7fc7508a8a --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Data/IAssetPreview.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; +using UnityEngine; + +namespace AssetStoreTools.Previews.UI.Data +{ + internal interface IAssetPreview + { + UnityEngine.Object Asset { get; } + string GetAssetPath(); + Task LoadImage(Action onSuccess); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Data/IAssetPreview.cs.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Data/IAssetPreview.cs.meta new file mode 100644 index 00000000000..280bf1c81dd --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Data/IAssetPreview.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0f9373dfc16d0fa4794dac29b75204ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Data/IAssetPreviewCollection.cs b/.github/Asset Store Tools/Previews/Scripts/UI/Data/IAssetPreviewCollection.cs new file mode 100644 index 00000000000..6e614c68c66 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Data/IAssetPreviewCollection.cs @@ -0,0 +1,15 @@ +using AssetStoreTools.Previews.Data; +using System; +using System.Collections.Generic; + +namespace AssetStoreTools.Previews.UI.Data +{ + internal interface IAssetPreviewCollection + { + event Action OnCollectionChanged; + + GenerationType GetGenerationType(); + IEnumerable GetPreviews(); + void Refresh(GenerationType generationType, IEnumerable previews); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Data/IAssetPreviewCollection.cs.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Data/IAssetPreviewCollection.cs.meta new file mode 100644 index 00000000000..d7c7c04f3f0 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Data/IAssetPreviewCollection.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc9d9abd80c070f44ac49d5ce23d2fc0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Data/IPreviewGeneratorSettings.cs b/.github/Asset Store Tools/Previews/Scripts/UI/Data/IPreviewGeneratorSettings.cs new file mode 100644 index 00000000000..51f626c9c47 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Data/IPreviewGeneratorSettings.cs @@ -0,0 +1,27 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Previews.Generators; +using System; +using System.Collections.Generic; + +namespace AssetStoreTools.Previews.UI.Data +{ + internal interface IPreviewGeneratorSettings + { + event Action OnGenerationTypeChanged; + event Action OnGenerationPathsChanged; + + void LoadSettings(PreviewGenerationSettings settings); + + GenerationType GetGenerationType(); + void SetGenerationType(GenerationType type); + List GetAvailableGenerationTypes(); + + List GetGenerationPaths(); + void AddGenerationPath(string path); + void RemoveGenerationPath(string path); + void ClearGenerationPaths(); + bool IsGenerationPathValid(string path, out string error); + + IPreviewGenerator CreateGenerator(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Data/IPreviewGeneratorSettings.cs.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Data/IPreviewGeneratorSettings.cs.meta new file mode 100644 index 00000000000..4ba406ede74 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Data/IPreviewGeneratorSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 55c9fcde15f06754588fd02fb8b99a60 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Data/PreviewGeneratorSettings.cs b/.github/Asset Store Tools/Previews/Scripts/UI/Data/PreviewGeneratorSettings.cs new file mode 100644 index 00000000000..86962e41aad --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Data/PreviewGeneratorSettings.cs @@ -0,0 +1,212 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Previews.Generators; +using AssetStoreTools.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; + +namespace AssetStoreTools.Previews.UI.Data +{ + internal class PreviewGeneratorSettings : IPreviewGeneratorSettings + { + private readonly GenerationType[] _availableGenerationTypes = new GenerationType[] + { + GenerationType.Native, + GenerationType.Custom + }; + + private List _inputPaths; + private GenerationType _generationType; + + public event Action OnGenerationTypeChanged; + public event Action OnGenerationPathsChanged; + + public PreviewGeneratorSettings() + { + _inputPaths = new List(); + _generationType = GenerationType.Native; + } + + public void LoadSettings(PreviewGenerationSettings settings) + { + if (settings == null) + return; + + _inputPaths = settings.InputPaths.ToList(); + OnGenerationPathsChanged?.Invoke(); + + switch (settings) + { + case NativePreviewGenerationSettings _: + _generationType = GenerationType.Native; + break; + case CustomPreviewGenerationSettings _: + _generationType = GenerationType.Custom; + break; + default: + return; + } + + OnGenerationTypeChanged?.Invoke(); + } + + public GenerationType GetGenerationType() + { + return _generationType; + } + + public void SetGenerationType(GenerationType type) + { + _generationType = type; + OnGenerationTypeChanged?.Invoke(); + } + + public List GetAvailableGenerationTypes() + { + return _availableGenerationTypes.ToList(); + } + + public List GetGenerationPaths() + { + return _inputPaths; + } + + public void AddGenerationPath(string path) + { + if (string.IsNullOrEmpty(path)) + return; + + if (_inputPaths.Contains(path)) + return; + + // Prevent redundancy for new paths + var existingPath = _inputPaths.FirstOrDefault(x => path.StartsWith(x + "/")); + if (existingPath != null) + { + Debug.LogWarning($"Path '{path}' is already included with existing path: '{existingPath}'"); + return; + } + + // Prevent redundancy for already added paths + var redundantPaths = _inputPaths.Where(x => x.StartsWith(path + "/")).ToArray(); + foreach (var redundantPath in redundantPaths) + { + Debug.LogWarning($"Existing validation path '{redundantPath}' has been made redundant by the inclusion of new validation path: '{path}'"); + _inputPaths.Remove(redundantPath); + } + + _inputPaths.Add(path); + + OnGenerationPathsChanged?.Invoke(); + } + + public void RemoveGenerationPath(string path) + { + if (!_inputPaths.Contains(path)) + return; + + _inputPaths.Remove(path); + + OnGenerationPathsChanged?.Invoke(); + } + + public void ClearGenerationPaths() + { + if (_inputPaths.Count == 0) + return; + + _inputPaths.Clear(); + + OnGenerationPathsChanged?.Invoke(); + } + + public bool IsGenerationPathValid(string path, out string error) + { + error = string.Empty; + + if (string.IsNullOrEmpty(path)) + { + error = "Path cannot be empty"; + return false; + } + + var isAssetsPath = path.StartsWith("Assets/") + || path.Equals("Assets"); + var isPackagePath = PackageUtility.GetPackageByManifestPath($"{path}/package.json", out _); + + if (!isAssetsPath && !isPackagePath) + { + error = "Selected path must be within the Assets folder or point to a root path of a package"; + return false; + } + + if (!Directory.Exists(path)) + { + error = "Path does not exist"; + return false; + } + + if (path.Split('/').Any(x => x.StartsWith(".") || x.EndsWith("~"))) + { + error = $"Path '{path}' cannot be selected as it is a hidden folder and not part of the Asset Database"; + return false; + } + + return true; + } + + public IPreviewGenerator CreateGenerator() + { + switch (_generationType) + { + case GenerationType.Native: + return CreateNativeGenerator(); + case GenerationType.Custom: + return CreateCustomGenerator(); + default: + throw new NotImplementedException("Undefined generator type"); + } + } + + private IPreviewGenerator CreateNativeGenerator() + { + var settings = new NativePreviewGenerationSettings() + { + InputPaths = _inputPaths.ToArray(), + OutputPath = Constants.Previews.Native.DefaultOutputPath, + PreviewFileNamingFormat = Constants.Previews.DefaultFileNameFormat, + Format = Constants.Previews.Native.DefaultFormat, + WaitForPreviews = Constants.Previews.Native.DefaultWaitForPreviews, + ChunkedPreviewLoading = Constants.Previews.Native.DefaultChunkedPreviewLoading, + ChunkSize = Constants.Previews.Native.DefaultChunkSize, + OverwriteExisting = true + }; + + return new NativePreviewGenerator(settings); + } + + private IPreviewGenerator CreateCustomGenerator() + { + var settings = new CustomPreviewGenerationSettings() + { + InputPaths = _inputPaths.ToArray(), + OutputPath = Constants.Previews.Custom.DefaultOutputPath, + Width = Constants.Previews.Custom.DefaultWidth, + Height = Constants.Previews.Custom.DefaultHeight, + Depth = Constants.Previews.Custom.DefaultDepth, + NativeWidth = Constants.Previews.Custom.DefaultNativeWidth, + NativeHeight = Constants.Previews.Custom.DefaultNativeHeight, + PreviewFileNamingFormat = Constants.Previews.DefaultFileNameFormat, + Format = Constants.Previews.Custom.DefaultFormat, + AudioSampleColor = Constants.Previews.Custom.DefaultAudioSampleColor, + AudioBackgroundColor = Constants.Previews.Custom.DefaultAudioBackgroundColor, + OverwriteExisting = true + }; + + var generator = new CustomPreviewGenerator(settings); + return generator; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Data/PreviewGeneratorSettings.cs.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Data/PreviewGeneratorSettings.cs.meta new file mode 100644 index 00000000000..1de481b7890 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Data/PreviewGeneratorSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9e6f754b1179d8d4cb40f62692619a63 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Elements.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Elements.meta new file mode 100644 index 00000000000..d81acd59984 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Elements.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 700ec0107b011824892281e880281bb1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Elements/AssetPreviewElement.cs b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/AssetPreviewElement.cs new file mode 100644 index 00000000000..aee6fa8a02c --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/AssetPreviewElement.cs @@ -0,0 +1,83 @@ +using AssetStoreTools.Previews.UI.Data; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Previews.UI.Elements +{ + internal class AssetPreviewElement : VisualElement + { + // Data + private IAssetPreview _assetPreview; + + // UI + private Image _image; + private Label _label; + + public AssetPreviewElement() + { + AddToClassList("preview-list-image"); + + Create(); + + RegisterCallback(OnImageClicked); + } + + private void Create() + { + CreateFiller(); + CreateImage(); + CreateLabel(); + } + + private void CreateImage() + { + _image = new Image(); + Add(_image); + } + + private void CreateFiller() + { + var filler = new VisualElement() { name = "Filler" }; + Add(filler); + } + + private void CreateLabel() + { + _label = new Label(); + Add(_label); + } + + private void SetImage(Texture2D texture) + { + _image.style.width = texture.width < 128 ? texture.width : 128; + _image.style.height = texture.height < 128 ? texture.height : 128; + _image.style.backgroundImage = texture; + } + + private void OnImageClicked(MouseDownEvent _) + { + EditorGUIUtility.PingObject(_assetPreview.Asset); + } + + public void SetSource(IAssetPreview assetPreview) + { + _assetPreview = assetPreview; + _assetPreview.LoadImage(SetImage); + + var assetPath = _assetPreview.GetAssetPath(); + + if (string.IsNullOrEmpty(assetPath)) + { + _label.text = "[Missing]"; + tooltip = "This asset has been deleted"; + return; + } + + var assetNameWithExtension = assetPath.Split('/').Last(); + _label.text = assetNameWithExtension; + tooltip = assetPath; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Elements/AssetPreviewElement.cs.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/AssetPreviewElement.cs.meta new file mode 100644 index 00000000000..56990b43103 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/AssetPreviewElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 28891b8cff841a44eb508494d62c190c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Elements/GridListElement.cs b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/GridListElement.cs new file mode 100644 index 00000000000..a1bf53a0a73 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/GridListElement.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Previews.UI.Elements +{ + internal class GridListElement : VisualElement + { + public int ElementWidth; + public int ElementHeight; + private int _visibilityHeadroom => ElementHeight; + + public IList ItemSource; + public Func MakeItem; + public Action BindItem; + + private ScrollView _scrollView; + + public GridListElement() + { + style.flexGrow = 1; + + Create(); + + _scrollView.contentViewport.RegisterCallback(OnGeometryChanged); + _scrollView.verticalScroller.valueChanged += OnVerticalScroll; +#if UNITY_2021_1_OR_NEWER + _scrollView.horizontalScrollerVisibility = ScrollerVisibility.Hidden; +#else + _scrollView.showHorizontal = false; +#endif + } + + private void Create() + { + _scrollView = new ScrollView(); + Add(_scrollView); + } + + private void OnGeometryChanged(GeometryChangedEvent evt) + { + Redraw(); + } + + private void OnVerticalScroll(float value) + { + Redraw(); + } + + public void Redraw() + { + if (ElementWidth == 0 + || ElementHeight == 0 + || ItemSource == null + || MakeItem == null + || BindItem == null) + return; + + _scrollView.Clear(); + + var rowCapacity = Mathf.FloorToInt(_scrollView.contentContainer.worldBound.width / ElementWidth); + if (rowCapacity == 0) + rowCapacity = 1; + + var totalRequiredRows = ItemSource.Count / rowCapacity; + if (ItemSource.Count % rowCapacity != 0) + totalRequiredRows++; + + _scrollView.contentContainer.style.height = totalRequiredRows * ElementHeight; + + var visibleRows = new List(); + for (int i = 0; i < totalRequiredRows; i++) + { + var visible = IsRowVisible(i); + if (!visible) + continue; + + var rowElement = CreateRow(i); + + for (int j = 0; j < rowCapacity; j++) + { + var elementIndex = i * rowCapacity + j; + if (elementIndex >= ItemSource.Count) + { + rowElement.Add(CreateFillerElement()); + continue; + } + + var element = MakeItem?.Invoke(); + BindItem?.Invoke(element, elementIndex); + + rowElement.Add(element); + } + + _scrollView.Add(rowElement); + } + } + + private bool IsRowVisible(int rowIndex) + { + var contentStartY = _scrollView.contentContainer.worldBound.yMin; + var visibleContentMinY = _scrollView.contentViewport.worldBound.yMin - _visibilityHeadroom; + var visibleContentMaxY = _scrollView.contentViewport.worldBound.yMax + _visibilityHeadroom; + if (_scrollView.contentViewport.worldBound.height == 0) + visibleContentMaxY = this.worldBound.yMax; + + var rowMinY = (rowIndex * ElementHeight) + contentStartY; + var rowMaxY = (rowIndex * ElementHeight) + ElementHeight + contentStartY; + + var fullyVisible = rowMinY >= visibleContentMinY && rowMaxY <= visibleContentMaxY; + var partiallyAbove = rowMinY < visibleContentMinY && rowMaxY > visibleContentMinY; + var partiallyBelow = rowMaxY > visibleContentMaxY && rowMinY < visibleContentMaxY; + + return fullyVisible || partiallyAbove || partiallyBelow; + } + + private VisualElement CreateRow(int rowIndex) + { + var rowElement = new VisualElement() { name = $"Row {rowIndex}" }; + rowElement.style.flexDirection = FlexDirection.Row; + rowElement.style.position = Position.Absolute; + rowElement.style.top = ElementHeight * rowIndex; + rowElement.style.width = _scrollView.contentViewport.worldBound.width; + rowElement.style.justifyContent = Justify.SpaceAround; + + return rowElement; + } + + private VisualElement CreateFillerElement() + { + var element = new VisualElement(); + element.style.width = ElementWidth; + element.style.height = ElementHeight; + + return element; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Elements/GridListElement.cs.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/GridListElement.cs.meta new file mode 100644 index 00000000000..bae1ff94303 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/GridListElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 81d9f779e8c2a464cbdc1e39a4864803 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewCollectionElement.cs b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewCollectionElement.cs new file mode 100644 index 00000000000..28a35b18c6f --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewCollectionElement.cs @@ -0,0 +1,116 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Previews.UI.Data; +using System.Linq; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.SceneManagement; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Previews.UI.Elements +{ + internal class PreviewCollectionElement : VisualElement + { + // Data + private IAssetPreviewCollection _collection; + + // UI + private Label _previewCountLabel; + private GridListElement _gridListElement; + + public PreviewCollectionElement(IAssetPreviewCollection collection) + { + AddToClassList("preview-list"); + + _collection = collection; + _collection.OnCollectionChanged += RefreshList; + + Create(); + RefreshList(); + + SubscribeToSceneChanges(); + } + + private void Create() + { + CreateLabel(); + CreateGridListElement(); + } + + private void CreateLabel() + { + _previewCountLabel = new Label(); + _previewCountLabel.style.display = DisplayStyle.None; + Add(_previewCountLabel); + } + + private void CreateGridListElement() + { + _gridListElement = new GridListElement(); + _gridListElement.MakeItem = CreatePreview; + _gridListElement.BindItem = BindPreview; + _gridListElement.ElementWidth = 140 + 10; // Accounting for margin style + _gridListElement.ElementHeight = 160 + 10; // Accounting for margin style + Add(_gridListElement); + } + + private VisualElement CreatePreview() + { + var preview = new AssetPreviewElement(); + return preview; + } + + private void BindPreview(VisualElement element, int index) + { + var previewElement = (AssetPreviewElement)element; + var preview = _collection.GetPreviews().ToList()[index]; + previewElement.SetSource(preview); + } + + private void RefreshList() + { + var type = _collection.GetGenerationType(); + var items = _collection.GetPreviews().ToList(); + _previewCountLabel.text = $"Displaying {items.Count} {ConvertGenerationTypeName(type)} previews"; + _previewCountLabel.style.display = DisplayStyle.Flex; + _previewCountLabel.style.alignSelf = Align.Center; + _previewCountLabel.style.marginBottom = 10; + _previewCountLabel.style.unityFontStyleAndWeight = FontStyle.Bold; + + _gridListElement.ItemSource = items; + _gridListElement.Redraw(); + } + + private string ConvertGenerationTypeName(GenerationType type) + { + switch (type) + { + case GenerationType.Custom: + return "high resolution"; + default: + return type.ToString().ToLower(); + } + } + + private void SubscribeToSceneChanges() + { + var windowToSubscribeTo = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + UnityAction sceneChanged = null; + sceneChanged = new UnityAction((_, __) => RefreshObjects(windowToSubscribeTo)); + EditorSceneManager.activeSceneChangedInEditMode += sceneChanged; + + void RefreshObjects(PreviewGeneratorWindow subscribedWindow) + { + // Remove callback if preview generator window instance changed + var activeWindow = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + if (subscribedWindow == null || subscribedWindow != activeWindow) + { + EditorSceneManager.activeSceneChangedInEditMode -= sceneChanged; + return; + } + + RefreshList(); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewCollectionElement.cs.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewCollectionElement.cs.meta new file mode 100644 index 00000000000..3068d08369c --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewCollectionElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 842a11e046ca5284d9de9f4a05b1fa26 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGenerateButtonElement.cs b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGenerateButtonElement.cs new file mode 100644 index 00000000000..3d2f9a86c0c --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGenerateButtonElement.cs @@ -0,0 +1,50 @@ +using AssetStoreTools.Previews.UI.Data; +using System; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Previews.UI.Elements +{ + internal class PreviewGenerateButtonElement : VisualElement + { + // Data + private IPreviewGeneratorSettings _settings; + + // UI + private Button _generateButton; + + public event Action OnGenerate; + + public PreviewGenerateButtonElement(IPreviewGeneratorSettings settings) + { + _settings = settings; + _settings.OnGenerationPathsChanged += GenerationPathsChanged; + + Create(); + Deserialize(); + } + + private void Create() + { + _generateButton = new Button(Validate) { text = "Generate" }; + _generateButton.AddToClassList("preview-generate-button"); + + Add(_generateButton); + } + + private void Validate() + { + OnGenerate?.Invoke(); + } + + private void GenerationPathsChanged() + { + var inputPathsPresent = _settings.GetGenerationPaths().Count > 0; + _generateButton.SetEnabled(inputPathsPresent); + } + + private void Deserialize() + { + GenerationPathsChanged(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGenerateButtonElement.cs.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGenerateButtonElement.cs.meta new file mode 100644 index 00000000000..1f3a7b4f706 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGenerateButtonElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1c8fbb0b13ba7d3479c0867c440821e6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGeneratorPathsElement.cs b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGeneratorPathsElement.cs new file mode 100644 index 00000000000..cf724404031 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGeneratorPathsElement.cs @@ -0,0 +1,122 @@ +using AssetStoreTools.Previews.UI.Data; +using AssetStoreTools.Utility; +using UnityEditor; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Validator.UI.Elements +{ + internal class PreviewGeneratorPathsElement : VisualElement + { + // Data + private IPreviewGeneratorSettings _settings; + + // UI + private ScrollView _pathBoxScrollView; + + public PreviewGeneratorPathsElement(IPreviewGeneratorSettings settings) + { + AddToClassList("preview-paths"); + + _settings = settings; + _settings.OnGenerationPathsChanged += InputPathsChanged; + + Create(); + Deserialize(); + } + + private void Create() + { + var pathSelectionRow = new VisualElement(); + pathSelectionRow.AddToClassList("preview-settings-selection-row"); + + VisualElement labelHelpRow = new VisualElement(); + labelHelpRow.AddToClassList("preview-settings-selection-label-help-row"); + labelHelpRow.style.alignSelf = Align.FlexStart; + + Label pathLabel = new Label { text = "Input paths" }; + Image pathLabelTooltip = new Image + { + tooltip = "Select the folder (or multiple folders) to generate asset previews for." + }; + + labelHelpRow.Add(pathLabel); + labelHelpRow.Add(pathLabelTooltip); + + var fullPathBox = new VisualElement() { name = "PreviewPaths" }; + fullPathBox.AddToClassList("preview-paths-box"); + + _pathBoxScrollView = new ScrollView { name = "PreviewPathsScrollView" }; + _pathBoxScrollView.AddToClassList("preview-paths-scroll-view"); + + VisualElement scrollViewBottomRow = new VisualElement(); + scrollViewBottomRow.AddToClassList("preview-paths-scroll-view-bottom-row"); + + var addExtraPathsButton = new Button(BrowsePath) { text = "Add a path" }; + addExtraPathsButton.AddToClassList("preview-paths-add-button"); + scrollViewBottomRow.Add(addExtraPathsButton); + + fullPathBox.Add(_pathBoxScrollView); + fullPathBox.Add(scrollViewBottomRow); + + pathSelectionRow.Add(labelHelpRow); + pathSelectionRow.Add(fullPathBox); + + Add(pathSelectionRow); + } + + private VisualElement CreateSinglePathElement(string path) + { + var validationPath = new VisualElement(); + validationPath.AddToClassList("preview-paths-path-row"); + + var folderPathLabel = new Label(path); + folderPathLabel.AddToClassList("preview-paths-path-row-input-field"); + + var removeButton = new Button(() => + { + _settings.RemoveGenerationPath(path); + }); + removeButton.text = "X"; + removeButton.AddToClassList("preview-paths-path-row-remove-button"); + + validationPath.Add(folderPathLabel); + validationPath.Add(removeButton); + + return validationPath; + } + + private void BrowsePath() + { + string absolutePath = EditorUtility.OpenFolderPanel("Select a directory", "Assets", ""); + + if (string.IsNullOrEmpty(absolutePath)) + return; + + var relativePath = FileUtility.AbsolutePathToRelativePath(absolutePath, ASToolsPreferences.Instance.EnableSymlinkSupport); + + if (!_settings.IsGenerationPathValid(relativePath, out var error)) + { + EditorUtility.DisplayDialog("Invalid path", error, "OK"); + return; + } + + _settings.AddGenerationPath(relativePath); + } + + private void InputPathsChanged() + { + var inputPaths = _settings.GetGenerationPaths(); + + _pathBoxScrollView.Clear(); + foreach (var path in inputPaths) + { + _pathBoxScrollView.Add(CreateSinglePathElement(path)); + } + } + + private void Deserialize() + { + InputPathsChanged(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGeneratorPathsElement.cs.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGeneratorPathsElement.cs.meta new file mode 100644 index 00000000000..afdcc933649 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGeneratorPathsElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cd3e3f7fbfc5f1e46835438be2756746 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGeneratorSettingsElement.cs b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGeneratorSettingsElement.cs new file mode 100644 index 00000000000..d5268f5e194 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGeneratorSettingsElement.cs @@ -0,0 +1,99 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Previews.UI.Data; +using AssetStoreTools.Validator.UI.Elements; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Previews.UI.Elements +{ + internal class PreviewGeneratorSettingsElement : VisualElement + { + // Data + private IPreviewGeneratorSettings _settings; + + // UI + private PreviewGeneratorPathsElement _previewPathsElement; + private ToolbarMenu _generationTypeMenu; + + public PreviewGeneratorSettingsElement(IPreviewGeneratorSettings settings) + { + AddToClassList("preview-settings"); + + _settings = settings; + _settings.OnGenerationTypeChanged += GenerationTypeChanged; + + Create(); + Deserialize(); + } + + private void Create() + { + CreateGenerationType(); + CreateInputPathsElement(); + } + + private void CreateInputPathsElement() + { + _previewPathsElement = new PreviewGeneratorPathsElement(_settings); + Add(_previewPathsElement); + } + + private void CreateGenerationType() + { + var typeSelectionBox = new VisualElement(); + typeSelectionBox.AddToClassList("preview-settings-selection-row"); + + VisualElement labelHelpRow = new VisualElement(); + labelHelpRow.AddToClassList("preview-settings-selection-label-help-row"); + + Label generationTypeLabel = new Label { text = "Generation type" }; + Image categoryLabelTooltip = new Image + { + tooltip = "Choose the generation type for your previews.\n\n" + + "- Native: retrieve previews from the Asset Database which are generated by Unity Editor internally\n" + + "- High Resolution (experimental): generate previews using a custom implementation. Resulting previews are of higher resolution " + + "than those generated by Unity Editor. Note that they may look slightly different from native previews" + }; + + labelHelpRow.Add(generationTypeLabel); + labelHelpRow.Add(categoryLabelTooltip); + + _generationTypeMenu = new ToolbarMenu { name = "GenerationTypeMenu" }; + _generationTypeMenu.AddToClassList("preview-settings-selection-dropdown"); + + typeSelectionBox.Add(labelHelpRow); + typeSelectionBox.Add(_generationTypeMenu); + + // Append available categories + var types = _settings.GetAvailableGenerationTypes(); + foreach (var t in types) + { + _generationTypeMenu.menu.AppendAction(ConvertGenerationTypeName(t), _ => _settings.SetGenerationType(t)); + } + + Add(typeSelectionBox); + } + + private string ConvertGenerationTypeName(GenerationType type) + { + switch (type) + { + case GenerationType.Custom: + return "High Resolution (experimental)"; + default: + return type.ToString(); + } + } + + private void GenerationTypeChanged() + { + var t = _settings.GetGenerationType(); + _generationTypeMenu.text = ConvertGenerationTypeName(t); + } + + private void Deserialize() + { + GenerationTypeChanged(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGeneratorSettingsElement.cs.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGeneratorSettingsElement.cs.meta new file mode 100644 index 00000000000..6e1abfb78ca --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewGeneratorSettingsElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6f38de8a438b8c94a81fe5f2cc45c110 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewWindowDescriptionElement.cs b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewWindowDescriptionElement.cs new file mode 100644 index 00000000000..1306f05b022 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewWindowDescriptionElement.cs @@ -0,0 +1,87 @@ +using UnityEngine.UIElements; + +namespace AssetStoreTools.Previews.UI.Elements +{ + internal class PreviewWindowDescriptionElement : VisualElement + { + private const string DescriptionFoldoutText = "Generate and inspect asset preview images to be displayed in your package listing page on the Asset Store."; + private const string DescriptionFoldoutContentText = "Images generated in this window will be reused when exporting a package. Any missing images generated during the package export process will also appear here.\n\n" + + "Preview images are displayed on the Asset Store under the 'Package Content' section of the package listing. " + + "They are also displayed in the package importer window that appears during the package import process. " + + "Note that these images will not replace the images used for the assets in the Project window after the package gets imported."; + + private VisualElement _descriptionSimpleContainer; + private Button _showMoreButton; + + private VisualElement _descriptionFullContainer; + private Button _showLessButton; + + public PreviewWindowDescriptionElement() + { + AddToClassList("asset-preview-description"); + Create(); + } + + private void Create() + { + CreateSimpleDescription(); + CreateFullDescription(); + } + + private void CreateSimpleDescription() + { + _descriptionSimpleContainer = new VisualElement(); + _descriptionSimpleContainer.AddToClassList("asset-preview-description-simple-container"); + + var simpleDescription = new Label(DescriptionFoldoutText); + simpleDescription.AddToClassList("asset-preview-description-simple-label"); + + _showMoreButton = new Button(ToggleFullDescription) { text = "Show more..." }; + _showMoreButton.AddToClassList("asset-preview-description-hyperlink-button"); + _showMoreButton.AddToClassList("asset-preview-description-show-button"); + + _descriptionSimpleContainer.Add(simpleDescription); + _descriptionSimpleContainer.Add(_showMoreButton); + + Add(_descriptionSimpleContainer); + } + + private void CreateFullDescription() + { + _descriptionFullContainer = new VisualElement(); + _descriptionFullContainer.AddToClassList("asset-preview-description-full-container"); + + var validatorDescription = new Label() + { + text = DescriptionFoldoutContentText + }; + validatorDescription.AddToClassList("asset-preview-description-full-label"); + + _showLessButton = new Button(ToggleFullDescription) { text = "Show less..." }; + _showLessButton.AddToClassList("asset-preview-description-hide-button"); + _showLessButton.AddToClassList("asset-preview-description-hyperlink-button"); + + _descriptionFullContainer.Add(validatorDescription); + _descriptionFullContainer.Add(_showLessButton); + + _descriptionFullContainer.style.display = DisplayStyle.None; + Add(_descriptionFullContainer); + } + + private void ToggleFullDescription() + { + var displayFullDescription = _descriptionFullContainer.style.display == DisplayStyle.None; + + if (displayFullDescription) + { + _showMoreButton.style.display = DisplayStyle.None; + _descriptionFullContainer.style.display = DisplayStyle.Flex; + } + else + { + _showMoreButton.style.display = DisplayStyle.Flex; + _descriptionFullContainer.style.display = DisplayStyle.None; + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewWindowDescriptionElement.cs.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewWindowDescriptionElement.cs.meta new file mode 100644 index 00000000000..20f514781d4 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Elements/PreviewWindowDescriptionElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2cab289a87b0ba74f89cb458ff6d44f8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/PreviewGeneratorWindow.cs b/.github/Asset Store Tools/Previews/Scripts/UI/PreviewGeneratorWindow.cs new file mode 100644 index 00000000000..3a2d36f8e2b --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/PreviewGeneratorWindow.cs @@ -0,0 +1,51 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Previews.Services; +using AssetStoreTools.Previews.UI.Views; +using AssetStoreTools.Utility; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Previews.UI +{ + internal class PreviewGeneratorWindow : AssetStoreToolsWindow + { + protected override string WindowTitle => "Preview Generator"; + + private ICachingService _cachingService; + + private PreviewListView _previewListView; + + protected override void Init() + { + minSize = new Vector2(350, 350); + + this.SetAntiAliasing(4); + + VisualElement root = rootVisualElement; + + // Getting a reference to the USS Document and adding stylesheet to the root + root.styleSheets.Add(StyleSelector.PreviewGeneratorWindow.PreviewGeneratorWindowStyle); + root.styleSheets.Add(StyleSelector.PreviewGeneratorWindow.PreviewGeneratorWindowTheme); + + GetServices(); + ConstructWindow(); + } + + private void GetServices() + { + _cachingService = PreviewServiceProvider.Instance.GetService(); + } + + private void ConstructWindow() + { + _previewListView = new PreviewListView(_cachingService); + rootVisualElement.Add(_previewListView); + } + + public void Load(PreviewGenerationSettings settings) + { + _previewListView.LoadSettings(settings); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/PreviewGeneratorWindow.cs.meta b/.github/Asset Store Tools/Previews/Scripts/UI/PreviewGeneratorWindow.cs.meta new file mode 100644 index 00000000000..c6f537eda0b --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/PreviewGeneratorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4cad15de2de8cdc46b48a4b05eac5d78 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Views.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Views.meta new file mode 100644 index 00000000000..598279e5939 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Views.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5e154861b0e2af64b93f6c831e6c0dc2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Views/PreviewListView.cs b/.github/Asset Store Tools/Previews/Scripts/UI/Views/PreviewListView.cs new file mode 100644 index 00000000000..36f526426d4 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Views/PreviewListView.cs @@ -0,0 +1,142 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Previews.Services; +using AssetStoreTools.Previews.UI.Data; +using AssetStoreTools.Previews.UI.Elements; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Previews.UI.Views +{ + internal class PreviewListView : VisualElement + { + //Data + private PreviewDatabase _previewDatabase; + private IPreviewGeneratorSettings _previewGeneratorSettings; + private IAssetPreviewCollection _previewCollection; + + private ICachingService _cachingService; + + // UI + private PreviewWindowDescriptionElement _descriptionElement; + private PreviewGeneratorSettingsElement _settingsElement; + private PreviewGenerateButtonElement _generateButtonElement; + private PreviewCollectionElement _previewCollectionElement; + + public PreviewListView(ICachingService cachingService) + { + _cachingService = cachingService; + + _previewGeneratorSettings = new PreviewGeneratorSettings(); + _previewCollection = new AssetPreviewCollection(); + + _previewGeneratorSettings.OnGenerationTypeChanged += RefreshPreviewList; + _previewGeneratorSettings.OnGenerationPathsChanged += RefreshPreviewList; + + Create(); + RefreshPreviewList(); + } + + private void Create() + { + CreateDescription(); + CreateSettings(); + CreateGenerateButton(); + CreatePreviewList(); + } + + private void CreateDescription() + { + _descriptionElement = new PreviewWindowDescriptionElement(); + Add(_descriptionElement); + } + + private void CreateSettings() + { + _settingsElement = new PreviewGeneratorSettingsElement(_previewGeneratorSettings); + Add(_settingsElement); + } + + private void CreateGenerateButton() + { + _generateButtonElement = new PreviewGenerateButtonElement(_previewGeneratorSettings); + _generateButtonElement.OnGenerate += GeneratePreviews; + Add(_generateButtonElement); + } + + private void CreatePreviewList() + { + _previewCollectionElement = new PreviewCollectionElement(_previewCollection); + Add(_previewCollectionElement); + } + + private async void GeneratePreviews() + { + try + { + _settingsElement.SetEnabled(false); + _generateButtonElement.SetEnabled(false); + _previewCollectionElement.SetEnabled(false); + + var generator = _previewGeneratorSettings.CreateGenerator(); + generator.OnProgressChanged += DisplayProgress; + var result = await generator.Generate(); + generator.OnProgressChanged -= DisplayProgress; + + if (!result.Success) + { + EditorUtility.DisplayDialog("Error", result.Exception.Message, "OK"); + Debug.LogException(result.Exception); + return; + } + + RefreshPreviewList(); + } + finally + { + _settingsElement.SetEnabled(true); + _generateButtonElement.SetEnabled(true); + _previewCollectionElement.SetEnabled(true); + EditorUtility.ClearProgressBar(); + } + } + + private void DisplayProgress(float progress) + { + EditorUtility.DisplayProgressBar("Generating", "Generating previews...", progress); + } + + public void LoadSettings(PreviewGenerationSettings settings) + { + _previewGeneratorSettings.LoadSettings(settings); + } + + private void RefreshPreviewList() + { + if (!_cachingService.GetCachedMetadata(out _previewDatabase)) + _previewDatabase = new PreviewDatabase(); + + var paths = _previewGeneratorSettings.GetGenerationPaths(); + var guids = AssetDatabase.FindAssets("", paths.ToArray()); + var displayedPreviews = new List(); + + foreach (var entry in _previewDatabase.Previews) + { + if (!entry.Exists()) + continue; + + if (entry.Type != _previewGeneratorSettings.GetGenerationType()) + continue; + + if (!guids.Any(x => x == entry.Guid)) + continue; + + displayedPreviews.Add(entry); + } + + _previewCollection.Refresh(_previewGeneratorSettings.GetGenerationType(), displayedPreviews); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/UI/Views/PreviewListView.cs.meta b/.github/Asset Store Tools/Previews/Scripts/UI/Views/PreviewListView.cs.meta new file mode 100644 index 00000000000..7ddbc60360b --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/UI/Views/PreviewListView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 94d417240bb510d469acb8a11f15b277 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Utility.meta b/.github/Asset Store Tools/Previews/Scripts/Utility.meta new file mode 100644 index 00000000000..92fdd4fef2e --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Utility.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 99cf24252c136f246bfa4b02a69fe992 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Utility/GraphicsUtility.cs b/.github/Asset Store Tools/Previews/Scripts/Utility/GraphicsUtility.cs new file mode 100644 index 00000000000..1a304f0aed9 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Utility/GraphicsUtility.cs @@ -0,0 +1,96 @@ +using UnityEngine; + +namespace AssetStoreTools.Previews.Utility +{ + internal static class GraphicsUtility + { + public static Texture2D GetTextureFromCamera(Camera camera, int desiredWidth, int desiredHeight, int desiredDepth) + { + var texture = new Texture2D(desiredWidth, desiredHeight); + var originalRenderTexture = RenderTexture.active; + var renderTexture = RenderTexture.GetTemporary(desiredWidth, desiredHeight, desiredDepth); + var cameraInitiallyEnabled = camera.enabled; + + try + { + if (cameraInitiallyEnabled) + camera.enabled = false; + + camera.targetTexture = renderTexture; + camera.Render(); + + RenderTexture.active = renderTexture; + texture.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0); + texture.Apply(); + } + finally + { + camera.targetTexture = null; + RenderTexture.active = originalRenderTexture; + RenderTexture.ReleaseTemporary(renderTexture); + camera.enabled = cameraInitiallyEnabled; + } + + return texture; + } + + public static Texture2D ResizeTexture(Texture2D source, int desiredWidth, int desiredHeight) + { + var texture = new Texture2D(desiredWidth, desiredHeight); + var originalRenderTexture = RenderTexture.active; + var renderTexture = RenderTexture.GetTemporary(desiredWidth, desiredHeight, 32); + + try + { + RenderTexture.active = renderTexture; + Graphics.Blit(source, renderTexture); + + texture.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0); + texture.Apply(); + } + finally + { + RenderTexture.active = originalRenderTexture; + RenderTexture.ReleaseTemporary(renderTexture); + } + + return texture; + } + + public static Texture2D ResizeTextureNormalMap(Texture2D source, int desiredWidth, int desiredHeight) + { + var texture = new Texture2D(desiredWidth, desiredHeight); + var originalRenderTexture = RenderTexture.active; + var renderTexture = RenderTexture.GetTemporary(desiredWidth, desiredHeight, 32, RenderTextureFormat.Default, RenderTextureReadWrite.Linear); + + try + { + RenderTexture.active = renderTexture; + Graphics.Blit(source, renderTexture); + + texture.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0); + + for (int i = 0; i < texture.width; i++) + { + for (int j = 0; j < texture.height; j++) + { + var color = texture.GetPixel(i, j); + color.b = color.r; + color.r = color.a; + color.a = 1; + texture.SetPixel(i, j, color); + } + } + + texture.Apply(); + } + finally + { + RenderTexture.active = originalRenderTexture; + RenderTexture.ReleaseTemporary(renderTexture); + } + + return texture; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Utility/GraphicsUtility.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Utility/GraphicsUtility.cs.meta new file mode 100644 index 00000000000..04602e6a9d6 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Utility/GraphicsUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f0a4fc8f266b4dd41a59693dd581e232 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Utility/PreviewConvertUtility.cs b/.github/Asset Store Tools/Previews/Scripts/Utility/PreviewConvertUtility.cs new file mode 100644 index 00000000000..ab93f979b8b --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Utility/PreviewConvertUtility.cs @@ -0,0 +1,72 @@ +using AssetStoreTools.Previews.Data; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Previews.Utility +{ + internal static class PreviewConvertUtility + { + public static string ConvertFilename(Object asset, FileNameFormat format) + { + string fileName = string.Empty; + + switch (format) + { + case FileNameFormat.Guid: + AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out var guid, out long _); + fileName = guid; + break; + case FileNameFormat.FullAssetPath: + var assetPath = AssetDatabase.GetAssetPath(asset); + + if (assetPath.StartsWith("Assets/")) + fileName = assetPath.Substring("Assets/".Length); + else if (assetPath.StartsWith("Packages/")) + fileName = assetPath.Substring("Packages/".Length); + + fileName = fileName.Replace("/", "_"); + break; + case FileNameFormat.AssetName: + fileName = asset.name; + break; + default: + throw new System.Exception("Undefined format"); + } + + return fileName; + } + + public static string ConvertExtension(PreviewFormat format) + { + switch (format) + { + case PreviewFormat.JPG: + return "jpg"; + case PreviewFormat.PNG: + return "png"; + default: + throw new System.Exception("Undefined format"); + } + } + + public static string ConvertFilenameWithExtension(Object asset, FileNameFormat nameFormat, PreviewFormat imageFormat) + { + var filename = ConvertFilename(asset, nameFormat); + var extension = ConvertExtension(imageFormat); + return $"{filename}.{extension}"; + } + + public static byte[] ConvertTexture(Texture2D texture, PreviewFormat format) + { + switch (format) + { + case PreviewFormat.JPG: + return texture.EncodeToJPG(); + case PreviewFormat.PNG: + return texture.EncodeToPNG(); + default: + throw new System.Exception("Undefined format"); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Utility/PreviewConvertUtility.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Utility/PreviewConvertUtility.cs.meta new file mode 100644 index 00000000000..241997ddbc8 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Utility/PreviewConvertUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 700eaf82299628d44853599774664bea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Utility/PreviewSceneUtility.cs b/.github/Asset Store Tools/Previews/Scripts/Utility/PreviewSceneUtility.cs new file mode 100644 index 00000000000..8ca2b3a1aab --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Utility/PreviewSceneUtility.cs @@ -0,0 +1,196 @@ +using System; +using System.Threading.Tasks; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +#if AST_URP_AVAILABLE +using UnityEngine.Rendering.Universal; +#endif +#if AST_HDRP_AVAILABLE +using UnityEngine.Rendering; +using UnityEngine.Rendering.HighDefinition; +#endif + +namespace AssetStoreTools.Previews.Utility +{ + internal static class PreviewSceneUtility + { + private const string PreviewSceneName = "Preview Generation In Progress"; + private static readonly Color BackgroundColor = new Color(82f / 255, 82f / 255, 82f / 255); + private static readonly Color BackgroundColorHDRP = new Color(38f / 255, 38f / 255, 38f / 255); + + public static async Task OpenPreviewSceneForCurrentPipeline() + { + // Wait for an Editor frame to avoid recursive player loop internal errors + await WaitForEditorUpdate(); + + switch (RenderPipelineUtility.GetCurrentPipeline()) + { + case RenderPipeline.BiRP: + await OpenPreviewSceneBiRP(); + break; +#if AST_URP_AVAILABLE + case RenderPipeline.URP: + await OpenPreviewSceneURP(); + break; +#endif +#if AST_HDRP_AVAILABLE + case RenderPipeline.HDRP: + await OpenPreviewSceneHDRP(); + break; +#endif + default: + throw new NotImplementedException("Undefined Render Pipeline"); + } + } + + private static async Task WaitForEditorUpdate() + { + var updateCalled = false; + var delayCalled = false; + + void Update() + { + EditorApplication.update -= Update; + updateCalled = true; + } + + EditorApplication.update += Update; + while (!updateCalled) + await Task.Delay(10); + + void DelayCall() + { + EditorApplication.delayCall -= DelayCall; + delayCalled = true; + } + + EditorApplication.delayCall += DelayCall; + while (!delayCalled) + await Task.Delay(10); + } + + public static async Task OpenPreviewSceneBiRP() + { + OpenNewScene(); + + CreateSceneCamera(); + CreateSceneLighting(); + + await WaitForLighting(); + } + + private static void OpenNewScene() + { + EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo(); + var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); + scene.name = PreviewSceneName; + } + + private static Camera CreateSceneCamera() + { + var cameraGO = new GameObject() { name = "Camera" }; + var camera = cameraGO.AddComponent(); + camera.enabled = false; + camera.tag = "MainCamera"; + + camera.nearClipPlane = 0.01f; + camera.farClipPlane = 100000; + camera.clearFlags = CameraClearFlags.SolidColor; + camera.backgroundColor = BackgroundColor; + + return camera; + } + + private static Light CreateSceneLighting() + { + var lightGO = new GameObject() { name = "Lights" }; + lightGO.transform.rotation = Quaternion.Euler(45, 225, 0); + var light = lightGO.AddComponent(); + light.intensity = 0.75f; + light.type = LightType.Directional; + light.shadows = LightShadows.None; + + return light; + } + + private static async Task WaitForLighting() + { + while (!DynamicGI.isConverged) + await Task.Delay(100); + + await Task.Yield(); + } + +#if AST_URP_AVAILABLE + public static async Task OpenPreviewSceneURP() + { + OpenNewScene(); + + var camera = CreateSceneCamera(); + camera.gameObject.AddComponent(); + + var lighting = CreateSceneLighting(); + lighting.intensity = 0.5f; + lighting.gameObject.AddComponent(); + + await WaitForLighting(); + } +#endif + +#if AST_HDRP_AVAILABLE + public static async Task OpenPreviewSceneHDRP() + { + OpenNewScene(); + + var camera = CreateSceneCamera(); + var cameraData = camera.gameObject.AddComponent(); + cameraData.clearColorMode = HDAdditionalCameraData.ClearColorMode.Color; + cameraData.backgroundColorHDR = BackgroundColorHDRP; + + var light = CreateSceneLighting(); + var lightData = light.gameObject.AddComponent(); + lightData.SetIntensity(5000, LightUnit.Lux); + + CreateHDRPVolumeProfile(); + + await WaitForLighting(); + } + + private static Volume CreateHDRPVolumeProfile() + { + var volumeGO = new GameObject() { name = "Volume" }; + var volume = volumeGO.gameObject.AddComponent(); + + var profile = VolumeProfile.CreateInstance(); + volume.profile = profile; + volume.isGlobal = true; + + var exposure = profile.Add(); + exposure.active = true; + + exposure.mode.overrideState = true; + exposure.mode.value = ExposureMode.Fixed; + + exposure.fixedExposure.overrideState = true; + exposure.fixedExposure.value = 11; + + var fog = profile.Add(); + fog.active = true; + + fog.enabled.overrideState = true; + fog.enabled.value = false; + +#if AST_HDRP_AVAILABLE_V12 + var volumetricClouds = profile.Add(); + volumetricClouds.active = true; + + volumetricClouds.enable.overrideState = true; + volumetricClouds.enable.value = false; +#endif + + return volume; + } +#endif + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Utility/PreviewSceneUtility.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Utility/PreviewSceneUtility.cs.meta new file mode 100644 index 00000000000..9f0056a3f82 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Utility/PreviewSceneUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 63fa5650920e7914dae6fe76badac249 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Utility/RenderPipeline.cs b/.github/Asset Store Tools/Previews/Scripts/Utility/RenderPipeline.cs new file mode 100644 index 00000000000..028f96db2b3 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Utility/RenderPipeline.cs @@ -0,0 +1,10 @@ +namespace AssetStoreTools.Previews.Utility +{ + internal enum RenderPipeline + { + Unknown = 0, + BiRP = 1, + URP = 2, + HDRP = 3 + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Utility/RenderPipeline.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Utility/RenderPipeline.cs.meta new file mode 100644 index 00000000000..1d404705a13 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Utility/RenderPipeline.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c43c7ce2b9090ab49bb8944bc6bdb3c7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Scripts/Utility/RenderPipelineUtility.cs b/.github/Asset Store Tools/Previews/Scripts/Utility/RenderPipelineUtility.cs new file mode 100644 index 00000000000..c8e79d5293e --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Utility/RenderPipelineUtility.cs @@ -0,0 +1,32 @@ +using UnityEngine.Rendering; +#if AST_URP_AVAILABLE +using UnityEngine.Rendering.Universal; +#endif +#if AST_HDRP_AVAILABLE +using UnityEngine.Rendering.HighDefinition; +#endif + +namespace AssetStoreTools.Previews.Utility +{ + internal static class RenderPipelineUtility + { + public static RenderPipeline GetCurrentPipeline() + { + var currentPipelineAsset = GraphicsSettings.currentRenderPipeline; + if (currentPipelineAsset == null) + return RenderPipeline.BiRP; + +#if AST_URP_AVAILABLE + if (currentPipelineAsset is UniversalRenderPipelineAsset) + return RenderPipeline.URP; +#endif + +#if AST_HDRP_AVAILABLE + if (currentPipelineAsset is HDRenderPipelineAsset) + return RenderPipeline.HDRP; +#endif + + return RenderPipeline.Unknown; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Scripts/Utility/RenderPipelineUtility.cs.meta b/.github/Asset Store Tools/Previews/Scripts/Utility/RenderPipelineUtility.cs.meta new file mode 100644 index 00000000000..4148f8a0626 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Scripts/Utility/RenderPipelineUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5e42bdf53cd8b51429b10a6742ec5272 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Styles.meta b/.github/Asset Store Tools/Previews/Styles.meta new file mode 100644 index 00000000000..50a00183825 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Styles.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 70d30555bce30014a9143c3d003105bf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Previews/Styles/Style.uss b/.github/Asset Store Tools/Previews/Styles/Style.uss new file mode 100644 index 00000000000..c363a3d6280 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Styles/Style.uss @@ -0,0 +1,210 @@ +/* Asset Preview Description */ + +.asset-preview-description { + flex-direction: column; + flex-shrink: 0; + + margin: 10px 5px 2px 5px; + padding: 2px 4px; +} + +.asset-preview-description-simple-container { + flex-direction: column; + flex-wrap: wrap; +} + +.asset-preview-description-simple-label { + white-space: normal; +} + +.asset-preview-description-hyperlink-button { + margin: 0; + padding: 0; + + align-self: flex-start; + cursor: link; +} + +.asset-preview-description-show-button { + margin-top: 12px; +} + +.asset-preview-description-hide-button { + margin-top: 12px; +} + +.asset-preview-description-full-container { + margin-top: 12px; +} + +.asset-preview-description-full-label { + white-space: normal; +} + +/* Asset Preview Settings */ + +.preview-settings { + flex-direction: column; + flex-shrink: 0; + + margin: 0px 5px 2px 5px; + padding: 2px 4px; +} + +.preview-settings-selection-row { + flex-direction: row; + flex-grow: 1; + + margin-top: 10px; + padding: 0 3px 0 2px; +} + +.preview-settings-selection-label-help-row { + flex-direction: row; + flex-shrink: 1; + flex-grow: 0; + + align-self: center; + align-items: center; + justify-content: flex-start; + + width: 120px; +} + +.preview-settings-selection-label-help-row > Label { + -unity-font-style: bold; +} + +.preview-settings-selection-label-help-row > Image { + height: 16px; + width: 16px; +} + +.preview-settings-selection-dropdown { + flex-grow: 1; + flex-shrink: 1; + + align-self: stretch; + + margin-right: 0; + margin-left: 3px; + padding: 1px 4px; +} + +/* Preview Paths */ + +.preview-paths { + flex-direction: column; + flex-grow: 1; + flex-shrink: 0; + + margin-bottom: 10px; + padding: 0; +} + +.preview-paths-box { + flex-grow: 1; + flex-direction: column; +} + +.preview-paths-scroll-view { + flex-grow: 1; + height: 100px; + margin-left: 3px; +} + +.preview-paths-scroll-view > .unity-scroll-view__content-viewport +{ + margin-left: 1px; +} + +.preview-paths-scroll-view > * > .unity-scroll-view__content-container +{ + padding: 0 0 0 0; +} + +.preview-paths-scroll-view > * > .unity-scroll-view__vertical-scroller +{ + margin: -1px 0; +} + +.preview-paths-scroll-view-bottom-row { + flex-direction: row-reverse; + margin: -1px 0 0 3px; +} + +.preview-paths-add-button { + margin: 3px 0 0 0; + align-self: center; +} + +.preview-paths-path-row { + flex-direction: row; + flex-grow: 1; + + margin-top: 2px; + padding: 0 5px 0 2px; +} + +.preview-paths-path-row-input-field { + flex-grow: 1; + flex-shrink: 1; + + padding-left: 5px; + + white-space: normal; + -unity-text-align: middle-left; +} + +.preview-paths-path-row-remove-button { + width: 20px; + height: 20px; + margin-left: 2px; + margin-right: 1px; + padding: 1px 0 0 0; +} + +/* Generate Button */ + +.preview-generate-button { + align-self: stretch; + + height: 25px; + margin-left: 2px; +} + +/* Asset Preview List Element */ + +.preview-list { + margin-top: 10px; + flex-grow: 1; +} + +.preview-list-image { + width: 140px; + height: 160px; + margin: 5px; + padding: 5px; + justify-content: space-between; +} + +.preview-list-image:hover{ + background-color: #444444; +} + +.preview-list-image > Image { + flex-shrink: 0; + max-width: 100%; + max-height: 100%; + -unity-background-scale-mode: scale-to-fit; + align-self: center; +} + +.preview-list-image > Label { + align-self: center; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 2px; + padding: 0; + -unity-text-align: middle-center; +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Styles/Style.uss.meta b/.github/Asset Store Tools/Previews/Styles/Style.uss.meta new file mode 100644 index 00000000000..4b97f84a0e6 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Styles/Style.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 095b74bd60b187c418dcc4cd47aa696d +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Previews/Styles/ThemeDark.uss b/.github/Asset Store Tools/Previews/Styles/ThemeDark.uss new file mode 100644 index 00000000000..6dff0ddc3fe --- /dev/null +++ b/.github/Asset Store Tools/Previews/Styles/ThemeDark.uss @@ -0,0 +1,67 @@ +.primary-colors { + /* Light - lighter */ + background-color: rgb(220, 220, 220); + /* Light - middle */ + background-color: rgb(200, 200, 200); + /* Light - darker */ + background-color: rgb(180, 180, 180); + + /* Dark - lighter */ + background-color: rgb(78, 78, 78); + /* Dark - middle */ + background-color: rgb(68, 68, 68); + /* Dark - darker */ + background-color: rgb(58, 58, 58); + + /* Border color - light */ + border-color: rgb(200, 200, 200); + /* Border color - dark */ + border-color: rgb(33, 33, 33); +} + +/* Asset Preview Description */ + +.asset-preview-description-hyperlink-button { + color: rgb(68, 113, 229); + border-width: 0; + background-color: rgba(0, 0, 0, 0); +} + +.asset-preview-description-hyperlink-button:hover { + color: rgb(68, 133, 229); +} + +.asset-preview-description-hyperlink-button:active { + color: rgb(68, 93, 229); +} + +/* Asset Preview Settings */ + +.preview-settings-selection-label-help-row > Image { + --unity-image: resource("d__Help@2x"); +} + +.preview-settings-selection-dropdown { + color: rgb(238, 238, 238); + background-color: rgb(88, 88, 88); + + border-width: 1px; + border-radius: 3px; + border-color: rgb(36, 36, 36); +} + +/* Preview Paths */ + +.preview-paths-scroll-view { + border-width: 1px; + border-color: rgb(33, 33, 33); + background-color: rgb(58, 58, 58); +} + +.preview-paths-scroll-view > * > .unity-scroll-view__vertical-scroller { + border-right-width: 0; +} + +.preview-paths-path-row-input-field:hover { + background-color: rgb(78, 78, 78); +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Styles/ThemeDark.uss.meta b/.github/Asset Store Tools/Previews/Styles/ThemeDark.uss.meta new file mode 100644 index 00000000000..cbc87a7980d --- /dev/null +++ b/.github/Asset Store Tools/Previews/Styles/ThemeDark.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1c04ee69303d45644bb3971a4e8ce952 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Previews/Styles/ThemeLight.uss b/.github/Asset Store Tools/Previews/Styles/ThemeLight.uss new file mode 100644 index 00000000000..b98e1cd4f29 --- /dev/null +++ b/.github/Asset Store Tools/Previews/Styles/ThemeLight.uss @@ -0,0 +1,67 @@ +.primary-colors { + /* Light - lighter */ + background-color: rgb(220, 220, 220); + /* Light - middle */ + background-color: rgb(200, 200, 200); + /* Light - darker */ + background-color: rgb(180, 180, 180); + + /* Dark - lighter */ + background-color: rgb(50, 50, 50); + /* Dark - middle */ + background-color: rgb(28, 28, 28); + /* Dark - darker */ + background-color: rgb(0, 0, 0); + + /* Border color - light */ + border-color: rgb(200, 200, 200); + /* Border color - dark */ + border-color: rgb(33, 33, 33); +} + +/* Asset Preview Description */ + +.asset-preview-description-hyperlink-button { + color: rgb(68, 113, 229); + border-width: 0; + background-color: rgba(0, 0, 0, 0); +} + +.asset-preview-description-hyperlink-button:hover { + color: rgb(68, 133, 229); +} + +.asset-preview-description-hyperlink-button:active { + color: rgb(68, 93, 229); +} + +/* Asset Preview Settings */ + +.preview-settings-selection-label-help-row > Image { + --unity-image: resource("_Help@2x"); +} + +.preview-settings-selection-dropdown { + color: rgb(9, 9, 9); + background-color: rgb(228, 228, 228); + + border-width: 1px; + border-radius: 3px; + border-color: rgb(178, 178, 178); +} + +/* Preview Paths */ + +.preview-paths-scroll-view { + border-width: 1px; + border-color: rgb(33, 33, 33); + background-color: rgb(180, 180, 180); +} + +.preview-paths-scroll-view > * > .unity-scroll-view__vertical-scroller { + border-right-width: 0; +} + +.preview-paths-path-row-input-field:hover { + background-color: rgb(200, 200, 200); +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Previews/Styles/ThemeLight.uss.meta b/.github/Asset Store Tools/Previews/Styles/ThemeLight.uss.meta new file mode 100644 index 00000000000..cbafb32732c --- /dev/null +++ b/.github/Asset Store Tools/Previews/Styles/ThemeLight.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 38ae9e6ef965cae43902ba22967938ee +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Unity.AssetStoreTools.Editor.asmdef b/.github/Asset Store Tools/Unity.AssetStoreTools.Editor.asmdef new file mode 100644 index 00000000000..b0ba405b7e8 --- /dev/null +++ b/.github/Asset Store Tools/Unity.AssetStoreTools.Editor.asmdef @@ -0,0 +1,36 @@ +{ + "name": "asset-store-tools-editor", + "rootNamespace": "", + "references": [ + "Unity.RenderPipelines.Universal.Runtime", + "Unity.RenderPipelines.Core.Runtime", + "Unity.RenderPipelines.HighDefinition.Runtime" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [ + { + "name": "com.unity.render-pipelines.universal", + "expression": "1.0.0", + "define": "AST_URP_AVAILABLE" + }, + { + "name": "com.unity.render-pipelines.high-definition", + "expression": "1.0.0", + "define": "AST_HDRP_AVAILABLE" + }, + { + "name": "com.unity.render-pipelines.high-definition", + "expression": "12.0.0", + "define": "AST_HDRP_AVAILABLE_V12" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Unity.AssetStoreTools.Editor.asmdef.meta b/.github/Asset Store Tools/Unity.AssetStoreTools.Editor.asmdef.meta new file mode 100644 index 00000000000..2f67bb9e0c1 --- /dev/null +++ b/.github/Asset Store Tools/Unity.AssetStoreTools.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c183be512f4485d40a3437fabd6c81cf +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader.meta b/.github/Asset Store Tools/Uploader.meta new file mode 100644 index 00000000000..b4b8661426b --- /dev/null +++ b/.github/Asset Store Tools/Uploader.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9722d52df16aab742b26fe301782c74c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Icons.meta b/.github/Asset Store Tools/Uploader/Icons.meta new file mode 100644 index 00000000000..7026063d3c9 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Icons.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ab9d0e254817f4f4589a6a378d77babc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Icons/account-dark.png b/.github/Asset Store Tools/Uploader/Icons/account-dark.png new file mode 100644 index 00000000000..f54f1a23228 Binary files /dev/null and b/.github/Asset Store Tools/Uploader/Icons/account-dark.png differ diff --git a/.github/Asset Store Tools/Uploader/Icons/account-dark.png.meta b/.github/Asset Store Tools/Uploader/Icons/account-dark.png.meta new file mode 100644 index 00000000000..6a7e9d8eda1 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Icons/account-dark.png.meta @@ -0,0 +1,123 @@ +fileFormatVersion: 2 +guid: 92f8a779a7c786a4f87ed8e1b36a66b3 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Icons/account-light.png b/.github/Asset Store Tools/Uploader/Icons/account-light.png new file mode 100644 index 00000000000..d6684e515f1 Binary files /dev/null and b/.github/Asset Store Tools/Uploader/Icons/account-light.png differ diff --git a/.github/Asset Store Tools/Uploader/Icons/account-light.png.meta b/.github/Asset Store Tools/Uploader/Icons/account-light.png.meta new file mode 100644 index 00000000000..0ff13ea0713 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Icons/account-light.png.meta @@ -0,0 +1,123 @@ +fileFormatVersion: 2 +guid: 7c0661b9a6385a3488c075711f368cf4 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Icons/open-in-browser.png b/.github/Asset Store Tools/Uploader/Icons/open-in-browser.png new file mode 100644 index 00000000000..245875b4df6 Binary files /dev/null and b/.github/Asset Store Tools/Uploader/Icons/open-in-browser.png differ diff --git a/.github/Asset Store Tools/Uploader/Icons/open-in-browser.png.meta b/.github/Asset Store Tools/Uploader/Icons/open-in-browser.png.meta new file mode 100644 index 00000000000..26ccaa5a259 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Icons/open-in-browser.png.meta @@ -0,0 +1,147 @@ +fileFormatVersion: 2 +guid: e7df43612bbf44d4692de879c751902a +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 2 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Icons/publisher-portal-dark.png b/.github/Asset Store Tools/Uploader/Icons/publisher-portal-dark.png new file mode 100644 index 00000000000..70f47037898 Binary files /dev/null and b/.github/Asset Store Tools/Uploader/Icons/publisher-portal-dark.png differ diff --git a/.github/Asset Store Tools/Uploader/Icons/publisher-portal-dark.png.meta b/.github/Asset Store Tools/Uploader/Icons/publisher-portal-dark.png.meta new file mode 100644 index 00000000000..a0f13697c28 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Icons/publisher-portal-dark.png.meta @@ -0,0 +1,128 @@ +fileFormatVersion: 2 +guid: 003e2710f9b29d94c87632022a3c7c48 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 18 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 2 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 2 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 2 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 2 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Icons/publisher-portal-light.png b/.github/Asset Store Tools/Uploader/Icons/publisher-portal-light.png new file mode 100644 index 00000000000..621e906e335 Binary files /dev/null and b/.github/Asset Store Tools/Uploader/Icons/publisher-portal-light.png differ diff --git a/.github/Asset Store Tools/Uploader/Icons/publisher-portal-light.png.meta b/.github/Asset Store Tools/Uploader/Icons/publisher-portal-light.png.meta new file mode 100644 index 00000000000..2f19af226e8 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Icons/publisher-portal-light.png.meta @@ -0,0 +1,135 @@ +fileFormatVersion: 2 +guid: 8e0749dce5b14cc46b73b0303375c162 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts.meta b/.github/Asset Store Tools/Uploader/Scripts.meta new file mode 100644 index 00000000000..63c6efc3d7e --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 15b24ad8f9d236249910fb8eef1e30ea +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data.meta b/.github/Asset Store Tools/Uploader/Scripts/Data.meta new file mode 100644 index 00000000000..1382bf8a923 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 930cfc857321b1048bf9607d4c8f89d3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions.meta new file mode 100644 index 00000000000..73728d0f16b --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 771776e4d51c47945b3449d4de948c00 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackage.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackage.cs new file mode 100644 index 00000000000..84033b51caa --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackage.cs @@ -0,0 +1,34 @@ +using System; +using UnityEngine; +using PackageModel = AssetStoreTools.Api.Models.Package; + +namespace AssetStoreTools.Uploader.Data +{ + internal interface IPackage + { + string PackageId { get; } + string VersionId { get; } + string Name { get; } + string Status { get; } + string Category { get; } + bool IsCompleteProject { get; } + string RootGuid { get; } + string RootPath { get; } + string ProjectPath { get; } + string Modified { get; } + string Size { get; } + bool IsDraft { get; } + Texture2D Icon { get; } + + event Action OnUpdate; + event Action OnIconUpdate; + + string FormattedSize(); + string FormattedModified(); + + void UpdateData(PackageModel source); + void UpdateIcon(Texture2D texture); + + PackageModel ToModel(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackage.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackage.cs.meta new file mode 100644 index 00000000000..f0a0de39fb0 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b92f2ed98d0b31a479aa2bfd95528fbd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackageContent.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackageContent.cs new file mode 100644 index 00000000000..d82520f8465 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackageContent.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace AssetStoreTools.Uploader.Data +{ + internal interface IPackageContent + { + event Action OnActiveWorkflowChanged; + + IWorkflow GetActiveWorkflow(); + List GetAvailableWorkflows(); + void SetActiveWorkflow(IWorkflow workflow); + } +} diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackageContent.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackageContent.cs.meta new file mode 100644 index 00000000000..3d538b857cc --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackageContent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 45ce41158c3174149b7056a30ac901db +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackageGroup.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackageGroup.cs new file mode 100644 index 00000000000..c35936b25e4 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackageGroup.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace AssetStoreTools.Uploader.Data +{ + internal interface IPackageGroup + { + string Name { get; } + List Packages { get; } + + event Action> OnPackagesSorted; + event Action> OnPackagesFiltered; + + void Sort(PackageSorting sortingType); + void Filter(string filter); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackageGroup.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackageGroup.cs.meta new file mode 100644 index 00000000000..f2475400601 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IPackageGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f683845071b8891498156d95a1a5c2dd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IWorkflow.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IWorkflow.cs new file mode 100644 index 00000000000..c6e9b813d0c --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IWorkflow.cs @@ -0,0 +1,36 @@ +using AssetStoreTools.Api; +using AssetStoreTools.Api.Responses; +using AssetStoreTools.Exporter; +using AssetStoreTools.Validator.Data; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AssetStoreTools.Uploader.Data +{ + internal interface IWorkflow + { + string Name { get; } + string DisplayName { get; } + string PackageName { get; } + string PackageExtension { get; } + bool IsPathSet { get; } + + event Action OnChanged; + event Action OnUploadStateChanged; + + bool GenerateHighQualityPreviews { get; set; } + ValidationSettings LastValidationSettings { get; } + ValidationResult LastValidationResult { get; } + + IEnumerable GetAllPaths(); + ValidationResult Validate(); + Task ExportPackage(string outputPath); + Task ValidatePackageUploadedVersions(); + + Task UploadPackage(string exportedPackagePath); + void AbortUpload(); + void ResetUploadStatus(); + Task RefreshPackage(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IWorkflow.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IWorkflow.cs.meta new file mode 100644 index 00000000000..6efa2810565 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IWorkflow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a2f796eadafa774bae89cf3939611dd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IWorkflowServices.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IWorkflowServices.cs new file mode 100644 index 00000000000..6987b911dc9 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IWorkflowServices.cs @@ -0,0 +1,18 @@ +using AssetStoreTools.Api; +using AssetStoreTools.Api.Responses; +using AssetStoreTools.Uploader.Services.Analytics.Data; +using System; +using System.Threading.Tasks; +using UnityEngine.Analytics; + +namespace AssetStoreTools.Uploader.Data +{ + internal interface IWorkflowServices + { + Task GetPackageUploadedVersions(IPackage package, int timeoutMs); + Task UploadPackage(IPackageUploader uploader, IProgress progress); + void StopUploading(IPackageUploader uploader); + AnalyticsResult SendAnalytic(IAssetStoreAnalytic data); + Task UpdatePackageData(IPackage package); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IWorkflowServices.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IWorkflowServices.cs.meta new file mode 100644 index 00000000000..70b77bed47c --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/IWorkflowServices.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0ae017363fa41ff4d9926dc4a5852246 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/WorkflowBase.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/WorkflowBase.cs new file mode 100644 index 00000000000..cd3462af4cf --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/WorkflowBase.cs @@ -0,0 +1,253 @@ +using AssetStoreTools.Api; +using AssetStoreTools.Api.Responses; +using AssetStoreTools.Exporter; +using AssetStoreTools.Previews; +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Previews.Generators; +using AssetStoreTools.Uploader.Services.Analytics.Data; +using AssetStoreTools.Utility; +using AssetStoreTools.Validator; +using AssetStoreTools.Validator.Data; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using UnityEngine; + +namespace AssetStoreTools.Uploader.Data +{ + internal abstract class WorkflowBase : IWorkflow + { + protected IPackage Package; + + public abstract string Name { get; } + public abstract string DisplayName { get; } + public string PackageName => Package.Name; + public abstract string PackageExtension { get; } + public abstract bool IsPathSet { get; } + + protected string LocalPackageGuid; + protected string LocalPackagePath; + protected string LocalProjectPath; + + public bool GenerateHighQualityPreviews { get; set; } + public ValidationSettings LastValidationSettings { get; private set; } + public ValidationResult LastValidationResult { get; private set; } + + private IWorkflowServices _services; + private IPackageUploader _activeUploader; + + public abstract event Action OnChanged; + public event Action OnUploadStateChanged; + + public WorkflowBase(IPackage package, IWorkflowServices services) + { + Package = package; + _services = services; + } + + public abstract IEnumerable GetAllPaths(); + + public abstract IValidator CreateValidator(); + + public ValidationResult Validate() + { + var validator = CreateValidator(); + var result = CreateValidator().Validate(); + + LastValidationSettings = validator.Settings; + LastValidationResult = result; + + return result; + } + + protected IPreviewGenerator CreatePreviewGenerator(List inputPaths) + { + PreviewGenerationSettings settings; + IPreviewGenerator generator; + + // Filter out ProjectSettings + inputPaths = inputPaths.Where(x => x == "Assets" || x.StartsWith("Assets/") || x.StartsWith("Packages/")).ToList(); + + if (!GenerateHighQualityPreviews) + { + settings = new NativePreviewGenerationSettings() + { + InputPaths = inputPaths.ToArray(), + OverwriteExisting = false, + OutputPath = Constants.Previews.Native.DefaultOutputPath, + Format = Constants.Previews.Native.DefaultFormat, + PreviewFileNamingFormat = Constants.Previews.DefaultFileNameFormat, + WaitForPreviews = Constants.Previews.Native.DefaultWaitForPreviews, + ChunkedPreviewLoading = Constants.Previews.Native.DefaultChunkedPreviewLoading, + ChunkSize = Constants.Previews.Native.DefaultChunkSize + }; + + generator = new NativePreviewGenerator((NativePreviewGenerationSettings)settings); + } + else + { + settings = new CustomPreviewGenerationSettings() + { + InputPaths = inputPaths.ToArray(), + OverwriteExisting = false, + Width = Constants.Previews.Custom.DefaultWidth, + Height = Constants.Previews.Custom.DefaultHeight, + Depth = Constants.Previews.Custom.DefaultDepth, + NativeWidth = Constants.Previews.Custom.DefaultNativeWidth, + NativeHeight = Constants.Previews.Custom.DefaultNativeHeight, + OutputPath = Constants.Previews.Custom.DefaultOutputPath, + Format = Constants.Previews.Custom.DefaultFormat, + PreviewFileNamingFormat = Constants.Previews.DefaultFileNameFormat, + AudioSampleColor = Constants.Previews.Custom.DefaultAudioSampleColor, + AudioBackgroundColor = Constants.Previews.Custom.DefaultAudioBackgroundColor, + }; + + generator = new CustomPreviewGenerator((CustomPreviewGenerationSettings)settings); + } + + return generator; + } + + public abstract IPackageExporter CreateExporter(string outputPath); + + public virtual async Task ExportPackage(string outputPath) + { + var exporter = CreateExporter(outputPath); + var result = await exporter.Export(); + return result; + } + + public async Task ValidatePackageUploadedVersions() + { + var unityVersionSupported = string.Compare(Application.unityVersion, Constants.Uploader.MinRequiredUnitySupportVersion, StringComparison.Ordinal) >= 0; + if (unityVersionSupported) + return true; + + var response = await _services.GetPackageUploadedVersions(Package, 5000); + if (response.Cancelled || response.Success == false) + return true; + + return response.UnityVersions.Any(x => string.Compare(x, Constants.Uploader.MinRequiredUnitySupportVersion, StringComparison.Ordinal) >= 0); + } + + private bool ValidatePackageBeforeUpload(string packagePath, out string error) + { + error = string.Empty; + + if (!File.Exists(packagePath)) + { + error = $"File '{packagePath}' was not found."; + return false; + } + + if (!ValidatePackageSize(packagePath, out error)) + { + return false; + } + + return true; + } + + private bool ValidatePackageSize(string packagePath, out string error) + { + error = string.Empty; + + long packageSize = new FileInfo(packagePath).Length; + long packageSizeLimit = Constants.Uploader.MaxPackageSizeBytes; + + if (packageSize <= packageSizeLimit) + return true; + + float packageSizeInGB = packageSize / (float)1073741824; // (1024 * 1024 * 1024) + float maxPackageSizeInGB = packageSizeLimit / (float)1073741824; + error = $"The size of your package ({packageSizeInGB:0.0} GB) exceeds the maximum allowed package size of {maxPackageSizeInGB:0} GB. " + + $"Please reduce the size of your package."; + + return false; + } + + public async Task UploadPackage(string packagePath) + { + if (!ValidatePackageBeforeUpload(packagePath, out var error)) + { + return new PackageUploadResponse() { Success = false, Status = UploadStatus.Fail, Exception = new Exception(error) }; + } + + _activeUploader = CreatePackageUploader(packagePath); + var progress = new Progress(); + + var time = System.Diagnostics.Stopwatch.StartNew(); + + progress.ProgressChanged += ReportUploadProgress; + var response = await _services.UploadPackage(_activeUploader, progress); + progress.ProgressChanged -= ReportUploadProgress; + + // Send analytics + time.Stop(); + if (!response.Cancelled) + SendAnalytics(packagePath, response.Status, time.Elapsed.TotalSeconds); + + OnUploadStateChanged?.Invoke(response.Status, null); + _activeUploader = null; + return response; + } + + protected abstract IPackageUploader CreatePackageUploader(string exportedPackagePath); + + private void ReportUploadProgress(object _, float value) + { + OnUploadStateChanged?.Invoke(null, value); + } + + private void SendAnalytics(string packagePath, UploadStatus uploadStatus, double timeTakenSeconds) + { + try + { + var analytic = new PackageUploadAnalytic( + packageId: Package.PackageId, + category: Package.Category, + usedValidator: LastValidationResult != null, + validationSettings: LastValidationSettings, + validationResult: LastValidationResult, + uploadFinishedReason: uploadStatus, + timeTaken: timeTakenSeconds, + packageSize: new FileInfo(packagePath).Length, + workflow: Name + ); + + var result = _services.SendAnalytic(analytic); + } + catch (Exception e) { ASDebug.LogError($"Could not send analytics: {e}"); } + } + + public void AbortUpload() + { + if (_activeUploader != null) + _services.StopUploading(_activeUploader); + + _activeUploader = null; + } + + public void ResetUploadStatus() + { + OnUploadStateChanged?.Invoke(UploadStatus.Default, 0f); + } + + public async Task RefreshPackage() + { + var response = await _services.UpdatePackageData(Package); + if (!response.Success) + return; + + Package.UpdateData(response.Package); + } + + public abstract bool IsPathValid(string path, out string reason); + + protected abstract void Serialize(); + + protected abstract void Deserialize(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/WorkflowBase.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/WorkflowBase.cs.meta new file mode 100644 index 00000000000..65c40e689e5 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Abstractions/WorkflowBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d0e87ee17aa944c42b1c335abe19daaf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/AssetsWorkflow.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/AssetsWorkflow.cs new file mode 100644 index 00000000000..967bd9baf90 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/AssetsWorkflow.cs @@ -0,0 +1,329 @@ +using AssetStoreTools.Api; +using AssetStoreTools.Exporter; +using AssetStoreTools.Uploader.Data.Serialization; +using AssetStoreTools.Utility; +using AssetStoreTools.Validator; +using AssetStoreTools.Validator.Data; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using PackageInfo = UnityEditor.PackageManager.PackageInfo; + +namespace AssetStoreTools.Uploader.Data +{ + internal class AssetsWorkflow : WorkflowBase + { + public override string Name => "AssetsWorkflow"; + public override string DisplayName => "From Assets Folder"; + public override string PackageExtension => ".unitypackage"; + public override bool IsPathSet => !string.IsNullOrEmpty(_mainExportPath); + public bool IsCompleteProject => Package.IsCompleteProject; + + private AssetsWorkflowState _stateData; + + private string _mainExportPath; + private bool _includeDependencies; + private List _dependencies; + private List _specialFolders; + + public override event Action OnChanged; + + // Special folders that would not work if not placed directly in the 'Assets' folder + private readonly string[] _extraAssetFolderNames = + { + "Editor Default Resources", "Gizmos", "Plugins", + "StreamingAssets", "Standard Assets", "WebGLTemplates", + "ExternalDependencyManager", "XR" + }; + + public AssetsWorkflow(IPackage package, AssetsWorkflowState stateData, IWorkflowServices services) + : base(package, services) + { + _stateData = stateData; + Deserialize(); + } + + public string GetMainExportPath() + { + return _mainExportPath; + } + + public void SetMainExportPath(string path, bool serialize) + { + _mainExportPath = path; + SetMetadata(); + if (serialize) + Serialize(); + } + + private void SetMetadata() + { + LocalPackageGuid = AssetDatabase.AssetPathToGUID(_mainExportPath); + LocalPackagePath = _mainExportPath; + LocalProjectPath = _mainExportPath; + } + + public bool GetIncludeDependencies() + { + return _includeDependencies; + } + + public void SetIncludeDependencies(bool value, bool serialize) + { + _includeDependencies = value; + // Note: make sure that exporting does not fail when + // a serialized dependency that has been removed from a project is sent to exporter + if (serialize) + Serialize(); + } + + public List GetDependencies() + { + return _dependencies; + } + + public void SetDependencies(IEnumerable dependencies, bool serialize) + { + _dependencies.Clear(); + foreach (var dependency in dependencies) + { + if (!PackageUtility.GetPackageByPackageName(dependency, out var package)) + continue; + _dependencies.Add(package); + } + + if (serialize) + Serialize(); + } + + public List GetSpecialFolders() + { + return _specialFolders; + } + + public void SetSpecialFolders(IEnumerable specialFolders, bool serialize) + { + _specialFolders.Clear(); + foreach (var folder in specialFolders) + { + _specialFolders.Add(folder); + } + + if (serialize) + Serialize(); + } + + public override bool IsPathValid(string path, out string error) + { + error = string.Empty; + + var pathIsFolder = Directory.Exists(path); + if (!pathIsFolder) + { + error = "Path must point to a valid folder"; + return false; + } + + var pathWithinAssetsFolder = path.StartsWith("Assets/") && path != "Assets/"; + if (pathWithinAssetsFolder) + return true; + + var pathIsAssetsFolder = path == "Assets" || path == "Assets/"; + if (pathIsAssetsFolder) + { + var assetsFolderSelectionAllowed = Package.IsCompleteProject; + if (assetsFolderSelectionAllowed) + return true; + + error = "'Assets' folder is only available for packages tagged as a 'Complete Project'."; + return false; + } + + error = "Selected folder path must be within the project's Assets."; + return false; + } + + public List GetAvailableDependencies() + { + var registryPackages = PackageUtility.GetAllRegistryPackages(); + return registryPackages.Select(x => x.name).ToList(); + } + + public List GetAvailableSpecialFolders() + { + var specialFolders = new List(); + + foreach (var extraAssetFolderName in _extraAssetFolderNames) + { + var fullExtraPath = "Assets/" + extraAssetFolderName; + + if (!Directory.Exists(fullExtraPath)) + continue; + + if (_mainExportPath.ToLower().StartsWith(fullExtraPath.ToLower())) + continue; + + // Don't include nested paths + if (!fullExtraPath.ToLower().StartsWith(_mainExportPath.ToLower())) + specialFolders.Add(fullExtraPath); + } + + return specialFolders; + } + + public override IEnumerable GetAllPaths() + { + var paths = new List() + { + _mainExportPath + }; + paths.AddRange(GetSpecialFolders()); + + return paths; + } + + public override IValidator CreateValidator() + { + var validationPaths = GetAllPaths(); + + var validationSettings = new CurrentProjectValidationSettings() + { + Category = Package.Category, + ValidationPaths = validationPaths.ToList(), + ValidationType = ValidationType.UnityPackage + }; + + var validator = new CurrentProjectValidator(validationSettings); + return validator; + } + + public override IPackageExporter CreateExporter(string outputPath) + { + var exportPaths = GetAllPaths().ToList(); + + if (IsCompleteProject && !exportPaths.Contains("ProjectSettings")) + { + exportPaths.Add("ProjectSettings"); + } + + var dependenciesToInclude = new List(); + if (_includeDependencies) + { + dependenciesToInclude.AddRange(_dependencies.Select(x => x.name)); + } + + if (ASToolsPreferences.Instance.UseLegacyExporting) + { + var exportSettings = new LegacyExporterSettings() + { + ExportPaths = exportPaths.ToArray(), + OutputFilename = outputPath, + IncludeDependencies = _includeDependencies, + }; + + return new LegacyPackageExporter(exportSettings); + } + else + { + var exportSettings = new DefaultExporterSettings() + { + ExportPaths = exportPaths.ToArray(), + OutputFilename = outputPath, + Dependencies = dependenciesToInclude.ToArray(), + PreviewGenerator = CreatePreviewGenerator(exportPaths), + }; + + return new DefaultPackageExporter(exportSettings); + } + } + + protected override IPackageUploader CreatePackageUploader(string exportedPackagePath) + { + var uploaderSettings = new UnityPackageUploadSettings() + { + UnityPackagePath = exportedPackagePath, + VersionId = Package.VersionId, + RootGuid = LocalPackageGuid, + RootPath = LocalPackagePath, + ProjectPath = LocalProjectPath + }; + + var uploader = new UnityPackageUploader(uploaderSettings); + return uploader; + } + + protected override void Serialize() + { + _stateData.SetMainPath(_mainExportPath); + _stateData.SetIncludeDependencies(_includeDependencies); + _stateData.SetDependencies(_dependencies.Select(x => x.name)); + _stateData.SetSpecialFolders(_specialFolders); + OnChanged?.Invoke(); + } + + protected override void Deserialize() + { + _mainExportPath = _stateData.GetMainPath(); + + _specialFolders = new List(); + foreach (var path in _stateData.GetSpecialFolders()) + { + _specialFolders.Add(path); + } + + _includeDependencies = _stateData.GetIncludeDependencies(); + + _dependencies = new List(); + foreach (var dependency in _stateData.GetDependencies()) + { + if (!PackageUtility.GetPackageByPackageName(dependency, out var package)) + continue; + + _dependencies.Add(package); + } + + DeserializeFromUploadedData(); + } + + private void DeserializeFromUploadedData() + { + DeserializeFromUploadedDataByGuid(); + DeserializeFromUploadedDataByPath(); + } + + private void DeserializeFromUploadedDataByGuid() + { + if (!string.IsNullOrEmpty(_mainExportPath)) + return; + + var lastUploadedGuid = Package.RootGuid; + if (string.IsNullOrEmpty(lastUploadedGuid)) + return; + + var potentialPackagePath = AssetDatabase.GUIDToAssetPath(lastUploadedGuid); + DeserializeFromUploadedDataByPath(potentialPackagePath); + } + + private void DeserializeFromUploadedDataByPath() + { + if (!string.IsNullOrEmpty(_mainExportPath)) + return; + + var lastUploadedPath = Package.ProjectPath; + if (string.IsNullOrEmpty(lastUploadedPath)) + return; + + DeserializeFromUploadedDataByPath(lastUploadedPath); + } + + private void DeserializeFromUploadedDataByPath(string path) + { + if (string.IsNullOrEmpty(path) || !IsPathValid(path, out var _)) + return; + + _mainExportPath = path; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/AssetsWorkflow.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/AssetsWorkflow.cs.meta new file mode 100644 index 00000000000..331e6ab5dfb --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/AssetsWorkflow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4657d35aaf9d70948a0840dc615f64ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/HybridPackageWorkflow.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/HybridPackageWorkflow.cs new file mode 100644 index 00000000000..3759313d77d --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/HybridPackageWorkflow.cs @@ -0,0 +1,251 @@ +using AssetStoreTools.Api; +using AssetStoreTools.Exporter; +using AssetStoreTools.Uploader.Data.Serialization; +using AssetStoreTools.Utility; +using AssetStoreTools.Validator; +using AssetStoreTools.Validator.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEditor.PackageManager; +using PackageInfo = UnityEditor.PackageManager.PackageInfo; +using PackageManager = UnityEditor.PackageManager; + +namespace AssetStoreTools.Uploader.Data +{ + internal class HybridPackageWorkflow : WorkflowBase + { + public override string Name => "HybridPackageWorkflow"; + public override string DisplayName => "Local UPM Package"; + public override string PackageExtension => ".unitypackage"; + public override bool IsPathSet => _packageInfo != null; + + private HybridPackageWorkflowState _stateData; + + private PackageInfo _packageInfo; + private List _dependencies; + + public override event Action OnChanged; + + public HybridPackageWorkflow(IPackage package, HybridPackageWorkflowState stateData, IWorkflowServices services) + : base(package, services) + { + _stateData = stateData; + Deserialize(); + } + + public PackageInfo GetPackage() + { + return _packageInfo; + } + + public void SetPackage(PackageInfo packageInfo, bool serialize) + { + if (packageInfo == null) + throw new ArgumentException("Package is null"); + + _packageInfo = packageInfo; + SetMetadata(); + if (serialize) + Serialize(); + } + + public void SetPackage(string packageManifestPath, bool serialize) + { + if (!PackageUtility.GetPackageByManifestPath(packageManifestPath, out var package)) + throw new ArgumentException("Path does not correspond to a valid package"); + + SetPackage(package, serialize); + } + + private void SetMetadata() + { + LocalPackageGuid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(_packageInfo.GetManifestAsset())); + LocalPackagePath = _packageInfo.assetPath; + LocalProjectPath = _packageInfo.name; + } + + public List GetDependencies() + { + return _dependencies; + } + + public void SetDependencies(IEnumerable dependencies, bool serialize) + { + _dependencies.Clear(); + foreach (var dependency in dependencies) + { + if (!PackageUtility.GetPackageByPackageName(dependency, out var package)) + continue; + _dependencies.Add(package); + } + + if (serialize) + Serialize(); + } + + public List GetAvailableDependencies() + { + var availableDependencies = new List(); + if (_packageInfo == null) + return availableDependencies; + + var packageDependencies = _packageInfo.dependencies.Select(x => x.name); + foreach (var dependency in packageDependencies) + { + if (!PackageUtility.GetPackageByPackageName(dependency, out var package)) + continue; + + if (package.source != PackageManager.PackageSource.Local + && package.source != PackageManager.PackageSource.Embedded) + continue; + + availableDependencies.Add(package); + } + + return availableDependencies; + } + + public override IEnumerable GetAllPaths() + { + var paths = new List(); + + if (_packageInfo == null) + return paths; + + paths.Add(_packageInfo.assetPath); + paths.AddRange(_dependencies.Select(x => x.assetPath)); + + return paths; + } + + public override bool IsPathValid(string path, out string reason) + { + reason = string.Empty; + + if (!PackageUtility.GetPackageByManifestPath(path, out var package)) + { + reason = "Selected path must point to a package manifest for a package that is imported into the current project"; + return false; + } + + var packageSourceValid = package.source == PackageSource.Embedded || package.source == PackageSource.Local; + if (!packageSourceValid) + { + reason = "Selected package must be a local or an embedded package"; + return false; + } + + return true; + } + + public override IValidator CreateValidator() + { + var validationPaths = GetAllPaths(); + + var validationSettings = new CurrentProjectValidationSettings() + { + Category = Package?.Category, + ValidationPaths = validationPaths.ToList(), + ValidationType = ValidationType.UnityPackage + }; + + var validator = new CurrentProjectValidator(validationSettings); + return validator; + } + + public override IPackageExporter CreateExporter(string outputPath) + { + var exportPaths = GetAllPaths(); + + var exportSettings = new DefaultExporterSettings() + { + ExportPaths = exportPaths.ToArray(), + OutputFilename = outputPath, + PreviewGenerator = CreatePreviewGenerator(exportPaths.ToList()) + }; + + return new DefaultPackageExporter(exportSettings); + } + + protected override IPackageUploader CreatePackageUploader(string exportedPackagePath) + { + var uploaderSettings = new UnityPackageUploadSettings() + { + UnityPackagePath = exportedPackagePath, + VersionId = Package.VersionId, + RootGuid = LocalPackageGuid, + RootPath = LocalPackagePath, + ProjectPath = LocalProjectPath + }; + + var uploader = new UnityPackageUploader(uploaderSettings); + return uploader; + } + + protected override void Serialize() + { + if (_packageInfo == null) + return; + + _stateData.SetPackageName(_packageInfo.name); + _stateData.SetPackageDependencies(_dependencies.Select(x => x.name).OrderBy(x => x)); + OnChanged?.Invoke(); + } + + protected override void Deserialize() + { + var packageName = _stateData.GetPackageName(); + if (PackageUtility.GetPackageByPackageName(packageName, out var package)) + _packageInfo = package; + + _dependencies = new List(); + var dependencies = _stateData.GetPackageDependencies(); + foreach (var dependency in dependencies) + { + if (PackageUtility.GetPackageByPackageName(dependency, out var packageDependency)) + _dependencies.Add(packageDependency); + } + + DeserializeFromUploadedData(); + } + + private void DeserializeFromUploadedData() + { + DeserializeFromUploadedDataByPackageName(); + DeserializeFromUploadedDataByPackageGuid(); + } + + private void DeserializeFromUploadedDataByPackageName() + { + if (_packageInfo != null) + return; + + var lastUploadedPackageName = Package.ProjectPath; + if (!PackageUtility.GetPackageByPackageName(lastUploadedPackageName, out var package)) + return; + + _packageInfo = package; + } + + private void DeserializeFromUploadedDataByPackageGuid() + { + if (_packageInfo != null) + return; + + var lastUploadedGuid = Package.RootGuid; + if (string.IsNullOrEmpty(lastUploadedGuid)) + return; + + var potentialPackageManifestPath = AssetDatabase.GUIDToAssetPath(lastUploadedGuid); + if (string.IsNullOrEmpty(potentialPackageManifestPath) || !IsPathValid(potentialPackageManifestPath, out var _)) + return; + + if (!PackageUtility.GetPackageByManifestPath(potentialPackageManifestPath, out var package)) + return; + + _packageInfo = package; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/HybridPackageWorkflow.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/HybridPackageWorkflow.cs.meta new file mode 100644 index 00000000000..f815b9de9c7 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/HybridPackageWorkflow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3061839aba3894246a20195639eeda1f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Package.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/Package.cs new file mode 100644 index 00000000000..1a984e56bf0 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Package.cs @@ -0,0 +1,91 @@ +using System; +using UnityEngine; +using PackageModel = AssetStoreTools.Api.Models.Package; + +namespace AssetStoreTools.Uploader.Data +{ + internal class Package : IPackage + { + private PackageModel _source; + + public string PackageId => _source.PackageId; + public string VersionId => _source.VersionId; + public string Name => _source.Name; + public string Status => _source.Status; + public string Category => _source.Category; + public bool IsCompleteProject => _source.IsCompleteProject; + public string RootGuid => _source.RootGuid; + public string RootPath => _source.RootPath; + public string ProjectPath => _source.ProjectPath; + public string Modified => _source.Modified; + public string Size => _source.Size; + public string IconUrl => _source.IconUrl; + public bool IsDraft => Status.Equals("draft", StringComparison.OrdinalIgnoreCase); + public Texture2D Icon { get; private set; } + + public event Action OnUpdate; + public event Action OnIconUpdate; + + public Package(PackageModel packageSource) + { + _source = packageSource; + } + + public void UpdateIcon(Texture2D texture) + { + if (texture == null) + return; + + Icon = texture; + OnIconUpdate?.Invoke(); + } + + public string FormattedSize() + { + var defaultSize = "0.00 MB"; + if (float.TryParse(Size, out var sizeBytes)) + return $"{sizeBytes / (1024f * 1024f):0.00} MB"; + + return defaultSize; + } + + public string FormattedModified() + { + var defaultDate = "Unknown"; + if (DateTime.TryParse(Modified, out var dt)) + return dt.Date.ToString("yyyy-MM-dd"); + + return defaultDate; + } + + public void UpdateData(PackageModel source) + { + if (source == null) + throw new ArgumentException("Provided package is null"); + + _source = source; + OnUpdate?.Invoke(); + } + + public PackageModel ToModel() + { + var model = new PackageModel() + { + PackageId = _source.PackageId, + VersionId = _source.VersionId, + Name = _source.Name, + Status = _source.Status, + Category = _source.Category, + IsCompleteProject = _source.IsCompleteProject, + RootGuid = _source.RootGuid, + RootPath = _source.RootPath, + ProjectPath = _source.ProjectPath, + Modified = _source.Modified, + Size = _source.Size, + IconUrl = _source.IconUrl + }; + + return model; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Package.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/Package.cs.meta new file mode 100644 index 00000000000..588a5fd0463 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Package.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc2198164bbd6394b87c51a74fe2915e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/PackageContent.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/PackageContent.cs new file mode 100644 index 00000000000..bac6c08e069 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/PackageContent.cs @@ -0,0 +1,68 @@ +using AssetStoreTools.Uploader.Data.Serialization; +using AssetStoreTools.Uploader.Services; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AssetStoreTools.Uploader.Data +{ + internal class PackageContent : IPackageContent + { + private IWorkflow _activeWorkflow; + private List _workflows; + private WorkflowStateData _workflowStateData; + + private ICachingService _cachingService; + + public event Action OnActiveWorkflowChanged; + + public PackageContent(List workflows, WorkflowStateData workflowStateData, ICachingService cachingService) + { + _workflows = workflows; + _workflowStateData = workflowStateData; + _cachingService = cachingService; + + foreach (var workflow in _workflows) + { + workflow.OnChanged += Serialize; + } + + Deserialize(); + } + + public IWorkflow GetActiveWorkflow() + { + return _activeWorkflow; + } + + public void SetActiveWorkflow(IWorkflow workflow) + { + _activeWorkflow = workflow; + + OnActiveWorkflowChanged?.Invoke(_activeWorkflow); + + Serialize(); + } + + public List GetAvailableWorkflows() + { + return _workflows; + } + + private void Serialize() + { + _workflowStateData.SetActiveWorkflow(_activeWorkflow.Name); + _cachingService.CacheWorkflowStateData(_workflowStateData); + } + + private void Deserialize() + { + var serializedWorkflow = _workflowStateData.GetActiveWorkflow(); + var workflow = _workflows.FirstOrDefault(x => x.Name == serializedWorkflow); + if (workflow != null) + _activeWorkflow = workflow; + else + _activeWorkflow = _workflows[0]; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/PackageContent.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/PackageContent.cs.meta new file mode 100644 index 00000000000..afce82d5842 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/PackageContent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f36086f9380a49949ab45463abc6fee8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/PackageGroup.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/PackageGroup.cs new file mode 100644 index 00000000000..78e9aa1ab36 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/PackageGroup.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AssetStoreTools.Uploader.Data +{ + internal class PackageGroup : IPackageGroup + { + private class FilteredPackage + { + public IPackage Package; + public bool IsInFilter; + } + + public string Name { get; private set; } + public List Packages { get; private set; } + + private List _filteredPackages; + + public event Action> OnPackagesSorted; + public event Action> OnPackagesFiltered; + + public PackageGroup(string name, List packages) + { + Name = name; + Packages = packages; + + _filteredPackages = new List(); + foreach (var package in Packages) + _filteredPackages.Add(new FilteredPackage() { Package = package, IsInFilter = true }); + } + + public void Sort(PackageSorting sortingType) + { + switch (sortingType) + { + case PackageSorting.Name: + _filteredPackages = _filteredPackages.OrderBy(x => x.Package.Name).ToList(); + break; + case PackageSorting.Date: + _filteredPackages = _filteredPackages.OrderByDescending(x => x.Package.Modified).ToList(); + break; + case PackageSorting.Category: + _filteredPackages = _filteredPackages.OrderBy(x => x.Package.Category).ThenBy(x => x.Package.Name).ToList(); + break; + default: + throw new NotImplementedException("Undefined sorting type"); + } + + OnPackagesSorted?.Invoke(_filteredPackages.Where(x => x.IsInFilter).Select(x => x.Package).ToList()); + } + + public void Filter(string filter) + { + foreach (var package in _filteredPackages) + { + bool inFilter = package.Package.Name.ToLower().Contains(filter.ToLower()); + package.IsInFilter = inFilter; + } + + OnPackagesFiltered?.Invoke(_filteredPackages.Where(x => x.IsInFilter).Select(x => x.Package).ToList()); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/PackageGroup.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/PackageGroup.cs.meta new file mode 100644 index 00000000000..2806af66530 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/PackageGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c9cc17f6b95bb2c42913a1451b9af29e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/PackageSorting.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/PackageSorting.cs new file mode 100644 index 00000000000..7ef00e4267d --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/PackageSorting.cs @@ -0,0 +1,9 @@ +namespace AssetStoreTools.Uploader.Data +{ + internal enum PackageSorting + { + Name, + Category, + Date + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/PackageSorting.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/PackageSorting.cs.meta new file mode 100644 index 00000000000..a9261e67bb6 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/PackageSorting.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b1d61d0de90e022469b5ed312d4b7beb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization.meta new file mode 100644 index 00000000000..2ba3d4ccc09 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0b05e199f21f636439844a8cc7e2c225 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/AssetPath.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/AssetPath.cs new file mode 100644 index 00000000000..4970e732dd1 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/AssetPath.cs @@ -0,0 +1,59 @@ +using AssetStoreTools.Utility; +using Newtonsoft.Json; +using System.IO; +using UnityEditor; + +namespace AssetStoreTools.Uploader.Data.Serialization +{ + internal class AssetPath + { + [JsonProperty("path")] + private string _path = string.Empty; + [JsonProperty("guid")] + private string _guid = string.Empty; + + [JsonIgnore] + public string Path { get => _path; set { SetAssetPath(value); } } + [JsonIgnore] + public string Guid { get => _guid; set { _guid = value; } } + + public AssetPath() { } + + public AssetPath(string path) + { + SetAssetPath(path); + } + + private void SetAssetPath(string path) + { + _path = path.Replace("\\", "/"); + if (TryGetGuid(_path, out var guid)) + _guid = guid; + } + + private bool TryGetGuid(string path, out string guid) + { + guid = string.Empty; + + var relativePath = FileUtility.AbsolutePathToRelativePath(path, ASToolsPreferences.Instance.EnableSymlinkSupport); + + if (!relativePath.StartsWith("Assets/") && !relativePath.StartsWith("Packages/")) + return false; + + guid = AssetDatabase.AssetPathToGUID(relativePath); + return !string.IsNullOrEmpty(guid); + } + + public override string ToString() + { + var pathFromGuid = AssetDatabase.GUIDToAssetPath(_guid); + if (!string.IsNullOrEmpty(pathFromGuid) && (File.Exists(pathFromGuid) || Directory.Exists(pathFromGuid))) + return pathFromGuid; + + if (File.Exists(_path) || Directory.Exists(_path)) + return _path; + + return string.Empty; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/AssetPath.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/AssetPath.cs.meta new file mode 100644 index 00000000000..d6ced932efe --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/AssetPath.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 920ff8e4ffe77ec44bede985593cc187 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/AssetsWorkflowStateData.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/AssetsWorkflowStateData.cs new file mode 100644 index 00000000000..8d955c36f61 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/AssetsWorkflowStateData.cs @@ -0,0 +1,77 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace AssetStoreTools.Uploader.Data.Serialization +{ + internal class AssetsWorkflowState + { + [JsonProperty("main_path")] + private AssetPath _mainPath; + [JsonProperty("special_folders")] + private List _specialFolders; + [JsonProperty("include_dependencies")] + private bool _includeDependencies; + [JsonProperty("dependencies")] + private List _dependencies; + + public AssetsWorkflowState() + { + _mainPath = new AssetPath(); + _includeDependencies = false; + _dependencies = new List(); + _specialFolders = new List(); + } + + public string GetMainPath() + { + return _mainPath?.ToString(); + } + + public void SetMainPath(string path) + { + _mainPath = new AssetPath(path); + } + + public bool GetIncludeDependencies() + { + return _includeDependencies; + } + + public void SetIncludeDependencies(bool value) + { + _includeDependencies = value; + } + + public List GetDependencies() + { + return _dependencies; + } + + public void SetDependencies(IEnumerable dependencies) + { + _dependencies = new List(); + foreach (var dependency in dependencies) + _dependencies.Add(dependency); + } + + public List GetSpecialFolders() + { + var specialFolders = new List(); + foreach (var folder in _specialFolders) + { + var path = folder.ToString(); + if (!string.IsNullOrEmpty(path)) + specialFolders.Add(path); + } + + return specialFolders; + } + + public void SetSpecialFolders(List specialFolders) + { + _specialFolders = new List(); + foreach (var path in specialFolders) + _specialFolders.Add(new AssetPath(path)); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/AssetsWorkflowStateData.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/AssetsWorkflowStateData.cs.meta new file mode 100644 index 00000000000..58c77a58f29 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/AssetsWorkflowStateData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 505f0a5aa753b4445a467539e150190a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/HybridPackageWorkflowState.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/HybridPackageWorkflowState.cs new file mode 100644 index 00000000000..58e428ea780 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/HybridPackageWorkflowState.cs @@ -0,0 +1,41 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace AssetStoreTools.Uploader.Data.Serialization +{ + internal class HybridPackageWorkflowState + { + [JsonProperty("package_name")] + private string _packageName; + [JsonProperty("dependencies")] + private List _dependencies; + + public HybridPackageWorkflowState() + { + _packageName = string.Empty; + _dependencies = new List(); + } + + public string GetPackageName() + { + return _packageName; + } + + public void SetPackageName(string packageName) + { + _packageName = packageName; + } + + public List GetPackageDependencies() + { + return _dependencies; + } + + public void SetPackageDependencies(IEnumerable dependencies) + { + _dependencies.Clear(); + foreach (var dependency in dependencies) + _dependencies.Add(dependency); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/HybridPackageWorkflowState.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/HybridPackageWorkflowState.cs.meta new file mode 100644 index 00000000000..6638cf8d95f --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/HybridPackageWorkflowState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2848375fcb0a4174495573190bfc3900 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/UnityPackageWorkflowStateData.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/UnityPackageWorkflowStateData.cs new file mode 100644 index 00000000000..ca14b882b6d --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/UnityPackageWorkflowStateData.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; + +namespace AssetStoreTools.Uploader.Data.Serialization +{ + internal class UnityPackageWorkflowState + { + [JsonProperty("package_path")] + private AssetPath _packagePath; + + public UnityPackageWorkflowState() + { + _packagePath = new AssetPath(); + } + + public string GetPackagePath() + { + return _packagePath?.ToString(); + } + + public void SetPackagePath(string path) + { + _packagePath = new AssetPath(path); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/UnityPackageWorkflowStateData.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/UnityPackageWorkflowStateData.cs.meta new file mode 100644 index 00000000000..a9d8b2c20d2 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/UnityPackageWorkflowStateData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 101a66adc88639b43b07cc28214474cf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/WorkflowStateData.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/WorkflowStateData.cs new file mode 100644 index 00000000000..25e72c62a5e --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/WorkflowStateData.cs @@ -0,0 +1,68 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace AssetStoreTools.Uploader.Data.Serialization +{ + internal class WorkflowStateData + { + [JsonProperty("package_id")] + private string _packageId; + [JsonProperty("active_workflow")] + private string _activeWorkflow; + [JsonProperty("assets_workflow")] + private AssetsWorkflowState _assetsWorkflow; + [JsonProperty("unitypackage_workflow")] + private UnityPackageWorkflowState _unityPackageWorkflow; + [JsonProperty("hybrid_workflow")] + private HybridPackageWorkflowState _hybridPackageWorkflow; + + public WorkflowStateData() + { + _activeWorkflow = string.Empty; + + _assetsWorkflow = new AssetsWorkflowState(); + _unityPackageWorkflow = new UnityPackageWorkflowState(); + _hybridPackageWorkflow = new HybridPackageWorkflowState(); + } + + public WorkflowStateData(string packageId) : this() + { + SetPackageId(packageId); + } + + public string GetPackageId() + { + return _packageId; + } + + public void SetPackageId(string packageId) + { + _packageId = packageId; + } + + public string GetActiveWorkflow() + { + return _activeWorkflow; + } + + public void SetActiveWorkflow(string activeWorkflow) + { + _activeWorkflow = activeWorkflow; + } + + public AssetsWorkflowState GetAssetsWorkflowState() + { + return _assetsWorkflow; + } + + public UnityPackageWorkflowState GetUnityPackageWorkflowState() + { + return _unityPackageWorkflow; + } + + public HybridPackageWorkflowState GetHybridPackageWorkflowState() + { + return _hybridPackageWorkflow; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/WorkflowStateData.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/WorkflowStateData.cs.meta new file mode 100644 index 00000000000..4a583284b46 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/Serialization/WorkflowStateData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eecebbc83661a4f41a14e293c9fc3331 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/UnityPackageWorkflow.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/UnityPackageWorkflow.cs new file mode 100644 index 00000000000..6668fd6ef80 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/UnityPackageWorkflow.cs @@ -0,0 +1,135 @@ +using AssetStoreTools.Api; +using AssetStoreTools.Exporter; +using AssetStoreTools.Uploader.Data.Serialization; +using AssetStoreTools.Validator; +using AssetStoreTools.Validator.Data; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace AssetStoreTools.Uploader.Data +{ + internal class UnityPackageWorkflow : WorkflowBase + { + public override string Name => "UnityPackageWorkflow"; + public override string DisplayName => "Pre-exported .unitypackage"; + public override string PackageExtension => ".unitypackage"; + public override bool IsPathSet => !string.IsNullOrEmpty(_packagePath); + + private UnityPackageWorkflowState _workflowState; + private string _packagePath; + + public override event Action OnChanged; + + public UnityPackageWorkflow(IPackage package, UnityPackageWorkflowState workflowState, IWorkflowServices services) + : base(package, services) + { + _workflowState = workflowState; + Deserialize(); + } + + public void SetPackagePath(string path, bool serialize) + { + _packagePath = path; + SetMetadata(); + if (serialize) + Serialize(); + } + + private void SetMetadata() + { + LocalPackageGuid = string.Empty; + LocalPackagePath = string.Empty; + LocalProjectPath = _packagePath; + } + + public string GetPackagePath() + { + return _packagePath; + } + + public override IEnumerable GetAllPaths() + { + return new List() { _packagePath }; + } + + public override bool IsPathValid(string path, out string error) + { + error = null; + + var pathIsUnityPackage = path.EndsWith(PackageExtension); + var pathExists = File.Exists(path); + + if (!pathIsUnityPackage || !pathExists) + { + error = "Path must point to a .unitypackage file"; + return false; + } + + return true; + } + + public override IValidator CreateValidator() + { + var validationSettings = new ExternalProjectValidationSettings() + { + Category = Package.Category, + PackagePath = GetPackagePath() + }; + + var validator = new ExternalProjectValidator(validationSettings); + return validator; + } + + public override IPackageExporter CreateExporter(string _) + { + // This workflow already takes exported packages as input + throw new InvalidOperationException($"{nameof(UnityPackageWorkflow)} already takes exported packages as input"); + } + + public override Task ExportPackage(string _) + { + return Task.FromResult(new PackageExporterResult() { Success = true, ExportedPath = GetPackagePath() }); + } + + protected override IPackageUploader CreatePackageUploader(string exportedPackagePath) + { + var uploaderSettings = new UnityPackageUploadSettings() + { + VersionId = Package.VersionId, + UnityPackagePath = exportedPackagePath, + RootGuid = LocalPackageGuid, + RootPath = LocalPackagePath, + ProjectPath = LocalProjectPath + }; + + var uploader = new UnityPackageUploader(uploaderSettings); + return uploader; + } + + protected override void Serialize() + { + _workflowState.SetPackagePath(_packagePath); + OnChanged?.Invoke(); + } + + protected override void Deserialize() + { + _packagePath = _workflowState.GetPackagePath(); + DeserializeFromUploadedData(); + } + + private void DeserializeFromUploadedData() + { + if (!string.IsNullOrEmpty(_packagePath)) + return; + + var potentialPackagePath = Package.ProjectPath; + if (string.IsNullOrEmpty(potentialPackagePath) || !IsPathValid(potentialPackagePath, out var _)) + return; + + _packagePath = potentialPackagePath; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/UnityPackageWorkflow.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/UnityPackageWorkflow.cs.meta new file mode 100644 index 00000000000..031a510ed03 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/UnityPackageWorkflow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 47ee1db30792bf84aa1af8be7ce0dee6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/WorkflowServices.cs b/.github/Asset Store Tools/Uploader/Scripts/Data/WorkflowServices.cs new file mode 100644 index 00000000000..525374dcef9 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/WorkflowServices.cs @@ -0,0 +1,53 @@ +using AssetStoreTools.Api; +using AssetStoreTools.Api.Responses; +using AssetStoreTools.Uploader.Services.Analytics; +using AssetStoreTools.Uploader.Services.Analytics.Data; +using AssetStoreTools.Uploader.Services.Api; +using System; +using System.Threading.Tasks; +using UnityEngine.Analytics; + +namespace AssetStoreTools.Uploader.Data +{ + internal class WorkflowServices : IWorkflowServices + { + private IPackageDownloadingService _downloadingService; + private IPackageUploadingService _uploadingService; + private IAnalyticsService _analyticsService; + + public WorkflowServices( + IPackageDownloadingService downloadingService, + IPackageUploadingService uploadingService, + IAnalyticsService analyticsService) + { + _downloadingService = downloadingService; + _uploadingService = uploadingService; + _analyticsService = analyticsService; + } + + public Task GetPackageUploadedVersions(IPackage package, int timeoutMs) + { + return _downloadingService.GetPackageUploadedVersions(package, timeoutMs); + } + + public Task UploadPackage(IPackageUploader uploader, IProgress progress) + { + return _uploadingService.UploadPackage(uploader, progress); + } + + public void StopUploading(IPackageUploader uploader) + { + _uploadingService.StopUploading(uploader); + } + + public Task UpdatePackageData(IPackage package) + { + return _downloadingService.UpdatePackageData(package); + } + + public AnalyticsResult SendAnalytic(IAssetStoreAnalytic data) + { + return _analyticsService.Send(data); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Data/WorkflowServices.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Data/WorkflowServices.cs.meta new file mode 100644 index 00000000000..24314624bb5 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Data/WorkflowServices.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a78b96ae30966e94ba9ffdddf19c1692 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services.meta b/.github/Asset Store Tools/Uploader/Scripts/Services.meta new file mode 100644 index 00000000000..6d1acef248f --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d9787842821f3d041904186d0e0cc61d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics.meta new file mode 100644 index 00000000000..30b3cbf549e --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 17f95678acdb51548908d81be7146b5b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/AnalyticsService.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/AnalyticsService.cs new file mode 100644 index 00000000000..a2f6e1e4ec4 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/AnalyticsService.cs @@ -0,0 +1,45 @@ +using AssetStoreTools.Uploader.Services.Analytics.Data; +using UnityEditor; +using UnityEngine.Analytics; +#if !UNITY_2023_2_OR_NEWER +using AnalyticsConstants = AssetStoreTools.Constants.Uploader.Analytics; +#endif + +namespace AssetStoreTools.Uploader.Services.Analytics +{ + internal class AnalyticsService : IAnalyticsService + { + public AnalyticsResult Send(IAssetStoreAnalytic analytic) + { + if (!EditorAnalytics.enabled) + return AnalyticsResult.AnalyticsDisabled; + + if (!Register(analytic)) + return AnalyticsResult.AnalyticsDisabled; + +#if UNITY_2023_2_OR_NEWER + return EditorAnalytics.SendAnalytic(analytic); +#else + return EditorAnalytics.SendEventWithLimit(analytic.EventName, + analytic.Data, + analytic.EventVersion); +#endif + } + + private bool Register(IAssetStoreAnalytic analytic) + { +#if UNITY_2023_2_OR_NEWER + return true; +#else + var result = EditorAnalytics.RegisterEventWithLimit( + eventName: analytic.EventName, + maxEventPerHour: AnalyticsConstants.MaxEventsPerHour, + maxItems: AnalyticsConstants.MaxNumberOfElements, + vendorKey: AnalyticsConstants.VendorKey, + ver: analytic.EventVersion); + + return result == AnalyticsResult.Ok; +#endif + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/AnalyticsService.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/AnalyticsService.cs.meta new file mode 100644 index 00000000000..a4acea13418 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/AnalyticsService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 408b5b0136da9ca4f9598b8688f6210e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data.meta new file mode 100644 index 00000000000..59ec0e40848 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: df1fca726619f2f4fae3bd93b0ef5a8b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/AuthenticationAnalytic.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/AuthenticationAnalytic.cs new file mode 100644 index 00000000000..74edf0e015f --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/AuthenticationAnalytic.cs @@ -0,0 +1,46 @@ +using AssetStoreTools.Api; +using System; +#if UNITY_2023_2_OR_NEWER +using UnityEngine.Analytics; +#endif +using AnalyticsConstants = AssetStoreTools.Constants.Uploader.Analytics; + +namespace AssetStoreTools.Uploader.Services.Analytics.Data +{ +#if UNITY_2023_2_OR_NEWER + [AnalyticInfo + (eventName: AnalyticsConstants.AuthenticationAnalytics.EventName, + vendorKey: AnalyticsConstants.VendorKey, + version: AnalyticsConstants.AuthenticationAnalytics.EventVersion, + maxEventsPerHour: AnalyticsConstants.MaxEventsPerHour, + maxNumberOfElements: AnalyticsConstants.MaxNumberOfElements)] +#endif + internal class AuthenticationAnalytic : BaseAnalytic + { + [Serializable] + public class AuthenticationAnalyticData : BaseAnalyticData + { + public string AuthenticationType; + public string PublisherId; + } + + public override string EventName => AnalyticsConstants.AuthenticationAnalytics.EventName; + public override int EventVersion => AnalyticsConstants.AuthenticationAnalytics.EventVersion; + + private AuthenticationAnalyticData _data; + + public AuthenticationAnalytic(IAuthenticationType authenticationType, string publisherId) + { + _data = new AuthenticationAnalyticData + { + AuthenticationType = authenticationType.GetType().Name, + PublisherId = publisherId + }; + } + + protected override BaseAnalyticData GetData() + { + return _data; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/AuthenticationAnalytic.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/AuthenticationAnalytic.cs.meta new file mode 100644 index 00000000000..83763d75696 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/AuthenticationAnalytic.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4b9389e3ee578484493d36775c75baa1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/BaseAnalytic.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/BaseAnalytic.cs new file mode 100644 index 00000000000..60c3396b14b --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/BaseAnalytic.cs @@ -0,0 +1,35 @@ +using System; +#if UNITY_2023_2_OR_NEWER +using UnityEngine.Analytics; +#endif + +namespace AssetStoreTools.Uploader.Services.Analytics.Data +{ + internal abstract class BaseAnalytic : IAssetStoreAnalytic + { + [Serializable] + public class BaseAnalyticData : IAssetStoreAnalyticData + { + public string ToolVersion = Constants.Api.ApiVersion; + } + + public abstract string EventName { get; } + public abstract int EventVersion { get; } + + public IAssetStoreAnalyticData Data => GetData(); + protected abstract BaseAnalyticData GetData(); + +#if UNITY_2023_2_OR_NEWER + public bool TryGatherData(out IAnalytic.IData data, [System.Diagnostics.CodeAnalysis.NotNullWhen(false)] out Exception error) + { + error = null; + data = Data; + + if (data == null) + error = new Exception("Analytic data is null"); + + return error == null; + } +#endif + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/BaseAnalytic.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/BaseAnalytic.cs.meta new file mode 100644 index 00000000000..b16de7f1364 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/BaseAnalytic.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 51ec1e4b6505b694ab01f7c523744fbc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/IAssetStoreAnalytic.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/IAssetStoreAnalytic.cs new file mode 100644 index 00000000000..5207ca01801 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/IAssetStoreAnalytic.cs @@ -0,0 +1,16 @@ +#if UNITY_2023_2_OR_NEWER +using UnityEngine.Analytics; +#endif + +namespace AssetStoreTools.Uploader.Services.Analytics.Data +{ + internal interface IAssetStoreAnalytic +#if UNITY_2023_2_OR_NEWER + : IAnalytic +#endif + { + string EventName { get; } + int EventVersion { get; } + IAssetStoreAnalyticData Data { get; } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/IAssetStoreAnalytic.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/IAssetStoreAnalytic.cs.meta new file mode 100644 index 00000000000..29bfff81077 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/IAssetStoreAnalytic.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6e9b53aa176bbed48bafa538c26df304 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/IAssetStoreAnalyticData.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/IAssetStoreAnalyticData.cs new file mode 100644 index 00000000000..e08b8f80406 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/IAssetStoreAnalyticData.cs @@ -0,0 +1,8 @@ +namespace AssetStoreTools.Uploader.Services.Analytics.Data +{ + interface IAssetStoreAnalyticData +#if UNITY_2023_2_OR_NEWER + : UnityEngine.Analytics.IAnalytic.IData +#endif + { } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/IAssetStoreAnalyticData.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/IAssetStoreAnalyticData.cs.meta new file mode 100644 index 00000000000..0c3280dc527 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/IAssetStoreAnalyticData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b639e25d9b9abd34d8eb67b0e17dde86 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/PackageUploadAnalytic.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/PackageUploadAnalytic.cs new file mode 100644 index 00000000000..d8a34793119 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/PackageUploadAnalytic.cs @@ -0,0 +1,72 @@ +using AssetStoreTools.Api; +using AssetStoreTools.Validator.Data; +using System; +#if UNITY_2023_2_OR_NEWER +using UnityEngine.Analytics; +#endif +using AnalyticsConstants = AssetStoreTools.Constants.Uploader.Analytics; + +namespace AssetStoreTools.Uploader.Services.Analytics.Data +{ +#if UNITY_2023_2_OR_NEWER + [AnalyticInfo + (eventName: AnalyticsConstants.PackageUploadAnalytics.EventName, + vendorKey: AnalyticsConstants.VendorKey, + version: AnalyticsConstants.PackageUploadAnalytics.EventVersion, + maxEventsPerHour: AnalyticsConstants.MaxEventsPerHour, + maxNumberOfElements: AnalyticsConstants.MaxNumberOfElements)] +#endif + internal class PackageUploadAnalytic : BaseAnalytic + { + [Serializable] + public class PackageUploadAnalyticData : BaseAnalyticData + { + public string PackageId; + public string Category; + public bool UsedValidator; + public string ValidatorResults; + public string UploadFinishedReason; + public double TimeTaken; + public long PackageSize; + public string Workflow; + public string EndpointUrl; + } + + public override string EventName => AnalyticsConstants.PackageUploadAnalytics.EventName; + public override int EventVersion => AnalyticsConstants.PackageUploadAnalytics.EventVersion; + + private PackageUploadAnalyticData _data; + + public PackageUploadAnalytic( + string packageId, + string category, + bool usedValidator, + ValidationSettings validationSettings, + ValidationResult validationResult, + UploadStatus uploadFinishedReason, + double timeTaken, + long packageSize, + string workflow + ) + { + _data = new PackageUploadAnalyticData() + { + PackageId = packageId, + Category = category, + UsedValidator = usedValidator, + ValidatorResults = usedValidator ? + ValidationResultsSerializer.ConstructValidationResultsJson(validationSettings, validationResult) : null, + UploadFinishedReason = uploadFinishedReason.ToString(), + TimeTaken = timeTaken, + PackageSize = packageSize, + Workflow = workflow, + EndpointUrl = Constants.Api.AssetStoreBaseUrl + }; + } + + protected override BaseAnalyticData GetData() + { + return _data; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/PackageUploadAnalytic.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/PackageUploadAnalytic.cs.meta new file mode 100644 index 00000000000..bb913932a1c --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/PackageUploadAnalytic.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6cc34de12dce9964b9c900d5bb159966 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/ValidationResultsSerializer.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/ValidationResultsSerializer.cs new file mode 100644 index 00000000000..1bb7b56ac57 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/ValidationResultsSerializer.cs @@ -0,0 +1,91 @@ +using AssetStoreTools.Validator.Data; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System.Collections.Generic; +using System.Reflection; + +namespace AssetStoreTools.Uploader.Services.Analytics.Data +{ + internal class ValidationResultsSerializer + { + private class ValidationResults + { + public bool HasCompilationErrors; + public string[] Paths; + public Dictionary Results; + } + + private class TestResultOutcome + { + public int IntegerValue; + public string StringValue; + + public TestResultOutcome(TestResultStatus status) + { + IntegerValue = (int)status; + StringValue = status.ToString(); + } + } + + private class ValidationResultsResolver : DefaultContractResolver + { + private static ValidationResultsResolver _instance; + public static ValidationResultsResolver Instance => _instance ?? (_instance = new ValidationResultsResolver()); + + private Dictionary _propertyConversion; + + private ValidationResultsResolver() + { + _propertyConversion = new Dictionary() + { + { nameof(ValidationResults.HasCompilationErrors), "has_compilation_errors" }, + { nameof(ValidationResults.Paths), "validation_paths" }, + { nameof(ValidationResults.Results), "validation_results" }, + { nameof(TestResultOutcome.IntegerValue), "int" }, + { nameof(TestResultOutcome.StringValue), "string" }, + }; + } + + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var property = base.CreateProperty(member, memberSerialization); + if (_propertyConversion.ContainsKey(property.PropertyName)) + property.PropertyName = _propertyConversion[property.PropertyName]; + + return property; + } + } + + public static string ConstructValidationResultsJson(ValidationSettings settings, ValidationResult result) + { + if (result == null) + return string.Empty; + + var resultObject = new ValidationResults(); + resultObject.HasCompilationErrors = result.HadCompilationErrors; + + switch (settings) + { + case CurrentProjectValidationSettings currentProjectValidationSettings: + resultObject.Paths = currentProjectValidationSettings.ValidationPaths.ToArray(); + break; + case ExternalProjectValidationSettings externalProjectValidationSettings: + resultObject.Paths = new string[] { externalProjectValidationSettings.PackagePath }; + break; + } + + resultObject.Results = new Dictionary(); + foreach (var test in result.Tests) + { + resultObject.Results.Add(test.Id.ToString(), new TestResultOutcome(test.Result.Status)); + } + + var serializerSettings = new JsonSerializerSettings() + { + ContractResolver = ValidationResultsResolver.Instance + }; + + return JsonConvert.SerializeObject(resultObject, serializerSettings); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/ValidationResultsSerializer.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/ValidationResultsSerializer.cs.meta new file mode 100644 index 00000000000..f5415b8aa3a --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/Data/ValidationResultsSerializer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fa15fc27c7f3d044884885b3dad73efc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/IAnalyticsService.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/IAnalyticsService.cs new file mode 100644 index 00000000000..a64dce1e040 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/IAnalyticsService.cs @@ -0,0 +1,10 @@ +using AssetStoreTools.Uploader.Services.Analytics.Data; +using UnityEngine.Analytics; + +namespace AssetStoreTools.Uploader.Services.Analytics +{ + internal interface IAnalyticsService : IUploaderService + { + AnalyticsResult Send(IAssetStoreAnalytic analytic); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/IAnalyticsService.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/IAnalyticsService.cs.meta new file mode 100644 index 00000000000..2db504270d3 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Analytics/IAnalyticsService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: faa1f39fc83b86b438f6e0f34f01167b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Api.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Api.meta new file mode 100644 index 00000000000..7075aa49767 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Api.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4d983b64bd0866a428f937434252f537 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Api/AuthenticationService.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/AuthenticationService.cs new file mode 100644 index 00000000000..e0028182918 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/AuthenticationService.cs @@ -0,0 +1,100 @@ +using AssetStoreTools.Api; +using AssetStoreTools.Api.Models; +using AssetStoreTools.Api.Responses; +using AssetStoreTools.Uploader.Services.Analytics; +using AssetStoreTools.Uploader.Services.Analytics.Data; +using AssetStoreTools.Utility; +using System; +using System.Threading.Tasks; +using UnityEditor; + +namespace AssetStoreTools.Uploader.Services.Api +{ + internal class AuthenticationService : IAuthenticationService + { + private IAssetStoreApi _api; + private ICachingService _cachingService; + private IAnalyticsService _analyticsService; + + public User User { get; private set; } + + public AuthenticationService(IAssetStoreApi api, ICachingService cachingService, IAnalyticsService analyticsService) + { + _api = api; + _cachingService = cachingService; + _analyticsService = analyticsService; + } + + public async Task AuthenticateWithCredentials(string email, string password) + { + var authenticationType = new CredentialsAuthentication(email, password); + return await Authenticate(authenticationType); + } + + public async Task AuthenticateWithSessionToken() + { + if (!_cachingService.GetCachedSessionToken(out var cachedSessionToken)) + { + return new AuthenticationResponse() { Success = false, Exception = new Exception("No cached session token found") }; + } + + var authenticationType = new SessionAuthentication(cachedSessionToken); + return await Authenticate(authenticationType); + } + + public async Task AuthenticateWithCloudToken() + { + var authenticationType = new CloudTokenAuthentication(CloudProjectSettings.accessToken); + return await Authenticate(authenticationType); + } + + private async Task Authenticate(IAuthenticationType authenticationType) + { + var response = await _api.Authenticate(authenticationType); + HandleLoginResponse(authenticationType, response); + return response; + } + + private void HandleLoginResponse(IAuthenticationType authenticationType, AuthenticationResponse response) + { + if (!response.Success) + { + Deauthenticate(); + return; + } + + User = response.User; + _cachingService.CacheSessionToken(User.SessionId); + SendAnalytics(authenticationType, User); + } + + public bool CloudAuthenticationAvailable(out string username, out string cloudToken) + { + username = CloudProjectSettings.userName; + cloudToken = CloudProjectSettings.accessToken; + return !username.Equals("anonymous"); + } + + public void Deauthenticate() + { + _api.Deauthenticate(); + + User = null; + _cachingService.ClearCachedSessionToken(); + } + + private void SendAnalytics(IAuthenticationType authenticationType, User user) + { + try + { + // Do not send session authentication events + if (authenticationType is SessionAuthentication) + return; + + var analytic = new AuthenticationAnalytic(authenticationType, user.PublisherId); + var result = _analyticsService.Send(analytic); + } + catch (Exception e) { ASDebug.LogError($"Could not send analytics: {e}"); } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Api/AuthenticationService.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/AuthenticationService.cs.meta new file mode 100644 index 00000000000..b36dd7ddcc4 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/AuthenticationService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c1c3d6578d298d049a8dcf858fd3686e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IAuthenticationService.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IAuthenticationService.cs new file mode 100644 index 00000000000..96c9ee3d0ee --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IAuthenticationService.cs @@ -0,0 +1,16 @@ +using AssetStoreTools.Api.Models; +using AssetStoreTools.Api.Responses; +using System.Threading.Tasks; + +namespace AssetStoreTools.Uploader.Services.Api +{ + internal interface IAuthenticationService : IUploaderService + { + User User { get; } + Task AuthenticateWithCredentials(string email, string password); + Task AuthenticateWithSessionToken(); + Task AuthenticateWithCloudToken(); + bool CloudAuthenticationAvailable(out string username, out string cloudToken); + void Deauthenticate(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IAuthenticationService.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IAuthenticationService.cs.meta new file mode 100644 index 00000000000..25b27c4d1e7 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IAuthenticationService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ff0518dc0d95d3540857d138215bb900 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IPackageDownloadingService.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IPackageDownloadingService.cs new file mode 100644 index 00000000000..4cef2c0f856 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IPackageDownloadingService.cs @@ -0,0 +1,16 @@ +using AssetStoreTools.Api.Responses; +using AssetStoreTools.Uploader.Data; +using System.Threading.Tasks; + +namespace AssetStoreTools.Uploader.Services.Api +{ + internal interface IPackageDownloadingService : IUploaderService + { + Task GetPackageData(); + Task UpdatePackageData(IPackage package); + void ClearPackageData(); + Task GetPackageThumbnail(IPackage package); + Task GetPackageUploadedVersions(IPackage package, int timeoutMs); + void StopDownloading(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IPackageDownloadingService.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IPackageDownloadingService.cs.meta new file mode 100644 index 00000000000..35eaf1ea89d --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IPackageDownloadingService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 96acd12a628311d429cc285f418f8b90 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IPackageUploadingService.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IPackageUploadingService.cs new file mode 100644 index 00000000000..f1ff1d4c28e --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IPackageUploadingService.cs @@ -0,0 +1,16 @@ +using AssetStoreTools.Api; +using AssetStoreTools.Api.Responses; +using System; +using System.Threading.Tasks; + +namespace AssetStoreTools.Uploader.Services.Api +{ + internal interface IPackageUploadingService : IUploaderService + { + bool IsUploading { get; } + + Task UploadPackage(IPackageUploader uploader, IProgress progress); + void StopUploading(IPackageUploader package); + void StopAllUploadinng(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IPackageUploadingService.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IPackageUploadingService.cs.meta new file mode 100644 index 00000000000..153b53a4fbc --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/IPackageUploadingService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a3d78f3bc68d3d44b4300bc8ffe69c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Api/PackageDownloadingService.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/PackageDownloadingService.cs new file mode 100644 index 00000000000..5ab052faf30 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/PackageDownloadingService.cs @@ -0,0 +1,109 @@ +using AssetStoreTools.Api; +using AssetStoreTools.Api.Responses; +using AssetStoreTools.Uploader.Data; +using System.Threading; +using System.Threading.Tasks; + +namespace AssetStoreTools.Uploader.Services.Api +{ + internal class PackageDownloadingService : IPackageDownloadingService + { + public const int MaxConcurrentTumbnailDownloads = 10; + + private IAssetStoreApi _api; + private ICachingService _cachingService; + + private int _currentDownloads; + private CancellationTokenSource _cancellationTokenSource; + + public PackageDownloadingService(IAssetStoreApi api, ICachingService cachingService) + { + _api = api; + _cachingService = cachingService; + _cancellationTokenSource = new CancellationTokenSource(); + } + + public void ClearPackageData() + { + _cachingService.DeletePackageMetadata(); + } + + public async Task GetPackageData() + { + if (!_cachingService.GetCachedPackageMetadata(out var models)) + { + var cancellationToken = _cancellationTokenSource.Token; + var packagesResponse = await _api.GetPackages(cancellationToken); + + if (packagesResponse.Cancelled || !packagesResponse.Success) + return packagesResponse; + + _cachingService.CachePackageMetadata(packagesResponse.Packages); + return packagesResponse; + } + + return new PackagesDataResponse() { Success = true, Packages = models }; + } + + public async Task UpdatePackageData(IPackage package) + { + var response = await _api.RefreshPackageMetadata(package.ToModel()); + + if (response.Success) + _cachingService.UpdatePackageMetadata(response.Package); + + return response; + } + + public async Task GetPackageThumbnail(IPackage package) + { + if (_cachingService.GetCachedPackageThumbnail(package.PackageId, out var cachedTexture)) + { + return new PackageThumbnailResponse() { Success = true, Thumbnail = cachedTexture }; + } + + var cancellationToken = _cancellationTokenSource.Token; + while (_currentDownloads >= MaxConcurrentTumbnailDownloads) + await Task.Delay(100); + + if (cancellationToken.IsCancellationRequested) + return new PackageThumbnailResponse() { Success = false, Cancelled = true }; + + _currentDownloads++; + var result = await _api.GetPackageThumbnail(package.ToModel(), cancellationToken); + _currentDownloads--; + + if (result.Success && result.Thumbnail != null) + _cachingService.CachePackageThumbnail(package.PackageId, result.Thumbnail); + + return result; + } + + public async Task GetPackageUploadedVersions(IPackage package, int timeoutMs) + { + var timeoutTokenSource = new CancellationTokenSource(); + try + { + var versionsTask = _api.GetPackageUploadedVersions(package.ToModel(), timeoutTokenSource.Token); + + // Wait for versions to be retrieved, or a timeout to occur, whichever is first + if (await Task.WhenAny(versionsTask, Task.Delay(timeoutMs)) != versionsTask) + { + timeoutTokenSource.Cancel(); + } + + return await versionsTask; + } + finally + { + timeoutTokenSource.Dispose(); + } + } + + public void StopDownloading() + { + _cancellationTokenSource.Cancel(); + _cancellationTokenSource = new CancellationTokenSource(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Api/PackageDownloadingService.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/PackageDownloadingService.cs.meta new file mode 100644 index 00000000000..5e82ecb5f1a --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/PackageDownloadingService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: adc44e974cb91b54fac3819284b7ba82 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Api/PackageUploadingService.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/PackageUploadingService.cs new file mode 100644 index 00000000000..53da1bb5bea --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/PackageUploadingService.cs @@ -0,0 +1,103 @@ +using AssetStoreTools.Api; +using AssetStoreTools.Api.Responses; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using UnityEditor; + +namespace AssetStoreTools.Uploader.Services.Api +{ + internal class PackageUploadingService : IPackageUploadingService + { + private class UploadInProgress + { + public IPackageUploader Uploader; + public IProgress Progress = new Progress(); + public CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); + + public UploadInProgress(IPackageUploader uploader, IProgress progress) + { + Uploader = uploader; + Progress = progress; + CancellationTokenSource = new CancellationTokenSource(); + } + } + + private IAssetStoreApi _api; + private List _uploadsInProgress; + + public bool IsUploading => _uploadsInProgress.Count > 0; + + public PackageUploadingService(IAssetStoreApi api) + { + _api = api; + _uploadsInProgress = new List(); + } + + public async Task UploadPackage(IPackageUploader uploader, IProgress progress) + { + using (var cancellationTokenSource = new CancellationTokenSource()) + { + var uploadInProgress = StartTrackingUpload(uploader, progress); + var response = await _api.UploadPackage(uploadInProgress.Uploader, uploadInProgress.Progress, uploadInProgress.CancellationTokenSource.Token); + StopTrackingUpload(uploadInProgress); + + return response; + } + } + + private UploadInProgress StartTrackingUpload(IPackageUploader uploader, IProgress progress) + { + // If this is the first upload - lock reload assemblies and prevent entering play mode + if (_uploadsInProgress.Count == 0) + { + EditorApplication.LockReloadAssemblies(); + EditorApplication.playModeStateChanged += PreventEnteringPlayMode; + } + + var uploadInProgress = new UploadInProgress(uploader, progress); + _uploadsInProgress.Add(uploadInProgress); + + return uploadInProgress; + } + + private void StopTrackingUpload(UploadInProgress uploadInProgress) + { + _uploadsInProgress.Remove(uploadInProgress); + + // If this was the last upload - unlock reload assemblies and allow entering play mode + if (_uploadsInProgress.Count > 0) + return; + + EditorApplication.UnlockReloadAssemblies(); + EditorApplication.playModeStateChanged -= PreventEnteringPlayMode; + } + + private void PreventEnteringPlayMode(PlayModeStateChange change) + { + if (change != PlayModeStateChange.ExitingEditMode) + return; + + EditorApplication.ExitPlaymode(); + EditorUtility.DisplayDialog("Notice", "Entering Play Mode is not allowed while there's a package upload in progress.\n\n" + + "Please wait until the upload is finished or cancel the upload from the Asset Store Uploader window", "OK"); + } + + public void StopUploading(IPackageUploader uploader) + { + var uploadInProgress = _uploadsInProgress.FirstOrDefault(x => x.Uploader == uploader); + if (uploadInProgress == null) + return; + + uploadInProgress.CancellationTokenSource.Cancel(); + } + + public void StopAllUploadinng() + { + foreach (var uploadInProgress in _uploadsInProgress) + uploadInProgress.CancellationTokenSource.Cancel(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Api/PackageUploadingService.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/PackageUploadingService.cs.meta new file mode 100644 index 00000000000..3ea1381781b --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Api/PackageUploadingService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 22e23997fe339a74bb5355d6a88ce731 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Caching.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Caching.meta new file mode 100644 index 00000000000..7267ddb5114 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Caching.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a834946d92154754493879c5fcc7dbc9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Caching/CachingService.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Caching/CachingService.cs new file mode 100644 index 00000000000..c86ee80ea43 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Caching/CachingService.cs @@ -0,0 +1,157 @@ +using AssetStoreTools.Api.Models; +using AssetStoreTools.Uploader.Data.Serialization; +using AssetStoreTools.Utility; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.Services +{ + internal class CachingService : ICachingService + { + private VisualElement _cachedUploaderWindow; + + public bool GetCachedUploaderWindow(out VisualElement uploaderWindow) + { + uploaderWindow = _cachedUploaderWindow; + return uploaderWindow != null; + } + + public void CacheUploaderWindow(VisualElement uploaderWindow) + { + _cachedUploaderWindow = uploaderWindow; + } + + public void CacheSessionToken(string sessionToken) + { + if (string.IsNullOrEmpty(sessionToken)) + throw new ArgumentException("Session token cannot be null"); + + EditorPrefs.SetString(Constants.Cache.SessionTokenKey, sessionToken); + } + + public bool GetCachedSessionToken(out string sessionToken) + { + sessionToken = EditorPrefs.GetString(Constants.Cache.SessionTokenKey, string.Empty); + return !string.IsNullOrEmpty(sessionToken); + } + + public void ClearCachedSessionToken() + { + EditorPrefs.DeleteKey(Constants.Cache.SessionTokenKey); + } + + public bool GetCachedPackageMetadata(out List data) + { + data = new List(); + if (!CacheUtil.GetFileFromTempCache(Constants.Cache.PackageDataFileName, out var filePath)) + return false; + + try + { + var serializerSettings = new JsonSerializerSettings() + { + ContractResolver = Package.CachedPackageResolver.Instance + }; + + data = JsonConvert.DeserializeObject>(File.ReadAllText(filePath, Encoding.UTF8), serializerSettings); + return true; + } + catch + { + return false; + } + } + + public void CachePackageMetadata(List data) + { + if (data == null) + throw new ArgumentException("Package data cannot be null"); + + var serializerSettings = new JsonSerializerSettings() + { + ContractResolver = Package.CachedPackageResolver.Instance, + Formatting = Formatting.Indented + }; + + CacheUtil.CreateFileInTempCache(Constants.Cache.PackageDataFileName, JsonConvert.SerializeObject(data, serializerSettings), true); + } + + public void DeletePackageMetadata() + { + CacheUtil.DeleteFileFromTempCache(Constants.Cache.PackageDataFileName); + } + + public void UpdatePackageMetadata(Package data) + { + if (!GetCachedPackageMetadata(out var cachedData)) + return; + + var index = cachedData.FindIndex(x => x.PackageId.Equals(data.PackageId)); + if (index == -1) + { + cachedData.Add(data); + } + else + { + cachedData.RemoveAt(index); + cachedData.Insert(index, data); + } + + CachePackageMetadata(cachedData); + } + + public bool GetCachedPackageThumbnail(string packageId, out Texture2D texture) + { + texture = null; + if (!CacheUtil.GetFileFromTempCache(Constants.Cache.PackageThumbnailFileName(packageId), out var filePath)) + return false; + + texture = new Texture2D(1, 1); + texture.LoadImage(File.ReadAllBytes(filePath)); + return true; + } + + public void CachePackageThumbnail(string packageId, Texture2D texture) + { + CacheUtil.CreateFileInTempCache(Constants.Cache.PackageThumbnailFileName(packageId), texture.EncodeToPNG(), true); + } + + public bool GetCachedWorkflowStateData(string packageId, out WorkflowStateData data) + { + data = null; + + if (string.IsNullOrEmpty(packageId)) + return false; + + if (!CacheUtil.GetFileFromPersistentCache(Constants.Cache.WorkflowStateDataFileName(packageId), out var filePath)) + return false; + + try + { + data = JsonConvert.DeserializeObject(File.ReadAllText(filePath, Encoding.UTF8)); + if (string.IsNullOrEmpty(data.GetPackageId())) + return false; + } + catch + { + return false; + } + + return true; + } + + public void CacheWorkflowStateData(WorkflowStateData data) + { + if (data == null) + throw new ArgumentException("Workflow state data cannot be null"); + + CacheUtil.CreateFileInPersistentCache(Constants.Cache.WorkflowStateDataFileName(data.GetPackageId()), JsonConvert.SerializeObject(data, Formatting.Indented), true); + } + } +} diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Caching/CachingService.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Caching/CachingService.cs.meta new file mode 100644 index 00000000000..b5893a54988 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Caching/CachingService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fffaed09a3f76f945a7ececfb355f3e0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Caching/ICachingService.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/Caching/ICachingService.cs new file mode 100644 index 00000000000..1077281447a --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Caching/ICachingService.cs @@ -0,0 +1,25 @@ +using AssetStoreTools.Api.Models; +using AssetStoreTools.Uploader.Data.Serialization; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.Services +{ + internal interface ICachingService : IUploaderService + { + void CacheUploaderWindow(VisualElement uploaderWindow); + bool GetCachedUploaderWindow(out VisualElement uploaderWindow); + void CacheSessionToken(string sessionToken); + bool GetCachedSessionToken(out string sessionToken); + void ClearCachedSessionToken(); + bool GetCachedPackageMetadata(out List data); + void UpdatePackageMetadata(Package data); + void CachePackageMetadata(List data); + void DeletePackageMetadata(); + bool GetCachedPackageThumbnail(string packageId, out Texture2D texture); + void CachePackageThumbnail(string packageId, Texture2D texture); + bool GetCachedWorkflowStateData(string packageId, out WorkflowStateData data); + void CacheWorkflowStateData(WorkflowStateData data); + } +} diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/Caching/ICachingService.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/Caching/ICachingService.cs.meta new file mode 100644 index 00000000000..559ee87bf87 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/Caching/ICachingService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a904477679e07bc4889bc15e894c0c48 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/IUploaderService.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/IUploaderService.cs new file mode 100644 index 00000000000..424dd9c0baa --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/IUploaderService.cs @@ -0,0 +1,4 @@ +namespace AssetStoreTools.Uploader.Services +{ + internal interface IUploaderService { } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/IUploaderService.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/IUploaderService.cs.meta new file mode 100644 index 00000000000..7168b6f84f2 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/IUploaderService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 757d7a4dc29863740859c936be514fea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/PackageFactory.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/PackageFactory.meta new file mode 100644 index 00000000000..cb9e723a414 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/PackageFactory.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 02e4a5ee9e2fb7941b876b207078e01d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/PackageFactory/IPackageFactoryService.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/PackageFactory/IPackageFactoryService.cs new file mode 100644 index 00000000000..83c9c489f95 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/PackageFactory/IPackageFactoryService.cs @@ -0,0 +1,18 @@ +using AssetStoreTools.Uploader.Data; +using AssetStoreTools.Uploader.Data.Serialization; +using System.Collections.Generic; +using PackageModel = AssetStoreTools.Api.Models.Package; + +namespace AssetStoreTools.Uploader.Services +{ + internal interface IPackageFactoryService : IUploaderService + { + IPackageGroup CreatePackageGroup(string groupName, List packages); + IPackage CreatePackage(PackageModel packageModel); + IPackageContent CreatePackageContent(IPackage package); + List CreateWorkflows(IPackage package, WorkflowStateData stateData); + AssetsWorkflow CreateAssetsWorkflow(IPackage package, AssetsWorkflowState stateData); + UnityPackageWorkflow CreateUnityPackageWorkflow(IPackage package, UnityPackageWorkflowState stateData); + HybridPackageWorkflow CreateHybridPackageWorkflow(IPackage package, HybridPackageWorkflowState stateData); + } +} diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/PackageFactory/IPackageFactoryService.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/PackageFactory/IPackageFactoryService.cs.meta new file mode 100644 index 00000000000..1a49e7c313d --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/PackageFactory/IPackageFactoryService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 14324b71768a1ea499baa06de33f05af +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/PackageFactory/PackageFactoryService.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/PackageFactory/PackageFactoryService.cs new file mode 100644 index 00000000000..a9fffa5ee73 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/PackageFactory/PackageFactoryService.cs @@ -0,0 +1,95 @@ +using AssetStoreTools.Uploader.Data; +using AssetStoreTools.Uploader.Data.Serialization; +using AssetStoreTools.Uploader.Services.Analytics; +using AssetStoreTools.Uploader.Services.Api; +using System.Collections.Generic; +using PackageModel = AssetStoreTools.Api.Models.Package; + +namespace AssetStoreTools.Uploader.Services +{ + internal class PackageFactoryService : IPackageFactoryService + { + private IWorkflowServices _workflowServices; + + // Service dependencies + private ICachingService _cachingService; + private IPackageDownloadingService _packageDownloadingService; + private IPackageUploadingService _packageUploadingService; + private IAnalyticsService _analyticsService; + + public PackageFactoryService( + ICachingService cachingService, + IPackageDownloadingService packageDownloadingService, + IPackageUploadingService packageUploadingService, + IAnalyticsService analyticsService + ) + { + _cachingService = cachingService; + _packageDownloadingService = packageDownloadingService; + _packageUploadingService = packageUploadingService; + _analyticsService = analyticsService; + + _workflowServices = new WorkflowServices(_packageDownloadingService, _packageUploadingService, _analyticsService); + } + + public IPackage CreatePackage(PackageModel packageModel) + { + var package = new Package(packageModel); + return package; + } + + public IPackageGroup CreatePackageGroup(string groupName, List packages) + { + return new PackageGroup(groupName, packages); + } + + public IPackageContent CreatePackageContent(IPackage package) + { + if (!package.IsDraft) + return null; + + WorkflowStateData stateData = GetOrCreateWorkflowStateData(package); + + var workflows = CreateWorkflows(package, stateData); + var packageContent = new PackageContent(workflows, stateData, _cachingService); + return packageContent; + } + + public List CreateWorkflows(IPackage package, WorkflowStateData stateData) + { + var workflows = new List + { + CreateAssetsWorkflow(package, stateData.GetAssetsWorkflowState()), + CreateUnityPackageWorkflow(package, stateData.GetUnityPackageWorkflowState()), +#if UNITY_ASTOOLS_EXPERIMENTAL + CreateHybridPackageWorkflow(package, stateData.GetHybridPackageWorkflowState()), +#endif + }; + + return workflows; + } + + public AssetsWorkflow CreateAssetsWorkflow(IPackage package, AssetsWorkflowState stateData) + { + return new AssetsWorkflow(package, stateData, _workflowServices); + } + + public UnityPackageWorkflow CreateUnityPackageWorkflow(IPackage package, UnityPackageWorkflowState stateData) + { + return new UnityPackageWorkflow(package, stateData, _workflowServices); + } + + public HybridPackageWorkflow CreateHybridPackageWorkflow(IPackage package, HybridPackageWorkflowState stateData) + { + return new HybridPackageWorkflow(package, stateData, _workflowServices); + } + + private WorkflowStateData GetOrCreateWorkflowStateData(IPackage package) + { + if (!_cachingService.GetCachedWorkflowStateData(package.PackageId, out var stateData)) + stateData = new WorkflowStateData(package.PackageId); + + return stateData; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/PackageFactory/PackageFactoryService.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/PackageFactory/PackageFactoryService.cs.meta new file mode 100644 index 00000000000..b99429f4291 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/PackageFactory/PackageFactoryService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4074a5b21b6201d449974dcfb652a00b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/UploaderServiceProvider.cs b/.github/Asset Store Tools/Uploader/Scripts/Services/UploaderServiceProvider.cs new file mode 100644 index 00000000000..0aaa6866a44 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/UploaderServiceProvider.cs @@ -0,0 +1,26 @@ +using AssetStoreTools.Api; +using AssetStoreTools.Uploader.Services.Analytics; +using AssetStoreTools.Uploader.Services.Api; +using AssetStoreTools.Utility; + +namespace AssetStoreTools.Uploader.Services +{ + internal class UploaderServiceProvider : ServiceProvider + { + public static UploaderServiceProvider Instance => _instance ?? (_instance = new UploaderServiceProvider()); + private static UploaderServiceProvider _instance; + + private UploaderServiceProvider() { } + + protected override void RegisterServices() + { + var api = new AssetStoreApi(new AssetStoreClient()); + Register(); + Register(); + Register(() => new AuthenticationService(api, GetService(), GetService())); + Register(() => new PackageDownloadingService(api, GetService())); + Register(() => new PackageUploadingService(api)); + Register(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/Services/UploaderServiceProvider.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/Services/UploaderServiceProvider.cs.meta new file mode 100644 index 00000000000..ccf2e7ba96a --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/Services/UploaderServiceProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e66f9c7f198baff41ba77f4d0ed7b60f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI.meta b/.github/Asset Store Tools/Uploader/Scripts/UI.meta new file mode 100644 index 00000000000..e39a22fb358 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ed759a6e886dbfd4fbcecc2beb7248b8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements.meta new file mode 100644 index 00000000000..497fc1d7caf --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d14df9cf4e7e9b54c8c94a8cc1aa70c0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/Abstractions.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/Abstractions.meta new file mode 100644 index 00000000000..9ec9955eedc --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/Abstractions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 144a518ff26df1e41845217c0f0002d7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/Abstractions/ValidationElementBase.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/Abstractions/ValidationElementBase.cs new file mode 100644 index 00000000000..27957b2c5c4 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/Abstractions/ValidationElementBase.cs @@ -0,0 +1,170 @@ +using AssetStoreTools.Uploader.Data; +using AssetStoreTools.Validator.Data; +using System.Linq; +using UnityEditor; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal abstract class ValidationElementBase : VisualElement + { + // Data + protected IWorkflow Workflow; + + // UI + protected VisualElement ResultsBox; + protected Image ResultsBoxImage; + protected Label ResultsBoxLabel; + + protected ValidationElementBase(IWorkflow workflow) + { + Workflow = workflow; + Create(); + } + + private void Create() + { + CreateInfoRow(); + CreateResultsBox(); + } + + private void CreateInfoRow() + { + VisualElement validatorButtonRow = new VisualElement(); + validatorButtonRow.AddToClassList("package-content-option-box"); + + VisualElement validatorLabelHelpRow = new VisualElement(); + validatorLabelHelpRow.AddToClassList("package-content-option-label-help-row"); + + Label validatorLabel = new Label { text = "Validation" }; + Image validatorLabelTooltip = new Image + { + tooltip = "You can use the Asset Store Validator to check your package for common publishing issues" + }; + + var validateButton = new Button(Validate) { name = "ValidateButton", text = "Validate" }; + + validatorLabelHelpRow.Add(validatorLabel); + validatorLabelHelpRow.Add(validatorLabelTooltip); + + validatorButtonRow.Add(validatorLabelHelpRow); + validatorButtonRow.Add(validateButton); + + Add(validatorButtonRow); + } + + private void CreateResultsBox() + { + ResultsBox = new Box { name = "InfoBox" }; + ResultsBox.style.display = DisplayStyle.None; + ResultsBox.AddToClassList("validation-result-box"); + + ResultsBoxImage = new Image(); + ResultsBoxLabel = new Label { name = "ValidationLabel" }; + + ResultsBox.Add(ResultsBoxImage); + ResultsBox.Add(ResultsBoxLabel); + + Add(ResultsBox); + } + + protected virtual bool ConfirmValidation() + { + // Child classes can implement pre-validation prompts + return true; + } + + private void Validate() + { + if (!ConfirmValidation()) + return; + + var validationResult = Workflow.Validate(); + + if (validationResult.Status == ValidationStatus.Cancelled) + return; + + if (validationResult.Status != ValidationStatus.RanToCompletion) + { + EditorUtility.DisplayDialog("Validation failed", $"Package validation failed: {validationResult.Exception.Message}", "OK"); + return; + } + + DisplayResult(validationResult); + } + + private void DisplayResult(ValidationResult result) + { + ResultsBox.style.display = DisplayStyle.Flex; + UpdateValidationResultImage(result); + UpdateValidationResultLabel(result); + } + + public void HideResult() + { + ResultsBox.style.display = DisplayStyle.None; + } + + protected void UpdateValidationResultImage(ValidationResult result) + { + switch (GetValidationSummaryStatus(result)) + { + case TestResultStatus.Pass: + ResultsBoxImage.image = EditorGUIUtility.IconContent("console.infoicon@2x").image; + break; + case TestResultStatus.Warning: + ResultsBoxImage.image = EditorGUIUtility.IconContent("console.warnicon@2x").image; + break; + case TestResultStatus.Fail: + ResultsBoxImage.image = EditorGUIUtility.IconContent("console.erroricon@2x").image; + break; + default: + ResultsBoxImage.image = EditorGUIUtility.IconContent("_Help@2x").image; + break; + } + } + + private void UpdateValidationResultLabel(ValidationResult result) + { + var errorCount = result.Tests.Where(x => x.Result.Status == TestResultStatus.Fail).Count(); + var warningCount = result.Tests.Where(x => x.Result.Status == TestResultStatus.Warning).Count(); + + string text = string.Empty; + if (result.HadCompilationErrors) + { + text += "- Package caused compilation errors\n"; + } + if (errorCount > 0) + { + text += $"- Validation reported {errorCount} error(s)\n"; + } + if (warningCount > 0) + { + text += $"- Validation reported {warningCount} warning(s)\n"; + } + + if (string.IsNullOrEmpty(text)) + { + text = "No issues were found!"; + } + else + { + text = text.Substring(0, text.Length - "\n".Length); + } + + ResultsBoxLabel.text = text; + } + + private TestResultStatus GetValidationSummaryStatus(ValidationResult result) + { + if (result.HadCompilationErrors || + result.Tests.Any(x => x.Result.Status == TestResultStatus.Fail)) + return TestResultStatus.Fail; + + if (result.Tests.Any(x => x.Result.Status == TestResultStatus.Warning)) + return TestResultStatus.Warning; + + return TestResultStatus.Pass; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/Abstractions/ValidationElementBase.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/Abstractions/ValidationElementBase.cs.meta new file mode 100644 index 00000000000..f96190a6804 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/Abstractions/ValidationElementBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cb20404763eac7144b562c18ad1c37fe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/Abstractions/WorkflowElementBase.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/Abstractions/WorkflowElementBase.cs new file mode 100644 index 00000000000..f4b22b78026 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/Abstractions/WorkflowElementBase.cs @@ -0,0 +1,151 @@ +using AssetStoreTools.Uploader.Data; +using AssetStoreTools.Utility; +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal abstract class WorkflowElementBase : VisualElement + { + // Data + protected IWorkflow Workflow; + public string Name => Workflow.Name; + public string DisplayName => Workflow.DisplayName; + + // UI Elements that all workflows have + protected PathSelectionElement PathSelectionElement; + protected PreviewGenerationElement PreviewGenerationElement; + protected ValidationElementBase ValidationElement; + protected PackageUploadElement UploadElement; + + public event Action OnInteractionAvailable; + public event Action OnInteractionUnavailable; + + public WorkflowElementBase(IWorkflow workflow) + { + Workflow = workflow; + } + + protected void CreatePathElement(string labelText, string labelTooltip) + { + PathSelectionElement = new PathSelectionElement(labelText, labelTooltip); + PathSelectionElement.OnBrowse += BrowsePath; + Add(PathSelectionElement); + } + + protected void CreatePreviewGenerationElement() + { + PreviewGenerationElement = new PreviewGenerationElement(Workflow); + PreviewGenerationElement.style.display = DisplayStyle.None; + var callback = new Action(() => + PreviewGenerationElement.style.display = ASToolsPreferences.Instance.UseLegacyExporting + ? DisplayStyle.None + : DisplayStyle.Flex); + RegisterCallback((_) => { ASToolsPreferences.OnSettingsChange += callback; }); + RegisterCallback((_) => { ASToolsPreferences.OnSettingsChange -= callback; }); + Add(PreviewGenerationElement); + } + + protected void CreateValidationElement(ValidationElementBase validationElement) + { + ValidationElement = validationElement; + ValidationElement.style.display = DisplayStyle.None; + Add(ValidationElement); + } + + protected void CreateUploadElement(IWorkflow workflow, bool exposeExportButton) + { + UploadElement = new PackageUploadElement(workflow, exposeExportButton); + UploadElement.OnInteractionAvailable += EnableInteraction; + UploadElement.OnInteractionUnavailable += DisableInteraction; + UploadElement.style.display = DisplayStyle.None; + Add(UploadElement); + } + + protected abstract void BrowsePath(); + + protected void SetPathSelectionTextField(string value) + { + if (string.IsNullOrEmpty(value)) + return; + + PathSelectionElement.SetPath(value); + ValidationElement.style.display = DisplayStyle.Flex; + UploadElement.style.display = DisplayStyle.Flex; + + if (PreviewGenerationElement != null && !ASToolsPreferences.Instance.UseLegacyExporting) + { + PreviewGenerationElement.style.display = DisplayStyle.Flex; + } + } + + protected void CheckForMissingMetas(IEnumerable paths) + { + bool displayDialog = ASToolsPreferences.Instance.DisplayHiddenMetaDialog && FileUtility.IsMissingMetaFiles(paths); + if (!displayDialog) + return; + + var selectedOption = EditorUtility.DisplayDialogComplex( + "Notice", + "Your package includes hidden folders which do not contain meta files. " + + "Hidden folders will not be exported unless they contain meta files.\n\nWould you like meta files to be generated?", + "Yes", "No", "No and do not display this again"); + + switch (selectedOption) + { + case 0: + try + { + FileUtility.GenerateMetaFiles(paths); + EditorUtility.DisplayDialog( + "Success", + "Meta files have been generated. Please note that further manual tweaking may be required to set up correct references", + "OK"); + } + catch (Exception e) + { + EditorUtility.DisplayDialog( + "Error", + $"Meta file generation failed: {e.Message}", + "OK" + ); + } + break; + case 1: + // Do nothing + return; + case 2: + ASToolsPreferences.Instance.DisplayHiddenMetaDialog = false; + ASToolsPreferences.Instance.Save(); + return; + } + } + + public bool Is(IWorkflow workflow) + { + return Workflow == workflow; + } + + protected virtual void EnableInteraction() + { + PathSelectionElement.SetEnabled(true); + ValidationElement.SetEnabled(true); + PreviewGenerationElement?.SetEnabled(true); + UploadElement.SetEnabled(true); + OnInteractionAvailable?.Invoke(); + } + + protected virtual void DisableInteraction() + { + PathSelectionElement.SetEnabled(false); + ValidationElement.SetEnabled(false); + PreviewGenerationElement?.SetEnabled(false); + UploadElement.SetEnabled(false); + OnInteractionUnavailable?.Invoke(); + } + + protected abstract void Deserialize(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/Abstractions/WorkflowElementBase.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/Abstractions/WorkflowElementBase.cs.meta new file mode 100644 index 00000000000..bdd8416f9d6 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/Abstractions/WorkflowElementBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 45d1bf267c3ea9048bfdd75d0d19c8bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/AccountToolbar.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/AccountToolbar.cs new file mode 100644 index 00000000000..393c4aa6a1d --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/AccountToolbar.cs @@ -0,0 +1,102 @@ +using AssetStoreTools.Api.Models; +using System; +using System.Threading.Tasks; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal class AccountToolbar : VisualElement + { + private Image _accountImage; + private Label _accountEmailLabel; + private Button _refreshButton; + + public event Func OnRefresh; + public event Action OnLogout; + + public AccountToolbar() + { + Create(); + } + + private void Create() + { + AddToClassList("account-toolbar"); + + // Left side of the toolbar + VisualElement leftSideContainer = new VisualElement { name = "LeftSideContainer" }; + leftSideContainer.AddToClassList("account-toolbar-left-side-container"); + + _accountImage = new Image(); + _accountImage.AddToClassList("account-toolbar-user-image"); + + _accountEmailLabel = new Label() { name = "AccountEmail" }; + _accountEmailLabel.AddToClassList("account-toolbar-email-label"); + + leftSideContainer.Add(_accountImage); + leftSideContainer.Add(_accountEmailLabel); + + // Right side of the toolbar + VisualElement rightSideContainer = new VisualElement { name = "RightSideContainer" }; + rightSideContainer.AddToClassList("account-toolbar-right-side-container"); + + // Refresh button + _refreshButton = new Button(Refresh) { name = "RefreshButton", text = "Refresh" }; + _refreshButton.AddToClassList("account-toolbar-button-refresh"); + + // Logout button + var logoutButton = new Button(Logout) { name = "LogoutButton", text = "Log out" }; + logoutButton.AddToClassList("account-toolbar-button-logout"); + + rightSideContainer.Add(_refreshButton); + rightSideContainer.Add(logoutButton); + + // Constructing the final toolbar + Add(leftSideContainer); + Add(rightSideContainer); + } + + private async void Refresh() + { + _refreshButton.SetEnabled(false); + await OnRefresh?.Invoke(); + _refreshButton.SetEnabled(true); + } + + private void Logout() + { + OnLogout?.Invoke(); + } + + public void SetUser(User user) + { + if (user == null) + { + _accountEmailLabel.text = string.Empty; + _accountImage.tooltip = string.Empty; + return; + } + + var userEmail = !string.IsNullOrEmpty(user.Username) ? user.Username : "Unknown"; + var publisherName = !string.IsNullOrEmpty(user.Name) ? user.Name : "Unknown"; + var publisherId = !string.IsNullOrEmpty(user.PublisherId) ? user.PublisherId : "Unknown"; + var userInfo = + $"Username: {userEmail}\n" + + $"Publisher Name: {publisherName}\n" + + $"Publisher ID: {publisherId}"; + + _accountEmailLabel.text = userEmail; + _accountImage.tooltip = userInfo; + } + + public void EnableButtons() + { + _refreshButton.SetEnabled(true); + } + + public void DisableButtons() + { + _refreshButton.SetEnabled(false); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/AccountToolbar.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/AccountToolbar.cs.meta new file mode 100644 index 00000000000..8ac16ee7e1f --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/AccountToolbar.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3c275be3817d1684ca1802c2738ac4d9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/AssetsWorkflowElement.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/AssetsWorkflowElement.cs new file mode 100644 index 00000000000..283017d4cc1 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/AssetsWorkflowElement.cs @@ -0,0 +1,215 @@ +using AssetStoreTools.Uploader.Data; +using AssetStoreTools.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal class AssetsWorkflowElement : WorkflowElementBase + { + // Data + private AssetsWorkflow _workflow; + + // UI + private VisualElement _dependenciesToggleElement; + private Toggle _dependenciesToggle; + private MultiToggleSelectionElement _dependenciesElement; + private MultiToggleSelectionElement _specialFoldersElement; + + private const string PathSelectionTooltip = "Select the main folder of your package" + + "\n\nAll files and folders of your package should preferably be contained within a single root folder that is named after your package" + + "\n\nExample: 'Assets/[MyPackageName]'" + + "\n\nNote: If your content makes use of special folders that are required to be placed in the root Assets folder (e.g. 'StreamingAssets')," + + " you will be able to include them after selecting the main folder"; + + public AssetsWorkflowElement(AssetsWorkflow workflow) : base(workflow) + { + _workflow = workflow; + + Create(); + Deserialize(); + } + + private void Create() + { + CreatePathElement("Folder path", PathSelectionTooltip); + CreateDependenciesToggleElement(); + CreateDependenciesSelectionElement(); + CreateSpecialFoldersElement(); + CreatePreviewGenerationElement(); + CreateValidationElement(new CurrentProjectValidationElement(_workflow)); + CreateUploadElement(_workflow, true); + } + + private void CreateDependenciesToggleElement() + { + _dependenciesToggleElement = new VisualElement() { name = "Dependencies Toggle" }; + _dependenciesToggleElement.AddToClassList("package-content-option-box"); + + VisualElement dependenciesLabelHelpRow = new VisualElement(); + dependenciesLabelHelpRow.AddToClassList("package-content-option-label-help-row"); + + Label dependenciesLabel = new Label { text = "Dependencies" }; + Image dependenciesLabelTooltip = new Image + { + tooltip = "Tick this checkbox if your package content has dependencies on Unity packages from the Package Manager" + }; + + _dependenciesToggle = new Toggle { name = "DependenciesToggle", text = "Include Package Manifest" }; + _dependenciesToggle.AddToClassList("package-content-option-toggle"); + + var callback = new Action(() => DependencyToggleValueChange(true)); + _dependenciesToggle.RegisterValueChangedCallback((_) => DependencyToggleValueChange(true)); + RegisterCallback((_) => { ASToolsPreferences.OnSettingsChange += callback; }); + RegisterCallback((_) => { ASToolsPreferences.OnSettingsChange -= callback; }); + + dependenciesLabelHelpRow.Add(dependenciesLabel); + dependenciesLabelHelpRow.Add(dependenciesLabelTooltip); + + _dependenciesToggleElement.Add(dependenciesLabelHelpRow); + _dependenciesToggleElement.Add(_dependenciesToggle); + + _dependenciesToggleElement.style.display = DisplayStyle.None; + Add(_dependenciesToggleElement); + } + + private void CreateDependenciesSelectionElement() + { + _dependenciesElement = new MultiToggleSelectionElement() + { + DisplayElementLabel = false, + ElementLabel = "Dependencies", + NoSelectionLabel = "No packages match this criteria" + }; + + var setDependencies = new Action>((dict) => _workflow.SetDependencies(dict.Where(x => x.Value).Select(x => x.Key), true)); + _dependenciesElement.OnValuesChanged += setDependencies; + _dependenciesElement.style.display = DisplayStyle.None; + Add(_dependenciesElement); + } + + private void CreateSpecialFoldersElement() + { + _specialFoldersElement = new MultiToggleSelectionElement() + { + ElementLabel = "Special Folders", + ElementTooltip = "If your package content relies on Special Folders (e.g. StreamingAssets), please select which of these folders should be included in the package.", + NoSelectionLabel = "No folders match this criteria." + }; + + var setSpecialFolders = new Action>((dict) => _workflow.SetSpecialFolders(dict.Where(x => x.Value).Select(x => x.Key), true)); + _specialFoldersElement.OnValuesChanged += setSpecialFolders; + _specialFoldersElement.style.display = DisplayStyle.None; + Add(_specialFoldersElement); + } + + protected override void BrowsePath() + { + string absoluteExportPath = string.Empty; + bool includeAllAssets = false; + + if (_workflow.IsCompleteProject) + { + includeAllAssets = EditorUtility.DisplayDialog("Notice", + "Your package draft is set to a category that is treated" + + " as a complete project. Project settings will be included automatically. Would you like everything in the " + + "'Assets' folder to be included?\n\nYou will still be able to change the selected assets before uploading", + "Yes, include all folders and assets", + "No, I'll select what to include manually"); + + if (includeAllAssets) + absoluteExportPath = Application.dataPath; + } + + if (!includeAllAssets) + { + absoluteExportPath = EditorUtility.OpenFolderPanel( + "Select folder to compress into a package", "Assets/", ""); + + if (string.IsNullOrEmpty(absoluteExportPath)) + return; + } + + var relativeExportPath = FileUtility.AbsolutePathToRelativePath(absoluteExportPath, ASToolsPreferences.Instance.EnableSymlinkSupport); + if (!_workflow.IsPathValid(relativeExportPath, out var error)) + { + EditorUtility.DisplayDialog("Invalid selection", error, "OK"); + return; + } + + HandlePathSelection(relativeExportPath, true); + CheckForMissingMetas(); + } + + private void HandlePathSelection(string relativeExportPath, bool serialize) + { + if (string.IsNullOrEmpty(relativeExportPath)) + return; + + _workflow.SetMainExportPath(relativeExportPath, serialize); + SetPathSelectionTextField(relativeExportPath + "/"); + + _dependenciesToggleElement.style.display = DisplayStyle.Flex; + UpdateSpecialFoldersElement(); + } + + private void CheckForMissingMetas() + { + var paths = new List() { _workflow.GetMainExportPath() }; + paths.AddRange(_workflow.GetSpecialFolders()); + CheckForMissingMetas(paths); + } + + private void DependencyToggleValueChange(bool serialize) + { + _workflow.SetIncludeDependencies(_dependenciesToggle.value, serialize); + + if (_dependenciesToggle.value && !ASToolsPreferences.Instance.UseLegacyExporting) + { + var allDependencies = _workflow.GetAvailableDependencies(); + var selectedDependencies = allDependencies.ToDictionary(x => x, y => _workflow.GetDependencies().Any(x => x.name == y)); + _dependenciesElement.Populate(selectedDependencies); + _dependenciesElement.style.display = DisplayStyle.Flex; + } + else + { + _dependenciesElement.style.display = DisplayStyle.None; + } + } + + private void UpdateSpecialFoldersElement() + { + var availableSpecialFolders = _workflow.GetAvailableSpecialFolders(); + var selectedSpecialFolders = availableSpecialFolders.ToDictionary(x => x, y => _workflow.GetSpecialFolders().Any(x => x == y)); + _specialFoldersElement.Populate(selectedSpecialFolders); + _specialFoldersElement.style.display = availableSpecialFolders.Count > 0 ? DisplayStyle.Flex : DisplayStyle.None; + } + + protected override void EnableInteraction() + { + base.EnableInteraction(); + _dependenciesToggleElement.SetEnabled(true); + _dependenciesElement.SetEnabled(true); + _specialFoldersElement.SetEnabled(true); + } + + protected override void DisableInteraction() + { + base.DisableInteraction(); + _dependenciesToggleElement.SetEnabled(false); + _dependenciesElement.SetEnabled(false); + _specialFoldersElement.SetEnabled(false); + } + + protected override void Deserialize() + { + HandlePathSelection(_workflow.GetMainExportPath(), false); + _dependenciesToggle.SetValueWithoutNotify(_workflow.GetIncludeDependencies()); + DependencyToggleValueChange(false); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/AssetsWorkflowElement.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/AssetsWorkflowElement.cs.meta new file mode 100644 index 00000000000..90d9e5adb14 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/AssetsWorkflowElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1f62ea8ab5c102e4fa574a3dcac7f6fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/CurrentProjectValidationElement.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/CurrentProjectValidationElement.cs new file mode 100644 index 00000000000..adfeab1f15b --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/CurrentProjectValidationElement.cs @@ -0,0 +1,31 @@ +using AssetStoreTools.Uploader.Data; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal class CurrentProjectValidationElement : ValidationElementBase + { + public CurrentProjectValidationElement(IWorkflow workflow) : base(workflow) + { + Create(); + } + + private void Create() + { + CreateResultsBox(); + } + + private void CreateResultsBox() + { + var _viewReportButton = new Button(ViewReport) { text = "View report" }; + _viewReportButton.AddToClassList("validation-result-view-report-button"); + + ResultsBox.Add(_viewReportButton); + } + + private void ViewReport() + { + AssetStoreTools.ShowAssetStoreToolsValidator(Workflow.LastValidationSettings, Workflow.LastValidationResult); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/CurrentProjectValidationElement.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/CurrentProjectValidationElement.cs.meta new file mode 100644 index 00000000000..4acf35f00d3 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/CurrentProjectValidationElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 21a1f13231b167b4c80079a2c1212101 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/ExternalProjectValidationElement.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/ExternalProjectValidationElement.cs new file mode 100644 index 00000000000..12c596b980e --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/ExternalProjectValidationElement.cs @@ -0,0 +1,92 @@ +using AssetStoreTools.Uploader.Data; +using AssetStoreTools.Utility; +using AssetStoreTools.Validator; +using System; +using System.IO; +using UnityEditor; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal class ExternalProjectValidationElement : ValidationElementBase + { + private VisualElement _projectButtonContainer; + + public ExternalProjectValidationElement(IWorkflow workflow) : base(workflow) + { + Create(); + } + + private void Create() + { + CreateProjectButtonContainer(); + CreateProjectButtons(); + } + + private void CreateProjectButtonContainer() + { + _projectButtonContainer = new VisualElement(); + _projectButtonContainer.AddToClassList("validation-result-view-report-button-container"); + + ResultsBox.Add(_projectButtonContainer); + } + + private void CreateProjectButtons() + { + var openButton = new Button(OpenProject) { text = "Open Project" }; + openButton.AddToClassList("validation-result-view-report-button"); + + var saveButton = new Button(SaveProject) { text = "Save Project" }; + saveButton.AddToClassList("validation-result-view-report-button"); + + _projectButtonContainer.Add(openButton); + _projectButtonContainer.Add(saveButton); + } + + private void OpenProject() + { + try + { + EditorUtility.DisplayProgressBar("Waiting...", "Validation project is open. Waiting for it to exit...", 0.4f); + var projectPath = Workflow.LastValidationResult.ProjectPath; + ExternalProjectValidator.OpenExternalValidationProject(projectPath); + } + finally + { + EditorUtility.ClearProgressBar(); + } + } + + private void SaveProject() + { + try + { + var projectPath = Workflow.LastValidationResult.ProjectPath; + var savePath = EditorUtility.SaveFolderPanel("Select a folder", Environment.GetFolderPath(Environment.SpecialFolder.Desktop), string.Empty); + if (string.IsNullOrEmpty(savePath)) + return; + + var saveDir = new DirectoryInfo(savePath); + if (!saveDir.Exists || saveDir.GetFileSystemInfos().Length != 0) + { + EditorUtility.DisplayDialog("Saving project failed", "Selected directory must be an empty folder", "OK"); + return; + } + + EditorUtility.DisplayProgressBar("Saving...", "Saving project...", 0.4f); + FileUtility.CopyDirectory(projectPath, savePath, true); + } + finally + { + EditorUtility.ClearProgressBar(); + } + } + + protected override bool ConfirmValidation() + { + return EditorUtility.DisplayDialog("Notice", "Pre-exported package validation is performed in a separate temporary project. " + + "It may take some time for the temporary project to be created, which will halt any actions in the current project. " + + "The current project will resume work after the temporary project is exited.\n\nDo you wish to proceed?", "Yes", "No"); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/ExternalProjectValidationElement.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/ExternalProjectValidationElement.cs.meta new file mode 100644 index 00000000000..63b8bea256a --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/ExternalProjectValidationElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 706f01d53c7eaf04bae07fb36684e31b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/HybridPackageWorkflowElement.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/HybridPackageWorkflowElement.cs new file mode 100644 index 00000000000..75981425325 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/HybridPackageWorkflowElement.cs @@ -0,0 +1,116 @@ +using AssetStoreTools.Uploader.Data; +using AssetStoreTools.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal class HybridPackageWorkflowElement : WorkflowElementBase + { + // Data + private HybridPackageWorkflow _workflow; + + // UI + private MultiToggleSelectionElement _dependenciesElement; + + public HybridPackageWorkflowElement(HybridPackageWorkflow workflow) : base(workflow) + { + _workflow = workflow; + + Create(); + Deserialize(); + } + + private void Create() + { + CreatePathElement("Package path", "Select a local Package you would like to export and upload to the Store."); + CreateDependenciesElement(); + CreatePreviewGenerationElement(); + CreateValidationElement(new CurrentProjectValidationElement(_workflow)); + CreateUploadElement(_workflow, true); + } + + private void CreateDependenciesElement() + { + _dependenciesElement = new MultiToggleSelectionElement() + { + ElementLabel = "Dependencies", + ElementTooltip = "Select which local package dependencies should be included when exporting." + + "\n\nNote that only local or embedded dependencies defined in the package.json can be selected.", + NoSelectionLabel = "No packages match this criteria" + }; + + var setDependencies = new Action>((dict) => _workflow.SetDependencies(dict.Where(x => x.Value).Select(x => x.Key), true)); + _dependenciesElement.OnValuesChanged += setDependencies; + Add(_dependenciesElement); + _dependenciesElement.style.display = DisplayStyle.None; + } + + protected override void BrowsePath() + { + var absoluteExportPath = EditorUtility.OpenFilePanel("Select a package.json file", "Packages/", "json"); + if (string.IsNullOrEmpty(absoluteExportPath)) + return; + + if (!_workflow.IsPathValid(absoluteExportPath, out var error)) + { + EditorUtility.DisplayDialog("Invalid selection", error, "OK"); + return; + } + + HandlePathSelection(absoluteExportPath, true); + CheckForMissingMetas(); + } + + private void HandlePathSelection(string packageManifestPath, bool serialize) + { + if (string.IsNullOrEmpty(packageManifestPath)) + return; + + _workflow.SetPackage(packageManifestPath, serialize); + var packageFolderPath = _workflow.GetPackage().assetPath; + SetPathSelectionTextField(packageFolderPath + "/"); + + UpdateDependenciesElement(); + } + + private void CheckForMissingMetas() + { + var paths = new List() { _workflow.GetPackage().assetPath }; + paths.AddRange(_workflow.GetDependencies().Select(x => x.assetPath)); + CheckForMissingMetas(paths); + } + + private void UpdateDependenciesElement() + { + var availableDependencies = _workflow.GetAvailableDependencies(); + var selectedDependencies = availableDependencies.ToDictionary(x => x.name, y => _workflow.GetDependencies().Any(x => x.name == y.name)); + _dependenciesElement.Populate(selectedDependencies); + _dependenciesElement.style.display = availableDependencies.Count > 0 ? DisplayStyle.Flex : DisplayStyle.None; + } + + protected override void EnableInteraction() + { + base.EnableInteraction(); + _dependenciesElement.SetEnabled(true); + } + + protected override void DisableInteraction() + { + base.DisableInteraction(); + _dependenciesElement.SetEnabled(false); + } + + protected override void Deserialize() + { + var package = _workflow.GetPackage(); + if (package == null) + return; + + HandlePathSelection(AssetDatabase.GetAssetPath(package.GetManifestAsset()), false); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/HybridPackageWorkflowElement.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/HybridPackageWorkflowElement.cs.meta new file mode 100644 index 00000000000..53ad66c2914 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/HybridPackageWorkflowElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 34cd1e5cbe87bb546937a521bd2bc69c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/LoadingSpinner.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/LoadingSpinner.cs new file mode 100644 index 00000000000..b2ad70b196d --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/LoadingSpinner.cs @@ -0,0 +1,52 @@ +using UnityEditor; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal class LoadingSpinner : VisualElement + { + // Data + private int _spinIndex; + private double _spinTimer; + private double _spinThreshold = 0.1; + + // UI + private Image _spinnerImage; + + public LoadingSpinner() + { + AddToClassList("loading-spinner-box"); + + _spinnerImage = new Image { name = "SpinnerImage" }; + _spinnerImage.AddToClassList("loading-spinner-image"); + + Add(_spinnerImage); + } + + public void Show() + { + EditorApplication.update += SpinnerLoop; + style.display = DisplayStyle.Flex; + } + + public void Hide() + { + EditorApplication.update -= SpinnerLoop; + style.display = DisplayStyle.None; + } + + private void SpinnerLoop() + { + if (_spinTimer + _spinThreshold > EditorApplication.timeSinceStartup) + return; + + _spinTimer = EditorApplication.timeSinceStartup; + _spinnerImage.image = EditorGUIUtility.IconContent($"WaitSpin{_spinIndex:00}").image; + + _spinIndex += 1; + + if (_spinIndex > 11) + _spinIndex = 0; + } + } +} diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/LoadingSpinner.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/LoadingSpinner.cs.meta new file mode 100644 index 00000000000..78031dc1875 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/LoadingSpinner.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7c7cdef91eb9a894091869ca10d9d178 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/MultiToggleSelectionElement.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/MultiToggleSelectionElement.cs new file mode 100644 index 00000000000..175a095e15a --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/MultiToggleSelectionElement.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal class MultiToggleSelectionElement : VisualElement + { + // Data + private Dictionary _selections; + private readonly List _selectionFilters = new List { "All", "Selected", "Not Selected" }; + private string _activeFilter; + + public bool DisplayElementLabel + { + get => _multiToggleSelectionHelpRow.style.visibility == Visibility.Visible; + set { _multiToggleSelectionHelpRow.style.visibility = value ? Visibility.Visible : Visibility.Hidden; } + } + + public string ElementLabel { get => _multiToggleSelectionLabel.text; set { _multiToggleSelectionLabel.text = value; } } + public string ElementTooltip { get => _multiToggleSelectionTooltip.tooltip; set { _multiToggleSelectionTooltip.tooltip = value; } } + public string NoSelectionLabel { get => _noSelectionsLabel.text; set { _noSelectionsLabel.text = value; } } + + // UI + private VisualElement _multiToggleSelectionHelpRow; + private Label _multiToggleSelectionLabel; + private Image _multiToggleSelectionTooltip; + + private ScrollView _selectionTogglesBox; + private Label _noSelectionsLabel; + private ToolbarMenu _filteringDropdown; + + public event Action> OnValuesChanged; + + public MultiToggleSelectionElement() + { + _activeFilter = _selectionFilters[0]; + AddToClassList("package-content-option-box"); + Create(); + } + + private void Create() + { + _multiToggleSelectionHelpRow = new VisualElement(); + _multiToggleSelectionHelpRow.AddToClassList("package-content-option-label-help-row"); + + _multiToggleSelectionLabel = new Label(); + _multiToggleSelectionTooltip = new Image(); + + VisualElement fullSelectionBox = new VisualElement(); + fullSelectionBox.AddToClassList("multi-toggle-box"); + + _selectionTogglesBox = new ScrollView { name = "DependencyToggles" }; + _selectionTogglesBox.AddToClassList("multi-toggle-box-scrollview"); + + _noSelectionsLabel = new Label(); + _noSelectionsLabel.AddToClassList("multi-toggle-box-empty-label"); + + var scrollContainer = _selectionTogglesBox.Q("unity-content-viewport"); + scrollContainer.Add(_noSelectionsLabel); + + VisualElement filteringBox = new VisualElement(); + filteringBox.AddToClassList("multi-toggle-box-toolbar"); + + // Select - deselect buttons + VisualElement selectingBox = new VisualElement(); + selectingBox.AddToClassList("multi-toggle-box-toolbar-selecting-box"); + + Button selectAllButton = new Button(SelectAllToggles) + { + text = "Select All" + }; + + Button deSelectAllButton = new Button(UnselectAllToggles) + { + text = "Deselect All" + }; + + selectingBox.Add(selectAllButton); + selectingBox.Add(deSelectAllButton); + + // Filtering dropdown + VisualElement filteringDropdownBox = new VisualElement(); + filteringDropdownBox.AddToClassList("multi-toggle-box-toolbar-filtering-box"); + + _filteringDropdown = new ToolbarMenu { text = _selectionFilters[0] }; + + foreach (var filter in _selectionFilters) + _filteringDropdown.menu.AppendAction(filter, (_) => { FilterDependencies(filter); }); + + filteringDropdownBox.Add(_filteringDropdown); + + // Final adding + filteringBox.Add(filteringDropdownBox); + filteringBox.Add(selectingBox); + + fullSelectionBox.Add(_selectionTogglesBox); + fullSelectionBox.Add(filteringBox); + + _multiToggleSelectionHelpRow.Add(_multiToggleSelectionLabel); + _multiToggleSelectionHelpRow.Add(_multiToggleSelectionTooltip); + + Add(_multiToggleSelectionHelpRow); + Add(fullSelectionBox); + } + + public void Populate(Dictionary selections) + { + _selectionTogglesBox.Clear(); + _selections = selections; + + EventCallback, string> callback = OnToggle; + + foreach (var kvp in selections) + { + var toggle = new Toggle() { text = kvp.Key, value = kvp.Value }; + toggle.AddToClassList("multi-toggle-box-toggle"); + toggle.RegisterCallback(callback, toggle.text); + _selectionTogglesBox.Add(toggle); + } + + FilterDependencies(_activeFilter); + } + + private void FilterDependencies(string filter) + { + _activeFilter = filter; + + var allToggles = _selectionTogglesBox.Children().Cast().ToArray(); + var selectedIndex = _selectionFilters.FindIndex(x => x == filter); + + switch (selectedIndex) + { + case 0: + foreach (var toggle in allToggles) + toggle.style.display = DisplayStyle.Flex; + break; + case 1: + foreach (var toggle in allToggles) + toggle.style.display = toggle.value ? DisplayStyle.Flex : DisplayStyle.None; + break; + case 2: + foreach (var toggle in allToggles) + toggle.style.display = toggle.value ? DisplayStyle.None : DisplayStyle.Flex; + break; + } + + // Check if any toggles are displayed + var count = allToggles.Count(toggle => toggle.style.display == DisplayStyle.Flex); + _noSelectionsLabel.style.display = count > 0 ? DisplayStyle.None : DisplayStyle.Flex; + + _filteringDropdown.text = filter; + } + + private void OnToggle(ChangeEvent evt, string text) + { + FilterDependencies(_activeFilter); + _selections[text] = evt.newValue; + OnValuesChanged?.Invoke(_selections); + } + + private void OnAllToggles(bool value) + { + var allToggles = _selectionTogglesBox.Children().Cast(); + foreach (var toggle in allToggles) + toggle.SetValueWithoutNotify(value); + + foreach (var key in _selections.Keys.ToArray()) + _selections[key] = value; + + FilterDependencies(_activeFilter); + OnValuesChanged?.Invoke(_selections); + } + + private void SelectAllToggles() + { + OnAllToggles(true); + } + + private void UnselectAllToggles() + { + OnAllToggles(false); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/MultiToggleSelectionElement.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/MultiToggleSelectionElement.cs.meta new file mode 100644 index 00000000000..8e339951012 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/MultiToggleSelectionElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 19e30766043794345beb432973e0eb3c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageContentElement.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageContentElement.cs new file mode 100644 index 00000000000..eac8455f0e3 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageContentElement.cs @@ -0,0 +1,137 @@ +using AssetStoreTools.Uploader.Data; +using AssetStoreTools.Utility; +using System.Collections.Generic; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal class PackageContentElement : VisualElement + { + // Data + private IPackageContent _content; + private List _workflowElements; + + // UI + private VisualElement _workflowSelectionBox; + private ToolbarMenu _toolbarMenu; + + public PackageContentElement(IPackageContent content) + { + _content = content; + content.OnActiveWorkflowChanged += ActiveWorkflowChanged; + + _workflowElements = new List(); + + Create(); + } + + private void Create() + { + AddToClassList("package-content-element"); + + CreateWorkflowSelection(); + CreateWorkflows(); + Deserialize(); + } + + private void CreateWorkflowSelection() + { + _workflowSelectionBox = new VisualElement(); + _workflowSelectionBox.AddToClassList("package-content-option-box"); + + VisualElement labelHelpRow = new VisualElement(); + labelHelpRow.AddToClassList("package-content-option-label-help-row"); + + Label workflowLabel = new Label { text = "Upload type" }; + Image workflowLabelTooltip = new Image + { + tooltip = "Select what content you are uploading to the Asset Store" + + "\n\n- From Assets Folder - content located within the project's 'Assets' folder or one of its subfolders" + + "\n\n- Pre-exported .unitypackage - content that has already been compressed into a .unitypackage file" +#if UNITY_ASTOOLS_EXPERIMENTAL + + "\n\n- Local UPM Package - content that is located within the project's 'Packages' folder. Only embedded and local packages are supported" +#endif + }; + + labelHelpRow.Add(workflowLabel); + labelHelpRow.Add(workflowLabelTooltip); + + _toolbarMenu = new ToolbarMenu(); + _toolbarMenu.AddToClassList("package-content-option-dropdown"); + + foreach (var workflow in _content.GetAvailableWorkflows()) + { + AppendToolbarActionForWorkflow(workflow); + } + + _workflowSelectionBox.Add(labelHelpRow); + _workflowSelectionBox.Add(_toolbarMenu); + + Add(_workflowSelectionBox); + } + + private void AppendToolbarActionForWorkflow(IWorkflow workflow) + { + _toolbarMenu.menu.AppendAction(workflow.DisplayName, _ => + { + _content.SetActiveWorkflow(workflow); + }); + } + + private void CreateWorkflows() + { + foreach (var workflow in _content.GetAvailableWorkflows()) + { + WorkflowElementBase element; + switch (workflow) + { + case AssetsWorkflow assetsWorkflow: + element = new AssetsWorkflowElement(assetsWorkflow); + break; + case UnityPackageWorkflow unityPackageWorkflow: + element = new UnityPackageWorkflowElement(unityPackageWorkflow); + break; +#if UNITY_ASTOOLS_EXPERIMENTAL + case HybridPackageWorkflow hybridPackageWorkflow: + element = new HybridPackageWorkflowElement(hybridPackageWorkflow); + break; +#endif + default: + ASDebug.LogWarning("Package Content Element received an undefined workflow"); + continue; + } + + element.OnInteractionAvailable += EnableInteraction; + element.OnInteractionUnavailable += DisableInteraction; + _workflowElements.Add(element); + Add(element); + } + } + + private void ActiveWorkflowChanged(IWorkflow workflow) + { + _toolbarMenu.text = workflow.DisplayName; + foreach (var workflowElement in _workflowElements) + { + bool show = workflowElement.Is(workflow); + workflowElement.style.display = show ? DisplayStyle.Flex : DisplayStyle.None; + } + } + + private void EnableInteraction() + { + _workflowSelectionBox.SetEnabled(true); + } + + private void DisableInteraction() + { + _workflowSelectionBox.SetEnabled(false); + } + + private void Deserialize() + { + ActiveWorkflowChanged(_content.GetActiveWorkflow()); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageContentElement.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageContentElement.cs.meta new file mode 100644 index 00000000000..6a3eca3abe4 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageContentElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 09927e9c8fd6e074fa451add92b7ab6f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageElement.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageElement.cs new file mode 100644 index 00000000000..f934979a1a1 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageElement.cs @@ -0,0 +1,215 @@ +using AssetStoreTools.Api; +using AssetStoreTools.Uploader.Data; +using AssetStoreTools.Uploader.Services; +using System; +#if !UNITY_2021_1_OR_NEWER +using UnityEditor.UIElements; +#endif +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal class PackageElement : VisualElement + { + // Data + private IPackage _package; + private bool _isSelected; + + private IPackageFactoryService _packageFactory; + + // UI + private Button _foldoutBox; + private Label _expanderLabel; + private Label _assetLabel; + private Label _lastDateSizeLabel; + private Image _assetImage; + + private ProgressBar _uploadProgressBar; + private VisualElement _uploadProgressBarBackground; + + private PackageContentElement _contentElement; + + public event Action OnSelected; + + public PackageElement(IPackage package, IPackageFactoryService packageFactory) + { + _package = package; + _package.OnUpdate += Refresh; + _package.OnIconUpdate += SetPackageThumbnail; + + _packageFactory = packageFactory; + + _isSelected = false; + + Create(); + } + + private void Create() + { + AddToClassList("package-full-box"); + + _foldoutBox = new Button { name = "Package" }; + _foldoutBox.AddToClassList("package-foldout-box"); + if (_package.IsDraft) + _foldoutBox.AddToClassList("package-foldout-box-draft"); + _foldoutBox.clickable.clicked += Toggle; + + // Expander, Icon and Asset Label + VisualElement foldoutBoxInfo = new VisualElement { name = "foldoutBoxInfo" }; + foldoutBoxInfo.AddToClassList("package-foldout-box-info"); + + VisualElement labelExpanderRow = new VisualElement { name = "labelExpanderRow" }; + labelExpanderRow.AddToClassList("package-expander-label-row"); + + _expanderLabel = new Label { name = "ExpanderLabel", text = "â–º" }; + _expanderLabel.AddToClassList("package-expander"); + _expanderLabel.style.display = _package.IsDraft ? DisplayStyle.Flex : DisplayStyle.None; + + _assetImage = new Image { name = "AssetImage" }; + _assetImage.AddToClassList("package-image"); + + VisualElement assetLabelInfoBox = new VisualElement { name = "assetLabelInfoBox" }; + assetLabelInfoBox.AddToClassList("package-label-info-box"); + + _assetLabel = new Label { name = "AssetLabel", text = _package.Name }; + _assetLabel.AddToClassList("package-label"); + + _lastDateSizeLabel = new Label { name = "AssetInfoLabel", text = FormatDateSize() }; + _lastDateSizeLabel.AddToClassList("package-info"); + + assetLabelInfoBox.Add(_assetLabel); + assetLabelInfoBox.Add(_lastDateSizeLabel); + + labelExpanderRow.Add(_expanderLabel); + labelExpanderRow.Add(_assetImage); + labelExpanderRow.Add(assetLabelInfoBox); + + var openInBrowserButton = new Button(OpenPackageInBrowser) + { + name = "OpenInBrowserButton", + tooltip = "View your package in the Publishing Portal." + }; + openInBrowserButton.AddToClassList("package-open-in-browser-button"); + + // Header Progress bar + _uploadProgressBar = new ProgressBar { name = "HeaderProgressBar" }; + _uploadProgressBar.AddToClassList("package-header-progress-bar"); + _uploadProgressBar.style.display = DisplayStyle.None; + _uploadProgressBarBackground = _uploadProgressBar.Q(className: "unity-progress-bar__progress"); + + // Connect it all + foldoutBoxInfo.Add(labelExpanderRow); + foldoutBoxInfo.Add(openInBrowserButton); + + _foldoutBox.Add(foldoutBoxInfo); + _foldoutBox.Add(_uploadProgressBar); + + Add(_foldoutBox); + } + + private void CreateFoldoutContent() + { + var content = _packageFactory.CreatePackageContent(_package); + if (content == null) + return; + + _contentElement = new PackageContentElement(content); + _contentElement.style.display = DisplayStyle.None; + Add(_contentElement); + + SubscribeToContentWorkflowUpdates(content); + } + + private void SubscribeToContentWorkflowUpdates(IPackageContent content) + { + foreach (var workflow in content.GetAvailableWorkflows()) + { + workflow.OnUploadStateChanged += UpdateProgressBar; + } + } + + private void UpdateProgressBar(UploadStatus? status, float? progress) + { + if (status != null) + { + _uploadProgressBarBackground.style.backgroundColor = PackageUploadElement.GetColorByStatus(status.Value); + } + + if (progress != null) + { + _uploadProgressBar.value = progress.Value; + } + } + + private void Toggle() + { + if (!_package.IsDraft) + return; + + if (!Contains(_contentElement)) + CreateFoldoutContent(); + + var shouldExpand = !_isSelected; + _expanderLabel.text = shouldExpand ? "â–¼" : "â–º"; + + if (shouldExpand) + _foldoutBox.AddToClassList("package-foldout-box-expanded"); + else + _foldoutBox.RemoveFromClassList("package-foldout-box-expanded"); + _contentElement.style.display = shouldExpand ? DisplayStyle.Flex : DisplayStyle.None; + + _isSelected = !_isSelected; + ToggleProgressBar(); + + if (_isSelected) + OnSelected?.Invoke(); + } + + private void ToggleProgressBar() + { + if (!_isSelected && _uploadProgressBar.value != 0) + _uploadProgressBar.style.display = DisplayStyle.Flex; + else + _uploadProgressBar.style.display = DisplayStyle.None; + } + + public bool Is(IPackage package) + { + return package == _package; + } + + public void Select() + { + if (!_isSelected) + Toggle(); + } + + public void Unselect() + { + if (_isSelected) + Toggle(); + } + + private void SetPackageThumbnail() + { + _assetImage.image = _package.Icon; + } + + private void Refresh() + { + _assetLabel.text = _package.Name; + _lastDateSizeLabel.text = FormatDateSize(); + } + + private string FormatDateSize() + { + return $"{_package.Category} | {_package.FormattedSize()} | {_package.FormattedModified()}"; + } + + private void OpenPackageInBrowser() + { + Application.OpenURL($"https://publisher.unity.com/packages/{_package.VersionId}/edit/upload"); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageElement.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageElement.cs.meta new file mode 100644 index 00000000000..e807599c6ae --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cef5f23043d318945b844bcac7a7a984 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageGroupElement.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageGroupElement.cs new file mode 100644 index 00000000000..6c549c0c3db --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageGroupElement.cs @@ -0,0 +1,149 @@ +using AssetStoreTools.Uploader.Data; +using AssetStoreTools.Uploader.Services; +using System.Collections.Generic; +using System.Linq; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal class PackageGroupElement : VisualElement + { + // Data + public string Name => _packageGroup.Name; + private IPackageGroup _packageGroup; + private List _packageElements; + private bool _isExpanded; + + private IPackageFactoryService _packageFactory; + + // UI + private Button _groupExpanderBox; + private VisualElement _groupContent; + + private Label _expanderLabel; + private Label _groupLabel; + + public PackageGroupElement(IPackageGroup packageGroup, IPackageFactoryService packageFactory) + { + _packageGroup = packageGroup; + _packageElements = new List(); + _packageGroup.OnPackagesSorted += RefreshPackages; + _packageGroup.OnPackagesFiltered += RefreshPackages; + + _packageFactory = packageFactory; + + Create(); + } + + private void Create() + { + CreatePackageGroup(); + CreatePackageGroupContent(); + AddPackagesToGroupContent(); + } + + protected void CreatePackageGroup() + { + _groupExpanderBox = new Button(OnPackageGroupClicked); + _groupExpanderBox.AddToClassList("package-group-expander-box"); + + _expanderLabel = new Label { name = "ExpanderLabel", text = "â–º" }; + _expanderLabel.AddToClassList("package-group-expander"); + + _groupLabel = new Label { text = $"{_packageGroup.Name} ({_packageGroup.Packages.Count})" }; + _groupLabel.AddToClassList("package-group-label"); + FormatGroupLabel(_packageGroup.Packages.Count); + + _groupExpanderBox.Add(_expanderLabel); + _groupExpanderBox.Add(_groupLabel); + + Add(_groupExpanderBox); + } + + private void CreatePackageGroupContent() + { + _groupContent = new VisualElement { name = "GroupContentBox" }; + _groupContent.AddToClassList("package-group-content-box"); + Toggle(false); + + var groupSeparator = new VisualElement { name = "GroupSeparator" }; + groupSeparator.AddToClassList("package-group-separator"); + + if (_packageGroup.Name.ToLower() != "draft") + { + _groupLabel.SetEnabled(false); + _groupContent.AddToClassList("unity-disabled"); + groupSeparator.style.display = DisplayStyle.Flex; + } + + Add(_groupContent); + Add(groupSeparator); + } + + private void AddPackagesToGroupContent() + { + foreach (var package in _packageGroup.Packages) + { + var packageElement = new PackageElement(package, _packageFactory); + packageElement.OnSelected += () => OnPackageSelected(packageElement); + _packageElements.Add(packageElement); + } + } + + private void FormatGroupLabel(int displayedPackageCount) + { + if (_packageGroup.Packages.Count == displayedPackageCount) + _groupLabel.text = $"{Name} ({displayedPackageCount})"; + else + _groupLabel.text = $"{Name} ({displayedPackageCount}/{_packageGroup.Packages.Count})"; + } + + private void RefreshPackages(List packages) + { + _groupContent.Clear(); + + foreach (var package in packages) + { + var correspondingElement = _packageElements.FirstOrDefault(x => x.Is(package)); + if (correspondingElement == null) + continue; + + _groupContent.Add(correspondingElement); + } + + FormatGroupLabel(packages.Count()); + } + + private void OnPackageGroupClicked() + { + Toggle(!_isExpanded); + } + + public void Toggle(bool expand) + { + if (expand) + { + _expanderLabel.text = "â–¼"; + _groupContent.style.display = DisplayStyle.Flex; + } + else + { + _expanderLabel.text = "â–º"; + _groupContent.style.display = DisplayStyle.None; + } + + _isExpanded = expand; + } + + private void OnPackageSelected(PackageElement packageElement) + { + foreach (var element in _packageElements) + { + if (element == packageElement) + continue; + + element.Unselect(); + } + } + } +} diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageGroupElement.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageGroupElement.cs.meta new file mode 100644 index 00000000000..64cde036987 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageGroupElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 41e322a1418ab824182eade111145dff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageListToolbar.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageListToolbar.cs new file mode 100644 index 00000000000..0dfc3c261fa --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageListToolbar.cs @@ -0,0 +1,58 @@ +using AssetStoreTools.Uploader.Data; +using System.Collections.Generic; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal class PackageListToolbar : VisualElement + { + private List _packageGroups; + + public PackageListToolbar() + { + Create(); + } + + private void Create() + { + AddToClassList("package-list-toolbar"); + + // Search + var searchField = new ToolbarSearchField { name = "SearchField" }; + searchField.AddToClassList("package-search-field"); + + // Sorting menu button + var sortMenu = new ToolbarMenu() { text = "Sort: Name ↓" }; + sortMenu.menu.AppendAction("Sort: Name ↓", (_) => { sortMenu.text = "Sort: Name ↓"; Sort(PackageSorting.Name); }); + sortMenu.menu.AppendAction("Sort: Updated ↓", (_) => { sortMenu.text = "Sort: Updated ↓"; Sort(PackageSorting.Date); }); + sortMenu.menu.AppendAction("Sort: Category ↓", (_) => { sortMenu.text = "Sort: Category ↓"; Sort(PackageSorting.Category); }); + sortMenu.AddToClassList("package-sort-menu"); + + // Finalize the bar + Add(searchField); + Add(sortMenu); + + // Add Callbacks and click events + searchField.RegisterCallback>(SearchFilter); + } + + public void SetPackageGroups(List packageGroups) + { + _packageGroups = packageGroups; + } + + private void SearchFilter(ChangeEvent evt) + { + var searchString = evt.newValue.ToLower(); + foreach (var packageGroup in _packageGroups) + packageGroup.Filter(searchString); + } + + public void Sort(PackageSorting sortingType) + { + foreach (var packageGroup in _packageGroups) + packageGroup.Sort(sortingType); + } + } +} diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageListToolbar.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageListToolbar.cs.meta new file mode 100644 index 00000000000..d877de7b212 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageListToolbar.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6d2659328222e0e4cb36cd194e023f4b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageUploadElement.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageUploadElement.cs new file mode 100644 index 00000000000..06fe8eaa0f7 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageUploadElement.cs @@ -0,0 +1,321 @@ +using AssetStoreTools.Api; +using AssetStoreTools.Api.Responses; +using AssetStoreTools.Exporter; +using AssetStoreTools.Uploader.Data; +using AssetStoreTools.Utility; +using System; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using UnityEditor; +#if !UNITY_2021_1_OR_NEWER +using UnityEditor.UIElements; +#endif +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal class PackageUploadElement : VisualElement + { + // Data + private IWorkflow _workflow; + private bool _enableExporting; + + // UI + private VisualElement _exportAndUploadContainer; + + private Button _cancelUploadButton; + private VisualElement _uploadProgressContainer; + private ProgressBar _uploadProgressBar; + private VisualElement _uploadProgressBarBackground; + + public event Action OnInteractionAvailable; + public event Action OnInteractionUnavailable; + + public PackageUploadElement(IWorkflow workflow, bool exposeExportButton) + { + _workflow = workflow; + _enableExporting = exposeExportButton; + + Create(); + } + + private void Create() + { + AddToClassList("uploading-box"); + + CreateButtonContainer(); + CreateProgressContainer(); + } + + private void CreateButtonContainer() + { + _exportAndUploadContainer = new VisualElement(); + _exportAndUploadContainer.AddToClassList("uploading-export-and-upload-container"); + + CreateExportButton(); + CreateUploadButton(); + Add(_exportAndUploadContainer); + } + + private void CreateExportButton() + { + if (!_enableExporting) + return; + + var _exportAndUploadButton = new Button(async () => await Export(true)) { name = "ExportButton", text = "Export" }; + _exportAndUploadButton.AddToClassList("uploading-export-button"); + + _exportAndUploadContainer.Add(_exportAndUploadButton); + } + + private void CreateUploadButton() + { + var _uploadButton = new Button(Upload) { name = "UploadButton" }; + _uploadButton.text = _enableExporting ? "Export and Upload" : "Upload"; + _uploadButton.AddToClassList("uploading-upload-button"); + + _exportAndUploadContainer.Add(_uploadButton); + } + + private void CreateProgressContainer() + { + _uploadProgressContainer = new VisualElement(); + _uploadProgressContainer.AddToClassList("uploading-progress-container"); + _uploadProgressContainer.style.display = DisplayStyle.None; + + _uploadProgressBar = new ProgressBar { name = "UploadProgressBar" }; + _uploadProgressBar.AddToClassList("uploading-progress-bar"); + _uploadProgressBarBackground = _uploadProgressBar.Q(className: "unity-progress-bar__progress"); + + _cancelUploadButton = new Button() { name = "CancelButton", text = "Cancel" }; + _cancelUploadButton.AddToClassList("uploading-cancel-button"); + + _uploadProgressContainer.Add(_uploadProgressBar); + _uploadProgressContainer.Add(_cancelUploadButton); + + Add(_uploadProgressContainer); + } + + private async Task Export(bool interactive) + { + try + { + DisableInteraction(); + + if (!_workflow.IsPathSet) + { + EditorUtility.DisplayDialog("Exporting failed", "No path was selected. Please " + + "select a path and try again.", "OK"); + return new PackageExporterResult() { Success = false, Exception = new Exception("No path was selected.") }; + } + + var rootProjectPath = Constants.RootProjectPath; + var packageNameStripped = Regex.Replace(_workflow.PackageName, "[^a-zA-Z0-9]", ""); + var outputName = $"{packageNameStripped}-{DateTime.Now:yyyy-dd-M--HH-mm-ss}"; + + string outputPath; + if (interactive) + { + outputPath = EditorUtility.SaveFilePanel("Export Package", rootProjectPath, + outputName, _workflow.PackageExtension.Remove(0, 1)); // Ignoring the '.' character since SaveFilePanel already appends it + + if (string.IsNullOrEmpty(outputPath)) + return new PackageExporterResult() { Success = false, Exception = null }; + } + else + { + outputPath = $"Temp/{outputName}{_workflow.PackageExtension}"; + } + + var exportResult = await _workflow.ExportPackage(outputPath); + if (!exportResult.Success) + { + Debug.LogError($"Package exporting failed: {exportResult.Exception}"); + EditorUtility.DisplayDialog("Exporting failed", exportResult.Exception.Message, "OK"); + } + else if (interactive) + Debug.Log($"Package exported to '{Path.GetFullPath(exportResult.ExportedPath).Replace("\\", "/")}'"); + + return exportResult; + } + finally + { + if (interactive) + EnableInteraction(); + } + } + + private async void Upload() + { + DisableInteraction(); + + if (await ValidateUnityVersionsBeforeUpload() == false) + { + EnableInteraction(); + return; + } + + var exportResult = await Export(false); + if (!exportResult.Success) + { + EnableInteraction(); + return; + } + + if (!_workflow.IsPathSet) + { + EditorUtility.DisplayDialog("Uploading failed", "No path was selected. Please " + + "select a path and try again.", "OK"); + EnableInteraction(); + return; + } + + _exportAndUploadContainer.style.display = DisplayStyle.None; + _uploadProgressContainer.style.display = DisplayStyle.Flex; + + _cancelUploadButton.clicked += Cancel; + _workflow.OnUploadStateChanged += UpdateProgressBar; + var response = await _workflow.UploadPackage(exportResult.ExportedPath); + _workflow.OnUploadStateChanged -= UpdateProgressBar; + + await OnUploadingStopped(response); + } + + private async Task ValidateUnityVersionsBeforeUpload() + { + var validationEnabled = ASToolsPreferences.Instance.UploadVersionCheck; + if (!validationEnabled) + return true; + + var requiredVersionUploaded = await _workflow.ValidatePackageUploadedVersions(); + if (requiredVersionUploaded) + return true; + + var result = EditorUtility.DisplayDialogComplex("Asset Store Tools", $"You may upload this package, but you will need to add a package using Unity version {Constants.Uploader.MinRequiredUnitySupportVersion} " + + "or higher to be able to submit a new asset", "Upload", "Cancel", "Upload and do not display this again"); + + switch (result) + { + case 1: + return false; + case 2: + ASToolsPreferences.Instance.UploadVersionCheck = false; + ASToolsPreferences.Instance.Save(); + break; + } + + return true; + } + + private void UpdateProgressBar(UploadStatus? status, float? progress) + { + if (status != null) + { + _uploadProgressBarBackground.style.backgroundColor = GetColorByStatus(status.Value); + } + + if (progress != null) + { + _uploadProgressBar.value = progress.Value; + _uploadProgressBar.title = $"{progress.Value:0.#}%"; + + if (progress == 100f && _cancelUploadButton.enabledInHierarchy) + _cancelUploadButton.SetEnabled(false); + } + } + + private void Cancel() + { + _cancelUploadButton.SetEnabled(false); + _workflow.AbortUpload(); + } + + private async Task OnUploadingStopped(PackageUploadResponse response) + { + if (!response.Success && !response.Cancelled) + { + Debug.LogException(response.Exception); + } + + if (response.Success) + { + await _workflow.RefreshPackage(); + } + + if (response.Status == UploadStatus.ResponseTimeout) + { + Debug.LogWarning($"All bytes for the package '{_workflow.PackageName}' have been uploaded, but a response " + + $"from the server was not received. This can happen because of Firewall restrictions. " + + $"Please make sure that a new version of your package has reached the Publishing Portal."); + } + + _uploadProgressBar.title = GetProgressBarTitleByStatus(response.Status); + + _cancelUploadButton.clickable = null; + _cancelUploadButton.clicked += Reset; + _cancelUploadButton.text = "Done"; + _cancelUploadButton.SetEnabled(true); + } + + private void Reset() + { + _cancelUploadButton.clickable = null; + _cancelUploadButton.text = "Cancel"; + + _workflow.ResetUploadStatus(); + UpdateProgressBar(UploadStatus.Default, 0f); + + _uploadProgressContainer.style.display = DisplayStyle.None; + _exportAndUploadContainer.style.display = DisplayStyle.Flex; + EnableInteraction(); + } + + public static Color GetColorByStatus(UploadStatus status) + { + switch (status) + { + default: + case UploadStatus.Default: + return new Color(0.13f, 0.59f, 0.95f); + case UploadStatus.Success: + case UploadStatus.ResponseTimeout: + return new Color(0f, 0.50f, 0.14f); + case UploadStatus.Cancelled: + return new Color(0.78f, 0.59f, 0f); + case UploadStatus.Fail: + return new Color(0.69f, 0.04f, 0.04f); + } + } + + private string GetProgressBarTitleByStatus(UploadStatus status) + { + var progressBarTitle = "Upload: "; + switch (status) + { + case UploadStatus.ResponseTimeout: + progressBarTitle += UploadStatus.Success; + break; + default: + progressBarTitle += status; + break; + } + + return progressBarTitle; + } + + private void EnableInteraction() + { + _exportAndUploadContainer.SetEnabled(true); + OnInteractionAvailable?.Invoke(); + } + + private void DisableInteraction() + { + _exportAndUploadContainer.SetEnabled(false); + OnInteractionUnavailable?.Invoke(); + SetEnabled(true); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageUploadElement.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageUploadElement.cs.meta new file mode 100644 index 00000000000..b3d5d3dac34 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PackageUploadElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 242524e968bd4484eaeb154d8013f427 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PathSelectionElement.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PathSelectionElement.cs new file mode 100644 index 00000000000..d8d79bfb433 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PathSelectionElement.cs @@ -0,0 +1,63 @@ +using System; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal class PathSelectionElement : VisualElement + { + // Data + private string _labelText; + private string _labelTooltip; + + public event Action OnBrowse; + + // UI + private TextField _pathSelectionTextField; + + public PathSelectionElement(string labelText, string labelTooltip) + { + AddToClassList("package-content-option-box"); + + _labelText = labelText; + _labelTooltip = labelTooltip; + + Create(); + } + + private void Create() + { + VisualElement labelHelpRow = new VisualElement(); + labelHelpRow.AddToClassList("package-content-option-label-help-row"); + + Label folderPathLabel = new Label { text = _labelText }; + Image folderPathLabelTooltip = new Image + { + tooltip = _labelTooltip + }; + + labelHelpRow.Add(folderPathLabel); + labelHelpRow.Add(folderPathLabelTooltip); + + _pathSelectionTextField = new TextField(); + _pathSelectionTextField.AddToClassList("package-content-option-textfield"); + _pathSelectionTextField.isReadOnly = true; + + Button browsePathButton = new Button(Browse) { name = "BrowsePathButton", text = "Browse" }; + browsePathButton.AddToClassList("package-content-option-button"); + + Add(labelHelpRow); + Add(_pathSelectionTextField); + Add(browsePathButton); + } + + private void Browse() + { + OnBrowse?.Invoke(); + } + + public void SetPath(string path) + { + _pathSelectionTextField.value = path; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PathSelectionElement.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PathSelectionElement.cs.meta new file mode 100644 index 00000000000..2ba9f4a442d --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PathSelectionElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 753d4442293e5cc4b9efcab089da4d59 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PreviewGenerationElement.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PreviewGenerationElement.cs new file mode 100644 index 00000000000..853320761a6 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PreviewGenerationElement.cs @@ -0,0 +1,109 @@ +using AssetStoreTools.Previews.Data; +using AssetStoreTools.Uploader.Data; +using System.Linq; +using UnityEditor; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal class PreviewGenerationElement : VisualElement + { + // Data + private IWorkflow _workflow; + + // UI + private VisualElement _toggleRow; + private Toggle _previewToggle; + + private VisualElement _buttonRow; + private VisualElement _viewButton; + + public PreviewGenerationElement(IWorkflow workflow) + { + _workflow = workflow; + + Create(); + } + + private void Create() + { + CreateInfoRow(); + CreateViewButton(); + } + + private void CreateInfoRow() + { + _toggleRow = new VisualElement(); + _toggleRow.AddToClassList("package-content-option-box"); + + VisualElement toggleLabelHelpRow = new VisualElement(); + toggleLabelHelpRow.AddToClassList("package-content-option-label-help-row"); + + Label toggleLabel = new Label { text = "Asset Previews" }; + Image toggleLabelTooltip = new Image + { + tooltip = "Select how the previews for your assets will be generated.\n\n" + + "Unity generates asset preview images natively up to a size of 128x128. " + + "You can try generating previews which are of higher resolution, up to 300x300.\n\n" + + "Note: these asset preview images will only be displayed in the 'Package Content' section of the " + + "Asset Store listing page once the package is published, and in the package importer window that appears during the package import process.\n" + + "They will not replace the images used for the assets in the Project window after the package gets imported." + }; + + _previewToggle = new Toggle { name = "PreviewToggle", text = "Generate Hi-Res (experimental)" }; + _previewToggle.AddToClassList("package-content-option-toggle"); + _previewToggle.RegisterValueChangedCallback((_) => DependencyToggleValueChange()); + + toggleLabelHelpRow.Add(toggleLabel); + toggleLabelHelpRow.Add(toggleLabelTooltip); + + _toggleRow.Add(toggleLabelHelpRow); + _toggleRow.Add(_previewToggle); + + Add(_toggleRow); + } + + private void CreateViewButton() + { + _buttonRow = new VisualElement(); + _buttonRow.AddToClassList("package-content-option-box"); + _buttonRow.style.display = DisplayStyle.None; + + var spaceFiller = new VisualElement(); + spaceFiller.AddToClassList("package-content-option-label-help-row"); + + _viewButton = new Button(ViewClicked) { text = "Inspect Previews" }; + + _buttonRow.Add(spaceFiller); + _buttonRow.Add(_viewButton); + + Add(_buttonRow); + } + + private void DependencyToggleValueChange() + { + _workflow.GenerateHighQualityPreviews = _previewToggle.value; + _buttonRow.style.display = _previewToggle.value ? DisplayStyle.Flex : DisplayStyle.None; + } + + private void ViewClicked() + { + PreviewGenerationSettings settings; + if (_workflow.GenerateHighQualityPreviews) + { + settings = new CustomPreviewGenerationSettings() { InputPaths = _workflow.GetAllPaths().ToArray() }; + } + else + { + settings = new NativePreviewGenerationSettings() { InputPaths = _workflow.GetAllPaths().ToArray() }; + } + + AssetStoreTools.ShowAssetStoreToolsPreviewGenerator(settings); + } + + private void DisplayProgress(float value) + { + EditorUtility.DisplayProgressBar("Generating", "Generating previews...", value); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PreviewGenerationElement.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PreviewGenerationElement.cs.meta new file mode 100644 index 00000000000..251024b35c2 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/PreviewGenerationElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 54c7971e2ad639644936d3552b4f4f49 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/UnityPackageWorkflowElement.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/UnityPackageWorkflowElement.cs new file mode 100644 index 00000000000..2d61e6d78a6 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/UnityPackageWorkflowElement.cs @@ -0,0 +1,58 @@ +using AssetStoreTools.Uploader.Data; +using AssetStoreTools.Utility; +using UnityEditor; + +namespace AssetStoreTools.Uploader.UI.Elements +{ + internal class UnityPackageWorkflowElement : WorkflowElementBase + { + // Data + private UnityPackageWorkflow _workflow; + + public UnityPackageWorkflowElement(UnityPackageWorkflow workflow) : base(workflow) + { + _workflow = workflow; + Create(); + } + + private void Create() + { + CreatePathElement("Package path", "Select the .unitypackage file you would like to upload."); + CreateValidationElement(new ExternalProjectValidationElement(_workflow)); + CreateUploadElement(_workflow, false); + Deserialize(); + } + + protected override void BrowsePath() + { + // Path retrieval + var absolutePackagePath = EditorUtility.OpenFilePanel("Select a .unitypackage file", Constants.RootProjectPath, "unitypackage"); + + if (string.IsNullOrEmpty(absolutePackagePath)) + return; + + var relativeExportPath = FileUtility.AbsolutePathToRelativePath(absolutePackagePath, ASToolsPreferences.Instance.EnableSymlinkSupport); + if (!_workflow.IsPathValid(relativeExportPath, out var error)) + { + EditorUtility.DisplayDialog("Invalid selection", error, "OK"); + return; + } + + HandleUnityPackageUploadPathSelection(relativeExportPath, true); + } + + private void HandleUnityPackageUploadPathSelection(string selectedPackagePath, bool serialize) + { + if (string.IsNullOrEmpty(selectedPackagePath)) + return; + + _workflow.SetPackagePath(selectedPackagePath, serialize); + SetPathSelectionTextField(selectedPackagePath); + } + + protected override void Deserialize() + { + HandleUnityPackageUploadPathSelection(_workflow.GetPackagePath(), false); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/UnityPackageWorkflowElement.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/UnityPackageWorkflowElement.cs.meta new file mode 100644 index 00000000000..cc30abb9cf7 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Elements/UnityPackageWorkflowElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a0c8a79b7dba9e458ddc54eec30ea66 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Views.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Views.meta new file mode 100644 index 00000000000..e5882f0d5f8 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Views.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7511d6ab930f33c469562bc3c1c2aab2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Views/LoginView.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Views/LoginView.cs new file mode 100644 index 00000000000..30395723bef --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Views/LoginView.cs @@ -0,0 +1,284 @@ +using AssetStoreTools.Api.Models; +using AssetStoreTools.Uploader.Services.Api; +using AssetStoreTools.Utility; +using System; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UI.Views +{ + internal class LoginView : VisualElement + { + // Data + private IAuthenticationService _authenticationService; + private double _cloudLoginRefreshTime = 1d; + private double _lastRefreshTime; + + // UI + private Button _cloudLoginButton; + private Label _cloudLoginLabel; + + private Box _errorBox; + private Label _errorLabel; + + private TextField _emailField; + private TextField _passwordField; + private Button _credentialsLoginButton; + + public event Action OnAuthenticated; + + public LoginView(IAuthenticationService authenticationService) + { + _authenticationService = authenticationService; + Create(); + } + + public void Create() + { + styleSheets.Add(StyleSelector.UploaderWindow.LoginViewStyle); + styleSheets.Add(StyleSelector.UploaderWindow.LoginViewTheme); + + CreateAssetStoreLogo(); + CreateCloudLogin(); + CreateErrorBox(); + CreateCredentialsLogin(); + } + + private void CreateAssetStoreLogo() + { + // Asset Store logo + Image assetStoreLogo = new Image { name = "AssetStoreLogo" }; + assetStoreLogo.AddToClassList("asset-store-logo"); + + Add(assetStoreLogo); + } + + private void CreateCloudLogin() + { + VisualElement cloudLogin = new VisualElement { name = "CloudLogin" }; + + _cloudLoginButton = new Button(LoginWithCloudToken) { name = "LoginButtonCloud" }; + _cloudLoginButton.AddToClassList("cloud-button-login"); + _cloudLoginButton.SetEnabled(false); + + _cloudLoginLabel = new Label { text = "Cloud login unavailable" }; + _cloudLoginLabel.AddToClassList("cloud-button-login-label"); + + Label orLabel = new Label { text = "or" }; + orLabel.AddToClassList("cloud-label-or"); + + _cloudLoginButton.Add(_cloudLoginLabel); + + cloudLogin.Add(_cloudLoginButton); + cloudLogin.Add(orLabel); + + UpdateCloudLoginButton(); + EditorApplication.update += UpdateCloudLoginButton; + Add(cloudLogin); + } + + private void CreateErrorBox() + { + _errorBox = new Box() { name = "LoginErrorBox" }; + _errorBox.AddToClassList("error-container"); + _errorBox.style.display = DisplayStyle.None; + + var errorImage = new Image(); + _errorBox.Add(errorImage); + + _errorLabel = new Label(); + _errorBox.Add(_errorLabel); + + Add(_errorBox); + } + + public void DisplayError(string message) + { + if (string.IsNullOrEmpty(message)) + return; + + _errorLabel.text = message; + Debug.LogError(message); + + _errorBox.style.display = DisplayStyle.Flex; + } + + private void ClearError() + { + _errorLabel.text = string.Empty; + _errorBox.style.display = DisplayStyle.None; + } + + private void CreateCredentialsLogin() + { + // Manual login + VisualElement manualLoginBox = new VisualElement { name = "ManualLoginBox" }; + manualLoginBox.AddToClassList("credentials-container"); + + // Email input box + VisualElement inputBoxEmail = new VisualElement(); + inputBoxEmail.AddToClassList("credentials-input-container"); + + Label emailTitle = new Label { text = "Email" }; + _emailField = new TextField(); + + inputBoxEmail.Add(emailTitle); + inputBoxEmail.Add(_emailField); + + manualLoginBox.Add(inputBoxEmail); + + // Password input box + VisualElement inputBoxPassword = new VisualElement(); + inputBoxPassword.AddToClassList("credentials-input-container"); + + Label passwordTitle = new Label { text = "Password" }; + _passwordField = new TextField { isPasswordField = true }; + + inputBoxPassword.Add(passwordTitle); + inputBoxPassword.Add(_passwordField); + + manualLoginBox.Add(inputBoxPassword); + + // Login button + _credentialsLoginButton = new Button(LoginWithCredentials) { name = "LoginButtonCredentials" }; + _credentialsLoginButton.AddToClassList("credentials-button-login"); + + Label loginDescriptionCredentials = new Label { text = "Login" }; + loginDescriptionCredentials.AddToClassList("credentials-button-login-label"); + + _credentialsLoginButton.Add(loginDescriptionCredentials); + + manualLoginBox.Add(_credentialsLoginButton); + + Add(manualLoginBox); + + // Credentials login helpers + VisualElement helperBox = new VisualElement { name = "HelperBox" }; + helperBox.AddToClassList("help-section-container"); + + Button createAccountButton = new Button { name = "CreateAccountButton", text = "Create Publisher ID" }; + Button forgotPasswordButton = new Button { name = "ForgotPasswordButton", text = "Reset Password" }; + + createAccountButton.AddToClassList("help-section-hyperlink-button"); + forgotPasswordButton.AddToClassList("help-section-hyperlink-button"); + + createAccountButton.clicked += () => Application.OpenURL(Constants.Uploader.AccountRegistrationUrl); + forgotPasswordButton.clicked += () => Application.OpenURL(Constants.Uploader.AccountForgottenPasswordUrl); + + helperBox.Add(createAccountButton); + helperBox.Add(forgotPasswordButton); + + Add(helperBox); + } + + public async void LoginWithSessionToken() + { + ASDebug.Log("Authenticating with session token..."); + ClearError(); + SetEnabled(false); + + var result = await _authenticationService.AuthenticateWithSessionToken(); + if (!result.Success) + { + // Session authentication fail does not display errors in the UI + ASDebug.Log("No existing session was found"); + SetEnabled(true); + return; + } + + OnLoginSuccess(result.User); + } + + private async void LoginWithCloudToken() + { + ASDebug.Log("Authenticating with cloud token..."); + ClearError(); + SetEnabled(false); + + var result = await _authenticationService.AuthenticateWithCloudToken(); + if (!result.Success) + { + OnLoginFail(result.Exception.Message); + return; + } + + OnLoginSuccess(result.User); + } + + private async void LoginWithCredentials() + { + ASDebug.Log("Authenticating with credentials..."); + ClearError(); + var isValid = IsLoginDataValid(_emailField.text, _passwordField.value); + SetEnabled(!isValid); + + if (!isValid) + return; + + var result = await _authenticationService.AuthenticateWithCredentials(_emailField.text, _passwordField.text); + if (result.Success) + OnLoginSuccess(result.User); + else + OnLoginFail(result.Exception.Message); + } + + private bool IsLoginDataValid(string email, string password) + { + if (string.IsNullOrEmpty(email)) + { + DisplayError("Email field cannot be empty."); + return false; + } + + if (string.IsNullOrEmpty(password)) + { + DisplayError("Password field cannot be empty."); + return false; + } + + return true; + } + + private void UpdateCloudLoginButton() + { + if (_cloudLoginLabel == null) + return; + + if (_lastRefreshTime + _cloudLoginRefreshTime > EditorApplication.timeSinceStartup) + return; + + _lastRefreshTime = EditorApplication.timeSinceStartup; + + // Cloud login + if (_authenticationService.CloudAuthenticationAvailable(out var username, out var _)) + { + _cloudLoginLabel.text = $"Login as {username}"; + _cloudLoginButton.SetEnabled(true); + } + else + { + _cloudLoginLabel.text = "Cloud login unavailable"; + _cloudLoginButton.SetEnabled(false); + } + } + + private void OnLoginSuccess(User user) + { + ASDebug.Log($"Successfully authenticated as {user.Username}\n{user}"); + + _emailField.value = string.Empty; + _passwordField.value = string.Empty; + + OnAuthenticated?.Invoke(user); + SetEnabled(true); + } + + private void OnLoginFail(string message) + { + ASDebug.LogError($"Authentication failed: {message}"); + DisplayError(message); + SetEnabled(true); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Views/LoginView.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Views/LoginView.cs.meta new file mode 100644 index 00000000000..1d2ed9e7720 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Views/LoginView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e20b8602a9bd8ca48a5689b3f32cdd90 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Views/PackageListView.cs b/.github/Asset Store Tools/Uploader/Scripts/UI/Views/PackageListView.cs new file mode 100644 index 00000000000..a980d66394e --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Views/PackageListView.cs @@ -0,0 +1,249 @@ +using AssetStoreTools.Uploader.Data; +using AssetStoreTools.Uploader.Services; +using AssetStoreTools.Uploader.Services.Api; +using AssetStoreTools.Uploader.UI.Elements; +using AssetStoreTools.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine.SceneManagement; +using UnityEngine.UIElements; +using PackageModel = AssetStoreTools.Api.Models.Package; + +namespace AssetStoreTools.Uploader.UI.Views +{ + internal class PackageListView : VisualElement + { + // Data + private List _packages; + private readonly string[] _priorityGroupNames = { "draft", "published" }; + + private IPackageDownloadingService _packageDownloadingService; + private IPackageFactoryService _packageFactory; + + // UI + private LoadingSpinner _loadingSpinner; + private ScrollView _packageScrollView; + private PackageListToolbar _packageListToolbar; + + public event Action OnInitializeError; + + public PackageListView(IPackageDownloadingService packageDownloadingService, IPackageFactoryService elementFactory) + { + _packages = new List(); + _packageDownloadingService = packageDownloadingService; + _packageFactory = elementFactory; + + Create(); + EditorSceneManager.activeSceneChangedInEditMode += OnSceneChange; + } + + private void Create() + { + styleSheets.Add(StyleSelector.UploaderWindow.PackageListViewStyle); + styleSheets.Add(StyleSelector.UploaderWindow.PackageListViewTheme); + + AddToClassList("package-list-view"); + + CreateFilteringTools(); + CreateLoadingSpinner(); + CreateScrollView(); + + ShowPackagesView(); + } + + private void CreateScrollView() + { + _packageScrollView = new ScrollView(); + Add(_packageScrollView); + } + + private void CreateFilteringTools() + { + _packageListToolbar = new PackageListToolbar(); + Add(_packageListToolbar); + } + + private void CreateLoadingSpinner() + { + _loadingSpinner = new LoadingSpinner(); + Add(_loadingSpinner); + } + + private void InsertReadOnlyInfoBox(string infoText) + { + var groupHeader = new Box { name = "GroupReadOnlyInfoBox" }; + groupHeader.AddToClassList("package-group-info-box"); + + var infoImage = new Image(); + groupHeader.Add(infoImage); + + var infoLabel = new Label { text = infoText }; + groupHeader.Add(infoLabel); + + _packageScrollView.Add(groupHeader); + } + + public async Task LoadPackages(bool useCachedData) + { + _packages.Clear(); + _packageScrollView.Clear(); + _packageListToolbar.SetEnabled(false); + + if (!useCachedData) + { + _packageDownloadingService.ClearPackageData(); + } + + _loadingSpinner.Show(); + await Task.Delay(100); + + try + { + var response = await _packageDownloadingService.GetPackageData(); + + if (response.Cancelled) + { + ASDebug.Log("Package retrieval was cancelled"); + return; + } + + if (!response.Success) + { + ASDebug.LogError(response.Exception); + OnInitializeError?.Invoke(response.Exception); + return; + } + + var packageModels = response.Packages; + ASDebug.Log($"Found {packageModels.Count} packages"); + + if (packageModels.Count == 0) + { + InsertReadOnlyInfoBox("You do not have any packages yet. Please visit the Publishing Portal if you " + + "would like to create one."); + return; + } + + // Create package groups + _packages = CreatePackages(packageModels); + var packageGroups = CreatePackageGroups(_packages); + var packageGroupElements = CreatePackageGroupElements(packageGroups); + PopulatePackageList(packageGroupElements); + + // Setup filtering and thumbnails + SetupFilteringToolbar(packageGroups); + DownloadAndSetThumbnails(); + } + finally + { + _loadingSpinner.Hide(); + } + } + + private List CreatePackages(List packageModels) + { + return _packages = packageModels.Select(x => _packageFactory.CreatePackage(x)).ToList(); + } + + private List CreatePackageGroups(List packages) + { + var packageGroups = new List(); + var packagesByStatus = packages.GroupBy(x => x.Status).ToDictionary(x => x.Key, x => x.ToList()); + + foreach (var kvp in packagesByStatus) + { + var groupName = char.ToUpper(kvp.Key[0]) + kvp.Key.Substring(1); + var groupPackages = kvp.Value; + var packageGroup = _packageFactory.CreatePackageGroup(groupName, groupPackages); + packageGroups.Add(packageGroup); + } + + return packageGroups; + } + + private List CreatePackageGroupElements(List packageGroups) + { + var elements = new List(); + foreach (var packageGroup in packageGroups) + elements.Add(new PackageGroupElement(packageGroup, _packageFactory)); + + return elements; + } + + private void PopulatePackageList(List packageGroups) + { + // Draft group + var draftGroup = packageGroups.FirstOrDefault(x => x.Name.Equals("draft", StringComparison.OrdinalIgnoreCase)); + if (draftGroup != null) + { + draftGroup.Toggle(true); + _packageScrollView.Add(draftGroup); + } + + // Infobox will only be shown if: + // 1) There is more than 1 group OR + // 2) There is only 1 group, but it is not draft + var showInfoBox = packageGroups.Count > 1 + || packageGroups.Count == 1 && !packageGroups[0].Name.Equals("draft", StringComparison.OrdinalIgnoreCase); + + if (showInfoBox) + InsertReadOnlyInfoBox("Only packages with a 'Draft' status can be selected for uploading Assets"); + + // Priority groups + foreach (var priorityName in _priorityGroupNames) + { + var priorityGroup = packageGroups.FirstOrDefault(x => x.Name.Equals(priorityName, StringComparison.OrdinalIgnoreCase)); + if (priorityGroup == null || _packageScrollView.Contains(priorityGroup)) + continue; + + _packageScrollView.Add(priorityGroup); + } + + // The rest + foreach (var group in packageGroups) + { + if (!_packageScrollView.Contains(group)) + _packageScrollView.Add(group); + } + } + + private void SetupFilteringToolbar(List packageGroups) + { + _packageListToolbar.SetPackageGroups(packageGroups); + _packageListToolbar.Sort(PackageSorting.Name); + _packageListToolbar.SetEnabled(true); + } + + private void DownloadAndSetThumbnails() + { + foreach (var package in _packages) + { + DownloadAndSetThumbnail(package); + } + } + + private async void DownloadAndSetThumbnail(IPackage package) + { + var response = await _packageDownloadingService.GetPackageThumbnail(package); + if (!response.Success) + return; + + package.UpdateIcon(response.Thumbnail); + } + + private void ShowPackagesView() + { + _packageScrollView.style.display = DisplayStyle.Flex; + _packageListToolbar.style.display = DisplayStyle.Flex; + } + + private void OnSceneChange(Scene _, Scene __) + { + DownloadAndSetThumbnails(); + } + } +} diff --git a/.github/Asset Store Tools/Uploader/Scripts/UI/Views/PackageListView.cs.meta b/.github/Asset Store Tools/Uploader/Scripts/UI/Views/PackageListView.cs.meta new file mode 100644 index 00000000000..ccae9d2e1bf --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Scripts/UI/Views/PackageListView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3c499c2d0a5e8fd4b9984184c59893e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Styles.meta b/.github/Asset Store Tools/Uploader/Styles.meta new file mode 100644 index 00000000000..6040ca4abb1 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f9398c14296d30f479b9de5f3ec3b827 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Styles/LoginView.meta b/.github/Asset Store Tools/Uploader/Styles/LoginView.meta new file mode 100644 index 00000000000..03dac6e6709 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/LoginView.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7564d91cabdce1344ba4a0fca25e13d5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Styles/LoginView/Style.uss b/.github/Asset Store Tools/Uploader/Styles/LoginView/Style.uss new file mode 100644 index 00000000000..d97cb7a5784 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/LoginView/Style.uss @@ -0,0 +1,134 @@ +/* Logo */ + +.asset-store-logo { + width: 90%; + height: 32px; + + align-self: center; + + margin: 40px 0; + + -unity-background-scale-mode: scale-to-fit; +} + +/* Cloud Login */ + +.cloud-button-login { + align-self: center; + + width: 75%; +} + +.cloud-button-login-label { + -unity-text-align: middle-center; + white-space: normal; + + min-height: 24px; + padding-right: 4px; +} + +.cloud-label-or { + align-self: center; + + -unity-text-align: middle-center; + + margin: 10px 0; +} + +/* Error Section */ + +.error-container { + flex-direction: row; + flex-shrink: 0; + + align-self: center; + + min-width: 300px; + max-width: 300px; + + margin: 5px 0 5px 1px; +} + +.error-container > Image { + flex-direction: row; + + width: 32px; + height: 32px; + + margin: 5px 10px; +} + +.error-container > Label { + flex-grow: 1; + flex-shrink: 1; + + -unity-text-align: middle-left; + white-space: normal; + + margin-right: 5px; + padding: 2px 0; +} + +/* Credentials Login */ + +.credentials-container { + align-self: center; + + width: 75%; + + padding: 15px; +} + +.credentials-input-container { + align-self: center; + + width: 100%; + + margin: 5px 0; +} + +.credentials-input-container > Label { + -unity-text-align: upper-left; + + margin: 2px 0; +} + +.credentials-input-container > TextField { + height: 20px; + margin: 0 1px; +} + +.credentials-button-login { + align-self: center; + + width: 100%; + margin: 10px 0 15px 0; +} + +.credentials-button-login-label { + -unity-text-align: middle-center; + white-space: normal; + + min-height: 24px; + padding-right: 4px; +} + +/* Help Section */ + +.help-section-container { + flex-direction: row; + + justify-content: space-between; + align-self: center; + + width: 75%; + margin: 5px 0; +} + +.help-section-hyperlink-button { + margin: 0 10px; + padding: 0; + + flex-shrink: 1; + white-space: normal; +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Styles/LoginView/Style.uss.meta b/.github/Asset Store Tools/Uploader/Styles/LoginView/Style.uss.meta new file mode 100644 index 00000000000..e15c190494a --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/LoginView/Style.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5e05fbdf7dd89a14985a87aa62a03a0e +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Uploader/Styles/LoginView/ThemeDark.uss b/.github/Asset Store Tools/Uploader/Styles/LoginView/ThemeDark.uss new file mode 100644 index 00000000000..39e3dfce207 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/LoginView/ThemeDark.uss @@ -0,0 +1,44 @@ +/* Logo */ + +.asset-store-logo { + background-image: url("../../Icons/publisher-portal-dark.png"); +} + +/* Cloud Login */ + +.cloud-label-or { + color: rgb(200, 200, 200); +} + +/* Error Section */ + +.error-container { + background-color: rgb(63, 63, 63); +} + +.error-container > Image { + --unity-image: resource("console.erroricon@2x"); +} + +/* Credentials Login */ + +.credentials-container { + background-color: rgb(63, 63, 63); +} + +/* Help Section */ + +.help-section-hyperlink-button { + color: rgb(68, 113, 229); + border-color: rgba(0, 0, 0, 0); + background-color: rgba(0, 0, 0, 0); +} + +.help-section-hyperlink-button:hover { + color: rgb(68, 133, 229); + cursor: link; +} + +.help-section-hyperlink-button:active { + color: rgb(68, 93, 229); +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Styles/LoginView/ThemeDark.uss.meta b/.github/Asset Store Tools/Uploader/Styles/LoginView/ThemeDark.uss.meta new file mode 100644 index 00000000000..525eb0e6c85 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/LoginView/ThemeDark.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bea9503736c358b4e99eac03c0db32b7 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Uploader/Styles/LoginView/ThemeLight.uss b/.github/Asset Store Tools/Uploader/Styles/LoginView/ThemeLight.uss new file mode 100644 index 00000000000..993c2b8ce6e --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/LoginView/ThemeLight.uss @@ -0,0 +1,44 @@ +/* Logo */ + +.asset-store-logo { + background-image: url("../../Icons/publisher-portal-light.png"); +} + +/* Cloud Login */ + +.cloud-label-or { + color: rgb(28, 28, 28); +} + +/* Error Section */ + +.error-container { + background-color: rgb(212, 212, 212); +} + +.error-container > Image { + --unity-image: resource("console.erroricon@2x"); +} + +/* Credentials Login */ + +.credentials-container { + background-color: rgb(212, 212, 212); +} + +/* Help Section */ + +.help-section-hyperlink-button { + color: rgb(68, 113, 229); + border-color: rgba(0, 0, 0, 0); + background-color: rgba(0, 0, 0, 0); +} + +.help-section-hyperlink-button:hover { + color: rgb(68, 133, 229); + cursor: link; +} + +.help-section-hyperlink-button:active { + color: rgb(68, 93, 229); +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Styles/LoginView/ThemeLight.uss.meta b/.github/Asset Store Tools/Uploader/Styles/LoginView/ThemeLight.uss.meta new file mode 100644 index 00000000000..498f53642fd --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/LoginView/ThemeLight.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 895ecc4cb6c82b144b8423996c89f7ef +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Uploader/Styles/PackageListView.meta b/.github/Asset Store Tools/Uploader/Styles/PackageListView.meta new file mode 100644 index 00000000000..f5446fd0941 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/PackageListView.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 46d44b66f60dc484897f0afeee574dbe +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Uploader/Styles/PackageListView/Style.uss b/.github/Asset Store Tools/Uploader/Styles/PackageListView/Style.uss new file mode 100644 index 00000000000..d36598c1ef4 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/PackageListView/Style.uss @@ -0,0 +1,485 @@ +.package-list-view { + flex-grow: 1; +} + +/* Package List Toolbar */ + +.package-list-toolbar { + flex-direction: row; + flex-shrink: 0; + + justify-content: space-between; + + height: 24px; +} + +.package-search-field { + flex-grow: 1; + flex-shrink: 1; + align-items: center; + + margin: 1px; +} + +.package-search-field > #unity-search { + margin-top: 0; +} + +.package-search-field > * > .unity-base-field__input { + font-size: 12px; + + padding-left: 5px; +} + +.package-sort-menu { + flex-shrink: 0.001; + align-self: center; + + height: 21px; + + margin: 1px; + padding: 1px 6px; +} + +/* Loading Spinner */ + +.loading-spinner-box +{ + flex-grow: 1; + flex-shrink: 1; + + justify-content: center; +} + +.loading-spinner-image { + align-self: center; + + width: 16px; + height: 16px; +} + +/* Package Group */ + +.package-group-expander-box { + flex-direction: row; + flex-grow: 0; + flex-shrink: 0; + + align-items: center; + + min-height: 30px; + + margin: 10px 0 2px 0; + padding: 1px 5px; +} + +.package-group-expander +{ + align-self: center; + + width: 30px; + height: 30px; +} + +.package-group-label { + font-size: 14px; + -unity-font-style: bold; +} + +.package-group-content-box { + margin: 0; +} + +.package-group-separator { + height: 2px; + + margin: 5px 15px; + + display: none; +} + +.package-group-info-box { + flex-direction: row; + align-items: center; + font-size: 12px; + margin: 5px; + margin-top: 5px; +} + +.package-group-info-box > Image +{ + flex-direction: row; + flex-shrink: 0; + + width: 32px; + height: 32px; + + margin: 5px 10px; +} + +.package-group-info-box > Label +{ + flex-grow: 1; + flex-shrink: 1; + + -unity-text-align: middle-left; + white-space: normal; + + margin-right: 5px; +} + +/* Package Element */ + +.package-full-box { + flex-direction: column; + flex-shrink: 0; + flex-grow: 0; + + min-width: 280px; +} + +.package-foldout-box { + flex-direction: column; + margin: 0; +} + +.package-foldout-box-expanded { + padding-top: 3px; + top: -2px; +} + +.package-foldout-box-info { + flex-direction: row; + flex-grow: 0; + flex-shrink: 0; + + align-items: center; + justify-content: space-between; + + min-width: 200px; + min-height: 50px; + + margin: 2px; +} + +.package-expander-label-row { + flex-basis: 200px; + flex-direction: row; + flex-grow: 1; +} + +.package-expander { + align-self: center; + + width: 30px; + height: 30px; +} + +.package-image { + flex-shrink: 0; + + width: 48px; + height: 48px; + + margin: 0 5px; +} + +.package-label-info-box { + flex-direction: column; + flex-grow: 1; + flex-shrink: 1; + + align-self: center; +} + +.package-label { + flex-shrink: 1; + + align-self: stretch; + + -unity-text-align: middle-left; + -unity-font-style: bold; + white-space: normal; +} + +.package-info { + font-size: 11px; + -unity-text-align: middle-left; + white-space: normal; +} + +.package-open-in-browser-button { + flex-shrink: 0; + + width: 16px; + height: 16px; + + -unity-background-scale-mode: scale-to-fit; +} + +.package-header-progress-bar { + margin: 0 -6px -1px -6px; + padding: 0 0 2px 0; +} + +.package-header-progress-bar > * > .unity-progress-bar__background { + height: 5px; +} + +/* Package Content Element & its nested elements */ + +.package-content-element { + flex-grow: 1; + flex-shrink: 0; + + overflow: hidden; + + margin: -5px 0px 2px 0px; + padding: 5px 10px 5px 40px; +} + +.package-content-option-box { + flex-direction: row; + + margin-top: 10px; +} + +.package-content-option-label-help-row { + flex-direction: row; + flex-shrink: 0; + + align-self: flex-start; + align-items: center; + justify-content: flex-start; + + width: 115px; +} + +.package-content-option-label-help-row > Label { + -unity-text-align: middle-left; +} + +.package-content-option-label-help-row > Image { + height: 16px; + width: 16px; +} + +.package-content-option-dropdown { + flex-grow: 1; + flex-shrink: 1; + + align-self: stretch; + + margin-right: 0; + margin-left: 3px; +} + +.package-content-option-textfield { + flex-grow: 1; + flex-shrink: 1; + + align-self: stretch; +} + +.package-content-option-button { + margin-right: 0; +} + +.package-content-option-toggle { + flex-shrink: 1; +} + +.package-content-option-toggle > * { + flex-shrink: 1; +} + +.package-content-option-toggle * > .unity-label { + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + margin-left: 5px; +} + +/* MutliToggleSelection Element */ + +.multi-toggle-box { + flex-grow: 1; + flex-direction: column; +} + +.multi-toggle-box-scrollview { + flex-grow: 1; + + height: 100px; + margin-left: 3px; +} + +.multi-toggle-box-scrollview > .unity-scroll-view__content-viewport +{ + margin-left: 1px; +} + +.multi-toggle-box-scrollview > * > .unity-scroll-view__content-container +{ + padding: 3px 0 5px 0; +} + +.multi-toggle-box-scrollview > * > .unity-scroll-view__vertical-scroller +{ + margin: -1px 0; +} + +.multi-toggle-box-empty-label { + flex-grow: 1; + + font-size: 11px; + -unity-text-align: middle-center; + white-space: normal; + + display: none; +} + +.multi-toggle-box-toolbar { + flex-direction: row; + margin: -1px 0 0 3px; +} + +.multi-toggle-box-toolbar-selecting-box { + flex-grow: 1; + flex-shrink: 1; + flex-direction: row; + + align-items: center; + justify-content: flex-end; + + margin-top: 1px; +} + +.multi-toggle-box-toolbar-selecting-box > Button { + flex-shrink: 1; + width: 75px; + margin-right: 0; +} + +.multi-toggle-box-toolbar-filtering-box { + flex-grow: 1; + flex-direction: row; + + align-items: center; + justify-content: flex-start; + + margin-top: 1px; + margin-left: 1px; +} + +.multi-toggle-box-toolbar-filtering-box > ToolbarMenu { + width: 100px; + margin: 0; +} + +.multi-toggle-box-toggle { + padding: 2px 0 0 5px; +} + +.multi-toggle-box-toggle > * > .unity-label { + margin-left: 5px; +} + +/* Validation Element */ + +.validation-result-box { + flex-direction: row; + + align-items: center; + + font-size: 12px; + + margin-top: 5px; +} + +.validation-result-box > Image { + flex-direction: row; + flex-shrink: 0; + + width: 32px; + height: 32px; + + margin: 5px 10px; +} + +.validation-result-box > Label { + flex-grow: 1; + flex-shrink: 1; + + -unity-text-align: middle-left; + white-space: normal; + + margin-right: 5px; +} + +.validation-result-box > Button { + margin-right: 10px; +} + +.validation-result-view-report-button-container { + flex-shrink: 0; + padding: 0 10px 0 10px; +} + +.validation-result-view-report-button { + margin: 0; + padding: 4px 0 4px 0; +} + +.validation-result-view-report-button:hover { + cursor: link; +} + +/* Uploading Element */ + +.uploading-box { + margin-top: 10px; +} + +.uploading-export-and-upload-container { + flex-direction: row; + flex-wrap: wrap; +} + +.uploading-export-button { + flex-basis: 110px; + flex-grow: 1; + height: 24px; + margin: 1px 2px 0 2px; +} + +.uploading-upload-button { + flex-basis: 110px; + flex-grow: 1; + height: 24px; + margin: 1px 2px 0 2px; +} + +.uploading-progress-container { + flex-direction: row; +} + +.uploading-progress-bar { + flex-grow: 1; + flex-shrink: 1; + + justify-content: center; + + padding: 0; + margin: 0 10px 0 0; +} + +.uploading-progress-bar > * > .unity-progress-bar__background { + height: 22px; +} + +.uploading-cancel-button { + width: 95px; + height: 24px; + margin: 0; +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Styles/PackageListView/Style.uss.meta b/.github/Asset Store Tools/Uploader/Styles/PackageListView/Style.uss.meta new file mode 100644 index 00000000000..9ac57e4f90d --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/PackageListView/Style.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4b3b52dc166e3e24bb1ce4acd2592f3a +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Uploader/Styles/PackageListView/ThemeDark.uss b/.github/Asset Store Tools/Uploader/Styles/PackageListView/ThemeDark.uss new file mode 100644 index 00000000000..a0ef9ed4716 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/PackageListView/ThemeDark.uss @@ -0,0 +1,238 @@ +/* Package List Toolbar */ + +.package-list-toolbar { + border-bottom-width: 1px; + border-color: rgb(33, 33, 33); + + background-color: rgb(68, 68, 68); +} + +.package-sort-menu { + color: rgb(238, 238, 238); + + border-width: 1px; + border-radius: 3px; + border-color: rgb(36, 36, 36); + + background-color: rgb(88, 88, 88); +} + +.package-sort-menu:hover { + background-color: rgb(103, 103, 103); +} + +.package-sort-menu:active { + background-color: rgb(73, 73, 73); +} + +/* Loading Spinner */ + +.loading-spinner-image { + --unity-image: resource("WaitSpin00"); +} + +/* Package Group */ + +.package-group-expander-box { + border-width: 0; + border-color: rgba(33, 33, 33, 0); + + background-color: rgba(68, 68, 68, 0); +} + +.package-group-expander { + color: rgb(104, 104, 104); +} + +.package-group-label { + color: rgb(255, 255, 255); +} + +.package-group-separator { + background-color: rgb(48, 48, 48); +} + +.package-group-info-box { + border-width: 1px; + border-color: rgb(33, 33, 33); +} + +.package-group-info-box > Image { + --unity-image: resource("console.infoicon@2x"); +} + +/* Package Element */ + +.package-foldout-box { + border-width: 0; + border-radius: 0; + background-color: rgb(56, 56, 56); +} + +.package-foldout-box:hover { + background-color: rgb(68, 68, 68); +} + +.package-foldout-box-draft:active { + background-color: rgb(48, 48, 48); +} + +.package-foldout-box-expanded { + background-color: rgb(68, 68, 68); +} + +.package-expander { + color: rgb(104, 104, 104); +} + +.package-image { + border-radius: 5px; + background-image: resource("Sprite Icon"); +} + +.package-label { + color: rgb(255, 255, 255); +} + +.package-info { + color: rgb(200, 200, 200); +} + +.package-open-in-browser-button { + border-width: 0; + border-radius: 0; + + background-color: rgba(0, 0, 0, 0); + background-image: url("../../Icons/open-in-browser.png"); + + -unity-background-image-tint-color: rgb(200, 200, 200); +} + +.package-open-in-browser-button:hover { + -unity-background-image-tint-color: rgb(255, 255, 255); +} + +.package-open-in-browser-button:active { + -unity-background-image-tint-color: rgb(155, 155, 155); +} + +.package-header-progress-bar > .unity-progress-bar__container > .unity-progress-bar__background { + border-width: 0; + background-color: rgba(0, 0, 0, 0); +} + +.package-header-progress-bar > .unity-progress-bar__container > .unity-progress-bar__background > .unity-progress-bar__progress { + background-image: none; + background-color: rgb(33, 150, 243); +} + +/* Package Content Element */ + +.package-content-element { + background-color: rgb(68, 68, 68); +} + +.package-content-option-label-help-row > Image { + --unity-image: resource("d__Help@2x"); +} + +.package-content-option-dropdown { + color: rgb(238, 238, 238); + + border-width: 1px; + border-radius: 3px; + border-color: rgb(36, 36, 36); + + background-color: rgb(88, 88, 88); +} + +.package-content-option-dropdown:hover { + background-color: rgb(103, 103, 103); +} + +/* MultiToggleSelection Element */ + +.multi-toggle-box-scrollview { + border-width: 1px; + border-color: rgb(33, 33, 33); + background-color: rgb(58, 58, 58); +} + +.multi-toggle-box-scrollview > * > .unity-scroll-view__vertical-scroller { + border-right-width: 0; +} + +.multi-toggle-box-empty-label { + color: rgb(200, 200, 200); +} + +.multi-toggle-box-toolbar { + border-width: 1px; + border-color: rgb(33, 33, 33); + background-color: rgb(58, 58, 58); +} + +.multi-toggle-box-toolbar-filtering-box > ToolbarMenu { + color: rgb(188, 188, 188); + + border-width: 1px; + border-radius: 3px; + border-color: rgb(36, 36, 36); + + background-color: rgb(88, 88, 88); +} + +/* Validation Element */ + +.validation-result-box { + border-width: 1px; + border-color: rgb(33, 33, 33); +} + +.validation-result-box > Image { + --unity-image: resource("console.infoicon@2x"); +} + +.validation-result-view-report-button { + color: rgb(68, 113, 229); + border-color: rgba(0, 0, 0, 0); + background-color: rgba(0, 0, 0, 0); +} + +.validation-result-view-report-button:hover { + color: rgb(68, 133, 229); +} + +.validation-result-view-report-button:active { + color: rgb(68, 93, 229); +} + +/* Uploading Element */ + +.uploading-export-button { + color: rgb(220, 220, 220); + border-width: 1px; + border-color: rgb(33, 33, 33); +} + +.uploading-upload-button { + color: rgb(220, 220, 220); + border-width: 1px; + border-color: rgb(33, 33, 33); +} + +.uploading-progress-bar > .unity-progress-bar__container > .unity-progress-bar__background { + border-width: 0; + border-radius: 2px; +} + +.uploading-progress-bar > .unity-progress-bar__container > .unity-progress-bar__background > .unity-progress-bar__progress { + background-image: none; + background-color: rgb(33, 150, 243); +} + +.uploading-cancel-button { + color: rgb(220, 220, 220); + border-width: 1px; + border-color: rgb(33, 33, 33); +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Styles/PackageListView/ThemeDark.uss.meta b/.github/Asset Store Tools/Uploader/Styles/PackageListView/ThemeDark.uss.meta new file mode 100644 index 00000000000..6e8e7be8e2c --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/PackageListView/ThemeDark.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2096f0ee5b05ec643b409ded0d725eb3 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Uploader/Styles/PackageListView/ThemeLight.uss b/.github/Asset Store Tools/Uploader/Styles/PackageListView/ThemeLight.uss new file mode 100644 index 00000000000..d375fca06d7 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/PackageListView/ThemeLight.uss @@ -0,0 +1,238 @@ +/* Package List Toolbar */ + +.package-list-toolbar { + border-bottom-width: 1px; + border-color: rgb(127, 127, 127); + + background-color: rgb(203, 203, 203); +} + +.package-sort-menu { + color: rgb(9, 9, 9); + + border-width: 1px; + border-radius: 3px; + border-color: rgb(178, 178, 178); + + background-color: rgb(228, 228, 228); +} + +.package-sort-menu:hover { + background-color: rgb(236, 236, 236); +} + +.package-sort-menu:active { + background-color: rgb(220, 220, 220); +} + +/* Loading Spinner */ + +.loading-spinner-image { + --unity-image: resource("WaitSpin00"); +} + +/* Package Group */ + +.package-group-expander-box { + border-width: 0; + border-color: rgba(0, 0, 0, 0); + + background-color: rgba(0, 0, 0, 0); +} + +.package-group-expander { + color: rgb(77, 77, 77); +} + +.package-group-label { + color: rgb(0, 0, 0); +} + +.package-group-separator +{ + background-color: rgb(48, 48, 48); +} + +.package-group-info-box +{ + border-width: 1px; + border-color: rgb(33, 33, 33); +} + +.package-group-info-box > Image +{ + --unity-image: resource("console.infoicon@2x"); +} + +/* Package Element */ + +.package-foldout-box { + border-width: 0; + border-radius: 0; + background-color: rgb(198, 198, 198); +} + +.package-foldout-box:hover { + background-color: rgb(212, 212, 212); +} + +.package-foldout-box-draft:active { + background-color: rgb(180, 180, 180); +} + +.package-foldout-box-expanded { + background-color: rgb(212, 212, 212); +} + +.package-expander { + color: rgb(77, 77, 77); +} + +.package-image { + border-radius: 5px; + background-image: resource("Sprite Icon"); +} + +.package-label { + color: rgb(0, 0, 0); +} + +.package-info { + color: rgb(48, 48, 48); +} + +.package-open-in-browser-button { + border-width: 0; + border-radius: 0; + + background-color: rgba(0, 0, 0, 0); + background-image: url("../../Icons/open-in-browser.png"); + + -unity-background-image-tint-color: rgb(150, 150, 150); +} + +.package-open-in-browser-button:hover { + -unity-background-image-tint-color: rgb(100, 100, 100); +} + +.package-open-in-browser-button:active { + -unity-background-image-tint-color: rgb(50, 50, 50); +} + +.package-header-progress-bar > .unity-progress-bar__container > .unity-progress-bar__background { + border-width: 0; + background-color: rgba(0, 0, 0, 0); +} + +.package-header-progress-bar > .unity-progress-bar__container > .unity-progress-bar__background > .unity-progress-bar__progress { + background-image: none; + background-color: rgb(108, 157, 243); +} + +/* Package Content Element */ + +.package-content-element { + background-color: rgb(212, 212, 212); +} + +.package-content-option-label-help-row > Image { + --unity-image: resource("_Help@2x"); +} + +.package-content-option-dropdown { + color: rgb(9, 9, 9); + + border-width: 1px; + border-radius: 3px; + border-color: rgb(178, 178, 178); + + background-color: rgb(228, 228, 228); +} + +.package-content-option-dropdown:hover { + background-color: rgb(236, 236, 236); +} + +/* MultiToggleSelection Element */ + +.multi-toggle-box-scrollview { + border-width: 1px; + border-color: rgb(33, 33, 33); + background-color: rgb(200, 200, 200); +} + +.multi-toggle-box-scrollview > * > .unity-scroll-view__vertical-scroller { + border-right-width: 0; +} + +.multi-toggle-box-empty-label { + color: rgb(0, 0, 0); +} + +.multi-toggle-box-toolbar { + border-width: 1px; + border-color: rgb(33, 33, 33); + background-color: rgb(200, 200, 200); +} + +.multi-toggle-box-toolbar-filtering-box > ToolbarMenu { + color: rgb(9, 9, 9); + + border-width: 1px; + border-radius: 3px; + border-color: rgb(178, 178, 178); + + background-color: rgb(228, 228, 228); +} + +/* Validation Element */ + +.validation-result-box { + border-width: 1px; + border-color: rgb(33, 33, 33); +} + +.validation-result-box > Image { + --unity-image: resource("console.infoicon@2x"); +} + +.validation-result-view-report-button { + color: rgb(68, 113, 229); + border-color: rgba(0, 0, 0, 0); + background-color: rgba(0, 0, 0, 0); +} + +.validation-result-view-report-button:hover { + color: rgb(68, 133, 229); +} + +.validation-result-view-report-button:active { + color: rgb(68, 93, 229); +} + +/* Uploading Element */ + +.uploading-export-button { + border-width: 1px; + border-color: rgb(33, 33, 33); +} + +.uploading-upload-button { + border-width: 1px; + border-color: rgb(33, 33, 33); +} + +.uploading-progress-bar > .unity-progress-bar__container > .unity-progress-bar__background { + border-width: 0; + border-radius: 2px; +} + +.uploading-progress-bar > .unity-progress-bar__container > .unity-progress-bar__background > .unity-progress-bar__progress { + background-image: none; + background-color: rgb(108, 157, 243); +} + +.uploading-cancel-button { + border-width: 1px; + border-color: rgb(33, 33, 33); +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Styles/PackageListView/ThemeLight.uss.meta b/.github/Asset Store Tools/Uploader/Styles/PackageListView/ThemeLight.uss.meta new file mode 100644 index 00000000000..1cc032a1894 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/PackageListView/ThemeLight.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9b79808cdce73b949abc32c5fada1bda +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Uploader/Styles/Style.uss b/.github/Asset Store Tools/Uploader/Styles/Style.uss new file mode 100644 index 00000000000..6a6926130e1 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/Style.uss @@ -0,0 +1,74 @@ +.uploader-window-root { + flex-grow: 1; +} + +.uploader-window-root-scrollview { + flex-grow: 1; +} + +.uploader-window-root-scrollview > * > .unity-scroll-view__content-container { + flex-grow: 1; +} + +.uploader-window-root-scrollview > * > .unity-scroll-view__content-viewport { + flex-shrink: 1; +} + +.uploader-window-root-scrollview > * > * > .unity-scroll-view__content-container { + flex-grow: 1; + flex-shrink: 1; +} + +.account-toolbar { + flex-direction: row; + flex-shrink: 0; + + justify-content: space-between; + margin: 0; +} + +.account-toolbar-left-side-container { + flex-grow: 1; + flex-shrink: 0; + flex-basis: 0px; + overflow: hidden; + min-width: 16px; + align-self: center; + flex-direction: row; + margin: 3px 0px 3px 1px; +} + +.account-toolbar-right-side-container { + flex-shrink: 1; + flex-grow: 0; + flex-wrap: wrap; + align-self: center; + flex-direction: row; + margin: 0; +} + +.account-toolbar-user-image { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 1px; +} + +.account-toolbar-email-label { + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + -unity-font-style: bold; +} + +.account-toolbar-button-refresh { + flex-grow: 1; + min-width: 50px; + margin-right: 1px; +} + +.account-toolbar-button-logout { + flex-grow: 1; + min-width: 50px; + margin-right: 1px; +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Styles/Style.uss.meta b/.github/Asset Store Tools/Uploader/Styles/Style.uss.meta new file mode 100644 index 00000000000..1a2d46282ac --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/Style.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6f0343bfa66bbb344bee68a23d24cca8 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Uploader/Styles/ThemeDark.uss b/.github/Asset Store Tools/Uploader/Styles/ThemeDark.uss new file mode 100644 index 00000000000..7f836d43b8d --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/ThemeDark.uss @@ -0,0 +1,9 @@ +.account-toolbar { + background-color: rgb(68, 68, 68); + border-color: rgb(33, 33, 33); + border-top-width: 1px; +} + +.account-toolbar-user-image { + background-image: url("../Icons/account-dark.png"); +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Styles/ThemeDark.uss.meta b/.github/Asset Store Tools/Uploader/Styles/ThemeDark.uss.meta new file mode 100644 index 00000000000..2ebe886a520 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/ThemeDark.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 26d328c2ffac2ff44af266e9c632f2ab +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Uploader/Styles/ThemeLight.uss b/.github/Asset Store Tools/Uploader/Styles/ThemeLight.uss new file mode 100644 index 00000000000..16e3de4d0c1 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/ThemeLight.uss @@ -0,0 +1,9 @@ +.account-toolbar { + background-color: rgb(203, 203, 203); + border-color: rgb(127, 127, 127); + border-top-width: 1px; +} + +.account-toolbar-user-image { + background-image: url("../Icons/account-light.png"); +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/Styles/ThemeLight.uss.meta b/.github/Asset Store Tools/Uploader/Styles/ThemeLight.uss.meta new file mode 100644 index 00000000000..03e9d7cadb7 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/Styles/ThemeLight.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a994594cc493b7346b4d4a71d39908dc +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Uploader/UploaderWindow.cs b/.github/Asset Store Tools/Uploader/UploaderWindow.cs new file mode 100644 index 00000000000..f9fffa9b7c3 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/UploaderWindow.cs @@ -0,0 +1,232 @@ +using AssetStoreTools.Api.Models; +using AssetStoreTools.Uploader.Services; +using AssetStoreTools.Uploader.Services.Analytics; +using AssetStoreTools.Uploader.Services.Api; +using AssetStoreTools.Uploader.UI.Elements; +using AssetStoreTools.Uploader.UI.Views; +using AssetStoreTools.Utility; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader +{ + internal class UploaderWindow : AssetStoreToolsWindow + { + private const string DebugPhrase = "debug"; + + // Services + private ICachingService _cachingService; + private IAuthenticationService _authenticationService; + private IPackageDownloadingService _packageDownloadingService; + private IPackageUploadingService _packageUploadingService; + private IAnalyticsService _analyticsService; + private IPackageFactoryService _packageFactoryService; + + // Data + private bool _isQuitting = false; + + // UI + private VisualElement _uploaderWindowRoot; + private ScrollView _rootScrollView; + + private LoginView _loginView; + private PackageListView _packageListView; + private AccountToolbar _accountToolbar; + + protected override string WindowTitle => "Asset Store Uploader"; + + protected override void Init() + { + RegisterServices(); + SetupWindow(); + } + + private void RegisterServices() + { + var uploaderServiceProvider = UploaderServiceProvider.Instance; + _analyticsService = uploaderServiceProvider.GetService(); + _cachingService = uploaderServiceProvider.GetService(); + _authenticationService = uploaderServiceProvider.GetService(); + _packageDownloadingService = uploaderServiceProvider.GetService(); + _packageUploadingService = uploaderServiceProvider.GetService(); + _packageFactoryService = uploaderServiceProvider.GetService(); + } + + private void SetupWindow() + { + minSize = new Vector2(400, 430); + this.SetAntiAliasing(4); + rootVisualElement.styleSheets.Add(StyleSelector.UploaderWindow.UploaderWindowStyle); + rootVisualElement.styleSheets.Add(StyleSelector.UploaderWindow.UploaderWindowTheme); + + if (_cachingService.GetCachedUploaderWindow(out _uploaderWindowRoot)) + { + rootVisualElement.Add(_uploaderWindowRoot); + return; + } + + _uploaderWindowRoot = new VisualElement(); + _uploaderWindowRoot.AddToClassList("uploader-window-root"); + rootVisualElement.Add(_uploaderWindowRoot); + + _rootScrollView = new ScrollView(); + _rootScrollView.AddToClassList("uploader-window-root-scrollview"); + _uploaderWindowRoot.Add(_rootScrollView); + + CreateLoginView(); + CreatePackageListView(); + CreateAccountToolbar(); + EditorApplication.wantsToQuit += OnWantsToQuit; + + _cachingService.CacheUploaderWindow(_uploaderWindowRoot); + + PerformAuthentication(); + } + + private void CreateLoginView() + { + _loginView = new LoginView(_authenticationService); + _loginView.OnAuthenticated += OnAuthenticationSuccess; + _rootScrollView.Add(_loginView); + } + + private void CreatePackageListView() + { + _packageListView = new PackageListView(_packageDownloadingService, _packageFactoryService); + _packageListView.OnInitializeError += PackageViewInitializationError; + _rootScrollView.Add(_packageListView); + } + + private void CreateAccountToolbar() + { + _accountToolbar = new AccountToolbar(); + _accountToolbar.OnRefresh += RefreshPackages; + _accountToolbar.OnLogout += LogOut; + _uploaderWindowRoot.Add(_accountToolbar); + } + + private void PerformAuthentication() + { + ShowAuthenticationView(); + _loginView.LoginWithSessionToken(); + } + + private async void OnAuthenticationSuccess(User user) + { + _accountToolbar.SetUser(user); + _accountToolbar.DisableButtons(); + + ShowAccountPackageView(); + await _packageListView.LoadPackages(true); + + _accountToolbar.EnableButtons(); + } + + private async Task RefreshPackages() + { + _packageUploadingService.StopAllUploadinng(); + _packageDownloadingService.StopDownloading(); + + await _packageListView.LoadPackages(false); + } + + private void LogOut() + { + _packageUploadingService.StopAllUploadinng(); + _packageDownloadingService.StopDownloading(); + + _authenticationService.Deauthenticate(); + _packageDownloadingService.ClearPackageData(); + + _accountToolbar.SetUser(null); + ShowAuthenticationView(); + } + + private void PackageViewInitializationError(Exception e) + { + _loginView.DisplayError(e.Message); + LogOut(); + } + + private void ShowAuthenticationView() + { + HideElement(_accountToolbar); + HideElement(_packageListView); + ShowElement(_loginView); + } + + private void ShowAccountPackageView() + { + HideElement(_loginView); + ShowElement(_accountToolbar); + ShowElement(_packageListView); + } + + private void ShowElement(params VisualElement[] elements) + { + foreach (var e in elements) + e.style.display = DisplayStyle.Flex; + } + + private void HideElement(params VisualElement[] elements) + { + foreach (var e in elements) + e.style.display = DisplayStyle.None; + } + + private void OnDestroy() + { + if (!_isQuitting && _packageUploadingService.IsUploading) + { + EditorUtility.DisplayDialog("Notice", "Assets are still being uploaded to the Asset Store. " + + "If you wish to check on the progress, please re-open the Asset Store Uploader window", "OK"); + } + } + + private bool OnWantsToQuit() + { + if (!_packageUploadingService.IsUploading) + return true; + + _isQuitting = EditorUtility.DisplayDialog("Notice", "Assets are still being uploaded to the Asset Store. " + + "Would you still like to close Unity Editor?", "Yes", "No"); + + return _isQuitting; + } + + #region Debug Utility + + private readonly List _debugBuffer = new List(); + + private void OnGUI() + { + CheckForDebugMode(); + } + + private void CheckForDebugMode() + { + Event e = Event.current; + + if (e.type != EventType.KeyDown || e.keyCode == KeyCode.None) + return; + + _debugBuffer.Add(e.keyCode.ToString().ToLower()[0]); + if (_debugBuffer.Count > DebugPhrase.Length) + _debugBuffer.RemoveAt(0); + + if (string.Join(string.Empty, _debugBuffer.ToArray()) != DebugPhrase) + return; + + ASDebug.DebugModeEnabled = !ASDebug.DebugModeEnabled; + ASDebug.Log($"DEBUG MODE ENABLED: {ASDebug.DebugModeEnabled}"); + _debugBuffer.Clear(); + } + + #endregion + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Uploader/UploaderWindow.cs.meta b/.github/Asset Store Tools/Uploader/UploaderWindow.cs.meta new file mode 100644 index 00000000000..ef78266e2f9 --- /dev/null +++ b/.github/Asset Store Tools/Uploader/UploaderWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7b5319699cc84194a9a768ad33b86c21 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Utility.meta b/.github/Asset Store Tools/Utility.meta new file mode 100644 index 00000000000..9a2bdffe58b --- /dev/null +++ b/.github/Asset Store Tools/Utility.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3dfd03e520c145b45a32c7e2915fe3cb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Utility/ASDebug.cs b/.github/Asset Store Tools/Utility/ASDebug.cs new file mode 100644 index 00000000000..16fc4c8b6ac --- /dev/null +++ b/.github/Asset Store Tools/Utility/ASDebug.cs @@ -0,0 +1,65 @@ +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Utility +{ + internal static class ASDebug + { + private enum LogType + { + Info, + Warning, + Error + } + + private static string FormatInfo(object message) => $"[AST Info] {message}"; + private static string FormatWarning(object message) => $"[AST Warning] {message}"; + private static string FormatError(object message) => $"[AST Error] {message}"; + + + private static bool s_debugModeEnabled = EditorPrefs.GetBool(Constants.Debug.DebugModeKey); + + public static bool DebugModeEnabled + { + get => s_debugModeEnabled; + set { s_debugModeEnabled = value; EditorPrefs.SetBool(Constants.Debug.DebugModeKey, value); } + } + + public static void Log(object message) + { + LogMessage(message, LogType.Info); + } + + public static void LogWarning(object message) + { + LogMessage(message, LogType.Warning); + } + + public static void LogError(object message) + { + LogMessage(message, LogType.Error); + } + + private static void LogMessage(object message, LogType type) + { + if (!DebugModeEnabled) + return; + + switch (type) + { + case LogType.Info: + Debug.Log(FormatInfo(message)); + break; + case LogType.Warning: + Debug.LogWarning(FormatWarning(message)); + break; + case LogType.Error: + Debug.LogError(FormatError(message)); + break; + default: + Debug.Log(message); + break; + } + } + } +} diff --git a/.github/Asset Store Tools/Utility/ASDebug.cs.meta b/.github/Asset Store Tools/Utility/ASDebug.cs.meta new file mode 100644 index 00000000000..2f4aab787eb --- /dev/null +++ b/.github/Asset Store Tools/Utility/ASDebug.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 478caa497d99100429a0509fa487bfe4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Utility/ASToolsPreferences.cs b/.github/Asset Store Tools/Utility/ASToolsPreferences.cs new file mode 100644 index 00000000000..73ad2bdec34 --- /dev/null +++ b/.github/Asset Store Tools/Utility/ASToolsPreferences.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Utility +{ + internal class ASToolsPreferences + { + private static ASToolsPreferences s_instance; + public static ASToolsPreferences Instance => s_instance ?? (s_instance = new ASToolsPreferences()); + + public static event Action OnSettingsChange; + + private ASToolsPreferences() + { + Load(); + } + + private void Load() + { + CheckForUpdates = PlayerPrefs.GetInt("AST_CheckForUpdates", 1) == 1; + LegacyVersionCheck = PlayerPrefs.GetInt("AST_LegacyVersionCheck", 1) == 1; + UploadVersionCheck = PlayerPrefs.GetInt("AST_UploadVersionCheck", 1) == 1; + DisplayHiddenMetaDialog = PlayerPrefs.GetInt("AST_HiddenFolderMetaCheck", 1) == 1; + EnableSymlinkSupport = PlayerPrefs.GetInt("AST_EnableSymlinkSupport", 0) == 1; + UseLegacyExporting = PlayerPrefs.GetInt("AST_UseLegacyExporting", 0) == 1; + } + + public void Save(bool triggerSettingsChange = false) + { + PlayerPrefs.SetInt("AST_CheckForUpdates", CheckForUpdates ? 1 : 0); + PlayerPrefs.SetInt("AST_LegacyVersionCheck", LegacyVersionCheck ? 1 : 0); + PlayerPrefs.SetInt("AST_UploadVersionCheck", UploadVersionCheck ? 1 : 0); + PlayerPrefs.SetInt("AST_HiddenFolderMetaCheck", DisplayHiddenMetaDialog ? 1 : 0); + PlayerPrefs.SetInt("AST_EnableSymlinkSupport", EnableSymlinkSupport ? 1 : 0); + PlayerPrefs.SetInt("AST_UseLegacyExporting", UseLegacyExporting ? 1 : 0); + PlayerPrefs.Save(); + + if (triggerSettingsChange) + OnSettingsChange?.Invoke(); + } + + /// + /// Periodically check if an update for the Asset Store Publishing Tools is available + /// + public bool CheckForUpdates; + + /// + /// Check if legacy Asset Store Tools are in the Project + /// + public bool LegacyVersionCheck; + + /// + /// Enables a DisplayDialog when hidden folders are found to be missing meta files + /// + public bool DisplayHiddenMetaDialog; + + /// + /// Check if the package has been uploaded from a correct Unity version at least once + /// + public bool UploadVersionCheck; + + /// + /// Enables Junction symlink support + /// + public bool EnableSymlinkSupport; + + /// + /// Enables legacy exporting for Folder Upload workflow + /// + public bool UseLegacyExporting; + } + + internal class ASToolsPreferencesProvider : SettingsProvider + { + private const string SettingsPath = "Project/Asset Store Tools"; + + private class Styles + { + public static readonly GUIContent CheckForUpdatesLabel = EditorGUIUtility.TrTextContent("Check for Updates", "Periodically check if an update for the Asset Store Publishing Tools is available."); + public static readonly GUIContent LegacyVersionCheckLabel = EditorGUIUtility.TrTextContent("Legacy ASTools Check", "Enable Legacy Asset Store Tools version checking."); + public static readonly GUIContent UploadVersionCheckLabel = EditorGUIUtility.TrTextContent("Upload Version Check", "Check if the package has been uploader from a correct Unity version at least once."); + public static readonly GUIContent DisplayHiddenMetaDialogLabel = EditorGUIUtility.TrTextContent("Display Hidden Folder Meta Dialog", "Show a DisplayDialog when hidden folders are found to be missing meta files.\nNote: this only affects hidden folders ending with a '~' character"); + public static readonly GUIContent EnableSymlinkSupportLabel = EditorGUIUtility.TrTextContent("Enable Symlink Support", "Enable Junction Symlink support. Note: folder selection validation will take longer."); + public static readonly GUIContent UseLegacyExportingLabel = EditorGUIUtility.TrTextContent("Use Legacy Exporting", "Enabling this option uses native Unity methods when exporting packages for the Folder Upload workflow.\nNote: individual package dependency selection when choosing to 'Include Package Manifest' is unavailable when this option is enabled."); + public static readonly GUIContent UseCustomPreviewsLabel = EditorGUIUtility.TrTextContent("Enable High Quality Previews (experimental)", "Override native asset preview retrieval with higher-quality preview generation"); + } + + public static void OpenSettings() + { + SettingsService.OpenProjectSettings(SettingsPath); + } + + private ASToolsPreferencesProvider(string path, SettingsScope scopes, IEnumerable keywords = null) + : base(path, scopes, keywords) { } + + public override void OnGUI(string searchContext) + { + var preferences = ASToolsPreferences.Instance; + + EditorGUI.BeginChangeCheck(); + using (CreateSettingsWindowGUIScope()) + { + preferences.CheckForUpdates = EditorGUILayout.Toggle(Styles.CheckForUpdatesLabel, preferences.CheckForUpdates); + preferences.LegacyVersionCheck = EditorGUILayout.Toggle(Styles.LegacyVersionCheckLabel, preferences.LegacyVersionCheck); + preferences.UploadVersionCheck = EditorGUILayout.Toggle(Styles.UploadVersionCheckLabel, preferences.UploadVersionCheck); + preferences.DisplayHiddenMetaDialog = EditorGUILayout.Toggle(Styles.DisplayHiddenMetaDialogLabel, preferences.DisplayHiddenMetaDialog); + preferences.EnableSymlinkSupport = EditorGUILayout.Toggle(Styles.EnableSymlinkSupportLabel, preferences.EnableSymlinkSupport); + preferences.UseLegacyExporting = EditorGUILayout.Toggle(Styles.UseLegacyExportingLabel, preferences.UseLegacyExporting); + } + + if (EditorGUI.EndChangeCheck()) + { + ASToolsPreferences.Instance.Save(true); + } + } + + [SettingsProvider] + public static SettingsProvider CreateAssetStoreToolsSettingProvider() + { + var provider = new ASToolsPreferencesProvider(SettingsPath, SettingsScope.Project, GetSearchKeywordsFromGUIContentProperties()); + return provider; + } + + private IDisposable CreateSettingsWindowGUIScope() + { + var unityEditorAssembly = Assembly.GetAssembly(typeof(EditorWindow)); + var type = unityEditorAssembly.GetType("UnityEditor.SettingsWindow+GUIScope"); + return Activator.CreateInstance(type) as IDisposable; + } + } +} diff --git a/.github/Asset Store Tools/Utility/ASToolsPreferences.cs.meta b/.github/Asset Store Tools/Utility/ASToolsPreferences.cs.meta new file mode 100644 index 00000000000..56807b0b82f --- /dev/null +++ b/.github/Asset Store Tools/Utility/ASToolsPreferences.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b75179c8d22a35b42a543d6fa7857ce0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Utility/ASToolsUpdater.cs b/.github/Asset Store Tools/Utility/ASToolsUpdater.cs new file mode 100644 index 00000000000..698b97486a5 --- /dev/null +++ b/.github/Asset Store Tools/Utility/ASToolsUpdater.cs @@ -0,0 +1,250 @@ +using AssetStoreTools.Api; +using System; +using System.Linq; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Utility +{ + [InitializeOnLoad] + internal class ASToolsUpdater : AssetStoreToolsWindow + { + protected override string WindowTitle => "Asset Store Tools Update Check"; + + private static IAssetStoreApi _api; + + private VisualElement _loadingContainer; + private VisualElement _versionInfoContainer; + + private Image _loadingImage; + private double _lastTimeSinceStartup; + private double _timeSinceLoadingImageChange; + private int _loadingImageIndex; + + private static bool _updateCheckPerformed + { + get + { + return SessionState.GetBool("AST_UpdateChecked", false); + } + set + { + SessionState.SetBool("AST_UpdateChecked", value); + } + } + + static ASToolsUpdater() + { + _api = new AssetStoreApi(new AssetStoreClient()); + // Retrieving cached SessionState/PlayerPrefs values is not allowed from an instance field initializer + EditorApplication.update += CheckForUpdatesAfterEditorUpdate; + } + + private static async void CheckForUpdatesAfterEditorUpdate() + { + EditorApplication.update -= CheckForUpdatesAfterEditorUpdate; + + if (!ShouldCheckForUpdates()) + return; + + await CheckForUpdates((success, currentVersion, latestVersion) => + { + if (success && currentVersion < latestVersion) + { + AssetStoreTools.OpenUpdateChecker(); + } + }); + } + + private static bool ShouldCheckForUpdates() + { + if (!ASToolsPreferences.Instance.CheckForUpdates) + return false; + + return _updateCheckPerformed == false; + } + + private static async Task CheckForUpdates(Action OnUpdatesChecked) + { + _updateCheckPerformed = true; + var latestVersionResult = await _api.GetLatestAssetStoreToolsVersion(); + if (!latestVersionResult.Success) + { + OnUpdatesChecked?.Invoke(false, null, null); + return; + } + + Version currentVersion = null; + Version latestVersion = null; + + try + { + var latestVersionStr = latestVersionResult.Version; + var currentVersionStr = PackageUtility.GetAllPackages().FirstOrDefault(x => x.name == "com.unity.asset-store-tools").version; + + currentVersion = new Version(currentVersionStr); + latestVersion = new Version(latestVersionStr); + } + catch + { + OnUpdatesChecked?.Invoke(false, null, null); + } + + OnUpdatesChecked?.Invoke(true, currentVersion, latestVersion); + } + + protected override void Init() + { + rootVisualElement.styleSheets.Add(StyleSelector.UpdaterWindow.UpdaterWindowStyle); + rootVisualElement.styleSheets.Add(StyleSelector.UpdaterWindow.UpdaterWindowTheme); + + SetupLoadingSpinner(); + _ = CheckForUpdates(OnVersionsRetrieved); + } + + private void OnVersionsRetrieved(bool success, Version currentVersion, Version latestVersion) + { + if (_loadingContainer != null) + _loadingContainer.style.display = DisplayStyle.None; + + if (success) + { + SetupVersionInfo(currentVersion, latestVersion); + } + else + { + SetupFailInfo(); + } + } + + private void SetupLoadingSpinner() + { + _loadingContainer = new VisualElement(); + _loadingContainer.AddToClassList("updater-loading-container"); + _loadingImage = new Image(); + EditorApplication.update += LoadingSpinLoop; + + _loadingContainer.Add(_loadingImage); + rootVisualElement.Add(_loadingContainer); + } + + private void SetupVersionInfo(Version currentVersion, Version latestVersion) + { + _versionInfoContainer = new VisualElement(); + _versionInfoContainer.AddToClassList("updater-info-container"); + + AddDescriptionLabels(currentVersion, latestVersion); + AddUpdateButtons(currentVersion, latestVersion); + AddCheckForUpdatesToggle(); + + rootVisualElement.Add(_versionInfoContainer); + } + + private void AddDescriptionLabels(Version currentVersion, Version latestVersion) + { + var descriptionText = currentVersion < latestVersion ? + "An update to the Asset Store Publishing Tools is available. Updating to the latest version is highly recommended." : + "Asset Store Publishing Tools are up to date!"; + + var labelContainer = new VisualElement(); + labelContainer.AddToClassList("updater-info-container-labels"); + var descriptionLabel = new Label(descriptionText); + descriptionLabel.AddToClassList("updater-info-container-labels-description"); + + var currentVersionRow = new VisualElement(); + currentVersionRow.AddToClassList("updater-info-container-labels-row"); + var latestVersionRow = new VisualElement(); + latestVersionRow.AddToClassList("updater-info-container-labels-row"); + + var currentVersionLabel = new Label("Current version:"); + currentVersionLabel.AddToClassList("updater-info-container-labels-row-identifier"); + var latestVersionLabel = new Label("Latest version:"); + latestVersionLabel.AddToClassList("updater-info-container-labels-row-identifier"); + + var currentVersionLabelValue = new Label(currentVersion.ToString()); + var latestVersionLabelValue = new Label(latestVersion.ToString()); + + currentVersionRow.Add(currentVersionLabel); + currentVersionRow.Add(currentVersionLabelValue); + latestVersionRow.Add(latestVersionLabel); + latestVersionRow.Add(latestVersionLabelValue); + + labelContainer.Add(descriptionLabel); + labelContainer.Add(currentVersionRow); + labelContainer.Add(latestVersionRow); + + _versionInfoContainer.Add(labelContainer); + } + + private void AddUpdateButtons(Version currentVersion, Version latestVersion) + { + if (currentVersion >= latestVersion) + return; + + var buttonContainer = new VisualElement(); + buttonContainer.AddToClassList("updater-info-container-buttons"); + var latestVersionButton = new Button(() => Application.OpenURL(Constants.Updater.AssetStoreToolsUrl)) { text = "Get the latest version" }; + var skipVersionButton = new Button(Close) { text = "Skip for now" }; + + buttonContainer.Add(latestVersionButton); + buttonContainer.Add(skipVersionButton); + + _versionInfoContainer.Add(buttonContainer); + } + + private void AddCheckForUpdatesToggle() + { + var toggleContainer = new VisualElement(); + toggleContainer.AddToClassList("updater-info-container-toggle"); + var checkForUpdatesToggle = new Toggle() { text = "Check for Updates", value = ASToolsPreferences.Instance.CheckForUpdates }; + checkForUpdatesToggle.RegisterValueChangedCallback(OnCheckForUpdatesToggleChanged); + + toggleContainer.Add(checkForUpdatesToggle); + _versionInfoContainer.Add(toggleContainer); + } + + private void OnCheckForUpdatesToggleChanged(ChangeEvent evt) + { + ASToolsPreferences.Instance.CheckForUpdates = evt.newValue; + ASToolsPreferences.Instance.Save(); + } + + private void SetupFailInfo() + { + var failContainer = new VisualElement(); + failContainer.AddToClassList("updater-fail-container"); + + var failImage = new Image(); + var failDescription = new Label("Asset Store Publishing Tools could not retrieve information about the latest version."); + + failContainer.Add(failImage); + failContainer.Add(failDescription); + + rootVisualElement.Add(failContainer); + } + + private void LoadingSpinLoop() + { + var currentTimeSinceStartup = EditorApplication.timeSinceStartup; + var deltaTime = EditorApplication.timeSinceStartup - _lastTimeSinceStartup; + _lastTimeSinceStartup = currentTimeSinceStartup; + + _timeSinceLoadingImageChange += deltaTime; + if (_timeSinceLoadingImageChange < 0.075) + return; + + _timeSinceLoadingImageChange = 0; + + _loadingImage.image = EditorGUIUtility.IconContent($"WaitSpin{_loadingImageIndex++:00}").image; + if (_loadingImageIndex > 11) + _loadingImageIndex = 0; + } + + private void OnDestroy() + { + EditorApplication.update -= LoadingSpinLoop; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Utility/ASToolsUpdater.cs.meta b/.github/Asset Store Tools/Utility/ASToolsUpdater.cs.meta new file mode 100644 index 00000000000..d0e41839ee5 --- /dev/null +++ b/.github/Asset Store Tools/Utility/ASToolsUpdater.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2ac370b9d2279ed4c9faec7134ba2759 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Utility/CacheUtil.cs b/.github/Asset Store Tools/Utility/CacheUtil.cs new file mode 100644 index 00000000000..a596352d85a --- /dev/null +++ b/.github/Asset Store Tools/Utility/CacheUtil.cs @@ -0,0 +1,266 @@ +using System.IO; +using CacheConstants = AssetStoreTools.Constants.Cache; + +namespace AssetStoreTools.Utility +{ + internal static class CacheUtil + { + public static bool GetFileFromTempCache(string fileName, out string filePath) + { + return GetCacheFile(CacheConstants.TempCachePath, fileName, out filePath); + } + + public static bool GetFileFromPersistentCache(string fileName, out string filePath) + { + return GetCacheFile(CacheConstants.PersistentCachePath, fileName, out filePath); + } + + public static bool GetFileFromProjectPersistentCache(string projectPath, string fileName, out string filePath) + { + return GetCacheFile(Path.Combine(projectPath, CacheConstants.PersistentCachePath), fileName, out filePath); + } + + private static bool GetCacheFile(string rootPath, string fileName, out string filePath) + { + filePath = Path.Combine(rootPath, fileName); + return File.Exists(filePath); + } + + public static void CreateFileInTempCache(string fileName, object content, bool overwrite) + { + CreateCacheFile(CacheConstants.TempCachePath, fileName, content, overwrite); + } + + public static void CreateFileInPersistentCache(string fileName, object content, bool overwrite) + { + CreateCacheFile(CacheConstants.PersistentCachePath, fileName, content, overwrite); + } + + private static void CreateCacheFile(string rootPath, string fileName, object content, bool overwrite) + { + if (!Directory.Exists(rootPath)) + Directory.CreateDirectory(rootPath); + + var fullPath = Path.Combine(rootPath, fileName); + + bool willUpdate = false; + if (File.Exists(fullPath)) + { + if (overwrite) + { + File.Delete(fullPath); + willUpdate = true; + } + else + return; + } + + switch (content) + { + case byte[] bytes: + File.WriteAllBytes(fullPath, bytes); + break; + default: + File.WriteAllText(fullPath, content.ToString()); + break; + } + + var keyword = willUpdate ? "Updating" : "Creating"; + ASDebug.Log($"{keyword} cache file: '{fullPath}'"); + } + + public static void DeleteFileFromTempCache(string fileName) + { + DeleteFileFromCache(CacheConstants.TempCachePath, fileName); + } + + public static void DeleteFileFromPersistentCache(string fileName) + { + DeleteFileFromCache(CacheConstants.PersistentCachePath, fileName); + } + + private static void DeleteFileFromCache(string rootPath, string fileName) + { + var path = Path.Combine(rootPath, fileName); + if (File.Exists(path)) + File.Delete(path); + } + + //private static void CreateFileInPersistentCache(string fileName, object content, bool overwrite) + //{ + // CreateCacheFile(CacheConstants.PersistentCachePath, fileName, content, overwrite); + //} + + //private static void CreateCacheFile(string rootPath, string fileName, object content, bool overwrite) + //{ + // if (!Directory.Exists(rootPath)) + // Directory.CreateDirectory(rootPath); + + // var fullPath = Path.Combine(rootPath, fileName); + + // if (File.Exists(fullPath)) + // { + // if (overwrite) + // File.Delete(fullPath); + // else + // return; + // } + + // switch (content) + // { + // case byte[] bytes: + // File.WriteAllBytes(fullPath, bytes); + // break; + // default: + // File.WriteAllText(fullPath, content.ToString()); + // break; + // } + // ASDebug.Log($"Creating cached file: '{fullPath}'"); + //} + + //public static void ClearTempCache() + //{ + // if (!File.Exists(Path.Combine(CacheConstants.TempCachePath, CacheConstants.PackageDataFile))) + // return; + + + // // Cache consists of package data and package texture thumbnails. We don't clear + // // texture thumbnails here since they are less likely to change. They are still + // // deleted and redownloaded every project restart (because of being stored in the 'Temp' folder) + // var fullPath = Path.Combine(CacheConstants.TempCachePath, CacheConstants.PackageDataFile); + // ASDebug.Log($"Deleting cached file '{fullPath}'"); + // File.Delete(fullPath); + //} + + //public static void CachePackageMetadata(List data) + //{ + // var serializerSettings = new JsonSerializerSettings() + // { + // ContractResolver = Package.CachedPackageResolver.Instance, + // Formatting = Formatting.Indented + // }; + + // CreateFileInTempCache(CacheConstants.PackageDataFile, JsonConvert.SerializeObject(data, serializerSettings), true); + //} + + //public static void UpdatePackageMetadata(Package data) + //{ + // if (!GetCachedPackageMetadata(out var cachedData)) + // return; + + // var index = cachedData.FindIndex(x => x.PackageId.Equals(data.PackageId)); + // if (index == -1) + // { + // cachedData.Add(data); + // } + // else + // { + // cachedData.RemoveAt(index); + // cachedData.Insert(index, data); + // } + + // CachePackageMetadata(cachedData); + //} + + //public static bool GetCachedPackageMetadata(out List data) + //{ + // data = new List(); + // var path = Path.Combine(CacheConstants.TempCachePath, CacheConstants.PackageDataFile); + // if (!File.Exists(path)) + // return false; + + // try + // { + // var serializerSettings = new JsonSerializerSettings() + // { + // ContractResolver = Package.CachedPackageResolver.Instance + // }; + + // data = JsonConvert.DeserializeObject>(File.ReadAllText(path, Encoding.UTF8), serializerSettings); + // return true; + // } + // catch + // { + // return false; + // } + //} + + //public static void CacheTexture(string packageId, Texture2D texture) + //{ + // CreateFileInTempCache($"{packageId}.png", texture.EncodeToPNG(), true); + //} + + //public static bool GetCachedTexture(string packageId, out Texture2D texture) + //{ + // texture = new Texture2D(1, 1); + // var path = Path.Combine(CacheConstants.TempCachePath, $"{packageId}.png"); + // if (!File.Exists(path)) + // return false; + + // texture.LoadImage(File.ReadAllBytes(path)); + // return true; + //} + + //public static void CacheWorkflowStateData(string packageId, WorkflowStateData data) + //{ + // var fileName = $"{packageId}-workflowStateData.asset"; + // CreateFileInPersistentCache(fileName, JsonConvert.SerializeObject(data, Formatting.Indented), true); + //} + + //public static bool GetCachedWorkflowStateData(string packageId, out WorkflowStateData data) + //{ + // data = null; + // var path = Path.Combine(CacheConstants.PersistentCachePath, $"{packageId}-workflowStateData.asset"); + // if (!File.Exists(path)) + // return false; + + // data = JsonConvert.DeserializeObject(File.ReadAllText(path, Encoding.UTF8)); + // return true; + //} + + //public static void CacheValidationStateData(ValidationStateData data) + //{ + // var serializerSettings = new JsonSerializerSettings() + // { + // ContractResolver = ValidationStateDataContractResolver.Instance, + // Formatting = Formatting.Indented, + // TypeNameHandling = TypeNameHandling.Auto, + // Converters = new List() { new StringEnumConverter() } + // }; + + // CreateFileInPersistentCache(CacheConstants.ValidationResultFile, JsonConvert.SerializeObject(data, serializerSettings), true); + //} + + //public static bool GetCachedValidationStateData(out ValidationStateData data) + //{ + // return GetCachedValidationStateData(Constants.RootProjectPath, out data); + //} + + //public static bool GetCachedValidationStateData(string projectPath, out ValidationStateData data) + //{ + // data = null; + // var path = Path.Combine(projectPath, CacheConstants.PersistentCachePath, CacheConstants.ValidationResultFile); + // if (!File.Exists(path)) + // return false; + + // try + // { + // var serializerSettings = new JsonSerializerSettings() + // { + // ContractResolver = ValidationStateDataContractResolver.Instance, + // Formatting = Formatting.Indented, + // TypeNameHandling = TypeNameHandling.Auto, + // Converters = new List() { new StringEnumConverter() } + // }; + + // data = JsonConvert.DeserializeObject(File.ReadAllText(path, Encoding.UTF8), serializerSettings); + // return true; + // } + // catch (System.Exception e) + // { + // UnityEngine.Debug.LogException(e); + // return false; + // } + //} + } +} diff --git a/.github/Asset Store Tools/Utility/CacheUtil.cs.meta b/.github/Asset Store Tools/Utility/CacheUtil.cs.meta new file mode 100644 index 00000000000..fca787fb9e5 --- /dev/null +++ b/.github/Asset Store Tools/Utility/CacheUtil.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2e5fee0cad7655f458d9b600f4ae6d02 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Utility/FileUtility.cs b/.github/Asset Store Tools/Utility/FileUtility.cs new file mode 100644 index 00000000000..5e545f81ccf --- /dev/null +++ b/.github/Asset Store Tools/Utility/FileUtility.cs @@ -0,0 +1,207 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; + +namespace AssetStoreTools.Utility +{ + internal static class FileUtility + { + private class RenameInfo + { + public string OriginalName; + public string CurrentName; + } + + public static string AbsolutePathToRelativePath(string path, bool allowSymlinks) + { + if (!File.Exists(path) && !Directory.Exists(path)) + return path; + + string convertedPath = path.Replace("\\", "/"); + + var allPackages = PackageUtility.GetAllPackages(); + foreach (var package in allPackages) + { + if (Path.GetFullPath(package.resolvedPath) != Path.GetFullPath(convertedPath) + && !Path.GetFullPath(convertedPath).StartsWith(Path.GetFullPath(package.resolvedPath) + Path.DirectorySeparatorChar)) + continue; + + convertedPath = Path.GetFullPath(convertedPath) + .Replace(Path.GetFullPath(package.resolvedPath), package.assetPath) + .Replace("\\", "/"); + + return convertedPath; + } + + if (convertedPath.StartsWith(Constants.RootProjectPath)) + { + convertedPath = convertedPath.Substring(Constants.RootProjectPath.Length); + } + else + { + if (allowSymlinks && SymlinkUtil.FindSymlinkFolderRelative(convertedPath, out var symlinkPath)) + convertedPath = symlinkPath; + } + + return convertedPath; + } + + public static void CopyDirectory(string sourceDir, string destinationDir, bool recursive) + { + // Get information about the source directory + var dir = new DirectoryInfo(sourceDir); + + // Check if the source directory exists + if (!dir.Exists) + throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}"); + + // Cache directories before we start copying + DirectoryInfo[] dirs = dir.GetDirectories(); + + // Create the destination directory + Directory.CreateDirectory(destinationDir); + + // Get the files in the source directory and copy to the destination directory + foreach (FileInfo file in dir.GetFiles()) + { + string targetFilePath = Path.Combine(destinationDir, file.Name); + file.CopyTo(targetFilePath); + } + + // If recursive and copying subdirectories, recursively call this method + if (recursive) + { + foreach (DirectoryInfo subDir in dirs) + { + string newDestinationDir = Path.Combine(destinationDir, subDir.Name); + CopyDirectory(subDir.FullName, newDestinationDir, true); + } + } + } + + public static bool ShouldHaveMeta(string assetPath) + { + if (string.IsNullOrEmpty(assetPath)) + return false; + + // Meta files never have other metas + if (assetPath.EndsWith(".meta", System.StringComparison.OrdinalIgnoreCase)) + return false; + + // File System entries ending with '~' are hidden in the context of ADB + if (assetPath.EndsWith("~")) + return false; + + // File System entries whose names start with '.' are hidden in the context of ADB + var assetName = assetPath.Replace("\\", "/").Split('/').Last(); + if (assetName.StartsWith(".")) + return false; + + return true; + } + + public static bool IsMissingMetaFiles(IEnumerable sourcePaths) + { + foreach (var sourcePath in sourcePaths) + { + if (!Directory.Exists(sourcePath)) + continue; + + var allDirectories = Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories); + foreach (var dir in allDirectories) + { + var dirInfo = new DirectoryInfo(dir); + if (!dirInfo.Name.EndsWith("~")) + continue; + + var nestedContent = dirInfo.GetFileSystemInfos("*", SearchOption.AllDirectories); + foreach (var nested in nestedContent) + { + if (!ShouldHaveMeta(nested.FullName)) + continue; + + if (!File.Exists(nested.FullName + ".meta")) + return true; + } + } + } + + return false; + } + + public static void GenerateMetaFiles(IEnumerable sourcePaths) + { + var renameInfos = new List(); + + foreach (var sourcePath in sourcePaths) + { + if (!Directory.Exists(sourcePath)) + continue; + + var hiddenDirectoriesInPath = Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories).Where(x => x.EndsWith("~")); + foreach (var hiddenDir in hiddenDirectoriesInPath) + { + var hiddenDirRelative = AbsolutePathToRelativePath(hiddenDir, ASToolsPreferences.Instance.EnableSymlinkSupport); + if (!hiddenDirRelative.StartsWith("Assets/") && !hiddenDirRelative.StartsWith("Packages/")) + { + ASDebug.LogWarning($"Path {sourcePath} is not part of the Asset Database and will be skipped"); + continue; + } + + renameInfos.Add(new RenameInfo() { CurrentName = hiddenDirRelative, OriginalName = hiddenDirRelative }); + } + } + + if (renameInfos.Count == 0) + return; + + try + { + EditorApplication.LockReloadAssemblies(); + + // Order paths from longest to shortest to avoid having to rename them multiple times + renameInfos = renameInfos.OrderByDescending(x => x.OriginalName.Length).ToList(); + + try + { + AssetDatabase.StartAssetEditing(); + foreach (var renameInfo in renameInfos) + { + renameInfo.CurrentName = renameInfo.OriginalName.TrimEnd('~'); + Directory.Move(renameInfo.OriginalName, renameInfo.CurrentName); + } + } + finally + { + AssetDatabase.StopAssetEditing(); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); + AssetDatabase.ReleaseCachedFileHandles(); + } + + // Restore the original path names in reverse order + renameInfos = renameInfos.OrderBy(x => x.OriginalName.Length).ToList(); + + try + { + AssetDatabase.StartAssetEditing(); + foreach (var renameInfo in renameInfos) + { + Directory.Move(renameInfo.CurrentName, renameInfo.OriginalName); + if (File.Exists($"{renameInfo.CurrentName}.meta")) + File.Delete($"{renameInfo.CurrentName}.meta"); + } + } + finally + { + AssetDatabase.StopAssetEditing(); + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); + } + } + finally + { + EditorApplication.UnlockReloadAssemblies(); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Utility/FileUtility.cs.meta b/.github/Asset Store Tools/Utility/FileUtility.cs.meta new file mode 100644 index 00000000000..db285bb4069 --- /dev/null +++ b/.github/Asset Store Tools/Utility/FileUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 80819cf6868374d478a8a38ebaba8e27 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Utility/LegacyToolsRemover.cs b/.github/Asset Store Tools/Utility/LegacyToolsRemover.cs new file mode 100644 index 00000000000..c9df7c21642 --- /dev/null +++ b/.github/Asset Store Tools/Utility/LegacyToolsRemover.cs @@ -0,0 +1,86 @@ +using System; +using System.IO; +using System.Reflection; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Utility +{ + [InitializeOnLoad] + internal class LegacyToolsRemover + { + private const string MessagePart1 = "A legacy version of Asset Store Tools " + + "was detected at the following path:\n"; + private const string MessagePart2 = "\n\nHaving both the legacy and the latest version installed at the same time is not supported " + + "and might prevent the latest version from functioning properly.\n\nWould you like the legacy version to be removed automatically?"; + + static LegacyToolsRemover() + { + try + { + if (Application.isBatchMode) + return; + + CheckAndRemoveLegacyTools(); + } + catch { } + } + + private static void CheckAndRemoveLegacyTools() + { + if (!ASToolsPreferences.Instance.LegacyVersionCheck || !ProjectContainsLegacyTools(out string path)) + return; + + var relativePath = path.Substring(Application.dataPath.Length - "Assets".Length).Replace("\\", "/"); + var result = EditorUtility.DisplayDialog("Asset Store Tools", MessagePart1 + relativePath + MessagePart2, "Yes", "No"); + + // If "No" - do nothing + if (!result) + return; + + // If "Yes" - remove legacy tools + File.Delete(path); + File.Delete(path + ".meta"); + RemoveEmptyFolders(Path.GetDirectoryName(path)?.Replace("\\", "/")); + AssetDatabase.Refresh(); + + // We could also optionally prevent future execution here + // but the ProjectContainsLegacyTools() function runs in less + // than a milisecond on an empty project + } + + private static bool ProjectContainsLegacyTools(out string path) + { + path = null; + + foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (assembly.ManifestModule.Name == "AssetStoreTools.dll") + { + path = assembly.Location; + break; + } + } + + if (string.IsNullOrEmpty(path)) + return false; + return true; + } + + private static void RemoveEmptyFolders(string directory) + { + if (directory.EndsWith(Application.dataPath)) + return; + + if (Directory.GetFiles(directory).Length == 0 && Directory.GetDirectories(directory).Length == 0) + { + var parentPath = Path.GetDirectoryName(directory).Replace("\\", "/"); + + Directory.Delete(directory); + File.Delete(directory + ".meta"); + + RemoveEmptyFolders(parentPath); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Utility/LegacyToolsRemover.cs.meta b/.github/Asset Store Tools/Utility/LegacyToolsRemover.cs.meta new file mode 100644 index 00000000000..c16ca0a51a6 --- /dev/null +++ b/.github/Asset Store Tools/Utility/LegacyToolsRemover.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 46ead42026b1f0b4e94153e1a7e10d12 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Utility/PackageUtility.cs b/.github/Asset Store Tools/Utility/PackageUtility.cs new file mode 100644 index 00000000000..a384f7aef6a --- /dev/null +++ b/.github/Asset Store Tools/Utility/PackageUtility.cs @@ -0,0 +1,170 @@ +#if !UNITY_2021_1_OR_NEWER +using System; +using System.Reflection; +#endif +using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEditor.PackageManager; +using UnityEngine; +using PackageInfo = UnityEditor.PackageManager.PackageInfo; + +namespace AssetStoreTools.Utility +{ + internal static class PackageUtility + { + public class PackageInfoSampleMetadata + { + public string DisplayName; + public string Description; + public string Path; + } + + public class PackageInfoUnityVersionMetadata + { + /// + /// Major bit of the Unity version, e.g. 2021.3 + /// + public string Version; + /// + /// Minor bit of the Unity version, e.g. 0f1 + /// + public string Release; + + public override string ToString() + { + if (string.IsNullOrEmpty(Version)) + return Release; + + if (string.IsNullOrEmpty(Release)) + return Release; + + return $"{Version}.{Release}"; + } + } + + /// + /// Returns a package identifier, consisting of package name and package version + /// + /// + /// + public static string GetPackageIdentifier(this PackageInfo package) + { + return $"{package.name}-{package.version}"; + } + + public static PackageInfo[] GetAllPackages() + { +#if !UNITY_2021_1_OR_NEWER + var method = typeof(PackageInfo).GetMethod("GetAll", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, new Type[0], null); + var packages = method?.Invoke(null, null) as PackageInfo[]; +#else + var packages = PackageInfo.GetAllRegisteredPackages(); +#endif + return packages; + } + + public static PackageInfo[] GetAllLocalPackages() + { + var packages = GetAllPackages(); + var localPackages = packages.Where(x => x.source == PackageSource.Embedded || x.source == PackageSource.Local) + .Where(x => x.isDirectDependency).ToArray(); + return localPackages; + } + + public static PackageInfo[] GetAllRegistryPackages() + { + var packages = GetAllPackages(); + var registryPackages = packages.Where(x => x.source == PackageSource.Registry || x.source == PackageSource.BuiltIn) + .OrderBy(x => string.Compare(x.type, "module", System.StringComparison.OrdinalIgnoreCase) == 0) + .ThenBy(x => x.name).ToArray(); + + return registryPackages; + } + + public static bool GetPackageByManifestPath(string packageManifestPath, out PackageInfo package) + { + package = null; + + if (string.IsNullOrEmpty(packageManifestPath)) + return false; + + var fileInfo = new FileInfo(packageManifestPath); + if (!fileInfo.Exists) + return false; + + var allPackages = GetAllPackages(); + + package = allPackages.FirstOrDefault(x => Path.GetFullPath(x.resolvedPath).Equals(fileInfo.Directory.FullName)); + return package != null; + } + + public static bool GetPackageByPackageName(string packageName, out PackageInfo package) + { + package = null; + + if (string.IsNullOrEmpty(packageName)) + return false; + + return GetPackageByManifestPath($"Packages/{packageName}/package.json", out package); + } + + public static TextAsset GetManifestAsset(this PackageInfo packageInfo) + { + return AssetDatabase.LoadAssetAtPath($"{packageInfo.assetPath}/package.json"); + } + + public static List GetSamples(this PackageInfo packageInfo) + { + var samples = new List(); + + var packageManifest = packageInfo.GetManifestAsset(); + var json = JObject.Parse(packageManifest.text); + + if (!json.ContainsKey("samples") || json["samples"].Type != JTokenType.Array) + return samples; + + var sampleList = json["samples"].ToList(); + foreach (JObject sample in sampleList) + { + var displayName = string.Empty; + var description = string.Empty; + var path = string.Empty; + + if (sample.ContainsKey("displayName")) + displayName = sample["displayName"].ToString(); + if (sample.ContainsKey("description")) + description = sample["description"].ToString(); + if (sample.ContainsKey("path")) + path = sample["path"].ToString(); + + if (!string.IsNullOrEmpty(displayName) || !string.IsNullOrEmpty(description) || !string.IsNullOrEmpty(path)) + samples.Add(new PackageInfoSampleMetadata() { DisplayName = displayName, Description = description, Path = path }); + } + + return samples; + } + + public static PackageInfoUnityVersionMetadata GetUnityVersion(this PackageInfo packageInfo) + { + var packageManifest = packageInfo.GetManifestAsset(); + var json = JObject.Parse(packageManifest.text); + + var unityVersion = string.Empty; + var unityRelease = string.Empty; + + if (json.ContainsKey("unity")) + unityVersion = json["unity"].ToString(); + if (json.ContainsKey("unityRelease")) + unityRelease = json["unityRelease"].ToString(); + + return new PackageInfoUnityVersionMetadata() + { + Version = unityVersion, + Release = unityRelease + }; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Utility/PackageUtility.cs.meta b/.github/Asset Store Tools/Utility/PackageUtility.cs.meta new file mode 100644 index 00000000000..6ce8b3dffe3 --- /dev/null +++ b/.github/Asset Store Tools/Utility/PackageUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 605ea62f8b11d674a95a53f895df4c67 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Utility/ServiceProvider.cs b/.github/Asset Store Tools/Utility/ServiceProvider.cs new file mode 100644 index 00000000000..975108178e9 --- /dev/null +++ b/.github/Asset Store Tools/Utility/ServiceProvider.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AssetStoreTools.Utility +{ + internal abstract class ServiceProvider + { + private Dictionary _services = new Dictionary(); + private Dictionary> _queuedServices = new Dictionary>(); + + protected class MissingServiceDependencyException : Exception + { + public Type ServiceType { get; private set; } + public Type MissingDependencyType { get; private set; } + + public MissingServiceDependencyException(Type serviceType, Type missingDependencyType) + { + ServiceType = serviceType; + MissingDependencyType = missingDependencyType; + } + } + + protected ServiceProvider() + { + RegisterServices(); + CreateRegisteredServices(); + } + + protected abstract void RegisterServices(); + + protected void Register() where TService : Service where TInstance : TService + { + Register(() => CreateServiceInstance(typeof(TInstance))); + } + + protected void Register(Func initializer) where TService : Service + { + _queuedServices.Add(typeof(TService), initializer); + } + + private void CreateRegisteredServices() + { + if (_queuedServices.Count == 0) + return; + + var createdAnyService = false; + var missingServices = new List(); + + foreach (var service in _queuedServices) + { + try + { + var instance = service.Value.Invoke(); + _services.Add(service.Key, instance); + createdAnyService = true; + } + catch (MissingServiceDependencyException e) + { + missingServices.Add(e); + } + } + + foreach (var createdService in _services) + { + _queuedServices.Remove(createdService.Key); + } + + if (!createdAnyService) + { + var message = string.Join(", ", missingServices.Select(x => $"{x.ServiceType} depends on {x.MissingDependencyType}")); + throw new Exception("Could not create the following services due to missing dependencies: " + message); + } + + // Recursively register remaining queued services that may have failed + // due to missing depenedencies that are now registered + CreateRegisteredServices(); + } + + private Service CreateServiceInstance(Type concreteType) + { + if (concreteType.IsAbstract) + throw new Exception($"Cannot create an instance of an abstract class {concreteType}"); + + var constructor = concreteType.GetConstructors().First(); + var expectedParameters = constructor.GetParameters(); + var parametersToUse = new List(); + + foreach (var parameter in expectedParameters) + { + if (!_services.ContainsKey(parameter.ParameterType)) + throw new MissingServiceDependencyException(concreteType, parameter.ParameterType); + + parametersToUse.Add(_services[parameter.ParameterType]); + } + + return (Service)constructor.Invoke(parametersToUse.ToArray()); + } + + public T GetService() where T : Service + { + return (T)GetService(typeof(T)); + } + + public object GetService(Type type) + { + if (!_services.ContainsKey(type)) + throw new Exception($"Service of type {type} is not registered"); + + return _services[type]; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Utility/ServiceProvider.cs.meta b/.github/Asset Store Tools/Utility/ServiceProvider.cs.meta new file mode 100644 index 00000000000..847bd4b6ab6 --- /dev/null +++ b/.github/Asset Store Tools/Utility/ServiceProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2fcadafa6431d1647a82d35e6e4a13c5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Utility/StyleSelector.cs b/.github/Asset Store Tools/Utility/StyleSelector.cs new file mode 100644 index 00000000000..1076bca3c80 --- /dev/null +++ b/.github/Asset Store Tools/Utility/StyleSelector.cs @@ -0,0 +1,55 @@ +using System; +using UnityEditor; +using UnityEngine.UIElements; +using WindowStyles = AssetStoreTools.Constants.WindowStyles; + +namespace AssetStoreTools.Utility +{ + internal static class StyleSelector + { + private static StyleSheet GetStylesheet(string rootPath, string filePath) + { + var path = $"{rootPath}/{filePath}.uss"; + var sheet = AssetDatabase.LoadAssetAtPath(path); + if (sheet == null) + throw new Exception($"Stylesheet '{path}' was not found"); + return sheet; + } + + private static StyleSheet GetStylesheetTheme(string rootPath, string filePath) + { + var suffix = !EditorGUIUtility.isProSkin ? "Light" : "Dark"; + return GetStylesheet(rootPath, filePath + suffix); + } + + public static class UploaderWindow + { + public static StyleSheet UploaderWindowStyle => GetStylesheet(WindowStyles.UploaderStylesPath, "Style"); + public static StyleSheet UploaderWindowTheme => GetStylesheetTheme(WindowStyles.UploaderStylesPath, "Theme"); + + public static StyleSheet LoginViewStyle => GetStylesheet(WindowStyles.UploaderStylesPath, "LoginView/Style"); + public static StyleSheet LoginViewTheme => GetStylesheetTheme(WindowStyles.UploaderStylesPath, "LoginView/Theme"); + + public static StyleSheet PackageListViewStyle => GetStylesheet(WindowStyles.UploaderStylesPath, "PackageListView/Style"); + public static StyleSheet PackageListViewTheme => GetStylesheetTheme(WindowStyles.UploaderStylesPath, "PackageListView/Theme"); + } + + public static class ValidatorWindow + { + public static StyleSheet ValidatorWindowStyle => GetStylesheet(WindowStyles.ValidatorStylesPath, "Style"); + public static StyleSheet ValidatorWindowTheme => GetStylesheetTheme(WindowStyles.ValidatorStylesPath, "Theme"); + } + + public static class PreviewGeneratorWindow + { + public static StyleSheet PreviewGeneratorWindowStyle => GetStylesheet(WindowStyles.PreviewGeneratorStylesPath, "Style"); + public static StyleSheet PreviewGeneratorWindowTheme => GetStylesheetTheme(WindowStyles.PreviewGeneratorStylesPath, "Theme"); + } + + public static class UpdaterWindow + { + public static StyleSheet UpdaterWindowStyle => GetStylesheet(WindowStyles.UpdaterStylesPath, "Style"); + public static StyleSheet UpdaterWindowTheme => GetStylesheetTheme(WindowStyles.UpdaterStylesPath, "Theme"); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Utility/StyleSelector.cs.meta b/.github/Asset Store Tools/Utility/StyleSelector.cs.meta new file mode 100644 index 00000000000..d4ab01c4278 --- /dev/null +++ b/.github/Asset Store Tools/Utility/StyleSelector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8b066ce502a289a4ca311a86fbf83f45 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Utility/Styles.meta b/.github/Asset Store Tools/Utility/Styles.meta new file mode 100644 index 00000000000..0175b81abb5 --- /dev/null +++ b/.github/Asset Store Tools/Utility/Styles.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f730eb0b8c48c434d93cc60a0b8aff40 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Utility/Styles/Updater.meta b/.github/Asset Store Tools/Utility/Styles/Updater.meta new file mode 100644 index 00000000000..315b99b2e25 --- /dev/null +++ b/.github/Asset Store Tools/Utility/Styles/Updater.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5367435d9abe935438f4d7b588a55488 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Utility/Styles/Updater/Style.uss b/.github/Asset Store Tools/Utility/Styles/Updater/Style.uss new file mode 100644 index 00000000000..1ee89cd5fe4 --- /dev/null +++ b/.github/Asset Store Tools/Utility/Styles/Updater/Style.uss @@ -0,0 +1,76 @@ +.updater-loading-container { + flex-grow: 1; + align-items: center; + justify-content: center; +} + +.updater-loading-container > Image { + width: 16px; + height: 16px; +} + +.updater-info-container { + flex-grow: 1; + margin: 0 5px 5px 5px; +} + +.updater-info-container-labels { + flex-grow: 1; + margin-bottom: 10px; + margin-top: 5px; +} + +.updater-info-container-labels-description { + flex-grow: 0.5; + margin-bottom: 5px; + white-space: normal; + -unity-text-align: middle-left; +} + +.updater-info-container-labels-row { + flex-direction: row; +} + +.updater-info-container-labels-row-identifier { + -unity-font-style: bold; +} + +.updater-info-container-buttons { + flex-direction: row; + margin-bottom: 5px; +} + +.updater-info-container-buttons > Button { + flex-grow: 1; + flex-shrink: 1; + flex-basis: 100%; + height: 25px; +} + +.updater-info-container-toggle { + align-self: flex-end; +} + +.updater-info-container-toggle > Toggle > VisualElement > Label { + margin-left: 5px; +} + +.updater-fail-container { + flex-grow: 1; + flex-direction: row; + margin: 0 5px 5px 5px; + justify-content: center; + align-items: center; +} + +.updater-fail-container > Image { + flex-shrink: 0; + width: 36px; + height: 36px; + margin-right: 5px; +} + +.updater-fail-container > Label { + flex-shrink: 1; + white-space: normal; +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Utility/Styles/Updater/Style.uss.meta b/.github/Asset Store Tools/Utility/Styles/Updater/Style.uss.meta new file mode 100644 index 00000000000..3ba354276c8 --- /dev/null +++ b/.github/Asset Store Tools/Utility/Styles/Updater/Style.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 23112eed1f211274c94028490f81007c +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Utility/Styles/Updater/ThemeDark.uss b/.github/Asset Store Tools/Utility/Styles/Updater/ThemeDark.uss new file mode 100644 index 00000000000..3510f8ac55f --- /dev/null +++ b/.github/Asset Store Tools/Utility/Styles/Updater/ThemeDark.uss @@ -0,0 +1,3 @@ +.updater-fail-container > Image { + --unity-image: resource("console.erroricon@2x"); +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Utility/Styles/Updater/ThemeDark.uss.meta b/.github/Asset Store Tools/Utility/Styles/Updater/ThemeDark.uss.meta new file mode 100644 index 00000000000..9fad31bd416 --- /dev/null +++ b/.github/Asset Store Tools/Utility/Styles/Updater/ThemeDark.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0cbf43b8dabcd1242b32ed3ed2167a54 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Utility/Styles/Updater/ThemeLight.uss b/.github/Asset Store Tools/Utility/Styles/Updater/ThemeLight.uss new file mode 100644 index 00000000000..3510f8ac55f --- /dev/null +++ b/.github/Asset Store Tools/Utility/Styles/Updater/ThemeLight.uss @@ -0,0 +1,3 @@ +.updater-fail-container > Image { + --unity-image: resource("console.erroricon@2x"); +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Utility/Styles/Updater/ThemeLight.uss.meta b/.github/Asset Store Tools/Utility/Styles/Updater/ThemeLight.uss.meta new file mode 100644 index 00000000000..0cdf6fd2791 --- /dev/null +++ b/.github/Asset Store Tools/Utility/Styles/Updater/ThemeLight.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d453bb92cd1f35943b1c5f652837ada9 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Utility/SymlinkUtil.cs b/.github/Asset Store Tools/Utility/SymlinkUtil.cs new file mode 100644 index 00000000000..93da1ee2452 --- /dev/null +++ b/.github/Asset Store Tools/Utility/SymlinkUtil.cs @@ -0,0 +1,67 @@ +using System.IO; + +namespace AssetStoreTools.Utility +{ + internal static class SymlinkUtil + { + private const FileAttributes FolderSymlinkAttributes = FileAttributes.Directory | FileAttributes.ReparsePoint; + + public static bool FindSymlinkFolderRelative(string folderPathAbsolute, out string relativePath) + { + // Get directory info for path outside of the project + var absoluteInfo = new DirectoryInfo(folderPathAbsolute); + + // Get all directories within the project + var allFolderPaths = Directory.GetDirectories("Assets", "*", SearchOption.AllDirectories); + foreach (var path in allFolderPaths) + { + var fullPath = path.Replace("\\", "/"); + + // Get directory info for one of the paths within the project + var relativeInfo = new DirectoryInfo(fullPath); + + // Check if project's directory is a symlink + if (!relativeInfo.Attributes.HasFlag(FolderSymlinkAttributes)) + continue; + + // Compare metadata of outside directory with a directories within the project + if (!CompareDirectories(absoluteInfo, relativeInfo)) + continue; + + // Found symlink within the project, assign it + relativePath = fullPath; + return true; + } + + relativePath = string.Empty; + return false; + } + + private static bool CompareDirectories(DirectoryInfo directory, DirectoryInfo directory2) + { + var contents = directory.EnumerateFileSystemInfos("*", SearchOption.AllDirectories).GetEnumerator(); + var contents2 = directory2.EnumerateFileSystemInfos("*", SearchOption.AllDirectories).GetEnumerator(); + + while (true) + { + var firstNext = contents.MoveNext(); + var secondNext = contents2.MoveNext(); + + if (firstNext != secondNext) + return false; + + if (!firstNext && !secondNext) + break; + + var equals = contents.Current?.Name == contents2.Current?.Name + && contents.Current?.LastWriteTime == contents2.Current?.LastWriteTime; + + if (!equals) + return false; + } + + return true; + } + + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Utility/SymlinkUtil.cs.meta b/.github/Asset Store Tools/Utility/SymlinkUtil.cs.meta new file mode 100644 index 00000000000..43c8aaf5ccb --- /dev/null +++ b/.github/Asset Store Tools/Utility/SymlinkUtil.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 92092535fd064bb1843017f98db213e1 +timeCreated: 1659013521 \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator.meta b/.github/Asset Store Tools/Validator.meta new file mode 100644 index 00000000000..e27c1efda16 --- /dev/null +++ b/.github/Asset Store Tools/Validator.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 980c7bb65c02d464684c2220c57fcd75 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Icons.meta b/.github/Asset Store Tools/Validator/Icons.meta new file mode 100644 index 00000000000..7f4c321dac8 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Icons.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8490c57c02b441e4dab99565da835c99 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Icons/error.png b/.github/Asset Store Tools/Validator/Icons/error.png new file mode 100644 index 00000000000..8d294bc199d Binary files /dev/null and b/.github/Asset Store Tools/Validator/Icons/error.png differ diff --git a/.github/Asset Store Tools/Validator/Icons/error.png.meta b/.github/Asset Store Tools/Validator/Icons/error.png.meta new file mode 100644 index 00000000000..4217052ea70 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Icons/error.png.meta @@ -0,0 +1,128 @@ +fileFormatVersion: 2 +guid: 0cc0ccdb7de3e964ab553ce3c299d83c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 0 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 0 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Icons/error_d.png b/.github/Asset Store Tools/Validator/Icons/error_d.png new file mode 100644 index 00000000000..451f64037ad Binary files /dev/null and b/.github/Asset Store Tools/Validator/Icons/error_d.png differ diff --git a/.github/Asset Store Tools/Validator/Icons/error_d.png.meta b/.github/Asset Store Tools/Validator/Icons/error_d.png.meta new file mode 100644 index 00000000000..5ccdf2678eb --- /dev/null +++ b/.github/Asset Store Tools/Validator/Icons/error_d.png.meta @@ -0,0 +1,128 @@ +fileFormatVersion: 2 +guid: cdf8d51df19d58341886cc474e810c7b +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 0 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 0 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Icons/success.png b/.github/Asset Store Tools/Validator/Icons/success.png new file mode 100644 index 00000000000..a5f78532b14 Binary files /dev/null and b/.github/Asset Store Tools/Validator/Icons/success.png differ diff --git a/.github/Asset Store Tools/Validator/Icons/success.png.meta b/.github/Asset Store Tools/Validator/Icons/success.png.meta new file mode 100644 index 00000000000..c08b38345bf --- /dev/null +++ b/.github/Asset Store Tools/Validator/Icons/success.png.meta @@ -0,0 +1,128 @@ +fileFormatVersion: 2 +guid: 832e106a677623145b3d8dbe015e31a0 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 0 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 0 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Icons/success_d.png b/.github/Asset Store Tools/Validator/Icons/success_d.png new file mode 100644 index 00000000000..094a810f9ec Binary files /dev/null and b/.github/Asset Store Tools/Validator/Icons/success_d.png differ diff --git a/.github/Asset Store Tools/Validator/Icons/success_d.png.meta b/.github/Asset Store Tools/Validator/Icons/success_d.png.meta new file mode 100644 index 00000000000..67411308132 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Icons/success_d.png.meta @@ -0,0 +1,128 @@ +fileFormatVersion: 2 +guid: 3dc139a2b2a28a54a8f39e266fc0af9c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 0 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 0 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Icons/undefined.png b/.github/Asset Store Tools/Validator/Icons/undefined.png new file mode 100644 index 00000000000..a587baa45ab Binary files /dev/null and b/.github/Asset Store Tools/Validator/Icons/undefined.png differ diff --git a/.github/Asset Store Tools/Validator/Icons/undefined.png.meta b/.github/Asset Store Tools/Validator/Icons/undefined.png.meta new file mode 100644 index 00000000000..47db8b0e159 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Icons/undefined.png.meta @@ -0,0 +1,128 @@ +fileFormatVersion: 2 +guid: 4a4eef842709db34cbb71baf22384730 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 0 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 0 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Icons/undefined_d.png b/.github/Asset Store Tools/Validator/Icons/undefined_d.png new file mode 100644 index 00000000000..b7e2681e62a Binary files /dev/null and b/.github/Asset Store Tools/Validator/Icons/undefined_d.png differ diff --git a/.github/Asset Store Tools/Validator/Icons/undefined_d.png.meta b/.github/Asset Store Tools/Validator/Icons/undefined_d.png.meta new file mode 100644 index 00000000000..029753f1085 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Icons/undefined_d.png.meta @@ -0,0 +1,128 @@ +fileFormatVersion: 2 +guid: 88a36c7e4d60b6b4385c95cfc2d00c22 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 0 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 0 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Icons/warning.png b/.github/Asset Store Tools/Validator/Icons/warning.png new file mode 100644 index 00000000000..aab9fee1e12 Binary files /dev/null and b/.github/Asset Store Tools/Validator/Icons/warning.png differ diff --git a/.github/Asset Store Tools/Validator/Icons/warning.png.meta b/.github/Asset Store Tools/Validator/Icons/warning.png.meta new file mode 100644 index 00000000000..05dd39d258a --- /dev/null +++ b/.github/Asset Store Tools/Validator/Icons/warning.png.meta @@ -0,0 +1,128 @@ +fileFormatVersion: 2 +guid: 83d0e58aa5f608a4b8232fbacca5ca89 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 0 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 0 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Icons/warning_d.png b/.github/Asset Store Tools/Validator/Icons/warning_d.png new file mode 100644 index 00000000000..553bb16d81f Binary files /dev/null and b/.github/Asset Store Tools/Validator/Icons/warning_d.png differ diff --git a/.github/Asset Store Tools/Validator/Icons/warning_d.png.meta b/.github/Asset Store Tools/Validator/Icons/warning_d.png.meta new file mode 100644 index 00000000000..3e0843ed11e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Icons/warning_d.png.meta @@ -0,0 +1,128 @@ +fileFormatVersion: 2 +guid: d27d359f48fa1a14e9e4f02196589805 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 0 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 0 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts.meta b/.github/Asset Store Tools/Validator/Scripts.meta new file mode 100644 index 00000000000..5a815351c14 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1b5ff7c95381e82438f6c9dc40069031 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Categories.meta b/.github/Asset Store Tools/Validator/Scripts/Categories.meta new file mode 100644 index 00000000000..0c29f2c0d5d --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Categories.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7a971a9a200a4438945853d71066f16a +timeCreated: 1657617558 \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Categories/CategoryEvaluator.cs b/.github/Asset Store Tools/Validator/Scripts/Categories/CategoryEvaluator.cs new file mode 100644 index 00000000000..ba9acc2bbee --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Categories/CategoryEvaluator.cs @@ -0,0 +1,52 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.TestDefinitions; + +namespace AssetStoreTools.Validator.Categories +{ + internal class CategoryEvaluator + { + private string _category; + + public CategoryEvaluator(string category) + { + if (string.IsNullOrEmpty(category)) + _category = string.Empty; + else + _category = category; + } + + public void SetCategory(string category) + { + if (category == null) + _category = string.Empty; + else + _category = category; + } + + public string GetCategory() + { + return _category; + } + + public TestResultStatus Evaluate(ValidationTest validation, bool slugify = false) + { + var result = validation.Result.Status; + if (result != TestResultStatus.VariableSeverityIssue) + return result; + + var category = _category; + + if (slugify) + category = validation.Slugify(category); + + return validation.CategoryInfo.EvaluateByFilter(category); + } + +#if AB_BUILDER + public TestResultStatus EvaluateAndSlugify(ValidationTest validation) + { + return Evaluate(validation, true); + } +#endif + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Categories/CategoryEvaluator.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Categories/CategoryEvaluator.cs.meta new file mode 100644 index 00000000000..3bc124c61a9 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Categories/CategoryEvaluator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: eb61fd62b94248e4b5a3a07665b1a2bf +timeCreated: 1661420659 \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Categories/ValidatorCategory.cs b/.github/Asset Store Tools/Validator/Scripts/Categories/ValidatorCategory.cs new file mode 100644 index 00000000000..631ef47b04c --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Categories/ValidatorCategory.cs @@ -0,0 +1,38 @@ +using AssetStoreTools.Validator.Data; +using System; +using System.Linq; + +namespace AssetStoreTools.Validator.Categories +{ + [System.Serializable] + internal class ValidatorCategory + { + public bool IsFailFilter = false; + public bool IsInclusiveFilter = true; + public bool AppliesToSubCategories = true; + public string[] Filter = { "Tools", "Art" }; + + public TestResultStatus EvaluateByFilter(string category) + { + if (AppliesToSubCategories) + category = category.Split('/')[0]; + + var isCategoryInFilter = Filter.Any(x => String.Compare(x, category, StringComparison.OrdinalIgnoreCase) == 0); + + if (IsInclusiveFilter) + { + if (isCategoryInFilter) + return IsFailFilter ? TestResultStatus.Fail : TestResultStatus.Warning; + else + return IsFailFilter ? TestResultStatus.Warning : TestResultStatus.Fail; + } + else + { + if (isCategoryInFilter) + return IsFailFilter ? TestResultStatus.Warning : TestResultStatus.Fail; + else + return IsFailFilter ? TestResultStatus.Fail : TestResultStatus.Warning; + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Categories/ValidatorCategory.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Categories/ValidatorCategory.cs.meta new file mode 100644 index 00000000000..269076976e4 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Categories/ValidatorCategory.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a5e60d3639f24063a4eabc21ea1a04a9 +timeCreated: 1657617578 \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/CurrentProjectValidator.cs b/.github/Asset Store Tools/Validator/Scripts/CurrentProjectValidator.cs new file mode 100644 index 00000000000..79ef18c47a5 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/CurrentProjectValidator.cs @@ -0,0 +1,71 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.TestDefinitions; +using System; +using System.IO; + +namespace AssetStoreTools.Validator +{ + internal class CurrentProjectValidator : ValidatorBase + { + private CurrentProjectValidationSettings _settings; + + public CurrentProjectValidator(CurrentProjectValidationSettings settings) : base(settings) + { + _settings = settings; + } + + protected override void ValidateSettings() + { + if (_settings == null) + throw new Exception("Validation Settings is null"); + + if (_settings.ValidationPaths == null + || _settings.ValidationPaths.Count == 0) + throw new Exception("No validation paths were set"); + + switch (_settings.ValidationType) + { + case ValidationType.Generic: + case ValidationType.UnityPackage: + ValidateUnityPackageSettings(); + break; + default: + throw new NotImplementedException("Undefined validation type"); + } + } + + private void ValidateUnityPackageSettings() + { + var invalidPaths = string.Empty; + foreach (var path in _settings.ValidationPaths) + { + if (!Directory.Exists(path)) + invalidPaths += $"\n{path}"; + } + + if (!string.IsNullOrEmpty(invalidPaths)) + throw new Exception("The following directories do not exist:" + invalidPaths); + } + + protected override ValidationResult GenerateValidationResult() + { + ITestConfig config; + var applicableTests = GetApplicableTests(ValidationType.Generic); + switch (_settings.ValidationType) + { + case ValidationType.Generic: + config = new GenericTestConfig() { ValidationPaths = _settings.ValidationPaths.ToArray() }; + break; + case ValidationType.UnityPackage: + applicableTests.AddRange(GetApplicableTests(ValidationType.UnityPackage)); + config = new GenericTestConfig() { ValidationPaths = _settings.ValidationPaths.ToArray() }; + break; + default: + return new ValidationResult() { Status = ValidationStatus.Failed, Exception = new Exception("Undefined validation type") }; + } + + var validationResult = RunTests(applicableTests, config); + return validationResult; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/CurrentProjectValidator.cs.meta b/.github/Asset Store Tools/Validator/Scripts/CurrentProjectValidator.cs.meta new file mode 100644 index 00000000000..ca8fe3d03c0 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/CurrentProjectValidator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3a6371dcfa8545c478545b4a43433599 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Data.meta b/.github/Asset Store Tools/Validator/Scripts/Data.meta new file mode 100644 index 00000000000..3b400636649 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1c2a38ded8e054c4088aff1db7224f66 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/CurrentProjectValidationSettings.cs b/.github/Asset Store Tools/Validator/Scripts/Data/CurrentProjectValidationSettings.cs new file mode 100644 index 00000000000..6b83057f917 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/CurrentProjectValidationSettings.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; + +namespace AssetStoreTools.Validator.Data +{ + internal class CurrentProjectValidationSettings : ValidationSettings + { + public List ValidationPaths; + public ValidationType ValidationType; + + public CurrentProjectValidationSettings() + { + Category = string.Empty; + ValidationPaths = new List(); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + public override bool Equals(object obj) + { + if (obj == null || obj.GetType() != typeof(CurrentProjectValidationSettings)) + return false; + + var other = (CurrentProjectValidationSettings)obj; + return Category == other.Category + && ValidationType == other.ValidationType + && ValidationPaths.OrderBy(x => x).SequenceEqual(other.ValidationPaths.OrderBy(x => x)); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/CurrentProjectValidationSettings.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Data/CurrentProjectValidationSettings.cs.meta new file mode 100644 index 00000000000..cf2630afa3c --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/CurrentProjectValidationSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9e4a4a4aa3f501847b1abb1e08505f9b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/ExternalProjectValidationSettings.cs b/.github/Asset Store Tools/Validator/Scripts/Data/ExternalProjectValidationSettings.cs new file mode 100644 index 00000000000..f770cd4cadd --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/ExternalProjectValidationSettings.cs @@ -0,0 +1,7 @@ +namespace AssetStoreTools.Validator.Data +{ + internal class ExternalProjectValidationSettings : ValidationSettings + { + public string PackagePath; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/ExternalProjectValidationSettings.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Data/ExternalProjectValidationSettings.cs.meta new file mode 100644 index 00000000000..a7a30e071e4 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/ExternalProjectValidationSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f79c895f4bb099b4983dd20eef72a7bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions.meta b/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions.meta new file mode 100644 index 00000000000..7f0bd9aa3db --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d51c5c866dcd449488caa10a40dd3301 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/HighlightObjectAction.cs b/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/HighlightObjectAction.cs new file mode 100644 index 00000000000..e642904acd2 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/HighlightObjectAction.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Validator.Data.MessageActions +{ + internal class HighlightObjectAction : IMessageAction + { + public string Tooltip => "Click to highlight the associated object in Hierarchy/Project view"; + public Object Target => _target?.GetObject(); + + [JsonProperty] + private TestResultObject _target; + + public HighlightObjectAction() { } + + public HighlightObjectAction(Object target) + { + _target = new TestResultObject(target); + } + + public void Execute() + { + var targetObject = _target.GetObject(); + if (targetObject == null) + return; + + EditorGUIUtility.PingObject(targetObject); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/HighlightObjectAction.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/HighlightObjectAction.cs.meta new file mode 100644 index 00000000000..f9d8e9c619b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/HighlightObjectAction.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de24c0a7f8a22c142a224e6abd0ddc68 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/IMessageAction.cs b/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/IMessageAction.cs new file mode 100644 index 00000000000..ec48b5b9fd8 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/IMessageAction.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; +using UnityEngine; + +namespace AssetStoreTools.Validator.Data.MessageActions +{ + internal interface IMessageAction + { + [JsonIgnore] + string Tooltip { get; } + + [JsonIgnore] + Object Target { get; } + + void Execute(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/IMessageAction.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/IMessageAction.cs.meta new file mode 100644 index 00000000000..db2e565beee --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/IMessageAction.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f1636d7241abdf1498368f841aa818a2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/OpenAssetAction.cs b/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/OpenAssetAction.cs new file mode 100644 index 00000000000..fb5dfa548c3 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/OpenAssetAction.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Validator.Data.MessageActions +{ + internal class OpenAssetAction : IMessageAction + { + public string Tooltip => "Click to open the associated asset"; + public Object Target => _target?.GetObject(); + + [JsonProperty] + private TestResultObject _target; + [JsonProperty] + private int _lineNumber; + + public OpenAssetAction() { } + + public OpenAssetAction(Object target) + { + _target = new TestResultObject(target); + } + + public OpenAssetAction(Object target, int lineNumber) : this(target) + { + _lineNumber = lineNumber; + } + + public void Execute() + { + var targetObject = _target.GetObject(); + if (targetObject == null) + return; + + AssetDatabase.OpenAsset(targetObject, _lineNumber); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/OpenAssetAction.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/OpenAssetAction.cs.meta new file mode 100644 index 00000000000..83519a6b2d3 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/MessageActions/OpenAssetAction.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9fb4fec293bf73f4a8f870c535750613 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/TestResult.cs b/.github/Asset Store Tools/Validator/Scripts/Data/TestResult.cs new file mode 100644 index 00000000000..21d1dd04c13 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/TestResult.cs @@ -0,0 +1,52 @@ +using AssetStoreTools.Validator.Data.MessageActions; +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace AssetStoreTools.Validator.Data +{ + internal struct TestResult + { + public TestResultStatus Status; + + [JsonProperty] + private List _messages; + + [JsonIgnore] + public int MessageCount => _messages != null ? _messages.Count : 0; + + public TestResultMessage GetMessage(int index) + { + return _messages[index]; + } + + public void AddMessage(string msg) + { + AddMessage(msg, null, null); + } + + public void AddMessage(string msg, IMessageAction clickAction) + { + AddMessage(msg, clickAction, null); + } + + public void AddMessage(string msg, IMessageAction clickAction, params UnityEngine.Object[] messageObjects) + { + if (_messages == null) + _messages = new List(); + + var message = new TestResultMessage(msg, clickAction); + _messages.Add(message); + + if (messageObjects == null) + return; + + foreach (var obj in messageObjects) + { + if (obj == null) + continue; + + message.AddMessageObject(obj); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/TestResult.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Data/TestResult.cs.meta new file mode 100644 index 00000000000..ec1467fa476 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/TestResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 05d7d92bbda6bf44f8ed5fbd0cde57e6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/TestResultMessage.cs b/.github/Asset Store Tools/Validator/Scripts/Data/TestResultMessage.cs new file mode 100644 index 00000000000..7ebe0f5d7e1 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/TestResultMessage.cs @@ -0,0 +1,53 @@ +using AssetStoreTools.Validator.Data.MessageActions; +using Newtonsoft.Json; +using System.Collections.Generic; +using Object = UnityEngine.Object; + +namespace AssetStoreTools.Validator.Data +{ + internal class TestResultMessage + { + [JsonIgnore] + public int MessageObjectCount => _messageObjects.Count; + + [JsonProperty] + private string _text; + [JsonProperty] + private List _messageObjects; + [JsonProperty] + private IMessageAction _clickAction; + + public TestResultMessage() { } + + public TestResultMessage(string text) + { + _text = text; + _messageObjects = new List(); + } + + public TestResultMessage(string text, IMessageAction clickAction) : this(text) + { + _clickAction = clickAction; + } + + public string GetText() + { + return _text; + } + + public IMessageAction GetClickAction() + { + return _clickAction; + } + + public void AddMessageObject(Object obj) + { + _messageObjects.Add(new TestResultObject(obj)); + } + + public TestResultObject GetMessageObject(int index) + { + return _messageObjects[index]; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/TestResultMessage.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Data/TestResultMessage.cs.meta new file mode 100644 index 00000000000..2f0262fa751 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/TestResultMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0761356c44140ca49917f93b42926471 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/TestResultObject.cs b/.github/Asset Store Tools/Validator/Scripts/Data/TestResultObject.cs new file mode 100644 index 00000000000..8b601d54d35 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/TestResultObject.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Validator.Data +{ + internal class TestResultObject + { + [JsonIgnore] + private Object _object; + [JsonProperty] + private string _objectGlobalId; + + public TestResultObject(Object obj) + { + _object = obj; + _objectGlobalId = GlobalObjectId.GetGlobalObjectIdSlow(obj).ToString(); + } + + public Object GetObject() + { + if (_object != null) + return _object; + + if (string.IsNullOrEmpty(_objectGlobalId)) + return null; + + if (!GlobalObjectId.TryParse(_objectGlobalId, out var globalObject)) + return null; + + _object = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(globalObject); + return _object; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/TestResultObject.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Data/TestResultObject.cs.meta new file mode 100644 index 00000000000..a688e8821da --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/TestResultObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: acce8e477b7fe2c4aa430ebdd65ea7d1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/TestResultStatus.cs b/.github/Asset Store Tools/Validator/Scripts/Data/TestResultStatus.cs new file mode 100644 index 00000000000..ccda879bf7a --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/TestResultStatus.cs @@ -0,0 +1,11 @@ +namespace AssetStoreTools.Validator.Data +{ + internal enum TestResultStatus + { + Undefined = 0, + Pass = 1, + Fail = 2, + Warning = 3, + VariableSeverityIssue = 4 + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/TestResultStatus.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Data/TestResultStatus.cs.meta new file mode 100644 index 00000000000..7e7401a96c4 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/TestResultStatus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eef1ba0cf35f1304d8929e23b94e7c23 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/ValidationResult.cs b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationResult.cs new file mode 100644 index 00000000000..89ae63371cc --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationResult.cs @@ -0,0 +1,24 @@ +using AssetStoreTools.Validator.TestDefinitions; +using System; +using System.Collections.Generic; + +namespace AssetStoreTools.Validator.Data +{ + internal class ValidationResult + { + public ValidationStatus Status; + public bool HadCompilationErrors; + public string ProjectPath; + public List Tests; + public Exception Exception; + + public ValidationResult() + { + Status = ValidationStatus.NotRun; + HadCompilationErrors = false; + ProjectPath = string.Empty; + Tests = new List(); + Exception = null; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/ValidationResult.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationResult.cs.meta new file mode 100644 index 00000000000..da481088550 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b15525b8dcf3e654ca2f895472ab7cb1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/ValidationSettings.cs b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationSettings.cs new file mode 100644 index 00000000000..5b51134a17c --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationSettings.cs @@ -0,0 +1,7 @@ +namespace AssetStoreTools.Validator.Data +{ + internal abstract class ValidationSettings + { + public string Category; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/ValidationSettings.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationSettings.cs.meta new file mode 100644 index 00000000000..7783188d52a --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 33e99d6b6e1e7ef4abd6cd2c0137741a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/ValidationStatus.cs b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationStatus.cs new file mode 100644 index 00000000000..f0fb66c68b8 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationStatus.cs @@ -0,0 +1,10 @@ +namespace AssetStoreTools.Validator.Data +{ + internal enum ValidationStatus + { + NotRun, + RanToCompletion, + Failed, + Cancelled + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/ValidationStatus.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationStatus.cs.meta new file mode 100644 index 00000000000..32b6657635b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationStatus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a1f1e1e94faa6284f8d71804ba2bbd24 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/ValidationType.cs b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationType.cs new file mode 100644 index 00000000000..dd3b51cc435 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationType.cs @@ -0,0 +1,8 @@ +namespace AssetStoreTools.Validator.Data +{ + internal enum ValidationType + { + Generic = 0, + UnityPackage = 1 + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Data/ValidationType.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationType.cs.meta new file mode 100644 index 00000000000..dca424c1cf5 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Data/ValidationType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 079f8963464230145853d86eff935e04 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/ExternalProjectValidator.cs b/.github/Asset Store Tools/Validator/Scripts/ExternalProjectValidator.cs new file mode 100644 index 00000000000..3556d1a34ae --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/ExternalProjectValidator.cs @@ -0,0 +1,259 @@ +using AssetStoreTools.Utility; +using AssetStoreTools.Validator.Data; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Validator +{ + internal class ExternalProjectValidator : ValidatorBase + { + private ExternalProjectValidationSettings _settings; + + public ExternalProjectValidator(ExternalProjectValidationSettings settings) : base(settings) + { + _settings = settings; + } + + protected override void ValidateSettings() + { + if (_settings == null) + throw new Exception("Validation Settings is null"); + + if (string.IsNullOrEmpty(_settings.PackagePath) + || !File.Exists(_settings.PackagePath)) + throw new Exception("Package was not found"); + } + + protected override ValidationResult GenerateValidationResult() + { + bool interactiveMode = false; + try + { + // Step 1 - prepare a temporary project + var result = PrepareTemporaryValidationProject(interactiveMode); + + // If preparation was cancelled or setting up project failed - return immediately + if (result.Status == ValidationStatus.Cancelled || result.Status == ValidationStatus.Failed) + return result; + + // Step 2 - load the temporary project and validate the package + result = ValidateTemporaryValidationProject(result, interactiveMode); + + // Step 3 - copy validation results + result = ParseValidationResult(result.ProjectPath); + + return result; + } + catch (Exception e) + { + return new ValidationResult() { Status = ValidationStatus.Failed, Exception = e }; + } + finally + { + EditorUtility.ClearProgressBar(); + } + } + + private ValidationResult PrepareTemporaryValidationProject(bool interactiveMode) + { + EditorUtility.DisplayProgressBar("Validating...", "Preparing the validation project. This may take a while.", 0.3f); + + var result = new ValidationResult(); + var tempProjectPath = Path.Combine(Constants.RootProjectPath, "Temp", GUID.Generate().ToString()).Replace("\\", "/"); + result.ProjectPath = tempProjectPath; + + if (!Directory.Exists(tempProjectPath)) + Directory.CreateDirectory(tempProjectPath); + + // Cannot edit a package.json file that does not yet exist - copy over AST instead + var tempPackagesPath = $"{tempProjectPath}/Packages"; + if (!Directory.Exists(tempPackagesPath)) + Directory.CreateDirectory(tempPackagesPath); + var assetStoreToolsPath = PackageUtility.GetAllPackages().FirstOrDefault(x => x.name == "com.unity.asset-store-tools").resolvedPath.Replace("\\", "/"); + FileUtility.CopyDirectory(assetStoreToolsPath, $"{tempPackagesPath}/com.unity.asset-store-tools", true); + + var logFilePath = $"{tempProjectPath}/preparation.log"; + + // Create the temporary project + var processInfo = new System.Diagnostics.ProcessStartInfo() + { + FileName = Constants.UnityPath, + Arguments = $"-createProject \"{tempProjectPath}\" -logFile \"{logFilePath}\" -importpackage \"{Path.GetFullPath(_settings.PackagePath)}\" -quit" + }; + + if (!interactiveMode) + processInfo.Arguments += " -batchmode"; + + var exitCode = 0; + + using (var process = System.Diagnostics.Process.Start(processInfo)) + { + while (!process.HasExited) + { + if (EditorUtility.DisplayCancelableProgressBar("Validating...", "Preparing the validation project. This may take a while.", 0.3f)) + process.Kill(); + + Thread.Sleep(10); + } + + exitCode = process.ExitCode; + + // Windows and MacOS exit codes + if (exitCode == -1 || exitCode == 137) + { + result.Status = ValidationStatus.Cancelled; + return result; + } + } + + if (exitCode != 0) + { + result.Status = ValidationStatus.Failed; + result.Exception = new Exception($"Setting up the temporary project failed (exit code {exitCode})\n\nMore information can be found in the log file: {logFilePath}"); + } + else + { + result.Status = ValidationStatus.RanToCompletion; + } + + return result; + } + + private ValidationResult ValidateTemporaryValidationProject(ValidationResult result, bool interactiveMode) + { + EditorUtility.DisplayProgressBar("Validating...", "Performing validation...", 0.6f); + + var logFilePath = $"{result.ProjectPath}/validation.log"; + var processInfo = new System.Diagnostics.ProcessStartInfo() + { + FileName = Constants.UnityPath, + Arguments = $"-projectPath \"{result.ProjectPath}\" -logFile \"{logFilePath}\" -executeMethod AssetStoreTools.Validator.ExternalProjectValidator.ValidateProject -category \"{_settings.Category}\"" + }; + + if (!interactiveMode) + processInfo.Arguments += " -batchmode -ignorecompilererrors"; + + var exitCode = 0; + + using (var process = System.Diagnostics.Process.Start(processInfo)) + { + process.WaitForExit(); + exitCode = process.ExitCode; + } + + if (exitCode != 0) + { + result.Status = ValidationStatus.Failed; + result.Exception = new Exception($"Validating the temporary project failed (exit code {exitCode})\n\nMore information can be found in the log file: {logFilePath}"); + } + else + { + result.Status = ValidationStatus.RanToCompletion; + } + + return result; + } + + private ValidationResult ParseValidationResult(string externalProjectPath) + { + if (!CachingService.GetCachedValidatorStateData(externalProjectPath, out var validationStateData)) + throw new Exception("Could not find external project's validation results"); + + var cachedResult = validationStateData.GetResults(); + var cachedTestResults = cachedResult.GetResults(); + var tests = GetApplicableTests(ValidationType.Generic, ValidationType.UnityPackage); + + foreach (var test in tests) + { + if (!cachedTestResults.Any(x => x.Key == test.Id)) + continue; + + var matchingTest = cachedTestResults.First(x => x.Key == test.Id); + test.Result = matchingTest.Value; + } + + var result = new ValidationResult() + { + Status = cachedResult.GetStatus(), + HadCompilationErrors = cachedResult.GetHadCompilationErrors(), + ProjectPath = cachedResult.GetProjectPath(), + Tests = tests + }; + + return result; + } + + public static void OpenExternalValidationProject(string projectPath) + { + var unityPath = Constants.UnityPath; + var logFilePath = $"{projectPath}/editor.log"; + + var processInfo = new System.Diagnostics.ProcessStartInfo() + { + FileName = unityPath, + Arguments = $"-projectPath \"{projectPath}\" -logFile \"{logFilePath}\" -executeMethod AssetStoreTools.AssetStoreTools.ShowAssetStoreToolsValidator" + }; + + using (var process = System.Diagnostics.Process.Start(processInfo)) + { + process.WaitForExit(); + } + } + + // Invoked via Command Line Arguments + private static void ValidateProject() + { + var exitCode = 0; + try + { + // Determine whether to validate Assets folder or Packages folders + var validationPaths = new List(); + var packageDirectories = Directory.GetDirectories("Packages", "*", SearchOption.TopDirectoryOnly) + .Select(x => x.Replace("\\", "/")) + .Where(x => x != "Packages/com.unity.asset-store-tools").ToArray(); + + if (packageDirectories.Length > 0) + validationPaths.AddRange(packageDirectories); + else + validationPaths.Add("Assets"); + + // Parse category + var category = string.Empty; + var args = Environment.GetCommandLineArgs().ToList(); + var categoryIndex = args.IndexOf("-category"); + if (categoryIndex != -1 && categoryIndex + 1 < args.Count) + category = args[categoryIndex + 1]; + + // Run validation + var validationSettings = new CurrentProjectValidationSettings() + { + Category = category, + ValidationPaths = validationPaths, + ValidationType = ValidationType.UnityPackage + }; + + var validator = new CurrentProjectValidator(validationSettings); + var result = validator.Validate(); + + // Display results + AssetStoreTools.ShowAssetStoreToolsValidator(validationSettings, result); + EditorUtility.DisplayDialog("Validation complete", "Package validation complete.\n\nTo resume work in the original project, close this Editor instance", "OK"); + } + catch + { + exitCode = 1; + throw; + } + finally + { + if (Application.isBatchMode) + EditorApplication.Exit(exitCode); + } + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/ExternalProjectValidator.cs.meta b/.github/Asset Store Tools/Validator/Scripts/ExternalProjectValidator.cs.meta new file mode 100644 index 00000000000..46e89fb64ad --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/ExternalProjectValidator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2664bbca63a2444498f13beb7e4fa731 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/IValidator.cs b/.github/Asset Store Tools/Validator/Scripts/IValidator.cs new file mode 100644 index 00000000000..bd16b586bd0 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/IValidator.cs @@ -0,0 +1,11 @@ +using AssetStoreTools.Validator.Data; + +namespace AssetStoreTools.Validator +{ + internal interface IValidator + { + ValidationSettings Settings { get; } + + ValidationResult Validate(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/IValidator.cs.meta b/.github/Asset Store Tools/Validator/Scripts/IValidator.cs.meta new file mode 100644 index 00000000000..5a4e71397df --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/IValidator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d49e9393288e0ed418c546e57c4cb425 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services.meta b/.github/Asset Store Tools/Validator/Scripts/Services.meta new file mode 100644 index 00000000000..03e4234460d --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9315c4052243ab2488208604c11c53c7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/CachingService.meta b/.github/Asset Store Tools/Validator/Scripts/Services/CachingService.meta new file mode 100644 index 00000000000..5d9a1cb49cb --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/CachingService.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0a52c1c4a2b7caa458af5b9a212b80a5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/CachingService.cs b/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/CachingService.cs new file mode 100644 index 00000000000..cff8d95f221 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/CachingService.cs @@ -0,0 +1,55 @@ +using AssetStoreTools.Utility; +using AssetStoreTools.Validator.UI.Data.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace AssetStoreTools.Validator.Services +{ + internal class CachingService : ICachingService + { + public bool GetCachedValidatorStateData(out ValidatorStateData stateData) + { + return GetCachedValidatorStateData(Constants.RootProjectPath, out stateData); + } + + public bool GetCachedValidatorStateData(string projectPath, out ValidatorStateData stateData) + { + stateData = null; + if (!CacheUtil.GetFileFromProjectPersistentCache(projectPath, Constants.Cache.ValidationResultFile, out var filePath)) + return false; + + try + { + var serializerSettings = new JsonSerializerSettings() + { + ContractResolver = ValidatorStateDataContractResolver.Instance, + TypeNameHandling = TypeNameHandling.Auto, + Converters = new List() { new StringEnumConverter() } + }; + + stateData = JsonConvert.DeserializeObject(File.ReadAllText(filePath, Encoding.UTF8), serializerSettings); + return true; + } + catch + { + return false; + } + } + + public void CacheValidatorStateData(ValidatorStateData stateData) + { + var serializerSettings = new JsonSerializerSettings() + { + ContractResolver = ValidatorStateDataContractResolver.Instance, + Formatting = Formatting.Indented, + TypeNameHandling = TypeNameHandling.Auto, + Converters = new List() { new StringEnumConverter() } + }; + + CacheUtil.CreateFileInPersistentCache(Constants.Cache.ValidationResultFile, JsonConvert.SerializeObject(stateData, serializerSettings), true); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/CachingService.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/CachingService.cs.meta new file mode 100644 index 00000000000..00ba6e287af --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/CachingService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b2d545f659acb4343bf485ffb20ecf72 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/ICachingService.cs b/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/ICachingService.cs new file mode 100644 index 00000000000..bee25d59e54 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/ICachingService.cs @@ -0,0 +1,11 @@ +using AssetStoreTools.Validator.UI.Data.Serialization; + +namespace AssetStoreTools.Validator.Services +{ + internal interface ICachingService : IValidatorService + { + void CacheValidatorStateData(ValidatorStateData stateData); + bool GetCachedValidatorStateData(out ValidatorStateData stateData); + bool GetCachedValidatorStateData(string projectPath, out ValidatorStateData stateData); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/ICachingService.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/ICachingService.cs.meta new file mode 100644 index 00000000000..4f5aef4cf48 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/ICachingService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a8a3e36c133848447b043a91e709c63e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/PreviewDatabaseContractResolver.cs b/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/PreviewDatabaseContractResolver.cs new file mode 100644 index 00000000000..aa495ee28ed --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/PreviewDatabaseContractResolver.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json.Serialization; + +namespace AssetStoreTools.Previews.Services +{ + internal class PreviewDatabaseContractResolver : DefaultContractResolver + { + private static PreviewDatabaseContractResolver _instance; + public static PreviewDatabaseContractResolver Instance => _instance ?? (_instance = new PreviewDatabaseContractResolver()); + + private NamingStrategy _namingStrategy; + + private PreviewDatabaseContractResolver() + { + _namingStrategy = new SnakeCaseNamingStrategy(); + } + + protected override string ResolvePropertyName(string propertyName) + { + var resolvedName = _namingStrategy.GetPropertyName(propertyName, false); + if (resolvedName.StartsWith("_")) + return resolvedName.Substring(1); + + return resolvedName; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/PreviewDatabaseContractResolver.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/PreviewDatabaseContractResolver.cs.meta new file mode 100644 index 00000000000..799994b4463 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/CachingService/PreviewDatabaseContractResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aee615e9aaf50fb4f989cd4698e8947e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/IValidatorService.cs b/.github/Asset Store Tools/Validator/Scripts/Services/IValidatorService.cs new file mode 100644 index 00000000000..3ff09b82adb --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/IValidatorService.cs @@ -0,0 +1,4 @@ +namespace AssetStoreTools.Validator.Services +{ + internal interface IValidatorService { } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/IValidatorService.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/IValidatorService.cs.meta new file mode 100644 index 00000000000..74923b3b02f --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/IValidatorService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 075953f4ab4a65d4fae6e891360df0d0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation.meta new file mode 100644 index 00000000000..bf10d70df51 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 184dcfbfe1d21454fa8cf49f1c637871 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions.meta new file mode 100644 index 00000000000..d6c2cf03415 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ed0af5acc22551645ae4cb7d75bd1c36 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IAssetUtilityService.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IAssetUtilityService.cs new file mode 100644 index 00000000000..a73e3ce1e46 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IAssetUtilityService.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Validator.Services.Validation +{ + internal interface IAssetUtilityService : IValidatorService + { + IEnumerable GetAssetPathsFromAssets(string[] searchPaths, AssetType type); + IEnumerable GetObjectsFromAssets(string[] searchPaths, AssetType type) where T : Object; + IEnumerable GetObjectsFromAssets(string[] searchPaths, AssetType type); + string ObjectToAssetPath(Object obj); + T AssetPathToObject(string assetPath) where T : Object; + Object AssetPathToObject(string assetPath); + AssetImporter GetAssetImporter(string assetPath); + AssetImporter GetAssetImporter(Object asset); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IAssetUtilityService.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IAssetUtilityService.cs.meta new file mode 100644 index 00000000000..cb8e4df6688 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IAssetUtilityService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d28c5ea40f4c9954bae02804e416b898 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IFileSignatureUtilityService.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IFileSignatureUtilityService.cs new file mode 100644 index 00000000000..7633ffdf0d5 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IFileSignatureUtilityService.cs @@ -0,0 +1,7 @@ +namespace AssetStoreTools.Validator.Services.Validation +{ + internal interface IFileSignatureUtilityService : IValidatorService + { + ArchiveType GetArchiveType(string filePath); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IFileSignatureUtilityService.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IFileSignatureUtilityService.cs.meta new file mode 100644 index 00000000000..132acba163e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IFileSignatureUtilityService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 609c423482ecf8844a71166b4ef49cb6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IMeshUtilityService.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IMeshUtilityService.cs new file mode 100644 index 00000000000..ba4959e8c07 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IMeshUtilityService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace AssetStoreTools.Validator.Services.Validation +{ + internal interface IMeshUtilityService : IValidatorService + { + IEnumerable GetCustomMeshesInObject(GameObject obj); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IMeshUtilityService.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IMeshUtilityService.cs.meta new file mode 100644 index 00000000000..952889ee2f3 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IMeshUtilityService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: acde6f9b97c9cac4b88a84aa9001a0fc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IModelUtilityService.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IModelUtilityService.cs new file mode 100644 index 00000000000..fa29424f319 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IModelUtilityService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace AssetStoreTools.Validator.Services.Validation +{ + internal interface IModelUtilityService : IValidatorService + { + Dictionary> GetImportLogs(params Object[] models); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IModelUtilityService.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IModelUtilityService.cs.meta new file mode 100644 index 00000000000..5e464c928f9 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IModelUtilityService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 91f6bacccdfecb84fb5ab0ba384353b4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/ISceneUtilityService.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/ISceneUtilityService.cs new file mode 100644 index 00000000000..d80f1841ff8 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/ISceneUtilityService.cs @@ -0,0 +1,13 @@ +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace AssetStoreTools.Validator.Services.Validation +{ + internal interface ISceneUtilityService : IValidatorService + { + string CurrentScenePath { get; } + + Scene OpenScene(string scenePath); + GameObject[] GetRootGameObjects(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/ISceneUtilityService.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/ISceneUtilityService.cs.meta new file mode 100644 index 00000000000..db0e94f1f72 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/ISceneUtilityService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cf5ef331063e5aa4e95dfe3eadedf9af +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IScriptUtilityService.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IScriptUtilityService.cs new file mode 100644 index 00000000000..04214c3cee8 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IScriptUtilityService.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using UnityEditor; +using Object = UnityEngine.Object; + +namespace AssetStoreTools.Validator.Services.Validation +{ + internal interface IScriptUtilityService : IValidatorService + { + IReadOnlyDictionary> GetTypeNamespacesFromScriptAssets(IList monoScripts); + IReadOnlyDictionary> GetTypesFromAssemblies(IList assemblies); + IReadOnlyDictionary> GetTypesFromScriptAssets(IList monoScripts); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IScriptUtilityService.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IScriptUtilityService.cs.meta new file mode 100644 index 00000000000..e5458be6db1 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Abstractions/IScriptUtilityService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e0a9f88d37222e4428853b6d3d00b1bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/AssetUtilityService.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/AssetUtilityService.cs new file mode 100644 index 00000000000..fb51d8a403f --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/AssetUtilityService.cs @@ -0,0 +1,216 @@ +using AssetStoreTools.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEditor.Compilation; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace AssetStoreTools.Validator.Services.Validation +{ + internal class AssetUtilityService : IAssetUtilityService + { + public IEnumerable GetAssetPathsFromAssets(string[] searchPaths, AssetType type) + { + string filter = string.Empty; + string[] extensions = null; + + switch (type) + { + // General Types + case AssetType.All: + filter = ""; + break; + case AssetType.Prefab: + filter = "t:prefab"; + break; + case AssetType.Material: + filter = "t:material"; + break; + case AssetType.Model: + filter = "t:model"; + break; + case AssetType.Scene: + filter = "t:scene"; + break; + case AssetType.Texture: + filter = "t:texture"; + break; + case AssetType.Video: + filter = "t:VideoClip"; + break; + // Specific Types + case AssetType.LossyAudio: + filter = "t:AudioClip"; + extensions = new[] { ".mp3", ".ogg" }; + break; + case AssetType.NonLossyAudio: + filter = "t:AudioClip"; + extensions = new[] { ".wav", ".aif", ".aiff" }; + break; + case AssetType.JavaScript: + filter = "t:TextAsset"; + extensions = new[] { ".js" }; + break; + case AssetType.Mixamo: + filter = "t:model"; + extensions = new[] { ".fbx" }; + break; + case AssetType.JPG: + filter = "t:texture"; + extensions = new[] { ".jpg", "jpeg" }; + break; + case AssetType.Executable: + filter = string.Empty; + extensions = new[] { ".exe", ".bat", ".msi", ".apk" }; + break; + case AssetType.Documentation: + filter = string.Empty; + extensions = new[] { ".txt", ".pdf", ".html", ".rtf", ".md" }; + break; + case AssetType.SpeedTree: + filter = string.Empty; + extensions = new[] { ".spm", ".srt", ".stm", ".scs", ".sfc", ".sme", ".st" }; + break; + case AssetType.Shader: + filter = string.Empty; + extensions = new[] { ".shader", ".shadergraph", ".raytrace", ".compute" }; + break; + case AssetType.MonoScript: + filter = "t:script"; + extensions = new[] { ".cs" }; + break; + case AssetType.UnityPackage: + filter = string.Empty; + extensions = new[] { ".unitypackage" }; + break; + case AssetType.PrecompiledAssembly: + var assemblyPaths = GetPrecompiledAssemblies(searchPaths); + return assemblyPaths; + default: + return Array.Empty(); + } + + var guids = AssetDatabase.FindAssets(filter, searchPaths); + var paths = guids.Select(AssetDatabase.GUIDToAssetPath); + + if (extensions != null) + paths = paths.Where(x => extensions.Any(x.ToLower().EndsWith)); + + if (type == AssetType.Mixamo) + paths = paths.Where(IsMixamoFbx); + + paths = paths.Distinct(); + return paths; + } + + public IEnumerable GetObjectsFromAssets(string[] searchPaths, AssetType type) where T : Object + { + var paths = GetAssetPathsFromAssets(searchPaths, type); +#if !AB_BUILDER + var objects = paths.Select(AssetDatabase.LoadAssetAtPath).Where(x => x != null); +#else + var objects = new AssetEnumerator(paths); +#endif + return objects; + } + + public IEnumerable GetObjectsFromAssets(string[] searchPaths, AssetType type) + { + return GetObjectsFromAssets(searchPaths, type); + } + + private IEnumerable GetPrecompiledAssemblies(string[] searchPaths) + { + // Note - for packages, Compilation Pipeline returns full paths, as they appear on disk, not Asset Database + var allDllPaths = CompilationPipeline.GetPrecompiledAssemblyPaths(CompilationPipeline.PrecompiledAssemblySources.UserAssembly); + var rootProjectPath = Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length); + var packages = PackageUtility.GetAllLocalPackages(); + + var result = new List(); + foreach (var dllPath in allDllPaths) + { + var absoluteDllPath = Path.GetFullPath(dllPath).Replace("\\", "/"); + foreach (var validationPath in searchPaths) + { + var absoluteValidationPath = Path.GetFullPath(validationPath).Replace("\\", "/"); + if (absoluteDllPath.StartsWith(absoluteValidationPath)) + { + int pathSeparatorLength = 1; + if (absoluteDllPath.StartsWith(Application.dataPath)) + { + var adbPath = $"Assets/{absoluteDllPath.Remove(0, Application.dataPath.Length + pathSeparatorLength)}"; + result.Add(adbPath); + } + else + { + // For non-Asset folder paths (i.e. local and embedded packages), convert disk path to ADB path + var package = packages.FirstOrDefault(x => dllPath.StartsWith(x.resolvedPath.Replace('\\', '/'))); + + if (package == null) + continue; + + var dllPathInPackage = absoluteDllPath.Remove(0, Path.GetFullPath(package.resolvedPath).Length + pathSeparatorLength); + var adbPath = $"Packages/{package.name}/{dllPathInPackage}"; + + result.Add(adbPath); + } + } + } + } + + return result; + } + + private bool IsMixamoFbx(string fbxPath) + { + // Location of Mixamo Header, this is located in every mixamo fbx file exported + //const int mixamoHeader = 0x4c0 + 2; // < this is the original location from A$ Tools, unsure if Mixamo file headers were changed since then + const int mixamoHeader = 1622; + // Length of Mixamo header + const int length = 0xa; + + var fs = new FileStream(fbxPath, FileMode.Open); + // Check if length is further than + if (fs.Length < mixamoHeader) + return false; + + byte[] buffer = new byte[length]; + using (BinaryReader reader = new BinaryReader(fs)) + { + reader.BaseStream.Seek(mixamoHeader, SeekOrigin.Begin); + reader.Read(buffer, 0, length); + } + + string result = System.Text.Encoding.ASCII.GetString(buffer); + return result.Contains("Mixamo"); + } + + public string ObjectToAssetPath(Object obj) + { + return AssetDatabase.GetAssetPath(obj); + } + + public T AssetPathToObject(string assetPath) where T : Object + { + return AssetDatabase.LoadAssetAtPath(assetPath); + } + + public Object AssetPathToObject(string assetPath) + { + return AssetPathToObject(assetPath); + } + + public AssetImporter GetAssetImporter(string assetPath) + { + return AssetImporter.GetAtPath(assetPath); + } + + public AssetImporter GetAssetImporter(Object asset) + { + return GetAssetImporter(ObjectToAssetPath(asset)); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/AssetUtilityService.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/AssetUtilityService.cs.meta new file mode 100644 index 00000000000..c91088ed10c --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/AssetUtilityService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9634968648d355c47b7cb12aead7abab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data.meta new file mode 100644 index 00000000000..32569358d0e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8dcc2f4da0b6cea4ab4733ebf32edab4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/ArchiveType.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/ArchiveType.cs new file mode 100644 index 00000000000..d225180ef82 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/ArchiveType.cs @@ -0,0 +1,19 @@ +namespace AssetStoreTools.Validator.Services.Validation +{ + internal enum ArchiveType + { + None, + TarGz, + Zip, + Rar, + Tar, + TarZip, + Bz2, + LZip, + SevenZip, + GZip, + QuickZip, + Xz, + Wim + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/ArchiveType.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/ArchiveType.cs.meta new file mode 100644 index 00000000000..82f4e851e8f --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/ArchiveType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4061cb7aed3883346a66494c23e2e77b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/AssetEnumerator.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/AssetEnumerator.cs new file mode 100644 index 00000000000..0df889a8207 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/AssetEnumerator.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEditor; +using Object = UnityEngine.Object; + +namespace AssetStoreTools.Validator.Services.Validation +{ + internal class AssetEnumerator : IEnumerator, IEnumerable where T : Object + { + public const int Capacity = 32; + + private Queue _pathQueue; + private Queue _loadedAssetQueue; + + private T _currentElement; + + public AssetEnumerator(IEnumerable paths) + { + _pathQueue = new Queue(paths); + _loadedAssetQueue = new Queue(); + } + + public bool MoveNext() + { + bool hasPathsButHasNoAssets = _pathQueue.Count != 0 && _loadedAssetQueue.Count == 0; + if (hasPathsButHasNoAssets) + { + LoadMore(); + } + + bool dequeued = false; + if (_loadedAssetQueue.Count != 0) + { + _currentElement = _loadedAssetQueue.Dequeue(); + dequeued = true; + } + + return dequeued; + } + + private void LoadMore() + { + int limit = Capacity; + while (limit > 0 && _pathQueue.Count != 0) + { + string path = _pathQueue.Dequeue(); + T asset = AssetDatabase.LoadAssetAtPath(path); + if (asset != null) + { + _loadedAssetQueue.Enqueue(asset); + limit--; + } + } + + // Unload other loose asset references + EditorUtility.UnloadUnusedAssetsImmediate(); + } + + public void Reset() + { + throw new NotSupportedException("Asset Enumerator cannot be reset."); + } + + public T Current => _currentElement; + + object IEnumerator.Current => Current; + + public void Dispose() + { + // No need to dispose + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this; + } + + public IEnumerator GetEnumerator() + { + return this; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/AssetEnumerator.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/AssetEnumerator.cs.meta new file mode 100644 index 00000000000..d82fc2f5bb0 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/AssetEnumerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0859579889cc56f4aa26eb863a1487b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/AssetType.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/AssetType.cs new file mode 100644 index 00000000000..2fc188d05a7 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/AssetType.cs @@ -0,0 +1,25 @@ +namespace AssetStoreTools.Validator.Services.Validation +{ + internal enum AssetType + { + All, + Documentation, + Executable, + JPG, + JavaScript, + LossyAudio, + Material, + Mixamo, + Model, + MonoScript, + NonLossyAudio, + PrecompiledAssembly, + Prefab, + Scene, + Shader, + SpeedTree, + Texture, + UnityPackage, + Video + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/AssetType.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/AssetType.cs.meta new file mode 100644 index 00000000000..3acaf4b71c2 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/AssetType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b81d00d4ed0a7da4289d4d6248ef9d34 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/LogEntry.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/LogEntry.cs new file mode 100644 index 00000000000..67663be5d7b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/LogEntry.cs @@ -0,0 +1,10 @@ +using UnityEngine; + +namespace AssetStoreTools.Validator.Services.Validation +{ + internal class LogEntry + { + public string Message; + public LogType Severity; + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/LogEntry.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/LogEntry.cs.meta new file mode 100644 index 00000000000..cad8e06d682 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/Data/LogEntry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a1e81104d6b0f4c449ee57503c3b6669 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/FileSignatureUtilityService.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/FileSignatureUtilityService.cs new file mode 100644 index 00000000000..2a140f45df5 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/FileSignatureUtilityService.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; + +namespace AssetStoreTools.Validator.Services.Validation +{ + internal class FileSignatureUtilityService : IFileSignatureUtilityService + { + private class FileSignature + { + public byte[] SignatureBytes; + public int Offset; + + public FileSignature(byte[] signatureBytes, int offset) + { + SignatureBytes = signatureBytes; + Offset = offset; + } + } + + private static readonly Dictionary ArchiveSignatures = new Dictionary + { + { new FileSignature(new byte[] { 0x1f, 0x8b }, 0), ArchiveType.TarGz }, + { new FileSignature(new byte[] { 0x50, 0x4b, 0x03, 0x04 }, 0), ArchiveType.Zip }, + { new FileSignature(new byte[] { 0x50, 0x4b, 0x05, 0x06 }, 0), ArchiveType.Zip }, // Empty Zip Archive + { new FileSignature(new byte[] { 0x50, 0x4b, 0x07, 0x08 }, 0), ArchiveType.Zip }, // Spanned Zip Archive + + { new FileSignature(new byte[] { 0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x00 }, 0), ArchiveType.Rar }, // RaR v1.50+ + { new FileSignature(new byte[] { 0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00 }, 0), ArchiveType.Rar }, // RaR v5.00+ + { new FileSignature(new byte[] { 0x75, 0x73, 0x74, 0x61, 0x72, 0x00, 0x30, 0x30 }, 257), ArchiveType.Tar }, + { new FileSignature(new byte[] { 0x75, 0x73, 0x74, 0x61, 0x72, 0x20, 0x20, 0x00 }, 257), ArchiveType.Tar }, + { new FileSignature(new byte[] { 0x1f, 0x9d }, 0), ArchiveType.TarZip }, // TarZip LZW algorithm + { new FileSignature(new byte[] { 0x1f, 0xa0 }, 0), ArchiveType.TarZip }, // TarZip LZH algorithm + { new FileSignature(new byte[] { 0x42, 0x5a, 0x68 }, 0), ArchiveType.Bz2 }, + { new FileSignature(new byte[] { 0x4c, 0x5a, 0x49, 0x50 }, 0), ArchiveType.LZip }, + { new FileSignature(new byte[] { 0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c }, 0), ArchiveType.SevenZip }, + { new FileSignature(new byte[] { 0x1f, 0x8b }, 0), ArchiveType.GZip }, + { new FileSignature(new byte[] { 0x52, 0x53, 0x56, 0x4b, 0x44, 0x41, 0x54, 0x41 }, 0), ArchiveType.QuickZip }, + { new FileSignature(new byte[] { 0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00 }, 0), ArchiveType.Xz }, + { new FileSignature(new byte[] { 0x4D, 0x53, 0x57, 0x49, 0x4D, 0x00, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x00, 0x00 }, 0), ArchiveType.Wim } + }; + + public ArchiveType GetArchiveType(string filePath) + { + if (!File.Exists(filePath)) + return ArchiveType.None; + + try + { + using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + { + foreach (var kvp in ArchiveSignatures) + { + var fileSignature = kvp.Key; + var archiveType = kvp.Value; + + if (stream.Length < fileSignature.SignatureBytes.Length) + continue; + + var bytes = new byte[fileSignature.SignatureBytes.Length]; + stream.Seek(fileSignature.Offset, SeekOrigin.Begin); + stream.Read(bytes, 0, bytes.Length); + + if (fileSignature.SignatureBytes.SequenceEqual(bytes.Take(fileSignature.SignatureBytes.Length))) + return archiveType; + } + } + } + catch (DirectoryNotFoundException) + { + Debug.LogWarning($"File '{filePath}' exists, but could not be opened for reading. Please make sure the project path lengths are not too long for the Operating System"); + } + + return ArchiveType.None; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/FileSignatureUtilityService.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/FileSignatureUtilityService.cs.meta new file mode 100644 index 00000000000..fc0dc33e4dc --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/FileSignatureUtilityService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 695ed79ad88c3b44b8ae41b650ebe16c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/MeshUtilityService.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/MeshUtilityService.cs new file mode 100644 index 00000000000..fba268ce789 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/MeshUtilityService.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Validator.Services.Validation +{ + internal class MeshUtilityService : IMeshUtilityService + { + public IEnumerable GetCustomMeshesInObject(GameObject obj) + { + var meshes = new List(); + + var meshFilters = obj.GetComponentsInChildren(true); + var skinnedMeshes = obj.GetComponentsInChildren(true); + + meshes.AddRange(meshFilters.Select(m => m.sharedMesh)); + meshes.AddRange(skinnedMeshes.Select(m => m.sharedMesh)); + + meshes = meshes.Where(m => AssetDatabase.GetAssetPath(m).StartsWith("Assets/") || + AssetDatabase.GetAssetPath(m).StartsWith("Packages/")).ToList(); + + return meshes; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/MeshUtilityService.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/MeshUtilityService.cs.meta new file mode 100644 index 00000000000..7c677a23ce2 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/MeshUtilityService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 307f5dd7be983e246adbda52ac50ecf3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/ModelUtilityService.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/ModelUtilityService.cs new file mode 100644 index 00000000000..b0189914520 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/ModelUtilityService.cs @@ -0,0 +1,147 @@ +#if !UNITY_2022_2_OR_NEWER +using System; +using System.Reflection; +#endif +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +#if UNITY_2022_2_OR_NEWER +using UnityEditor.AssetImporters; +#endif +using UnityEngine; +using Object = UnityEngine.Object; + +namespace AssetStoreTools.Validator.Services.Validation +{ + internal class ModelUtilityService : IModelUtilityService + { + private IAssetUtilityService _assetUtility; + +#if !UNITY_2022_2_OR_NEWER + // Rig fields + private const string RigImportWarningsField = "m_RigImportWarnings"; + private const string RigImportErrorsField = "m_RigImportErrors"; + + // Animation fields + private const string AnimationImportWarningsField = "m_AnimationImportWarnings"; + private const string AnimationImportErrorsField = "m_AnimationImportErrors"; + + private static Editor _modelImporterEditor = null; +#endif + + public ModelUtilityService(IAssetUtilityService assetUtility) + { + _assetUtility = assetUtility; + } + + public Dictionary> GetImportLogs(params Object[] models) + { +#if UNITY_2022_2_OR_NEWER + return GetImportLogsDefault(models); +#else + return GetImportLogsLegacy(models); +#endif + } + +#if UNITY_2022_2_OR_NEWER + private Dictionary> GetImportLogsDefault(params Object[] models) + { + var modelsWithLogs = new Dictionary>(); + + foreach (var model in models) + { + var modelLogs = new List(); + + var importLog = AssetImporter.GetImportLog(_assetUtility.ObjectToAssetPath(model)); + + if (importLog == null) + continue; + + var entries = importLog.logEntries.Where(x => x.flags.HasFlag(ImportLogFlags.Warning) || x.flags.HasFlag(ImportLogFlags.Error)); + foreach (var entry in entries) + { + var severity = entry.flags.HasFlag(ImportLogFlags.Error) ? LogType.Error : LogType.Warning; + modelLogs.Add(new LogEntry() { Message = entry.message, Severity = severity }); + } + + if (modelLogs.Count > 0) + modelsWithLogs.Add(model, modelLogs); + } + + return modelsWithLogs; + } +#endif + +#if !UNITY_2022_2_OR_NEWER + private Dictionary> GetImportLogsLegacy(params Object[] models) + { + var modelsWithLogs = new Dictionary>(); + + foreach (var model in models) + { + var modelLogs = new List(); + + // Load the Model Importer + var modelImporter = _assetUtility.GetAssetImporter(model) as ModelImporter; + + var editorAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.GetName().Name.Equals("UnityEditor")); + + var modelImporterEditorType = editorAssembly.GetType("UnityEditor.ModelImporterEditor"); + + // Load its Model Importer Editor + Editor.CreateCachedEditorWithContext(new Object[] { modelImporter }, model, modelImporterEditorType, ref _modelImporterEditor); + + // Find the base type + var modelImporterEditorTypeBase = _modelImporterEditor.GetType().BaseType; + + // Get the tabs value + var tabsArrayType = modelImporterEditorTypeBase.GetRuntimeProperties().FirstOrDefault(x => x.Name == "tabs"); + var tabsArray = (Array)tabsArrayType.GetValue(_modelImporterEditor); + + // Get the tabs (Model | Rig | Animation | Materials) + var rigTab = tabsArray.GetValue(1); + var animationTab = tabsArray.GetValue(2); + + var rigErrorsCheckSuccess = CheckFieldForSerializedProperty(rigTab, RigImportErrorsField, out var rigErrors); + var rigWarningsCheckSuccess = CheckFieldForSerializedProperty(rigTab, RigImportWarningsField, out var rigWarnings); + var animationErrorsCheckSuccess = CheckFieldForSerializedProperty(animationTab, AnimationImportErrorsField, out var animationErrors); + var animationWarningsCheckSuccess = CheckFieldForSerializedProperty(animationTab, AnimationImportWarningsField, out var animationWarnings); + + if (!rigErrorsCheckSuccess || !rigWarningsCheckSuccess || !animationErrorsCheckSuccess || !animationWarningsCheckSuccess) + UnityEngine.Debug.LogWarning($"An error was encountered when checking import logs for model '{model.name}'"); + + if (!string.IsNullOrEmpty(rigWarnings)) + modelLogs.Add(new LogEntry() { Message = rigWarnings, Severity = LogType.Warning }); + if (!string.IsNullOrEmpty(rigErrors)) + modelLogs.Add(new LogEntry() { Message = rigErrors, Severity = LogType.Error }); + if (!string.IsNullOrEmpty(animationWarnings)) + modelLogs.Add(new LogEntry() { Message = animationWarnings, Severity = LogType.Warning }); + if (!string.IsNullOrEmpty(animationErrors)) + modelLogs.Add(new LogEntry() { Message = animationErrors, Severity = LogType.Error }); + + if (modelLogs.Count > 0) + modelsWithLogs.Add(model, modelLogs); + } + + return modelsWithLogs; + } + + private static bool CheckFieldForSerializedProperty(object source, string propertyName, out string message) + { + message = string.Empty; + + try + { + var propertyType = source.GetType().GetRuntimeFields().FirstOrDefault(x => x.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); + var propertyValue = propertyType.GetValue(source) as SerializedProperty; + message = propertyValue.stringValue; + return true; + } + catch + { + return false; + } + } +#endif + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/ModelUtilityService.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/ModelUtilityService.cs.meta new file mode 100644 index 00000000000..ec4a95ad04c --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/ModelUtilityService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c50ca4c87e66f1b478279e5d1db4a08e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/SceneUtilityService.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/SceneUtilityService.cs new file mode 100644 index 00000000000..ec7a43b96c7 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/SceneUtilityService.cs @@ -0,0 +1,26 @@ +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace AssetStoreTools.Validator.Services.Validation +{ + internal class SceneUtilityService : ISceneUtilityService + { + public string CurrentScenePath => SceneManager.GetActiveScene().path; + + public Scene OpenScene(string scenePath) + { + EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo(); + if (string.IsNullOrEmpty(scenePath) || AssetDatabase.LoadAssetAtPath(scenePath) == null) + return EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects); + else + return EditorSceneManager.OpenScene(scenePath); + } + + public GameObject[] GetRootGameObjects() + { + return SceneManager.GetSceneByPath(CurrentScenePath).GetRootGameObjects(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/SceneUtilityService.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/SceneUtilityService.cs.meta new file mode 100644 index 00000000000..9f20ad4ade3 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/SceneUtilityService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 53e8deb0ebfb7ea47956f3a859580cd4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/ScriptUtilityService.cs b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/ScriptUtilityService.cs new file mode 100644 index 00000000000..2e85c2a44fa --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/ScriptUtilityService.cs @@ -0,0 +1,658 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using Object = UnityEngine.Object; + +namespace AssetStoreTools.Validator.Services.Validation +{ + internal class ScriptUtilityService : IScriptUtilityService + { + private const int ScriptTimeoutMs = 10000; + private const string IgnoredAssemblyCharacters = "!@#$%^*&()-+=[]{}\\|;:'\",.<>/?"; + + /// + /// For a given list of script assets, retrieves a list of types and their namespaces + /// + /// + /// A dictionary mapping each script asset with a list of its types. + /// The type tuple contains a name (e.g. class MyClass) and its namespace (e.g. MyNamespace) + /// + public IReadOnlyDictionary> GetTypeNamespacesFromScriptAssets(IList monoScripts) + { + var typesAndNamespaces = new Dictionary>(); + var typeInfos = GetTypeInfosFromScriptAssets(monoScripts); + + foreach (var kvp in typeInfos) + { + var namespacesInScript = new List<(string Name, string Namespace)>(); + foreach (var typeInfo in kvp.Value) + { + bool isValidType = typeInfo.TypeName == ScriptParser.TypeName.Class || typeInfo.TypeName == ScriptParser.TypeName.Struct || + typeInfo.TypeName == ScriptParser.TypeName.Interface || typeInfo.TypeName == ScriptParser.TypeName.Enum; + + if (isValidType) + namespacesInScript.Add(($"{typeInfo.TypeName.ToString().ToLower()} {typeInfo.Name}", typeInfo.Namespace)); + } + + typesAndNamespaces.Add(kvp.Key, namespacesInScript); + } + + return typesAndNamespaces; + } + + /// + /// Scans the given precompiled assembly assets to retrieve a list of their contained types + /// + /// + /// A dictionary mapping each precompiled assembly asset with a list of System.Type objects. + public IReadOnlyDictionary> GetTypesFromAssemblies(IList assemblies) + { + var dllPaths = assemblies.ToDictionary(t => AssetDatabase.GetAssetPath(t), t => t); + var types = new ConcurrentDictionary>(); + var failedDllPaths = new ConcurrentBag(); + + var allAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + + Parallel.ForEach(dllPaths.Keys, + (assemblyPath) => + { + try + { + var assembly = allAssemblies.FirstOrDefault(x => Path.GetFullPath(x.Location).Equals(Path.GetFullPath(assemblyPath), StringComparison.OrdinalIgnoreCase)); + if (assembly == null) + return; + + var assemblyTypes = assembly.GetTypes().Where(x => !IgnoredAssemblyCharacters.Any(c => x.Name.Contains(c))).ToList(); + types.TryAdd(dllPaths[assemblyPath], assemblyTypes); + } + catch + { + failedDllPaths.Add(assemblyPath); + } + }); + + if (failedDllPaths.Count > 0) + { + var message = new StringBuilder("The following precompiled assemblies could not be checked:"); + foreach (var path in failedDllPaths) + message.Append($"\n{path}"); + UnityEngine.Debug.LogWarning(message); + } + + // Types are sorted randomly due to parallelism, therefore need to be sorted before returning + var sortedTypes = dllPaths.Where(x => types.ContainsKey(x.Value)) + .Select(x => new KeyValuePair>(x.Value, types[x.Value])) + .ToDictionary(t => t.Key, t => t.Value); + + return sortedTypes; + } + + /// + /// Scans the given script assets to retrieve a list of their contained types + /// + /// + /// A dictionary mapping each precompiled assembly asset with a list of System.Type objects. + public IReadOnlyDictionary> GetTypesFromScriptAssets(IList monoScripts) + { + var realTypes = new Dictionary>(); + var typeInfos = GetTypeInfosFromScriptAssets(monoScripts); + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + + foreach (var kvp in typeInfos) + { + var realTypesInScript = new List(); + foreach (var typeInfo in kvp.Value) + { + bool isValidType = typeInfo.TypeName == ScriptParser.TypeName.Class || typeInfo.TypeName == ScriptParser.TypeName.Struct || + typeInfo.TypeName == ScriptParser.TypeName.Interface || typeInfo.TypeName == ScriptParser.TypeName.Enum; + + if (isValidType) + { + var realType = assemblies.Where(a => a.GetType(typeInfo.GetReflectionFriendlyFullName()) != null) + .Select(a => a.GetType(typeInfo.GetReflectionFriendlyFullName())).FirstOrDefault(); + if (realType != null) + realTypesInScript.Add(realType); + } + } + + realTypes.Add(kvp.Key, realTypesInScript); + } + + return realTypes; + } + + /// + /// Scans the given MonoScript assets to retrieve a list of their contained types + /// + /// + /// A dictionary mapping each script asset with a list of TypeInfo objects. + private IReadOnlyDictionary> GetTypeInfosFromScriptAssets(IList monoScripts) + { + var types = new ConcurrentDictionary>(); + var monoScriptContents = new Dictionary(); + var failedScripts = new ConcurrentBag(); + + // A separate dictionary is needed because MonoScript contents cannot be accessed outside of the main thread + foreach (var kvp in monoScripts) + monoScriptContents.Add(kvp, kvp.text); + + var tasks = new List>(); + + try + { + foreach (var kvp in monoScriptContents) + { + var cancellationTokenSource = new CancellationTokenSource(ScriptTimeoutMs); + + var task = Task.Run(() => + { + var parsingTask = new ScriptParser(cancellationTokenSource.Token); + var parsed = parsingTask.GetTypesInScript(kvp.Value, out IList parsedTypes); + if (parsed) + types.TryAdd(kvp.Key, parsedTypes); + else + failedScripts.Add(kvp.Key); + }); + + tasks.Add(new Tuple(task, cancellationTokenSource)); + } + + foreach (var t in tasks) + t.Item1.Wait(); + } + finally + { + foreach (var t in tasks) + t.Item2.Dispose(); + } + + if (failedScripts.Count > 0) + { + var message = new StringBuilder("The following scripts could not be checked:"); + foreach (var s in failedScripts) + message.Append($"\n{AssetDatabase.GetAssetPath(s)}"); + UnityEngine.Debug.LogWarning(message); + } + + // Types are sorted randomly due to parallelism, therefore need to be sorted before returning + var sortedTypes = monoScriptContents.Where(x => types.ContainsKey(x.Key)) + .Select(x => new KeyValuePair>(x.Key, types[x.Key])) + .ToDictionary(t => t.Key, t => t.Value); + + return sortedTypes; + } + + /// + /// A simple script parser class to detect types declared within a script + /// + private class ScriptParser + { + /// + /// Types that can be identified by the script parser + /// + public enum TypeName + { + Undefined, + Namespace, + Class, + Struct, + Interface, + Enum, + IdentationStart, + IdentationEnd + } + + /// + /// A class containing information about each block of a C# script + /// + /// A block in this context is defined as script text that is contained within curly brackets. + /// If it's a type, it may have a preceding name and a namespace + /// + public class BlockInfo + { + public TypeName TypeName = TypeName.Undefined; + public string Name = string.Empty; + public string FullName = string.Empty; + public string Namespace = string.Empty; + public int FoundIndex; + public int StartIndex; + + public BlockInfo ParentBlock; + + public string GetReflectionFriendlyFullName() + { + StringBuilder sb = new StringBuilder(FullName); + for (int i = sb.Length - 1; i >= Namespace.Length + 1; i--) + if (sb[i] == '.') + sb[i] = '+'; + + return sb.ToString(); + } + } + + private CancellationToken _token; + + public ScriptParser(CancellationToken token) + { + _token = token; + } + + public bool GetTypesInScript(string text, out IList types) + { + types = null; + + try + { + var sanitized = SanitizeScript(text); + types = ScanForTypes(sanitized); + return true; + } + catch + { + return false; + } + } + + private string SanitizeScript(string source) + { + var sb = new StringBuilder(source); + + // Remove comments and strings + sb = RemoveStringsAndComments(sb); + + // Replace newlines with spaces + sb.Replace("\r", " ").Replace("\n", " "); + + // Space out the brackets + sb.Replace("{", " { ").Replace("}", " } "); + + // Insert a space at the start for more convenient parsing + sb.Insert(0, " "); + + // Remove repeating spaces + var sanitized = Regex.Replace(sb.ToString(), @"\s{2,}", " "); + + return sanitized; + } + + private StringBuilder RemoveStringsAndComments(StringBuilder sb) + { + void CheckStringIdentifiers(int index, out bool isVerbatim, out bool isInterpolated) + { + isVerbatim = false; + isInterpolated = false; + + string precedingChars = string.Empty; + for (int i = index - 1; i >= 0; i--) + { + if (sb[i] == ' ') + break; + precedingChars += sb[i]; + } + + if (precedingChars.Contains("@")) + isVerbatim = true; + if (precedingChars.Contains("$")) + isInterpolated = true; + } + + bool IsRegion(int index) + { + if (sb.Length - index < "#region".Length) + return false; + if (sb[index] == '#' && sb[index + 1] == 'r' && sb[index + 2] == 'e' && sb[index + 3] == 'g' && sb[index + 4] == 'i' && + sb[index + 5] == 'o' && sb[index + 6] == 'n') + return true; + return false; + } + + var removeRanges = new List>(); + + for (int i = 0; i < sb.Length; i++) + { + _token.ThrowIfCancellationRequested(); + + // Comment code + if (sb[i] == '/') + { + if (sb[i + 1] == '/') + { + for (int j = i + 1; j < sb.Length; j++) + { + _token.ThrowIfCancellationRequested(); + if (sb[j] == '\n' || j == sb.Length - 1) + { + removeRanges.Add(new Tuple(i, j - i + 1)); + i = j; + break; + } + } + } + else if (sb[i + 1] == '*') + { + for (int j = i + 2; j < sb.Length; j++) + { + _token.ThrowIfCancellationRequested(); + if (sb[j] == '/' && sb[j - 1] == '*') + { + removeRanges.Add(new Tuple(i, j - i + 1)); + i = j + 1; + break; + } + } + } + } + // Char code + else if (sb[i] == '\'') + { + for (int j = i + 1; j < sb.Length; j++) + { + _token.ThrowIfCancellationRequested(); + if (sb[j] == '\'') + { + if (sb[j - 1] == '\\') + { + int slashCount = 0; + int k = j - 1; + while (sb[k--] == '\\') + slashCount++; + if (slashCount % 2 != 0) + continue; + } + removeRanges.Add(new Tuple(i, j - i + 1)); + i = j; + break; + } + } + } + // String code + else if (sb[i] == '"') + { + if (sb[i - 1] == '\'' && sb[i + 1] == '\'' || (sb[i - 2] == '\'' && sb[i - 1] == '\\' && sb[i + 1] == '\'')) + continue; + + CheckStringIdentifiers(i, out bool isVerbatim, out bool isInterpolated); + + var bracketCount = 0; + bool interpolationEnd = true; + for (int j = i + 1; j < sb.Length; j++) + { + _token.ThrowIfCancellationRequested(); + if (isInterpolated && (sb[j] == '{' || sb[j] == '}')) + { + if (sb[j] == '{') + { + if (sb[j + 1] != '{') + bracketCount++; + else + j += 1; + } + else if (sb[j] == '}') + { + if (sb[j + 1] != '}') + bracketCount--; + else + j += 1; + } + + if (bracketCount == 0) + interpolationEnd = true; + else + interpolationEnd = false; + + continue; + } + + if (sb[j] == '\"') + { + if (isVerbatim) + { + if (sb[j + 1] != '\"') + { + if (!isInterpolated || isInterpolated && interpolationEnd == true) + { + removeRanges.Add(new Tuple(i, j - i + 1)); + i = j + 1; + break; + } + } + else + j += 1; + } + else + { + bool endOfComment = false; + if (sb[j - 1] != '\\') + endOfComment = true; + else + { + int slashCount = 0; + int k = j - 1; + while (sb[k--] == '\\') + slashCount++; + if (slashCount % 2 == 0) + endOfComment = true; + } + + if (!isInterpolated && endOfComment || (isInterpolated && interpolationEnd && endOfComment)) + { + removeRanges.Add(new Tuple(i, j - i + 1)); + i = j + 1; + break; + } + } + } + } + } + // Region code + else if (IsRegion(i)) + { + i += "#region".Length; + for (int j = i; j < sb.Length; j++) + { + _token.ThrowIfCancellationRequested(); + if (sb[j] == '\n') + { + removeRanges.Add(new Tuple(i, j - i + 1)); + i = j; + break; + } + } + } + } + + for (int i = removeRanges.Count - 1; i >= 0; i--) + sb = sb.Remove(removeRanges[i].Item1, removeRanges[i].Item2); + + return sb; + } + + private IList ScanForTypes(string script) + { + var typeList = new SortedList(); + BlockInfo currentActiveBlock = new BlockInfo(); + + int i = 0; + + BlockInfo nextNamespace = null; + BlockInfo nextClass = null; + BlockInfo nextStruct = null; + BlockInfo nextInterface = null; + BlockInfo nextEnum = null; + + while (i < script.Length) + { + _token.ThrowIfCancellationRequested(); + if (nextNamespace == null) + nextNamespace = FindNextTypeBlock(script, i, TypeName.Namespace); + if (nextClass == null) + nextClass = FindNextTypeBlock(script, i, TypeName.Class); + if (nextStruct == null) + nextStruct = FindNextTypeBlock(script, i, TypeName.Struct); + if (nextInterface == null) + nextInterface = FindNextTypeBlock(script, i, TypeName.Interface); + if (nextEnum == null) + nextEnum = FindNextTypeBlock(script, i, TypeName.Enum); + + var nextIdentationIncrease = FindNextTypeBlock(script, i, TypeName.IdentationStart); + var nextIdentationDecrease = FindNextTypeBlock(script, i, TypeName.IdentationEnd); + + if (!TryFindClosestBlock(out var closestBlock, nextNamespace, nextClass, + nextStruct, nextInterface, nextEnum, nextIdentationIncrease, nextIdentationDecrease)) + break; + + switch (closestBlock) + { + case var _ when closestBlock == nextIdentationIncrease: + closestBlock.ParentBlock = currentActiveBlock; + currentActiveBlock = closestBlock; + break; + case var _ when closestBlock == nextIdentationDecrease: + if (currentActiveBlock.TypeName != TypeName.Undefined) + typeList.Add(currentActiveBlock.StartIndex, currentActiveBlock); + currentActiveBlock = currentActiveBlock.ParentBlock; + break; + case var _ when closestBlock == nextNamespace: + closestBlock.Namespace = currentActiveBlock.TypeName == TypeName.Namespace ? currentActiveBlock.FullName : currentActiveBlock.Namespace; + closestBlock.FullName = string.IsNullOrEmpty(currentActiveBlock.FullName) ? closestBlock.Name : $"{currentActiveBlock.FullName}.{closestBlock.Name}"; + closestBlock.ParentBlock = currentActiveBlock; + currentActiveBlock = closestBlock; + nextNamespace = null; + break; + case var _ when closestBlock == nextClass: + case var _ when closestBlock == nextStruct: + case var _ when closestBlock == nextInterface: + case var _ when closestBlock == nextEnum: + closestBlock.FullName = string.IsNullOrEmpty(currentActiveBlock.FullName) ? closestBlock.Name : $"{currentActiveBlock.FullName}.{closestBlock.Name}"; + closestBlock.Namespace = currentActiveBlock.TypeName == TypeName.Namespace ? currentActiveBlock.FullName : currentActiveBlock.Namespace; + closestBlock.ParentBlock = currentActiveBlock; + currentActiveBlock = closestBlock; + switch (closestBlock) + { + case var _ when closestBlock == nextClass: + nextClass = null; + break; + case var _ when closestBlock == nextStruct: + nextStruct = null; + break; + case var _ when closestBlock == nextInterface: + nextInterface = null; + break; + case var _ when closestBlock == nextEnum: + nextEnum = null; + break; + } + break; + } + + i = closestBlock.StartIndex; + } + + return typeList.Select(x => x.Value).ToList(); + } + + private bool TryFindClosestBlock(out BlockInfo closestBlock, params BlockInfo[] blocks) + { + closestBlock = null; + for (int i = 0; i < blocks.Length; i++) + { + if (blocks[i].FoundIndex == -1) + continue; + + if (closestBlock == null || closestBlock.FoundIndex > blocks[i].FoundIndex) + closestBlock = blocks[i]; + } + + return closestBlock != null; + } + + private BlockInfo FindNextTypeBlock(string text, int startIndex, TypeName blockType) + { + string typeKeyword; + switch (blockType) + { + case TypeName.Namespace: + typeKeyword = "namespace"; + break; + case TypeName.Class: + typeKeyword = "class"; + break; + case TypeName.Struct: + typeKeyword = "struct"; + break; + case TypeName.Interface: + typeKeyword = "interface"; + break; + case TypeName.Enum: + typeKeyword = "enum"; + break; + case TypeName.IdentationStart: + var identationStart = text.IndexOf("{", startIndex); + return new BlockInfo() { FoundIndex = identationStart, StartIndex = identationStart + 1, TypeName = TypeName.Undefined }; + case TypeName.IdentationEnd: + var identationEnd = text.IndexOf("}", startIndex); + return new BlockInfo() { FoundIndex = identationEnd, StartIndex = identationEnd + 1, TypeName = TypeName.Undefined }; + default: + throw new ArgumentException("Invalid block type provided"); + } + + int start = -1; + int blockStart = -1; + string name = string.Empty; + while (startIndex < text.Length) + { + _token.ThrowIfCancellationRequested(); + start = text.IndexOf($" {typeKeyword} ", startIndex); + if (start == -1) + return new BlockInfo { FoundIndex = -1 }; + + // Check if the caught type keyword matches the type definition + var openingBracket = text.IndexOf("{", start); + if (openingBracket == -1) + return new BlockInfo { FoundIndex = -1 }; + + var declaration = text.Substring(start, openingBracket - start); + var split = declaration.Split(' '); + + // Namespace detection + if (typeKeyword == "namespace") + { + // Expected result: [null] [namespace] [null] + if (split.Length == 4) + { + name = split[2]; + blockStart = openingBracket + 1; + break; + } + else + startIndex = openingBracket + 1; + } + // Class, Interface, Struct, Enum detection + else + { + // Expected result: [null] [keywordName] [typeName] ... [null] + // Skip any keywords that only contains [null] [keywordName] [null] + if (split.Length != 3) + { + name = split[2]; + blockStart = openingBracket + 1; + break; + } + else + startIndex = openingBracket + 1; + } + } + + var info = new BlockInfo() { FoundIndex = start, StartIndex = blockStart, Name = name, TypeName = blockType }; + return info; + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/Validation/ScriptUtilityService.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/ScriptUtilityService.cs.meta new file mode 100644 index 00000000000..76d8018941a --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/Validation/ScriptUtilityService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9db4298044e2add44bc3aa6ba898d7c3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/ValidatorServiceProvider.cs b/.github/Asset Store Tools/Validator/Scripts/Services/ValidatorServiceProvider.cs new file mode 100644 index 00000000000..0407f8ffea6 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/ValidatorServiceProvider.cs @@ -0,0 +1,24 @@ +using AssetStoreTools.Utility; +using AssetStoreTools.Validator.Services.Validation; + +namespace AssetStoreTools.Validator.Services +{ + internal class ValidatorServiceProvider : ServiceProvider + { + public static ValidatorServiceProvider Instance => _instance ?? (_instance = new ValidatorServiceProvider()); + private static ValidatorServiceProvider _instance; + + private ValidatorServiceProvider() { } + + protected override void RegisterServices() + { + Register(); + Register(); + Register(); + Register(); + Register(); + Register(); + Register(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Services/ValidatorServiceProvider.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Services/ValidatorServiceProvider.cs.meta new file mode 100644 index 00000000000..d2f5d3dd4c0 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Services/ValidatorServiceProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 47ac495c61171824abb2b72b1b7ef676 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions.meta b/.github/Asset Store Tools/Validator/Scripts/Test Definitions.meta new file mode 100644 index 00000000000..7b3ebc2b424 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 462cf5f916fad974a831f6a44aadcc82 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/AutomatedTest.cs b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/AutomatedTest.cs new file mode 100644 index 00000000000..fa49d8c51b8 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/AutomatedTest.cs @@ -0,0 +1,121 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestDefinitions +{ + internal class AutomatedTest : ValidationTest + { + public AutomatedTest(ValidationTestScriptableObject source) : base(source) { } + + public override void Run(ITestConfig config) + { + Type testClass = null; + MethodInfo testMethod = null; + + try + { + ValidateTestMethod(ref testClass, ref testMethod); + ValidateConfig(config); + } + catch (Exception e) + { + Debug.LogError(e.Message); + return; + } + + object testClassInstance; + try + { + testClassInstance = CreateInstance(testClass, config); + } + catch (Exception e) + { + Debug.LogError($"Could not create an instance of class {testClass}:\n{e}"); + return; + } + + try + { + Result = (TestResult)testMethod.Invoke(testClassInstance, new object[0]); + } + catch (Exception e) + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + result.AddMessage("An exception was caught when running this test case. See Console for more details"); + Debug.LogError($"An exception was caught when running validation for test case '{Title}'\n{e}"); + Result = result; + } + } + + private void ValidateTestMethod(ref Type testClass, ref MethodInfo testMethod) + { + if (TestScript == null || (testClass = TestScript.GetClass()) == null) + throw new Exception($"Cannot run test {Title} - Test Script class was not found"); + + var interfaces = testClass.GetInterfaces(); + if (!interfaces.Contains(typeof(ITestScript))) + throw new Exception($"Cannot run test {Title} - Test Script class is not derived from {nameof(ITestScript)}"); + + testMethod = testClass.GetMethod("Run"); + if (testMethod == null) + throw new Exception($"Cannot run test {Title} - Run() method was not found"); + } + + private void ValidateConfig(ITestConfig config) + { + switch (ValidationType) + { + case ValidationType.Generic: + case ValidationType.UnityPackage: + if (config is GenericTestConfig) + return; + break; + default: + throw new NotImplementedException("Undefined validation type"); + } + + throw new Exception("Config does not match the validation type"); + } + + private object CreateInstance(Type testClass, ITestConfig testConfig) + { + var constructors = testClass.GetConstructors(); + if (constructors.Length != 1) + throw new Exception($"Test class {testClass} should only contain a single constructor"); + + var constructor = constructors[0]; + var expectedParameters = constructor.GetParameters(); + var parametersToUse = new List(); + foreach (var expectedParam in expectedParameters) + { + var paramType = expectedParam.ParameterType; + + if (paramType == testConfig.GetType()) + { + parametersToUse.Add(testConfig); + continue; + } + + if (typeof(IValidatorService).IsAssignableFrom(paramType)) + { + var matchingService = ValidatorServiceProvider.Instance.GetService(paramType); + if (matchingService == null) + throw new Exception($"Service {paramType} is not registered and could not be retrieved"); + + parametersToUse.Add(matchingService); + continue; + } + + throw new Exception($"Invalid parameter type: {paramType}"); + } + + var instance = constructor.Invoke(parametersToUse.ToArray()); + return instance; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/AutomatedTest.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/AutomatedTest.cs.meta new file mode 100644 index 00000000000..14f79c30cc5 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/AutomatedTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b284048af6fef0d49b8c3a37f7083d04 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/GenericTestConfig.cs b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/GenericTestConfig.cs new file mode 100644 index 00000000000..22dfedfdafe --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/GenericTestConfig.cs @@ -0,0 +1,7 @@ +namespace AssetStoreTools.Validator.TestDefinitions +{ + internal class GenericTestConfig : ITestConfig + { + public string[] ValidationPaths { get; set; } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/GenericTestConfig.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/GenericTestConfig.cs.meta new file mode 100644 index 00000000000..d982131e0df --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/GenericTestConfig.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ba1ae4e7b45a6c84ca8ad0eb391bf95d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ITestConfig.cs b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ITestConfig.cs new file mode 100644 index 00000000000..5db2e328a25 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ITestConfig.cs @@ -0,0 +1,4 @@ +namespace AssetStoreTools.Validator.TestDefinitions +{ + internal interface ITestConfig { } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ITestConfig.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ITestConfig.cs.meta new file mode 100644 index 00000000000..ee4293910ec --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ITestConfig.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c7e57766d04022c4dac58caf8ebe339a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ITestScript.cs b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ITestScript.cs new file mode 100644 index 00000000000..e705ab8b9c0 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ITestScript.cs @@ -0,0 +1,9 @@ +using AssetStoreTools.Validator.Data; + +namespace AssetStoreTools.Validator.TestDefinitions +{ + internal interface ITestScript + { + TestResult Run(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ITestScript.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ITestScript.cs.meta new file mode 100644 index 00000000000..f8c474344cc --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ITestScript.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 839ef1f3e773ab347b66932d3f810aec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects.meta b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects.meta new file mode 100644 index 00000000000..1dfe2af70b2 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d62652f91f698904ea662c6ab252ea59 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/AutomatedTestScriptableObject.cs b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/AutomatedTestScriptableObject.cs new file mode 100644 index 00000000000..31e64989b04 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/AutomatedTestScriptableObject.cs @@ -0,0 +1,11 @@ +#if UNITY_ASTOOLS_DEVELOPMENT +using UnityEngine; +#endif + +namespace AssetStoreTools.Validator.TestDefinitions +{ +#if UNITY_ASTOOLS_DEVELOPMENT + [CreateAssetMenu(fileName = "AutomatedTest", menuName = "Asset Store Validator/Automated Test")] +#endif + internal class AutomatedTestScriptableObject : ValidationTestScriptableObject { } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/AutomatedTestScriptableObject.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/AutomatedTestScriptableObject.cs.meta new file mode 100644 index 00000000000..28f0608efcf --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/AutomatedTestScriptableObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d813ff809ae82f643bf975031305d541 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/Editor.meta b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/Editor.meta new file mode 100644 index 00000000000..eebb8673b2c --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7cd52466a2239344d90c3043b7afc1e4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/Editor/ValidationTestScriptableObjectInspector.cs b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/Editor/ValidationTestScriptableObjectInspector.cs new file mode 100644 index 00000000000..5b283899257 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/Editor/ValidationTestScriptableObjectInspector.cs @@ -0,0 +1,196 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Utility; +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestDefinitions +{ + [CustomEditor(typeof(ValidationTestScriptableObject), true)] + internal class ValidationTestScriptableObjectInspector : UnityEditor.Editor + { + private enum FilterSeverity + { + Warning, + Fail + } + + private enum FilterType + { + UseFilter, + ExcludeFilter + } + + private ValidationTestScriptableObject _data; + private ValidationTestScriptableObject[] _allObjects; + + private SerializedProperty _script; + private SerializedProperty _validationType; + + private SerializedProperty _testScript; + private SerializedProperty _category; + private SerializedProperty _failFilterProperty; + private SerializedProperty _isInclusiveProperty; + private SerializedProperty _appliesToSubCategories; + private SerializedProperty _categoryFilter; + + private bool _hadChanges; + + private void OnEnable() + { + if (target == null) return; + + _data = target as ValidationTestScriptableObject; + + _script = serializedObject.FindProperty("m_Script"); + + _validationType = serializedObject.FindProperty(nameof(ValidationTestScriptableObject.ValidationType)); + + _testScript = serializedObject.FindProperty(nameof(ValidationTestScriptableObject.TestScript)); + _category = serializedObject.FindProperty(nameof(ValidationTestScriptableObject.CategoryInfo)); + _failFilterProperty = _category.FindPropertyRelative(nameof(ValidationTestScriptableObject.CategoryInfo.IsFailFilter)); + _isInclusiveProperty = _category.FindPropertyRelative(nameof(ValidationTestScriptableObject.CategoryInfo.IsInclusiveFilter)); + _appliesToSubCategories = _category.FindPropertyRelative(nameof(ValidationTestScriptableObject.CategoryInfo.AppliesToSubCategories)); + _categoryFilter = _category.FindPropertyRelative(nameof(ValidationTestScriptableObject.CategoryInfo.Filter)); + + _allObjects = ValidatorUtility.GetAutomatedTestCases(ValidatorUtility.SortType.Id); + _hadChanges = false; + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.LabelField(GetInspectorTitle(), new GUIStyle(EditorStyles.centeredGreyMiniLabel) { fontSize = 24 }, GUILayout.MinHeight(50)); + + EditorGUI.BeginDisabledGroup(true); + EditorGUILayout.PropertyField(_script); + + EditorGUI.BeginChangeCheck(); + // ID field + EditorGUILayout.IntField("Test Id", _data.Id); + if (!ValidateID()) + EditorGUILayout.HelpBox("ID is already in use", MessageType.Warning); + EditorGUI.EndDisabledGroup(); + + EditorGUILayout.Space(8); + EditorGUILayout.LabelField("Test Data", new GUIStyle(EditorStyles.centeredGreyMiniLabel) { alignment = TextAnchor.MiddleLeft, fontSize = 14, padding = new RectOffset(0, 0, 0, 0) }); + + // Validation Type + var validationType = (ValidationType)EditorGUILayout.EnumPopup("Validation Type", (ValidationType)_validationType.enumValueIndex); + _validationType.enumValueIndex = (int)validationType; + + // Other fields + _data.Title = EditorGUILayout.TextField("Title", _data.Title); + if (string.IsNullOrEmpty(_data.Title)) + EditorGUILayout.HelpBox("Title cannot be empty", MessageType.Warning); + + EditorGUILayout.LabelField("Description"); + GUIStyle myTextAreaStyle = new GUIStyle(EditorStyles.textArea) { wordWrap = true }; + _data.Description = EditorGUILayout.TextArea(_data.Description, myTextAreaStyle); + + // Test script + EditorGUILayout.Space(8); + EditorGUILayout.LabelField("Test Script", new GUIStyle(EditorStyles.centeredGreyMiniLabel) { alignment = TextAnchor.MiddleLeft, fontSize = 14, padding = new RectOffset(0, 0, 0, 0) }); + + EditorGUILayout.PropertyField(_testScript); + if (_testScript.objectReferenceValue != null) + { + var generatedScriptType = (_testScript.objectReferenceValue as MonoScript).GetClass(); + if (generatedScriptType == null || !generatedScriptType.GetInterfaces().Contains(typeof(ITestScript))) + EditorGUILayout.HelpBox($"Test Script does not derive from {nameof(ITestScript)}. Test execution will fail", MessageType.Warning); + } + else if (!string.IsNullOrEmpty(_data.Title)) + { + var generatedScriptName = GenerateTestScriptName(); + EditorGUILayout.LabelField($"Proposed script name: {generatedScriptName}.cs", new GUIStyle("Label") { richText = true }); + EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Generate Test Method Script", GUILayout.MaxWidth(200f))) + { + var generatedScript = ValidatorUtility.GenerateTestScript(generatedScriptName, (ValidationType)_validationType.enumValueIndex); + _testScript.objectReferenceValue = generatedScript; + } + EditorGUILayout.EndHorizontal(); + } + + // Variable Sevetity Options + EditorGUILayout.Space(8); + EditorGUILayout.LabelField("Variable Severity Status Filtering", new GUIStyle(EditorStyles.centeredGreyMiniLabel) { alignment = TextAnchor.MiddleLeft, fontSize = 14, padding = new RectOffset(0, 0, 0, 0) }); + + var filterSeverity = (FilterSeverity)EditorGUILayout.EnumPopup("Fail Type", _failFilterProperty.boolValue ? FilterSeverity.Fail : FilterSeverity.Warning); + _failFilterProperty.boolValue = filterSeverity == FilterSeverity.Fail ? true : false; + var filterType = (FilterType)EditorGUILayout.EnumPopup("Filtering rule", _isInclusiveProperty.boolValue ? FilterType.UseFilter : FilterType.ExcludeFilter); + _isInclusiveProperty.boolValue = filterType == FilterType.UseFilter ? true : false; + + EditorGUILayout.PropertyField(_appliesToSubCategories); + + EditorGUILayout.Space(10); + + EditorGUILayout.BeginHorizontal(GUI.skin.FindStyle("HelpBox")); + EditorGUILayout.LabelField(GetFilterDescription(_failFilterProperty.boolValue, _isInclusiveProperty.boolValue), new GUIStyle(GUI.skin.label) { wordWrap = true, richText = true }); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(10); + + EditorGUILayout.PropertyField(_categoryFilter); + + if (EditorGUI.EndChangeCheck()) + { + EditorUtility.SetDirty(target); + _hadChanges = true; + } + + _hadChanges = serializedObject.ApplyModifiedProperties() || _hadChanges; + } + + private string GetInspectorTitle() + { + switch (_data) + { + case AutomatedTestScriptableObject _: + return "Automated Test"; + default: + return "Miscellaneous Test"; + } + } + + private string GenerateTestScriptName() + { + var name = _data.Title.Replace(" ", ""); + return name; + } + + private string GetFilterDescription(bool isFailFilter, bool isInclusive) + { + string text = $"When a {TestResultStatus.VariableSeverityIssue} result type is returned from the test method:\n\n"; + if (isFailFilter) + { + if (isInclusive) + return text + "• Categories IN the filter will result in a FAIL.\n• Categories NOT in the filter will result in a WARNING"; + else + return text + "• Categories NOT in the filter will result in a FAIL.\n• Categories IN the filter will result in a WARNING"; + } + else + { + if (isInclusive) + return text + "• Categories IN the filter will result in a WARNING.\n• Categories NOT in the filter will result in a FAIL"; + else + return text + "• Categories NOT in the filter will result in a WARNING.\n• Categories IN the filter will result in a FAIL"; + } + } + + private bool ValidateID() + { + return !_allObjects.Any(x => x.Id == _data.Id && x != _data); + } + + private void OnDisable() + { + if (!_hadChanges) return; + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/Editor/ValidationTestScriptableObjectInspector.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/Editor/ValidationTestScriptableObjectInspector.cs.meta new file mode 100644 index 00000000000..37b320c2196 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/Editor/ValidationTestScriptableObjectInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 06d76b0e6df91eb43ac956f883c4a2da +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/ValidationTestScriptableObject.cs b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/ValidationTestScriptableObject.cs new file mode 100644 index 00000000000..9c2a29f9b3b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/ValidationTestScriptableObject.cs @@ -0,0 +1,35 @@ +using AssetStoreTools.Validator.Categories; +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Utility; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestDefinitions +{ + internal abstract class ValidationTestScriptableObject : ScriptableObject + { + [SerializeField, HideInInspector] + private bool HasBeenInitialized; + + public int Id; + public string Title; + public string Description; + public ValidatorCategory CategoryInfo; + public ValidationType ValidationType; + public MonoScript TestScript; + + private void OnEnable() + { + // To do: maybe replace with Custom Inspector + if (HasBeenInitialized) + return; + + var existingTestCases = ValidatorUtility.GetAutomatedTestCases(ValidatorUtility.SortType.Id); + if (existingTestCases.Length > 0) + Id = existingTestCases[existingTestCases.Length - 1].Id + 1; + else + Id = 1; + HasBeenInitialized = true; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/ValidationTestScriptableObject.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/ValidationTestScriptableObject.cs.meta new file mode 100644 index 00000000000..66bee1dfe01 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/Scriptable Objects/ValidationTestScriptableObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 11c2422f057b75a458e184d169a00eb6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ValidationTest.cs b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ValidationTest.cs new file mode 100644 index 00000000000..abb77f69064 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ValidationTest.cs @@ -0,0 +1,38 @@ +using AssetStoreTools.Validator.Categories; +using AssetStoreTools.Validator.Data; +using UnityEditor; + +namespace AssetStoreTools.Validator.TestDefinitions +{ + internal abstract class ValidationTest + { + public int Id; + public string Title; + public string Description; + public MonoScript TestScript; + + public ValidationType ValidationType; + public ValidatorCategory CategoryInfo; + + public TestResult Result; + + protected ValidationTest(ValidationTestScriptableObject source) + { + Id = source.Id; + Title = source.Title; + Description = source.Description; + TestScript = source.TestScript; + CategoryInfo = source.CategoryInfo; + ValidationType = source.ValidationType; + Result = new TestResult(); + } + + public abstract void Run(ITestConfig config); + + public string Slugify(string value) + { + string newValue = value.Replace(' ', '-').ToLower(); + return newValue; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ValidationTest.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ValidationTest.cs.meta new file mode 100644 index 00000000000..5f7071bbe0a --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Definitions/ValidationTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 095d629656748914bb6202598876fdf4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods.meta new file mode 100644 index 00000000000..5a3363e9038 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: daedaf78228b5184297e7ca334ea2a12 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic.meta new file mode 100644 index 00000000000..a9b691ed72c --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ecfb23f95f16d2347a4063411aad8063 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckAnimationClips.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckAnimationClips.cs new file mode 100644 index 00000000000..e7543b04e24 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckAnimationClips.cs @@ -0,0 +1,64 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityObject = UnityEngine.Object; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckAnimationClips : ITestScript + { + private static readonly string[] InvalidNames = new[] { "Take 001" }; + + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public CheckAnimationClips(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + var badModels = new Dictionary>(); + var models = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.Model); + + foreach (var model in models) + { + var badClips = new List(); + var clips = AssetDatabase.LoadAllAssetsAtPath(_assetUtility.ObjectToAssetPath(model)); + foreach (var clip in clips) + { + if (InvalidNames.Any(x => x.ToLower().Equals(clip.name.ToLower()))) + { + badClips.Add(clip); + } + } + + if (badClips.Count > 0) + badModels.Add(model, badClips); + } + + if (badModels.Count > 0) + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following models have animation clips with invalid names. Animation clip names should be unique and reflective of the animation itself"); + foreach (var kvp in badModels) + { + result.AddMessage(_assetUtility.ObjectToAssetPath(kvp.Key), null, kvp.Value.ToArray()); + } + } + else + { + result.AddMessage("No animation clips with invalid names were found!"); + result.Status = TestResultStatus.Pass; + } + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckAnimationClips.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckAnimationClips.cs.meta new file mode 100644 index 00000000000..4529bdf0164 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckAnimationClips.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a28985886f182c4bacc89a44777c742 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckAudioClipping.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckAudioClipping.cs new file mode 100644 index 00000000000..6f960e9239f --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckAudioClipping.cs @@ -0,0 +1,128 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckAudioClipping : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public CheckAudioClipping(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + // How many peaks above threshold are required for Audio Clips to be considered clipping + const int TOLERANCE = 2; + // Min. amount of consecutive samples above threshold required for peak detection + const int PEAK_STEPS = 1; + // Clipping threshold. More lenient here than Submission Guidelines (-0.3db) due to the problematic nature of + // correctly determining how sensitive the audio clipping flagging should be, as well as to account for any + // distortion introduced when AudioClips are compresssed after importing to Unity. + const float THRESHOLD = -0.05f; + // Samples for 16-bit audio files + const float S16b = 32767f; + float clippingThreshold = (S16b - (S16b / (2 * Mathf.Log10(1 / S16b)) * THRESHOLD)) / S16b; + TestResult result = new TestResult(); + var clippingAudioClips = new Dictionary(); + + var losslessAudioClips = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.NonLossyAudio).Select(x => x as AudioClip).ToList(); + foreach (var clip in losslessAudioClips) + { + var path = AssetDatabase.GetAssetPath(clip.GetInstanceID()); + + if (IsClipping(clip, TOLERANCE, PEAK_STEPS, clippingThreshold)) + clippingAudioClips.Add(clip, path); + } + + var lossyAudioClips = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.LossyAudio).Select(x => x as AudioClip).ToList(); + foreach (var clip in lossyAudioClips) + { + var path = AssetDatabase.GetAssetPath(clip.GetInstanceID()); + + if (IsClipping(clip, TOLERANCE, PEAK_STEPS, clippingThreshold)) + clippingAudioClips.Add(clip, path); + } + + if (clippingAudioClips.Count > 0) + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following AudioClips are clipping or are very close to 0db ceiling. Please ensure your exported audio files have at least 0.3db of headroom (should peak at no more than -0.3db):", null, clippingAudioClips.Select(x => x.Key).ToArray()); + } + else + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No clipping audio files were detected."); + } + + return result; + } + + private bool IsClipping(AudioClip clip, int tolerance, int peakTolerance, float clippingThreshold) + { + if (DetectNumPeaksAboveThreshold(clip, peakTolerance, clippingThreshold) >= tolerance) + return true; + + return false; + } + + private int DetectNumPeaksAboveThreshold(AudioClip clip, int peakTolerance, float clippingThreshold) + { + float[] samples = new float[clip.samples * clip.channels]; + var data = clip.GetData(samples, 0); + + float[] samplesLeft = samples.Where((s, i) => i % 2 == 0).ToArray(); + float[] samplesRight = samples.Where((s, i) => i % 2 == 1).ToArray(); + + int peaks = 0; + + peaks = GetPeaksInChannel(samplesLeft, peakTolerance, clippingThreshold) + + GetPeaksInChannel(samplesRight, peakTolerance, clippingThreshold); + + return peaks; + } + + private int GetPeaksInChannel(float[] samples, int peakTolerance, float clippingThreshold) + { + int peaks = 0; + bool evalPeak = false; + int peakSteps = 0; + int step = 0; + + while (step < samples.Length) + { + if (Mathf.Abs(samples[step]) >= clippingThreshold && evalPeak) + { + peakSteps++; + } + + if (Mathf.Abs(samples[step]) >= clippingThreshold && !evalPeak) + { + evalPeak = true; + peakSteps++; + } + + if (Mathf.Abs(samples[step]) < clippingThreshold && evalPeak) + { + evalPeak = false; + if (peakSteps >= peakTolerance) + peaks++; + peakSteps = 0; + } + + step++; + } + + return peaks; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckAudioClipping.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckAudioClipping.cs.meta new file mode 100644 index 00000000000..06f08922569 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckAudioClipping.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f604db0353da0cb46bb048f5cd37186f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckColliders.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckColliders.cs new file mode 100644 index 00000000000..5f4499b9dfd --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckColliders.cs @@ -0,0 +1,55 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckColliders : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + private IMeshUtilityService _meshUtility; + + public CheckColliders(GenericTestConfig config, IAssetUtilityService assetUtility, IMeshUtilityService meshUtility) + { + _config = config; + _assetUtility = assetUtility; + _meshUtility = meshUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var prefabs = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.Prefab); + var badPrefabs = new List(); + + foreach (var p in prefabs) + { + var meshes = _meshUtility.GetCustomMeshesInObject(p); + + if (!p.isStatic || !meshes.Any()) + continue; + + var colliders = p.GetComponentsInChildren(true); + if (!colliders.Any()) + badPrefabs.Add(p); + } + + if (badPrefabs.Count == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("All found prefabs have colliders!"); + return result; + } + + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following prefabs contain meshes, but colliders were not found", null, badPrefabs.ToArray()); + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckColliders.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckColliders.cs.meta new file mode 100644 index 00000000000..c90549a08a1 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckColliders.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 308b3d7b7a883b949a14f47cfd5c7ebe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckCompressedFiles.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckCompressedFiles.cs new file mode 100644 index 00000000000..330c01754f0 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckCompressedFiles.cs @@ -0,0 +1,121 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityObject = UnityEngine.Object; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckCompressedFiles : ITestScript + { + private enum ArchiveResult + { + Allowed, + NotAllowed, + TarGzWithIssues, + ZipWithIssues + } + + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + private IFileSignatureUtilityService _fileSignatureUtility; + + public CheckCompressedFiles(GenericTestConfig config, IAssetUtilityService assetUtility, IFileSignatureUtilityService fileSignatureUtility) + { + _config = config; + _assetUtility = assetUtility; + _fileSignatureUtility = fileSignatureUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var checkedArchives = new Dictionary(); + + // Retrieving assets via GetObjectsFromAssets() is insufficient because + // archives might be renamed and not use the expected extension + var allAssetPaths = _assetUtility.GetAssetPathsFromAssets(_config.ValidationPaths, AssetType.All); + + foreach (var assetPath in allAssetPaths) + { + ArchiveType archiveType; + if ((archiveType = _fileSignatureUtility.GetArchiveType(assetPath)) == ArchiveType.None) + continue; + + var archiveObj = _assetUtility.AssetPathToObject(assetPath); + + switch (archiveType) + { + case ArchiveType.TarGz: + if (assetPath.ToLower().EndsWith(".unitypackage")) + checkedArchives.Add(archiveObj, ArchiveResult.Allowed); + else + checkedArchives.Add(archiveObj, ArchiveResult.TarGzWithIssues); + break; + case ArchiveType.Zip: + if (FileNameContainsKeyword(assetPath, "source") && assetPath.ToLower().EndsWith(".zip")) + checkedArchives.Add(archiveObj, ArchiveResult.Allowed); + else + checkedArchives.Add(archiveObj, ArchiveResult.ZipWithIssues); + break; + default: + checkedArchives.Add(archiveObj, ArchiveResult.NotAllowed); + break; + } + } + + if (checkedArchives.Count == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No archives were found in the package content!"); + return result; + } + + if (checkedArchives.Any(x => x.Value == ArchiveResult.Allowed)) + { + result.Status = TestResultStatus.Warning; + result.AddMessage("The following archives of allowed format were found in the package content.\n" + + "Please make sure they adhere to the nested archive guidelines:", null, + checkedArchives.Where(x => x.Value == ArchiveResult.Allowed).Select(x => x.Key).ToArray()); + } + + if (checkedArchives.Any(x => x.Value == ArchiveResult.TarGzWithIssues)) + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following .gz archives were found in the package content.\n" + + "• Gz archives are only allowed in form of '.unitypackage' files", null, + checkedArchives.Where(x => x.Value == ArchiveResult.TarGzWithIssues).Select(x => x.Key).ToArray()); + } + + if (checkedArchives.Any(x => x.Value == ArchiveResult.ZipWithIssues)) + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following .zip archives were found in the package content.\n" + + "• Zip archives should contain the keyword 'source' in the file name", null, + checkedArchives.Where(x => x.Value == ArchiveResult.ZipWithIssues).Select(x => x.Key).ToArray()); + } + + if (checkedArchives.Any(x => x.Value == ArchiveResult.NotAllowed)) + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following archives are using formats that are not allowed:", null, + checkedArchives.Where(x => x.Value == ArchiveResult.NotAllowed).Select(x => x.Key).ToArray()); + } + + return result; + } + + private bool FileNameContainsKeyword(string filePath, string keyword) + { + var fileInfo = new FileInfo(filePath); + + if (!fileInfo.Exists) + return false; + + return fileInfo.Name.Remove(fileInfo.Name.Length - fileInfo.Extension.Length).ToLower().Contains(keyword.ToLower()); + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckCompressedFiles.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckCompressedFiles.cs.meta new file mode 100644 index 00000000000..8bd989c3bac --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckCompressedFiles.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 84b23febe0d923842aef73b95da5f25b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckEmptyPrefabs.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckEmptyPrefabs.cs new file mode 100644 index 00000000000..8a87dad87aa --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckEmptyPrefabs.cs @@ -0,0 +1,46 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckEmptyPrefabs : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public CheckEmptyPrefabs(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var prefabs = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.Prefab); + var badPrefabs = new List(); + + foreach (var p in prefabs) + { + if (p.GetComponents().Length == 1 && p.transform.childCount == 0) + badPrefabs.Add(p); + } + + if (badPrefabs.Count == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No empty prefabs were found!"); + return result; + } + + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following prefabs are empty", null, badPrefabs.ToArray()); + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckEmptyPrefabs.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckEmptyPrefabs.cs.meta new file mode 100644 index 00000000000..fafd18127e3 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckEmptyPrefabs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8055bed9373283e4793463b90b42f08f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckFileMenuNames.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckFileMenuNames.cs new file mode 100644 index 00000000000..3f51144fef8 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckFileMenuNames.cs @@ -0,0 +1,153 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEditor; +using UnityObject = UnityEngine.Object; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckFileMenuNames : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + private IScriptUtilityService _scriptUtility; + + public CheckFileMenuNames(GenericTestConfig config, IAssetUtilityService assetUtility, IScriptUtilityService scriptUtility) + { + _config = config; + _assetUtility = assetUtility; + _scriptUtility = scriptUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; + + #region Scripts + + var scripts = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.MonoScript).ToArray(); + var scriptTypes = _scriptUtility.GetTypesFromScriptAssets(scripts); + var affectedScripts = new Dictionary>(); + + foreach (var kvp in scriptTypes) + { + var badMethods = new List(); + foreach (var type in kvp.Value) + { + foreach (var method in type.GetMethods(bindingFlags)) + { + var attributes = method.GetCustomAttributes().ToList(); + if (attributes.Count == 0) + continue; + + var badAttributes = attributes.Where(x => !IsValidMenuItem(x.menuItem)).ToList(); + if (badAttributes.Count > 0) + badMethods.Add($"{string.Join("\n", badAttributes.Select(x => $"\'{x.menuItem}\'"))}\n(for method '{method.Name}')\n"); + } + } + + if (badMethods.Count > 0) + affectedScripts.Add(kvp.Key, badMethods); + } + + #endregion + + #region Precompiled Assemblies + + var assemblies = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.PrecompiledAssembly).ToArray(); + var assemblyTypes = _scriptUtility.GetTypesFromAssemblies(assemblies); + var affectedAssemblies = new Dictionary>(); + + foreach (var kvp in assemblyTypes) + { + var badMethods = new List(); + foreach (var type in kvp.Value) + { + foreach (var method in type.GetMethods(bindingFlags)) + { + var attributes = method.GetCustomAttributes().ToList(); + if (attributes.Count == 0) + continue; + + var badAttributes = attributes.Where(x => !IsValidMenuItem(x.menuItem)).ToList(); + if (badAttributes.Count > 0) + badMethods.Add($"{string.Join("\n", badAttributes.Select(x => (x as MenuItem).menuItem))}\n(Method '{method.Name}')\n"); + } + } + + if (badMethods.Count > 0) + affectedAssemblies.Add(kvp.Key, badMethods); + } + + #endregion + + if (affectedScripts.Count > 0 || affectedAssemblies.Count > 0) + { + if (affectedScripts.Count > 0) + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following scripts contain invalid MenuItem names:"); + foreach (var kvp in affectedScripts) + { + var message = string.Empty; + foreach (var type in kvp.Value) + message += type + "\n"; + + message = message.Remove(message.Length - "\n".Length); + result.AddMessage(message, null, kvp.Key); + } + } + + if (affectedAssemblies.Count > 0) + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following assemblies contain invalid MenuItem names:"); + foreach (var kvp in affectedAssemblies) + { + var message = string.Empty; + foreach (var type in kvp.Value) + message += type + "\n"; + + message = message.Remove(message.Length - "\n".Length); + result.AddMessage(message, null, kvp.Key); + } + } + } + else + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No MenuItems with invalid names were found!"); + } + + return result; + } + + private bool IsValidMenuItem(string menuItemName) + { + var acceptableMenuItems = new string[] + { + "File", + "Edit", + "Assets", + "GameObject", + "Component", + "Window", + "Help", + "CONTEXT", + "Tools" + }; + + menuItemName = menuItemName.Replace("\\", "/"); + if (acceptableMenuItems.Any(x => menuItemName.ToLower().StartsWith($"{x.ToLower()}/"))) + return true; + + return false; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckFileMenuNames.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckFileMenuNames.cs.meta new file mode 100644 index 00000000000..0e0872b9dc4 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckFileMenuNames.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d8e3b12ecc1fcd74d9a9f8d2b549fc63 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckLODs.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckLODs.cs new file mode 100644 index 00000000000..8b8975c5318 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckLODs.cs @@ -0,0 +1,79 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Data.MessageActions; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckLODs : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public CheckLODs(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var prefabs = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.Prefab); + var badPrefabs = new Dictionary>(); + + foreach (var p in prefabs) + { + var meshFilters = p.GetComponentsInChildren(true); + var badMeshFilters = new List(); + var lodGroups = p.GetComponentsInChildren(true); + + foreach (var mf in meshFilters) + { + if (mf.name.Contains("LOD") && !IsPartOfLodGroup(mf, lodGroups)) + badMeshFilters.Add(mf); + } + + if (badMeshFilters.Count > 0) + badPrefabs.Add(p, badMeshFilters); + } + + if (badPrefabs.Count == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("All found prefabs are meeting the LOD requirements!"); + return result; + } + + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following prefabs do not meet the LOD requirements"); + + foreach (var p in badPrefabs) + { + var resultList = new List(); + resultList.Add(p.Key); + resultList.AddRange(p.Value); + result.AddMessage($"{p.Key.name}.prefab", new OpenAssetAction(p.Key), resultList.ToArray()); + } + + return result; + } + + private bool IsPartOfLodGroup(MeshFilter mf, LODGroup[] lodGroups) + { + foreach (var lodGroup in lodGroups) + { + // If MeshFilter is a child/deep child of a LodGroup AND is referenced in this LOD group - it is valid + if (mf.transform.IsChildOf(lodGroup.transform) && + lodGroup.GetLODs().Any(lod => lod.renderers.Any(renderer => renderer != null && renderer.gameObject == mf.gameObject))) + return true; + } + + return false; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckLODs.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckLODs.cs.meta new file mode 100644 index 00000000000..060c427834c --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckLODs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 43b2158602f87704fa7b91561cfc8678 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckLineEndings.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckLineEndings.cs new file mode 100644 index 00000000000..adb381c7d2b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckLineEndings.cs @@ -0,0 +1,77 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using UnityEditor; +using UnityObject = UnityEngine.Object; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckLineEndings : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public CheckLineEndings(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var scripts = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.MonoScript); + + var affectedScripts = new ConcurrentBag(); + var scriptContents = new ConcurrentDictionary(); + + // A separate dictionary is needed because MonoScript contents cannot be accessed outside of the main thread + foreach (var s in scripts) + if (s != null) + scriptContents.TryAdd(s, s.text); + + Parallel.ForEach(scriptContents, (s) => + { + if (HasInconsistentLineEndings(s.Value)) + affectedScripts.Add(s.Key); + }); + + if (affectedScripts.Count > 0) + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following scripts have inconsistent line endings:", null, affectedScripts.ToArray()); + } + else + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No scripts with inconsistent line endings were found!"); + } + + return result; + } + + private bool HasInconsistentLineEndings(string text) + { + int crlfEndings = 0; + int lfEndings = 0; + + var split = text.Split(new[] { "\n" }, StringSplitOptions.None); + for (int i = 0; i < split.Length; i++) + { + var line = split[i]; + if (line.EndsWith("\r")) + crlfEndings++; + else if (i != split.Length - 1) + lfEndings++; + } + + if (crlfEndings > 0 && lfEndings > 0) + return true; + return false; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckLineEndings.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckLineEndings.cs.meta new file mode 100644 index 00000000000..fb521497969 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckLineEndings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 85885005d1c594f42826de3555e98365 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMeshPrefabs.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMeshPrefabs.cs new file mode 100644 index 00000000000..2c378033cc3 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMeshPrefabs.cs @@ -0,0 +1,106 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckMeshPrefabs : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + private IMeshUtilityService _meshUtility; + + public CheckMeshPrefabs(GenericTestConfig config, IAssetUtilityService assetUtility, IMeshUtilityService meshUtility) + { + _config = config; + _assetUtility = assetUtility; + _meshUtility = meshUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var usedModelPaths = new List(); + var prefabs = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.Prefab); + var missingMeshReferencePrefabs = new List(); + + // Get all meshes in existing prefabs and check if prefab has missing mesh references + foreach (var p in prefabs) + { + var meshes = _meshUtility.GetCustomMeshesInObject(p); + foreach (var mesh in meshes) + { + string meshPath = _assetUtility.ObjectToAssetPath(mesh); + usedModelPaths.Add(meshPath); + } + + if (HasMissingMeshReferences(p)) + missingMeshReferencePrefabs.Add(p); + } + + // Get all meshes in existing models + var allModelPaths = GetAllModelMeshPaths(_config.ValidationPaths); + + // Get the list of meshes without prefabs + List unusedModels = allModelPaths.Except(usedModelPaths).ToList(); + + if (unusedModels.Count == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("All found prefabs have meshes!"); + return result; + } + + result.Status = TestResultStatus.VariableSeverityIssue; + var models = unusedModels.Select(_assetUtility.AssetPathToObject).ToArray(); + result.AddMessage("The following models do not have associated prefabs", null, models); + + if (missingMeshReferencePrefabs.Count > 0) + result.AddMessage("The following prefabs have missing mesh references", null, missingMeshReferencePrefabs.ToArray()); + + return result; + } + + private IEnumerable GetAllModelMeshPaths(string[] validationPaths) + { + var models = _assetUtility.GetObjectsFromAssets(validationPaths, AssetType.Model); + var paths = new List(); + + foreach (var o in models) + { + var m = (GameObject)o; + var modelPath = _assetUtility.ObjectToAssetPath(m); + var assetImporter = _assetUtility.GetAssetImporter(modelPath); + if (assetImporter is UnityEditor.ModelImporter modelImporter) + { + var clips = modelImporter.clipAnimations.Count(); + var meshes = _meshUtility.GetCustomMeshesInObject(m); + + // Only add if the model has meshes and no clips + if (meshes.Any() && clips == 0) + paths.Add(modelPath); + } + } + + return paths; + } + + private bool HasMissingMeshReferences(GameObject go) + { + var meshes = go.GetComponentsInChildren(true); + var skinnedMeshes = go.GetComponentsInChildren(true); + + if (meshes.Length == 0 && skinnedMeshes.Length == 0) + return false; + + if (meshes.Any(x => x.sharedMesh == null) || skinnedMeshes.Any(x => x.sharedMesh == null)) + return true; + + return false; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMeshPrefabs.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMeshPrefabs.cs.meta new file mode 100644 index 00000000000..3419dd18f9b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMeshPrefabs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3c3d0d642ac6a6a48aa124a93dae3734 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMissingComponentsinAssets.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMissingComponentsinAssets.cs new file mode 100644 index 00000000000..089d95ec158 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMissingComponentsinAssets.cs @@ -0,0 +1,66 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckMissingComponentsinAssets : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public CheckMissingComponentsinAssets(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var assets = GetAllAssetsWithMissingComponents(_config.ValidationPaths); + + if (assets.Length == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No assets have missing components!"); + return result; + } + + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following assets contain missing components", null, assets); + + return result; + } + + private GameObject[] GetAllAssetsWithMissingComponents(string[] validationPaths) + { + var missingReferenceAssets = new List(); + var prefabObjects = _assetUtility.GetObjectsFromAssets(validationPaths, AssetType.Prefab); + + foreach (var p in prefabObjects) + { + if (p != null && IsMissingReference(p)) + missingReferenceAssets.Add(p); + } + + return missingReferenceAssets.ToArray(); + } + + private bool IsMissingReference(GameObject asset) + { + var components = asset.GetComponentsInChildren(); + + foreach (var c in components) + { + if (!c) + return true; + } + + return false; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMissingComponentsinAssets.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMissingComponentsinAssets.cs.meta new file mode 100644 index 00000000000..d7e72f3ecc8 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMissingComponentsinAssets.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 22d8f814e2363e34ea220736a4042728 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMissingComponentsinScenes.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMissingComponentsinScenes.cs new file mode 100644 index 00000000000..3c0ee061122 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMissingComponentsinScenes.cs @@ -0,0 +1,93 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Data.MessageActions; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using SceneAsset = UnityEditor.SceneAsset; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckMissingComponentsinScenes : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + private ISceneUtilityService _sceneUtility; + + public CheckMissingComponentsinScenes(GenericTestConfig config, IAssetUtilityService assetUtility, ISceneUtilityService sceneUtility) + { + _config = config; + _assetUtility = assetUtility; + _sceneUtility = sceneUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var originalScenePath = _sceneUtility.CurrentScenePath; + + var scenePaths = _assetUtility.GetAssetPathsFromAssets(_config.ValidationPaths, AssetType.Scene); + foreach (var scenePath in scenePaths) + { + var missingComponentGOs = GetMissingComponentGOsInScene(scenePath); + + if (missingComponentGOs.Count == 0) + continue; + + result.Status = TestResultStatus.VariableSeverityIssue; + var message = $"GameObjects with missing components or prefab references found in {scenePath}.\n\nClick this message to open the Scene and see the affected GameObjects:"; + result.AddMessage(message, new OpenAssetAction(_assetUtility.AssetPathToObject(scenePath)), missingComponentGOs.ToArray()); + } + + _sceneUtility.OpenScene(originalScenePath); + + if (result.Status == TestResultStatus.Undefined) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No missing components were found!"); + } + + return result; + } + + private List GetMissingComponentGOsInScene(string path) + { + var missingComponentGOs = new List(); + + var scene = _sceneUtility.OpenScene(path); + + if (!scene.IsValid()) + { + Debug.LogWarning("Unable to get Scene in " + path); + return new List(); + } + + var rootObjects = scene.GetRootGameObjects(); + + foreach (var obj in rootObjects) + { + missingComponentGOs.AddRange(GetMissingComponentGOs(obj)); + } + + return missingComponentGOs; + } + + private List GetMissingComponentGOs(GameObject root) + { + var missingComponentGOs = new List(); + var rootComponents = root.GetComponents(); + + if (UnityEditor.PrefabUtility.GetPrefabInstanceStatus(root) == UnityEditor.PrefabInstanceStatus.MissingAsset || rootComponents.Any(c => !c)) + { + missingComponentGOs.Add(root); + } + + foreach (Transform child in root.transform) + missingComponentGOs.AddRange(GetMissingComponentGOs(child.gameObject)); + + return missingComponentGOs; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMissingComponentsinScenes.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMissingComponentsinScenes.cs.meta new file mode 100644 index 00000000000..3ff34085ab4 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckMissingComponentsinScenes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 511e76d0ebcb23d40a7b49dda0e2980f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelImportLogs.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelImportLogs.cs new file mode 100644 index 00000000000..b628aac8c20 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelImportLogs.cs @@ -0,0 +1,64 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using System.Linq; +using UnityObject = UnityEngine.Object; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckModelImportLogs : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + private IModelUtilityService _modelUtility; + + public CheckModelImportLogs(GenericTestConfig config, IAssetUtilityService assetUtility, IModelUtilityService modelUtility) + { + _config = config; + _assetUtility = assetUtility; + _modelUtility = modelUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var models = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.Model); + var importLogs = _modelUtility.GetImportLogs(models.ToArray()); + + var warningModels = new List(); + var errorModels = new List(); + + foreach (var kvp in importLogs) + { + if (kvp.Value.Any(x => x.Severity == UnityEngine.LogType.Error)) + errorModels.Add(kvp.Key); + if (kvp.Value.Any(x => x.Severity == UnityEngine.LogType.Warning)) + warningModels.Add(kvp.Key); + } + + if (warningModels.Count > 0 || errorModels.Count > 0) + { + if (warningModels.Count > 0) + { + result.Status = TestResultStatus.Warning; + result.AddMessage("The following models contain import warnings:", null, warningModels.ToArray()); + } + + if (errorModels.Count > 0) + { + result.Status = TestResultStatus.Warning; + result.AddMessage("The following models contain import errors:", null, errorModels.ToArray()); + } + } + else + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No issues were detected when importing your models!"); + } + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelImportLogs.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelImportLogs.cs.meta new file mode 100644 index 00000000000..884b6cb1109 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelImportLogs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 98f3ec209166855408eaf4abe5bff591 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelOrientation.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelOrientation.cs new file mode 100644 index 00000000000..82e9dd08efd --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelOrientation.cs @@ -0,0 +1,71 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckModelOrientation : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + private IMeshUtilityService _meshUtility; + + public CheckModelOrientation(GenericTestConfig config, IAssetUtilityService assetUtility, IMeshUtilityService meshUtility) + { + _config = config; + _assetUtility = assetUtility; + _meshUtility = meshUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var models = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.Model); + var badModels = new List(); + + foreach (var m in models) + { + var meshes = _meshUtility.GetCustomMeshesInObject(m); + var assetImporter = _assetUtility.GetAssetImporter(m); + + if (!(assetImporter is UnityEditor.ModelImporter modelImporter)) + continue; + + var clips = modelImporter.clipAnimations.Length; + + // Only check if the model has meshes and no clips + if (!meshes.Any() || clips != 0) + continue; + + Transform[] transforms = m.GetComponentsInChildren(true); + + foreach (var t in transforms) + { + var hasMeshComponent = t.TryGetComponent(out _) || t.TryGetComponent(out _); + + if (t.localRotation == Quaternion.identity || !hasMeshComponent) + continue; + + badModels.Add(m); + break; + } + } + + if (badModels.Count == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("All found models are facing the right way!"); + return result; + } + + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following models have incorrect rotation", null, badModels.ToArray()); + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelOrientation.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelOrientation.cs.meta new file mode 100644 index 00000000000..7e8243e8352 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelOrientation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 56cdcdc41a80fbc46b5b2b83ec8d66d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelTypes.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelTypes.cs new file mode 100644 index 00000000000..a434a0cba4e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelTypes.cs @@ -0,0 +1,58 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityObject = UnityEngine.Object; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckModelTypes : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public CheckModelTypes(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var allowedExtensions = new string[] { ".fbx", ".dae", ".abc", ".obj" }; + // Should retrieve All assets and not models here since ADB will not recognize certain + // types if appropriate software is not installed (e.g. .blend without Blender) + var allAssetPaths = _assetUtility.GetAssetPathsFromAssets(_config.ValidationPaths, AssetType.All); + var badModels = new List(); + + foreach (var assetPath in allAssetPaths) + { + var importer = _assetUtility.GetAssetImporter(assetPath); + if (importer == null || !(importer is ModelImporter)) + continue; + + if (allowedExtensions.Any(x => importer.assetPath.ToLower().EndsWith(x))) + continue; + + badModels.Add(_assetUtility.AssetPathToObject(assetPath)); + } + + if (badModels.Count == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("All models are of allowed formats!"); + } + else + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following models are of formats that should not be used for Asset Store packages:", null, badModels.ToArray()); + } + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelTypes.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelTypes.cs.meta new file mode 100644 index 00000000000..6f45fa79039 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckModelTypes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 428b1fb838e6f5a469bbfd26ca3fbfd2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckNormalMapTextures.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckNormalMapTextures.cs new file mode 100644 index 00000000000..d9113511b06 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckNormalMapTextures.cs @@ -0,0 +1,94 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckNormalMapTextures : ITestScript + { + public const int TextureCacheLimit = 8; + + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public CheckNormalMapTextures(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var materials = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.Material); + var badTextures = new List(); + var badPaths = new List(); + + foreach (var mat in materials) + { + for (int i = 0; i < mat.shader.GetPropertyCount(); i++) + { + if ((mat.shader.GetPropertyFlags(i) & UnityEngine.Rendering.ShaderPropertyFlags.Normal) != 0) + { + var propertyName = mat.shader.GetPropertyName(i); + var assignedTexture = mat.GetTexture(propertyName); + + if (assignedTexture == null) + continue; + + var texturePath = _assetUtility.ObjectToAssetPath(assignedTexture); + var textureImporter = _assetUtility.GetAssetImporter(texturePath) as TextureImporter; + if (textureImporter == null) + continue; + + if (textureImporter.textureType != TextureImporterType.NormalMap && !badTextures.Contains(assignedTexture)) + { + if (badTextures.Count < TextureCacheLimit) + { + badTextures.Add(assignedTexture); + } + else + { + string path = AssetDatabase.GetAssetPath(assignedTexture); + badPaths.Add(path); + } + } + } + } + + EditorUtility.UnloadUnusedAssetsImmediate(); + } + + if (badTextures.Count == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("All normal map textures have the correct texture type!"); + } + else if (badPaths.Count != 0) + { + foreach (Texture texture in badTextures) + { + string path = AssetDatabase.GetAssetPath(texture); + badPaths.Add(path); + } + + string paths = string.Join("\n", badPaths); + + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following textures are not set to type 'Normal Map'", null); + result.AddMessage(paths); + } + else + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following textures are not set to type 'Normal Map'", null, badTextures.ToArray()); + } + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckNormalMapTextures.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckNormalMapTextures.cs.meta new file mode 100644 index 00000000000..f9967022281 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckNormalMapTextures.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d55cea510248f814eb2194c2b53f88d2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPackageNaming.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPackageNaming.cs new file mode 100644 index 00000000000..5b9bf466dbf --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPackageNaming.cs @@ -0,0 +1,295 @@ +using AssetStoreTools.Utility; +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckPackageNaming : ITestScript + { + private const string ForbiddenCharacters = "~`!@#$%^&*()-_=+[{]}\\|;:'\",<>?/"; + private readonly string[] PathsToCheckForForbiddenCharacters = new string[] + { + "Assets/", + "Assets/Editor/", + "Assets/Plugins/", + "Assets/Resources/", + "Assets/StreamingAssets/", + "Assets/WebGLTemplates/" + }; + + private class PathCheckResult + { + public Object[] InvalidMainPaths; + public Object[] InvalidMainPathContentPaths; + public Object[] InvalidMainPathLeadingUpPaths; + public Object[] InvalidHybridPackages; + public Object[] PotentiallyInvalidContent; + + public bool HasIssues => InvalidMainPaths.Length > 0 + || InvalidMainPathContentPaths.Length > 0 + || InvalidMainPathLeadingUpPaths.Length > 0 + || InvalidHybridPackages.Length > 0 + || PotentiallyInvalidContent.Length > 0; + } + + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + // Constructor also accepts dependency injection of registered IValidatorService types + public CheckPackageNaming(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var checkResult = GetInvalidPathsInAssets(); + + if (checkResult.HasIssues) + { + result.Status = TestResultStatus.Warning; + + if (checkResult.InvalidMainPaths.Length > 0) + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following assets appear to be artificially bumped up in the project hierarchy within commonly used folders", null, checkResult.InvalidMainPaths); + } + + if (checkResult.InvalidMainPathContentPaths.Length > 0) + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following assets appear to be artificially bumped up in the project hierarchy within commonly used folders", null, checkResult.InvalidMainPathContentPaths); + } + + if (checkResult.InvalidMainPathLeadingUpPaths.Length > 0) + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("Despite not being directly validated, this path would be automatically created by the Unity Importer when importing your package", null, checkResult.InvalidMainPathLeadingUpPaths); + } + + if (checkResult.InvalidHybridPackages.Length > 0) + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following packages appear to be artificially bumped up in the Package hierarchy with their 'Display Name' configuration", null, checkResult.InvalidHybridPackages); + } + + if (checkResult.PotentiallyInvalidContent.Length > 0) + { + // Do not override previously set severities + result.AddMessage("It is recommended that nested package content refrains from starting with a special character", null, checkResult.PotentiallyInvalidContent); + } + } + else + { + result.Status = TestResultStatus.Pass; + result.AddMessage("All package asset names are valid!"); + } + + return result; + } + + private PathCheckResult GetInvalidPathsInAssets() + { + var allInvalidMainPaths = new List(); + var allInvalidMainContentPaths = new List(); + var allInvalidMainLeadingUpPaths = new List(); + var allInvalidPackagePaths = new List(); + var allInvalidOtherContentPaths = new List(); + + foreach (var validationPath in _config.ValidationPaths) + { + // Is path itself not forbidden e.g.: when validating Assets/_Package, the folder _Package would be invalid + if (!IsDirectMainPathValid(validationPath)) + allInvalidMainPaths.Add(validationPath); + + // Are path contents not forbidden e.g.: when validating Assets/, the folder _Package would be invalid + if (!IsDirectMainPathContentValid(validationPath, out var invalidContentPaths)) + allInvalidMainContentPaths.AddRange(invalidContentPaths); + + // Is the path leading up to this path not forbidden e.g: when validating Assets/_WorkDir/Package, the folder _Workdir would be invalid + if (!IsPathLeadingUpToMainPathValid(validationPath, out var invalidLeadingUpPath)) + allInvalidMainLeadingUpPaths.Add(invalidLeadingUpPath); + + // Is the path pointing to a package valid, e.g.: when validating Packages/com.company.product its display name _Product would be invalid + if (!IsHybridPackageMainPathValid(validationPath, out string invalidPackagePath)) + allInvalidPackagePaths.Add(invalidPackagePath); + } + + var ignoredPaths = new List(); + ignoredPaths.AddRange(allInvalidMainPaths); + ignoredPaths.AddRange(allInvalidMainContentPaths); + ignoredPaths.AddRange(allInvalidMainLeadingUpPaths); + ignoredPaths.AddRange(allInvalidPackagePaths); + + // Mark any other paths that start with a forbidden character + if (!ArePackageContentsValid(ignoredPaths, out var invalidContents)) + allInvalidOtherContentPaths.AddRange(invalidContents); + + return new PathCheckResult() + { + InvalidMainPaths = PathsToObjects(allInvalidMainPaths), + InvalidMainPathContentPaths = PathsToObjects(allInvalidMainContentPaths), + InvalidMainPathLeadingUpPaths = PathsToObjects(allInvalidMainLeadingUpPaths), + InvalidHybridPackages = PathsToObjects(allInvalidPackagePaths), + PotentiallyInvalidContent = PathsToObjects(allInvalidOtherContentPaths) + }; + } + + private bool IsDirectMainPathValid(string validationPath) + { + foreach (var forbiddenPath in PathsToCheckForForbiddenCharacters) + { + var forbiddenPathWithSeparator = forbiddenPath.EndsWith("/") ? forbiddenPath : forbiddenPath + "/"; + if (!validationPath.StartsWith(forbiddenPathWithSeparator)) + continue; + + var truncatedPath = validationPath.Remove(0, forbiddenPathWithSeparator.Length); + var truncatedPathSplit = truncatedPath.Split('/'); + + // It is not a direct main path if it has deeper paths + if (truncatedPathSplit.Length != 1) + continue; + + if (ForbiddenCharacters.Any(x => truncatedPath.StartsWith(x.ToString()))) + return false; + } + + return true; + } + + private bool IsDirectMainPathContentValid(string validationPath, out List invalidContentPaths) + { + invalidContentPaths = new List(); + + var contents = Directory.EnumerateFileSystemEntries(validationPath, "*", SearchOption.AllDirectories) + .Where(x => !x.EndsWith(".meta")) + .Select(GetAdbPath); + + foreach (var contentPath in contents) + { + foreach (var forbiddenPath in PathsToCheckForForbiddenCharacters) + { + var forbiddenPathWithSeparator = forbiddenPath.EndsWith("/") ? forbiddenPath : forbiddenPath + "/"; + if (!contentPath.StartsWith(forbiddenPathWithSeparator)) + continue; + + var truncatedPath = contentPath.Remove(0, forbiddenPathWithSeparator.Length); + var truncatedPathSplit = truncatedPath.Split('/'); + + // Only check the first level of content relative to the forbidden path + if (truncatedPathSplit.Length > 1) + continue; + + if (ForbiddenCharacters.Any(x => truncatedPathSplit[0].StartsWith(x.ToString()))) + invalidContentPaths.Add(contentPath); + } + } + + return invalidContentPaths.Count == 0; + } + + private bool IsPathLeadingUpToMainPathValid(string validationPath, out string invalidLeadingUpPath) + { + invalidLeadingUpPath = string.Empty; + + foreach (var forbiddenPath in PathsToCheckForForbiddenCharacters) + { + var forbiddenPathWithSeparator = forbiddenPath.EndsWith("/") ? forbiddenPath : forbiddenPath + "/"; + if (!validationPath.StartsWith(forbiddenPathWithSeparator)) + continue; + + var truncatedPath = validationPath.Remove(0, forbiddenPathWithSeparator.Length); + var truncatedPathSplit = truncatedPath.Split('/'); + + // It is not a leading up path if it has no deeper path + if (truncatedPathSplit.Length == 1) + continue; + + if (ForbiddenCharacters.Any(x => truncatedPathSplit[0].StartsWith(x.ToString()))) + { + invalidLeadingUpPath = forbiddenPathWithSeparator + truncatedPathSplit[0]; + return false; + } + } + + return true; + } + + private bool IsHybridPackageMainPathValid(string validationPath, out string invalidPackagePath) + { + invalidPackagePath = string.Empty; + + if (!PackageUtility.GetPackageByManifestPath($"{validationPath}/package.json", out var package)) + return true; + + var packageName = package.displayName; + if (ForbiddenCharacters.Any(x => packageName.StartsWith(x.ToString()))) + { + invalidPackagePath = validationPath; + return false; + } + + return true; + } + + private bool ArePackageContentsValid(IEnumerable ignoredPaths, out List invalidContentPaths) + { + invalidContentPaths = new List(); + + foreach (var validationPath in _config.ValidationPaths) + { + var validationPathFolderName = validationPath.Split('/').Last(); + if (!ignoredPaths.Contains(validationPath) && ForbiddenCharacters.Any(x => validationPathFolderName.StartsWith(x.ToString()))) + invalidContentPaths.Add(validationPath); + + var contents = Directory.EnumerateFileSystemEntries(validationPath, "*", SearchOption.AllDirectories) + .Where(x => !x.EndsWith(".meta")) + .Select(GetAdbPath); + + foreach (var contentEntry in contents) + { + if (ignoredPaths.Contains(contentEntry)) + continue; + + var entryName = contentEntry.Split('/').Last(); + if (ForbiddenCharacters.Any(x => entryName.StartsWith(x.ToString()))) + invalidContentPaths.Add(contentEntry); + } + } + + return invalidContentPaths.Count == 0; + } + + private string GetAdbPath(string path) + { + path = path.Replace("\\", "/"); + if (path.StartsWith(Constants.RootProjectPath)) + path = path.Remove(Constants.RootProjectPath.Length); + + return path; + } + + private Object[] PathsToObjects(IEnumerable paths) + { + var objects = new List(); + + foreach (var path in paths) + { + var obj = _assetUtility.AssetPathToObject(path); + if (obj != null) + objects.Add(obj); + } + + return objects.ToArray(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPackageNaming.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPackageNaming.cs.meta new file mode 100644 index 00000000000..bf1c234b637 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPackageNaming.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: afe9e04825c7d904981a54404b222290 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckParticleSystems.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckParticleSystems.cs new file mode 100644 index 00000000000..8e69b105a6a --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckParticleSystems.cs @@ -0,0 +1,76 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Data.MessageActions; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckParticleSystems : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + private ISceneUtilityService _sceneUtility; + + public CheckParticleSystems(GenericTestConfig config, IAssetUtilityService assetUtility, ISceneUtilityService sceneUtility) + { + _config = config; + _assetUtility = assetUtility; + _sceneUtility = sceneUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var originalScenePath = _sceneUtility.CurrentScenePath; + + var scenePaths = _assetUtility.GetAssetPathsFromAssets(_config.ValidationPaths, AssetType.Scene); + + foreach (var path in scenePaths) + { + var badParticleSystems = new List(); + + var scene = _sceneUtility.OpenScene(path); + + if (!scene.IsValid()) + { + Debug.LogWarning("Unable to get Scene in " + path); + continue; + } + +#if UNITY_2023_1_OR_NEWER + var particleSystems = GameObject.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); +#else + var particleSystems = GameObject.FindObjectsOfType(); +#endif + + foreach (var ps in particleSystems) + { + if (PrefabUtility.IsPartOfAnyPrefab(ps.gameObject)) + continue; + badParticleSystems.Add(ps); + } + + if (badParticleSystems.Count == 0) + continue; + + result.Status = TestResultStatus.VariableSeverityIssue; + var message = $"Particle Systems not belonging to any Prefab were found in {path}.\n\nClick this message to open the Scene and see the affected Particle Systems:"; + result.AddMessage(message, new OpenAssetAction(AssetDatabase.LoadAssetAtPath(path)), badParticleSystems.ToArray()); + } + + _sceneUtility.OpenScene(originalScenePath); + + if (result.Status == TestResultStatus.Undefined) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No Particle Systems without Prefabs were found!"); + } + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckParticleSystems.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckParticleSystems.cs.meta new file mode 100644 index 00000000000..66a1881d0ec --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckParticleSystems.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6a623f7988c75884bb17b169ccd3e993 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPathLengths.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPathLengths.cs new file mode 100644 index 00000000000..27750c5334d --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPathLengths.cs @@ -0,0 +1,98 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using AssetStoreTools.Validator.Utility; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityObject = UnityEngine.Object; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckPathLengths : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public CheckPathLengths(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + TestResult result = new TestResult(); + + int pathLengthLimit = 140; + // Get all project paths and sort by length so that folders always come before files + var allPaths = ValidatorUtility.GetProjectPaths(_config.ValidationPaths).OrderBy(x => x.Length); + + var filteredDirs = new Dictionary(); + var filteredFiles = new Dictionary(); + + foreach (var path in allPaths) + { + // Truncated path examples: + // Assets/[Scenes/SampleScene.unity] + // Packages/com.example.package/[Editor/EditorScript.cs] + var truncatedPath = path; + if (path.StartsWith("Assets/")) + truncatedPath = path.Remove(0, "Assets/".Length); + else if (path.StartsWith("Packages/")) + { + var splitPath = path.Split('/'); + truncatedPath = string.Join("/", splitPath.Skip(2)); + } + + // Skip paths under the character limit + if (truncatedPath.Length < pathLengthLimit) + continue; + + // Skip children of already added directories + if (filteredDirs.Keys.Any(x => truncatedPath.StartsWith(x))) + continue; + + if (AssetDatabase.IsValidFolder(path)) + { + filteredDirs.Add(truncatedPath, _assetUtility.AssetPathToObject(path)); + continue; + } + + if (!filteredFiles.ContainsKey(truncatedPath)) + filteredFiles.Add(truncatedPath, _assetUtility.AssetPathToObject(path)); + } + + if (filteredDirs.Count == 0 && filteredFiles.Count == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("All package content matches the path limit criteria!"); + return result; + } + + if (filteredDirs.Count > 0) + { + var maxDirLength = filteredDirs.Keys.Aggregate("", (max, cur) => max.Length > cur.Length ? max : cur); + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage($"The following folders exceed the path length limit:"); + foreach (var kvp in filteredDirs) + { + result.AddMessage($"Path length: {kvp.Key.Length} characters", null, kvp.Value); + } + } + + if (filteredFiles.Count > 0) + { + var maxFileLength = filteredFiles.Keys.Aggregate("", (max, cur) => max.Length > cur.Length ? max : cur); + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage($"The following files exceed the path length limit:"); + foreach (var kvp in filteredFiles) + { + result.AddMessage($"Path length: {kvp.Key.Length} characters", null, kvp.Value); + } + } + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPathLengths.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPathLengths.cs.meta new file mode 100644 index 00000000000..ff8f8ed771b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPathLengths.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ae379305e9165e84584373a8272c09e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPrefabTransforms.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPrefabTransforms.cs new file mode 100644 index 00000000000..aa018814f3d --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPrefabTransforms.cs @@ -0,0 +1,71 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckPrefabTransforms : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + private IMeshUtilityService _meshUtility; + + public CheckPrefabTransforms(GenericTestConfig config, IAssetUtilityService assetUtility, IMeshUtilityService meshUtility) + { + _config = config; + _assetUtility = assetUtility; + _meshUtility = meshUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var prefabs = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.Prefab); + var badPrefabs = new List(); + var badPrefabsLowOffset = new List(); + + foreach (var p in prefabs) + { + var hasRectTransform = p.TryGetComponent(out RectTransform _); + if (hasRectTransform || !_meshUtility.GetCustomMeshesInObject(p).Any()) + continue; + + var positionString = p.transform.position.ToString("F12"); + var rotationString = p.transform.rotation.eulerAngles.ToString("F12"); + var localScaleString = p.transform.localScale.ToString("F12"); + + var vectorZeroString = Vector3.zero.ToString("F12"); + var vectorOneString = Vector3.one.ToString("F12"); + + if (positionString != vectorZeroString || rotationString != vectorZeroString || localScaleString != vectorOneString) + { + if (p.transform.position == Vector3.zero && p.transform.rotation.eulerAngles == Vector3.zero && p.transform.localScale == Vector3.one) + badPrefabsLowOffset.Add(p); + else + badPrefabs.Add(p); + } + } + + if (badPrefabs.Count == 0 && badPrefabsLowOffset.Count == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("All found prefabs were reset!"); + return result; + } + + result.Status = TestResultStatus.VariableSeverityIssue; + if (badPrefabs.Count > 0) + result.AddMessage("The following prefabs' transforms do not fit the requirements", null, badPrefabs.ToArray()); + if (badPrefabsLowOffset.Count > 0) + result.AddMessage("The following prefabs have unusually low transform values, which might not be accurately displayed " + + "in the Inspector window. Please use the 'Debug' Inspector mode to review the Transform component of these prefabs " + + "or reset the Transform components using the right-click context menu", null, badPrefabsLowOffset.ToArray()); + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPrefabTransforms.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPrefabTransforms.cs.meta new file mode 100644 index 00000000000..60db2691bb6 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckPrefabTransforms.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f712c17a60bf2d049a4e61c8f79e56c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckScriptCompilation.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckScriptCompilation.cs new file mode 100644 index 00000000000..034f4571d05 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckScriptCompilation.cs @@ -0,0 +1,30 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.TestDefinitions; +using UnityEditor; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckScriptCompilation : ITestScript + { + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var hasCompilationErrors = EditorUtility.scriptCompilationFailed; + + if (hasCompilationErrors) + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("One or more scripts in the project failed to compile.\n" + + "Please check the Console window to see the list of errors"); + } + else + { + result.Status = TestResultStatus.Pass; + result.AddMessage("All scripts in the project compiled successfully!"); + } + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckScriptCompilation.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckScriptCompilation.cs.meta new file mode 100644 index 00000000000..47d2849ce3e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckScriptCompilation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 59db88f43969db8499299bce7f4fb967 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckShaderCompilation.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckShaderCompilation.cs new file mode 100644 index 00000000000..a004febd9bd --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckShaderCompilation.cs @@ -0,0 +1,63 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Linq; +using UnityEditor; +using UnityEngine; +#if !UNITY_2023_1_OR_NEWER +using UnityEngine.Experimental.Rendering; +#endif +#if UNITY_2023_1_OR_NEWER +using UnityEngine.Rendering; +#endif + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckShaderCompilation : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public CheckShaderCompilation(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var shaders = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.Shader); + var badShaders = shaders.Where(ShaderHasError).ToArray(); + + if (badShaders.Length > 0) + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following shader files have errors", null, badShaders); + } + else + { + result.Status = TestResultStatus.Pass; + result.AddMessage("All found Shaders have no compilation errors!"); + } + + return result; + } + + private bool ShaderHasError(Object obj) + { + switch (obj) + { + case Shader shader: + return ShaderUtil.ShaderHasError(shader); + case ComputeShader shader: + return ShaderUtil.GetComputeShaderMessageCount(shader) > 0; + case RayTracingShader shader: + return ShaderUtil.GetRayTracingShaderMessageCount(shader) > 0; + default: + return false; + } + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckShaderCompilation.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckShaderCompilation.cs.meta new file mode 100644 index 00000000000..43f501a7e03 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckShaderCompilation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7abb278a6082bde4391e0779394cb85b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckTextureDimensions.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckTextureDimensions.cs new file mode 100644 index 00000000000..d05c1aede14 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckTextureDimensions.cs @@ -0,0 +1,57 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckTextureDimensions : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public CheckTextureDimensions(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var textures = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.Texture); + var badTextures = new List(); + + foreach (var texture in textures) + { + if (Mathf.IsPowerOfTwo(texture.width) && Mathf.IsPowerOfTwo(texture.height)) + continue; + + var importer = _assetUtility.GetAssetImporter(_assetUtility.ObjectToAssetPath(texture)); + + if (importer == null || !(importer is TextureImporter textureImporter) + || textureImporter.textureType == TextureImporterType.Sprite + || textureImporter.textureType == TextureImporterType.GUI) + continue; + + badTextures.Add(texture); + } + + if (badTextures.Count == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("All texture dimensions are a power of 2!"); + } + else + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following texture dimensions are not a power of 2:", null, badTextures.ToArray()); + } + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckTextureDimensions.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckTextureDimensions.cs.meta new file mode 100644 index 00000000000..bc0d1cfb06e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckTextureDimensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 073f1dacf3da34d4783140ae9d485d5f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckTypeNamespaces.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckTypeNamespaces.cs new file mode 100644 index 00000000000..35c9d4d1058 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckTypeNamespaces.cs @@ -0,0 +1,233 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityObject = UnityEngine.Object; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckTypeNamespaces : ITestScript + { + private readonly string[] ForbiddenNamespaces = new string[] { "Unity" }; + + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + private IScriptUtilityService _scriptUtility; + + private enum NamespaceEligibility + { + NoNamespace, + Ok, + Forbidden + } + + private class AnalysisResult + { + public Dictionary> TypesWithoutNamespaces; + public Dictionary> ForbiddenNamespaces; + + public bool HasIssues => TypesWithoutNamespaces.Count > 0 + || ForbiddenNamespaces.Count > 0; + } + + public CheckTypeNamespaces(GenericTestConfig config, IAssetUtilityService assetUtility, IScriptUtilityService scriptUtility) + { + _config = config; + _assetUtility = assetUtility; + _scriptUtility = scriptUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var scriptResult = CheckScripts(); + var assemblyResult = CheckAssemblies(); + + if (scriptResult.HasIssues || assemblyResult.HasIssues) + { + result.Status = TestResultStatus.Warning; + + // Error conditions for forbidden namespaces + + if (scriptResult.ForbiddenNamespaces.Count > 0) + { + result.Status = TestResultStatus.Fail; + result.AddMessage("The following scripts contain namespaces starting with a 'Unity' keyword:"); + AddJoinedMessage(result, scriptResult.ForbiddenNamespaces); + } + + if (assemblyResult.ForbiddenNamespaces.Count > 0) + { + result.Status = TestResultStatus.Fail; + result.AddMessage("The following assemblies contain namespaces starting with a 'Unity' keyword:"); + AddJoinedMessage(result, assemblyResult.ForbiddenNamespaces); + } + + // Variable severity conditions for no-namespace types + + if (scriptResult.TypesWithoutNamespaces.Count > 0) + { + if (result.Status != TestResultStatus.Fail) + result.Status = TestResultStatus.VariableSeverityIssue; + + result.AddMessage("The following scripts contain types not nested under a namespace:"); + AddJoinedMessage(result, scriptResult.TypesWithoutNamespaces); + } + + if (assemblyResult.TypesWithoutNamespaces.Count > 0) + { + if (result.Status != TestResultStatus.Fail) + result.Status = TestResultStatus.VariableSeverityIssue; + + result.AddMessage("The following assemblies contain types not nested under a namespace:"); + AddJoinedMessage(result, assemblyResult.TypesWithoutNamespaces); + } + } + else + { + result.Status = TestResultStatus.Pass; + result.AddMessage("All scripts contain valid namespaces!"); + } + + return result; + } + + private AnalysisResult CheckScripts() + { + var scripts = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.MonoScript).ToArray(); + var scriptNamespaces = _scriptUtility.GetTypeNamespacesFromScriptAssets(scripts); + + var scriptsWithoutNamespaces = new Dictionary>(); + var scriptsWithForbiddenNamespaces = new Dictionary>(); + + foreach (var kvp in scriptNamespaces) + { + var scriptAsset = kvp.Key; + var typesInScriptAsset = kvp.Value; + + var typesWithoutNamespace = new List(); + var discouragedNamespaces = new List(); + var forbiddenNamespaces = new List(); + + foreach (var t in typesInScriptAsset) + { + var eligibility = CheckNamespaceEligibility(t.Namespace); + + switch (eligibility) + { + case NamespaceEligibility.NoNamespace: + typesWithoutNamespace.Add(t.Name); + break; + case NamespaceEligibility.Forbidden: + if (!forbiddenNamespaces.Contains(t.Namespace)) + forbiddenNamespaces.Add(t.Namespace); + break; + case NamespaceEligibility.Ok: + break; + } + } + + if (typesWithoutNamespace.Count > 0) + scriptsWithoutNamespaces.Add(scriptAsset, typesWithoutNamespace); + + if (forbiddenNamespaces.Count > 0) + scriptsWithForbiddenNamespaces.Add(scriptAsset, forbiddenNamespaces); + } + + return new AnalysisResult + { + TypesWithoutNamespaces = scriptsWithoutNamespaces, + ForbiddenNamespaces = scriptsWithForbiddenNamespaces + }; + } + + private AnalysisResult CheckAssemblies() + { + var assemblies = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.PrecompiledAssembly).ToList(); + var assemblyTypes = _scriptUtility.GetTypesFromAssemblies(assemblies); + + var assembliesWithoutNamespaces = new Dictionary>(); + var assembliesWithForbiddenNamespaces = new Dictionary>(); + + foreach (var kvp in assemblyTypes) + { + var assemblyAsset = kvp.Key; + var typesInAssembly = kvp.Value; + + var typesWithoutNamespace = new List(); + var discouragedNamespaces = new List(); + var forbiddenNamespaces = new List(); + + foreach (var t in typesInAssembly) + { + var eligibility = CheckNamespaceEligibility(t.Namespace); + + switch (eligibility) + { + case NamespaceEligibility.NoNamespace: + typesWithoutNamespace.Add($"{GetTypeName(t)} {t.Name}"); + break; + case NamespaceEligibility.Forbidden: + if (!forbiddenNamespaces.Contains(t.Namespace)) + forbiddenNamespaces.Add(t.Namespace); + break; + case NamespaceEligibility.Ok: + break; + } + } + + if (typesWithoutNamespace.Count > 0) + assembliesWithoutNamespaces.Add(assemblyAsset, typesWithoutNamespace); + + if (forbiddenNamespaces.Count > 0) + assembliesWithForbiddenNamespaces.Add(assemblyAsset, forbiddenNamespaces); + } + + return new AnalysisResult + { + TypesWithoutNamespaces = assembliesWithoutNamespaces, + ForbiddenNamespaces = assembliesWithForbiddenNamespaces + }; + } + + private NamespaceEligibility CheckNamespaceEligibility(string fullNamespace) + { + if (string.IsNullOrEmpty(fullNamespace)) + return NamespaceEligibility.NoNamespace; + + var split = fullNamespace.Split('.'); + var topLevelNamespace = split[0]; + if (ForbiddenNamespaces.Any(x => topLevelNamespace.StartsWith(x, StringComparison.OrdinalIgnoreCase))) + return NamespaceEligibility.Forbidden; + + return NamespaceEligibility.Ok; + } + + private string GetTypeName(Type type) + { + if (type.IsClass) + return "class"; + if (type.IsInterface) + return "interface"; + if (type.IsEnum) + return "enum"; + if (type.IsValueType) + return "struct"; + + throw new ArgumentException($"Received an unrecognizable type {type}. Type must be either a class, interface, struct or enum"); + } + + private void AddJoinedMessage(TestResult result, Dictionary> assetsWithMessages) + { + foreach (var kvp in assetsWithMessages) + { + var message = string.Join("\n", kvp.Value); + result.AddMessage(message, null, kvp.Key); + } + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckTypeNamespaces.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckTypeNamespaces.cs.meta new file mode 100644 index 00000000000..e18155ac5cd --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/CheckTypeNamespaces.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 279249fa7ef8c2446b3a9f013eeedbf0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveExecutableFiles.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveExecutableFiles.cs new file mode 100644 index 00000000000..71f31bcaede --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveExecutableFiles.cs @@ -0,0 +1,38 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Linq; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class RemoveExecutableFiles : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public RemoveExecutableFiles(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var executables = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.Executable).ToArray(); + + if (executables.Length == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No executable files were found!"); + return result; + } + + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following executable files were found", null, executables); + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveExecutableFiles.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveExecutableFiles.cs.meta new file mode 100644 index 00000000000..04899bc8ebd --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveExecutableFiles.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8e4450592cc60e54286ad089b66db94d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveJPGFiles.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveJPGFiles.cs new file mode 100644 index 00000000000..3a5a4c7c419 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveJPGFiles.cs @@ -0,0 +1,38 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Linq; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class RemoveJPGFiles : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public RemoveJPGFiles(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var jpgs = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.JPG).ToArray(); + + if (jpgs.Length == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No JPG/JPEG textures were found!"); + return result; + } + + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following textures are compressed as JPG/JPEG", null, jpgs); + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveJPGFiles.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveJPGFiles.cs.meta new file mode 100644 index 00000000000..bc2151398cb --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveJPGFiles.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5634a12b3a8544c4585bbc280ae59ce2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveJavaScriptFiles.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveJavaScriptFiles.cs new file mode 100644 index 00000000000..69a7681e735 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveJavaScriptFiles.cs @@ -0,0 +1,38 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Linq; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class RemoveJavaScriptFiles : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public RemoveJavaScriptFiles(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var javascriptObjects = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.JavaScript).ToArray(); + + if (javascriptObjects.Length == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No UnityScript / JS files were found!"); + return result; + } + + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following assets are UnityScript / JS files", null, javascriptObjects); + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveJavaScriptFiles.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveJavaScriptFiles.cs.meta new file mode 100644 index 00000000000..791abf859c2 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveJavaScriptFiles.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ab1676bde9afba442b35fd3319c18063 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveLossyAudioFiles.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveLossyAudioFiles.cs new file mode 100644 index 00000000000..6cdc77abc22 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveLossyAudioFiles.cs @@ -0,0 +1,83 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using UnityObject = UnityEngine.Object; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class RemoveLossyAudioFiles : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public RemoveLossyAudioFiles(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + string SanitizeForComparison(UnityObject o) + { + Regex alphanumericRegex = new Regex("[^a-zA-Z0-9]"); + string path = _assetUtility.ObjectToAssetPath(o); + path = path.ToLower(); + + int extensionIndex = path.LastIndexOf('.'); + string extension = path.Substring(extensionIndex + 1); + string sanitized = path.Substring(0, extensionIndex); + + int separatorIndex = sanitized.LastIndexOf('/'); + sanitized = sanitized.Substring(separatorIndex); + sanitized = alphanumericRegex.Replace(sanitized, String.Empty); + sanitized = sanitized.Replace(extension, String.Empty); + sanitized = sanitized.Trim(); + + return sanitized; + } + + var lossyAudioObjects = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.LossyAudio).ToArray(); + if (lossyAudioObjects.Length == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No lossy audio files were found!"); + return result; + } + + // Try to find and match variants + var nonLossyAudioObjects = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.NonLossyAudio); + HashSet nonLossyPathSet = new HashSet(); + foreach (var asset in nonLossyAudioObjects) + { + var path = SanitizeForComparison(asset); + nonLossyPathSet.Add(path); + } + + var unmatchedAssets = new List(); + foreach (var asset in lossyAudioObjects) + { + var path = SanitizeForComparison(asset); + if (!nonLossyPathSet.Contains(path)) + unmatchedAssets.Add(asset); + } + + if (unmatchedAssets.Count == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No lossy audio files were found!"); + return result; + } + + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following lossy audio files were found without identically named non-lossy variants:", null, unmatchedAssets.ToArray()); + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveLossyAudioFiles.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveLossyAudioFiles.cs.meta new file mode 100644 index 00000000000..729af7fa4c2 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveLossyAudioFiles.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b7205a85061273a4eb50586f13f35d35 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveMixamoFiles.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveMixamoFiles.cs new file mode 100644 index 00000000000..17a1cb2653a --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveMixamoFiles.cs @@ -0,0 +1,38 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Linq; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class RemoveMixamoFiles : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public RemoveMixamoFiles(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var mixamoFiles = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.Mixamo).ToArray(); + + if (mixamoFiles.Length == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No Mixamo files were found!"); + return result; + } + + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following Mixamo files were found", null, mixamoFiles); + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveMixamoFiles.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveMixamoFiles.cs.meta new file mode 100644 index 00000000000..7a1e4f8e521 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveMixamoFiles.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9df432e52aa958b44bb5e20c13d16552 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveSpeedTreeFiles.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveSpeedTreeFiles.cs new file mode 100644 index 00000000000..feef230638f --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveSpeedTreeFiles.cs @@ -0,0 +1,38 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Linq; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class RemoveSpeedTreeFiles : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public RemoveSpeedTreeFiles(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var speedtreeObjects = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.SpeedTree).ToArray(); + + if (speedtreeObjects.Length == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No SpeedTree assets have been found!"); + return result; + } + + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following SpeedTree assets have been found", null, speedtreeObjects); + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveSpeedTreeFiles.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveSpeedTreeFiles.cs.meta new file mode 100644 index 00000000000..855dbe22e67 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveSpeedTreeFiles.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e06bb7e0aa4f9944abc18281c002dff4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveVideoFiles.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveVideoFiles.cs new file mode 100644 index 00000000000..ae0e5ff688e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveVideoFiles.cs @@ -0,0 +1,38 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Linq; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class RemoveVideoFiles : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public RemoveVideoFiles(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var videos = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.Video).ToArray(); + + if (videos.Length == 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No video files were found, looking good!"); + return result; + } + + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("The following video files were found", null, videos); + + return result; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveVideoFiles.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveVideoFiles.cs.meta new file mode 100644 index 00000000000..98aea88ed5e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/Generic/RemoveVideoFiles.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f99724c71b0de66419b5d6e8e9bfcc2d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage.meta new file mode 100644 index 00000000000..b8fde811b74 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 016d62b2cd8346a49815615efd1d6e39 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckDemoScenes.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckDemoScenes.cs new file mode 100644 index 00000000000..6c635c2cd9a --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckDemoScenes.cs @@ -0,0 +1,172 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityObject = UnityEngine.Object; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckDemoScenes : ITestScript + { + private class DemoSceneScanResult + { + public List ValidAdbScenes; + public List HybridScenePaths; + public List NestedUnityPackages; + } + + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + private ISceneUtilityService _sceneUtility; + + public CheckDemoScenes(GenericTestConfig config, IAssetUtilityService assetUtility, ISceneUtilityService sceneUtility) + { + _config = config; + _assetUtility = assetUtility; + _sceneUtility = sceneUtility; + } + + public TestResult Run() + { + var result = new TestResult(); + var demoSceneScanResult = CheckForDemoScenes(_config); + + // Valid demo scenes were found in ADB + if (demoSceneScanResult.ValidAdbScenes.Count > 0) + { + result.Status = TestResultStatus.Pass; + result.AddMessage("Demo scenes found", null, demoSceneScanResult.ValidAdbScenes.ToArray()); + return result; + } + + // Valid demo scenes found in UPM package.json + if (demoSceneScanResult.HybridScenePaths.Count > 0) + { + result.Status = TestResultStatus.Pass; + + var upmSampleSceneList = string.Join("\n-", demoSceneScanResult.HybridScenePaths); + upmSampleSceneList = upmSampleSceneList.Insert(0, "-"); + + result.AddMessage($"Demo scenes found:\n{upmSampleSceneList}"); + return result; + } + + // No valid scenes found, but package contains nested .unitypackages + if (demoSceneScanResult.NestedUnityPackages.Count > 0) + { + result.Status = TestResultStatus.Warning; + result.AddMessage("Could not find any valid Demo scenes in the selected validation paths."); + result.AddMessage("The following nested .unitypackage files were found. " + + "If they contain any demo scenes, you can ignore this warning.", null, demoSceneScanResult.NestedUnityPackages.ToArray()); + return result; + } + + // No valid scenes were found and there is nothing pointing to their inclusion in the package + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("Could not find any valid Demo Scenes in the selected validation paths."); + return result; + } + + private DemoSceneScanResult CheckForDemoScenes(GenericTestConfig config) + { + var scanResult = new DemoSceneScanResult(); + scanResult.ValidAdbScenes = CheckForDemoScenesInAssetDatabase(config); + scanResult.HybridScenePaths = CheckForDemoScenesInUpmSamples(config); + scanResult.NestedUnityPackages = CheckForNestedUnityPackages(config); + + return scanResult; + } + + private List CheckForDemoScenesInAssetDatabase(GenericTestConfig config) + { + var scenePaths = _assetUtility.GetAssetPathsFromAssets(config.ValidationPaths, AssetType.Scene).ToArray(); + if (scenePaths.Length == 0) + return new List(); + + var originalScenePath = _sceneUtility.CurrentScenePath; + var validScenePaths = scenePaths.Where(CanBeDemoScene).ToArray(); + _sceneUtility.OpenScene(originalScenePath); + + if (validScenePaths.Length == 0) + return new List(); + + return validScenePaths.Select(x => AssetDatabase.LoadAssetAtPath(x)).ToList(); + } + + private bool CanBeDemoScene(string scenePath) + { + // Check skybox + var sceneSkyboxPath = _assetUtility.ObjectToAssetPath(RenderSettings.skybox).Replace("\\", "").Replace("/", ""); + var defaultSkyboxPath = "Resources/unity_builtin_extra".Replace("\\", "").Replace("/", ""); + + if (!sceneSkyboxPath.Equals(defaultSkyboxPath, StringComparison.OrdinalIgnoreCase)) + return true; + + // Check GameObjects + _sceneUtility.OpenScene(scenePath); + var rootObjects = _sceneUtility.GetRootGameObjects(); + var count = rootObjects.Length; + + if (count == 0) + return false; + + if (count != 2) + return true; + + var cameraGOUnchanged = rootObjects.Any(o => o.TryGetComponent(out _) && o.GetComponents(typeof(Component)).Length == 3); + var lightGOUnchanged = rootObjects.Any(o => o.TryGetComponent(out _) && o.GetComponents(typeof(Component)).Length == 2); + + return !cameraGOUnchanged || !lightGOUnchanged; + } + + private List CheckForDemoScenesInUpmSamples(GenericTestConfig config) + { + var scenePaths = new List(); + + foreach (var path in config.ValidationPaths) + { + if (!File.Exists($"{path}/package.json")) + continue; + + var packageJsonText = File.ReadAllText($"{path}/package.json"); + var json = JObject.Parse(packageJsonText); + + if (!json.ContainsKey("samples") || json["samples"].Type != JTokenType.Array || json["samples"].ToList().Count == 0) + continue; + + foreach (var sample in json["samples"].ToList()) + { + var samplePath = sample["path"].ToString(); + samplePath = $"{path}/{samplePath}"; + if (!Directory.Exists(samplePath)) + continue; + + var sampleScenePaths = Directory.GetFiles(samplePath, "*.unity", SearchOption.AllDirectories); + foreach (var scenePath in sampleScenePaths) + { + // If meta file is not found, the sample will not be included with the exported .unitypackage + if (!File.Exists($"{scenePath}.meta")) + continue; + + if (!scenePaths.Contains(scenePath.Replace("\\", "/"))) + scenePaths.Add(scenePath.Replace("\\", "/")); + } + } + } + + return scenePaths; + } + + private List CheckForNestedUnityPackages(GenericTestConfig config) + { + var unityPackages = _assetUtility.GetObjectsFromAssets(config.ValidationPaths, AssetType.UnityPackage).ToArray(); + return unityPackages.ToList(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckDemoScenes.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckDemoScenes.cs.meta new file mode 100644 index 00000000000..2707290f829 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckDemoScenes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f844c2dfa4669ff4eacf5591b544edaf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckDocumentation.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckDocumentation.cs new file mode 100644 index 00000000000..1799c1d0e2e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckDocumentation.cs @@ -0,0 +1,73 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System; +using System.IO; +using System.Linq; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckDocumentation : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + public CheckDocumentation(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var textFilePaths = _assetUtility.GetAssetPathsFromAssets(_config.ValidationPaths, AssetType.Documentation).ToArray(); + var documentationFilePaths = textFilePaths.Where(CouldBeDocumentation).ToArray(); + + if (textFilePaths.Length == 0) + { + result.Status = TestResultStatus.VariableSeverityIssue; + result.AddMessage("No potential documentation files ('.txt', '.pdf', " + + "'.html', '.rtf', '.md') found within the given path."); + } + else if (documentationFilePaths.Length == 0) + { + result.Status = TestResultStatus.Warning; + var textFileObjects = textFilePaths.Select(_assetUtility.AssetPathToObject).ToArray(); + result.AddMessage("The following files have been found to match the documentation file format," + + " but may not be documentation in content", + null, textFileObjects); + } + else + { + result.Status = TestResultStatus.Pass; + var documentationFileObjects = documentationFilePaths.Select(_assetUtility.AssetPathToObject).ToArray(); + result.AddMessage("Found documentation files", null, documentationFileObjects); + } + + return result; + } + + private bool CouldBeDocumentation(string filePath) + { + if (filePath.EndsWith(".pdf")) + return true; + + using (var fs = File.Open(filePath, FileMode.Open)) + using (var bs = new BufferedStream(fs)) + using (var sr = new StreamReader(bs)) + { + string line; + while ((line = sr.ReadLine()) != null) + { + var mentionsDocumentation = line.IndexOf("documentation", StringComparison.OrdinalIgnoreCase) >= 0; + if (mentionsDocumentation) + return true; + } + } + + return false; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckDocumentation.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckDocumentation.cs.meta new file mode 100644 index 00000000000..f8cea26f908 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckDocumentation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3c8425198983eda4c9b35aa0d59ea33c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckPackageSize.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckPackageSize.cs new file mode 100644 index 00000000000..caa8ac60fe4 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckPackageSize.cs @@ -0,0 +1,69 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.TestDefinitions; +using System.IO; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckPackageSize : ITestScript + { + private GenericTestConfig _config; + + public CheckPackageSize(GenericTestConfig config) + { + _config = config; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var packageSize = CalculatePackageSize(_config.ValidationPaths); + float packageSizeInGB = packageSize / (1024f * 1024f * 1024f); + float maxPackageSizeInGB = Constants.Uploader.MaxPackageSizeBytes / (1024f * 1024f * 1024f); + + if (packageSizeInGB - maxPackageSizeInGB >= 0.1f) + { + result.Status = TestResultStatus.Warning; + + result.AddMessage($"The uncompressed size of your package ({packageSizeInGB:0.#} GB) exceeds the maximum allowed package size of {maxPackageSizeInGB:0.#} GB. " + + $"Please make sure that the compressed .unitypackage size does not exceed the size limit."); + } + else + { + result.Status = TestResultStatus.Pass; + result.AddMessage("Your package does not exceed the maximum allowed package size!"); + } + + return result; + } + + private long CalculatePackageSize(string[] assetPaths) + { + long totalSize = 0; + + foreach (var path in assetPaths) + { + totalSize += CalculatePathSize(path); + } + + return totalSize; + } + + private long CalculatePathSize(string path) + { + long size = 0; + + var dirInfo = new DirectoryInfo(path); + if (!dirInfo.Exists) + return size; + + foreach (var file in dirInfo.EnumerateFiles()) + size += file.Length; + + foreach (var nestedDir in dirInfo.EnumerateDirectories()) + size += CalculatePathSize(nestedDir.FullName); + + return size; + } + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckPackageSize.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckPackageSize.cs.meta new file mode 100644 index 00000000000..1909c93f634 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckPackageSize.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a8601b99f4afa5049954f3a2dd5996d6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckProjectTemplateAssets.cs b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckProjectTemplateAssets.cs new file mode 100644 index 00000000000..eab7aee5533 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckProjectTemplateAssets.cs @@ -0,0 +1,217 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services.Validation; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Validator.TestMethods +{ + internal class CheckProjectTemplateAssets : ITestScript + { + private GenericTestConfig _config; + private IAssetUtilityService _assetUtility; + + // Constructor also accepts dependency injection of registered IValidatorService types + public CheckProjectTemplateAssets(GenericTestConfig config, IAssetUtilityService assetUtility) + { + _config = config; + _assetUtility = assetUtility; + } + + public TestResult Run() + { + var result = new TestResult() { Status = TestResultStatus.Undefined }; + + var assets = _assetUtility.GetObjectsFromAssets(_config.ValidationPaths, AssetType.All); + var invalidAssetsByGuid = CheckGuids(assets); + var invalidAssetsByPath = CheckPaths(assets); + + var hasIssues = invalidAssetsByGuid.Length > 0 + || invalidAssetsByPath.Length > 0; + + if (hasIssues) + { + result.Status = TestResultStatus.VariableSeverityIssue; + + if (invalidAssetsByPath.Length > 0) + { + result.AddMessage("The following assets were found to have an asset path which is common to project template asset paths. They should be renamed or moved:", null, invalidAssetsByPath); + } + + if (invalidAssetsByGuid.Length > 0) + { + result.AddMessage("The following assets were found to be using a GUID which is common to project template asset GUIDs. They should be assigned a new GUID:", null, invalidAssetsByGuid); + } + } + else + { + result.Status = TestResultStatus.Pass; + result.AddMessage("No common assets that might cause asset clashing were found!"); + } + + return result; + } + + private Object[] CheckGuids(IEnumerable assets) + { + var clashingAssets = new List(); + foreach (var asset in assets) + { + if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out var guid, out long _)) + continue; + + if (CommonTemplateAssets.Any(x => x.Key.Equals(guid, System.StringComparison.OrdinalIgnoreCase))) + clashingAssets.Add(asset); + } + + return clashingAssets.ToArray(); + } + + private Object[] CheckPaths(IEnumerable assets) + { + var clashingAssets = new List(); + foreach (var asset in assets) + { + var assetPath = AssetDatabase.GetAssetPath(asset); + if (CommonTemplateAssets.Any(x => x.Value.Equals(assetPath, System.StringComparison.OrdinalIgnoreCase))) + clashingAssets.Add(asset); + } + + return clashingAssets.ToArray(); + } + + private Dictionary CommonTemplateAssets = new Dictionary() + { + {"3f9215ea0144899419cfbc0957140d3f", "Assets/DefaultVolumeProfile.asset"}, + {"3d4c13846a3e9bd4c8ccfbd0657ed847", "Assets/DefaultVolumeProfile.asset"}, + {"4cee8bca36f2ab74b8feb832747fa6f4", "Assets/Editor/com.unity.mobile.notifications/NotificationSettings.asset"}, + {"45a04f37e0f48c744acc0874c4a8918a", "Assets/Editor/com.unity.mobile.notifications/NotificationSettings.asset"}, + {"54a3a0570aebe8949bec4966f1376581", "Assets/HDRPDefaultResources/DefaultHDRISky.exr"}, + {"e93c35b24eb03c74284e7dc0b755bfcc", "Assets/HDRPDefaultResources/DefaultHDRPAsset.asset"}, + {"254320a857a30444da2c99496a186368", "Assets/HDRPDefaultResources/DefaultLookDevProfile.asset"}, + {"2bfa7b9d63fa79e4abdc033f54a868d2", "Assets/HDRPDefaultResources/DefaultSceneRoot.prefab"}, + {"f9e3ff5a1b8f49c4fa8686e68d2dadae", "Assets/HDRPDefaultResources/DefaultSceneRoot.prefab"}, + {"d87f7d7815073e840834a16a518c1237", "Assets/HDRPDefaultResources/DefaultSettingsVolumeProfile.asset"}, + {"145290c901d58b343bdeb3b4362c9ff2", "Assets/HDRPDefaultResources/DefaultVFXResources.asset"}, + {"acc11144f57719542b5fa25f02e74afb", "Assets/HDRPDefaultResources/HDRenderPipelineGlobalSettings.asset"}, + {"582adbd84082fdb4faf7cd4beb1ccd14", "Assets/HDRPDefaultResources/HDRPDefaultSettings.asset"}, + {"2801c2ff7303a7543a8727f862f6c236", "Assets/HDRPDefaultResources/Sky and Fog Settings Profile.asset"}, + {"ea5c25297f0c0a04da0eabb1c26a7509", "Assets/HDRPDefaultResources/SkyFogSettingsProfile.asset"}, + {"3590b91b4603b465dbb4216d601bff33", "Assets/InputSystem_Actions.inputactions"}, + {"289c1b55c9541489481df5cc06664110", "Assets/InputSystem_Actions.inputactions"}, + {"dc70d2c4f369241dd99afd7c451b813e", "Assets/InputSystem_Actions.inputactions"}, + {"2bcd2660ca9b64942af0de543d8d7100", "Assets/InputSystem_Actions.inputactions"}, + {"052faaac586de48259a63d0c4782560b", "Assets/InputSystem_Actions.inputactions"}, + {"35845fe01580c41289b024647b1d1c53", "Assets/InputSystem_Actions.inputactions"}, + {"8124e5870f4fd4c779e7a5f994e84ad1", "Assets/OutdoorsScene.unity"}, + {"2dd802e4d37c65149922028d3e973832", "Assets/Presets/AudioCompressedInMemory.preset"}, + {"e18fd6ecd9cdb524ca99844f39b9d9ac", "Assets/Presets/AudioCompressedInMemory.preset"}, + {"86bcce7f5575b54408aa0f3a7d321039", "Assets/Presets/AudioStreaming.preset"}, + {"460e573eb8466884baaa0b8475505f83", "Assets/Presets/AudioStreaming.preset"}, + {"e8537455c6c08bd4e8bf0be3707da685", "Assets/Presets/Defaults/AlbedoTexture_Default.preset"}, + {"7a99f8aa944efe94cb9bd74562b7d5f9", "Assets/Presets/Defaults/AlbedoTexture_Default.preset"}, + {"0cd792cc87e492d43b4e95b205fc5cc6", "Assets/Presets/Defaults/AudioDecompressOnLoad_Default.preset"}, + {"e7689051185d12f4298e1ebb2693a29f", "Assets/Presets/Defaults/AudioDecompressOnLoad.preset"}, + {"463065d4f17d1d94d848aa127b94dd43", "Assets/Presets/Defaults/DirectionalLight_Default.preset"}, + {"c1cf8506f04ef2c4a88b64b6c4202eea", "Assets/Presets/Defaults/DirectionalLight_Default.preset"}, + {"8fa3055e2a1363246838debd20206d37", "Assets/Presets/Defaults/SSSSettings_Default.preset"}, + {"78830bb1431cab940b74be615e2a739f", "Assets/Presets/HDRTexture.preset"}, + {"14a57cf3b9fa1c74b884aa7e0dcf1faa", "Assets/Presets/NormalTexture.preset"}, + {"1d826a4c23450f946b19c20560595a1f", "Assets/Presets/NormalTexture.preset"}, + {"45f7b2e3c78185248b3adbb14429c2ab", "Assets/Presets/UtilityTexture.preset"}, + {"78fae3569c6c66c46afc3d9d4fb0b8d4", "Assets/Presets/UtilityTexture.preset"}, + {"9303d565bd8aa6948ba775e843320e4d", "Assets/Presets/UtilityTexture.preset"}, + {"34f54ff1ff9415249a847506b6f2fec5", "Assets/Scenes/PrefabEditingScene.unity"}, + {"cbfe36cfddfde964d9dfce63a355d5dd", "Assets/Scenes/samplescene.unity"}, + {"2cda990e2423bbf4892e6590ba056729", "Assets/Scenes/SampleScene.unity"}, + {"9fc0d4010bbf28b4594072e72b8655ab", "Assets/Scenes/SampleScene.unity"}, + {"3db1837cc97a95e4c98610966fac2b0b", "Assets/Scenes/SampleScene.unity"}, + {"3fc8acdd13e6c734bafef6554d6fdbcd", "Assets/Scenes/SampleScene.unity"}, + {"8c9cfa26abfee488c85f1582747f6a02", "Assets/Scenes/SampleScene.unity"}, + {"c850ee8c3b14cc8459e7e186857cf567", "Assets/Scenes/SampleScene.unity"}, + {"99c9720ab356a0642a771bea13969a05", "Assets/Scenes/SampleScene.unity"}, + {"d1c3109bdb54ad54c8a2b2838528e640", "Assets/Scenes/SampleScene.unity"}, + {"477cc4148fad3449482a3bc3178594e2", "Assets/Scenes/SampleSceneLightingSettings.lighting"}, + {"4eb578550bc4f824e97f0a72eac1f3a5", "Assets/Scripts/LookWithMouse.cs"}, + {"87f6dfceb3e39a947a312f7eeaa2a113", "Assets/Scripts/PlayerMovement.cs"}, + {"be76e5f14cfee674cb30b491fb72b09b", "Assets/Scripts/SimpleCameraController.cs"}, + {"6547d18b2bc62c94aa5ec1e87434da4e", "Assets/Scripts/SimpleCameraController.cs"}, + {"e8a636f62116c0a40bbfefdf876d4608", "Assets/Scripts/SimpleCameraController.cs"}, + {"14e519c409be4a1428028347410f5677", "Assets/Scripts/SimpleCameraController.cs"}, + {"a04c28107d77d5e42b7155783b8475b6", "Assets/Settings/Cockpit_Renderer.asset"}, + {"ab09877e2e707104187f6f83e2f62510", "Assets/Settings/DefaultVolumeProfile.asset"}, + {"238cd62f6b58cb04e9c94749c4a015a7", "Assets/Settings/DefaultVolumeProfile.asset"}, + {"5a9a2dc462c7bde4f86d0615a19c2c72", "Assets/Settings/DiffusionProfiles/BambooLeaves.asset"}, + {"78322c7f82657514ebe48203160e3f39", "Assets/Settings/Foliage.asset"}, + {"3e2e6bfc59709614ab90c0cd7d755e48", "Assets/Settings/HDRP Balanced.asset"}, + {"36dd385e759c96147b6463dcd1149c11", "Assets/Settings/HDRP High Fidelity.asset"}, + {"168a2336534e4e043b2a210b6f8d379a", "Assets/Settings/HDRP Performant.asset"}, + {"4594f4a3fb14247e192bcca6dc23c8ed", "Assets/Settings/HDRPDefaultResources/DefaultLookDevProfile.asset"}, + {"14b392ee213d25a48b1feddbd9f5a9be", "Assets/Settings/HDRPDefaultResources/DefaultSettingsVolumeProfile.asset"}, + {"879ffae44eefa4412bb327928f1a96dd", "Assets/Settings/HDRPDefaultResources/FoliageDiffusionProfile.asset"}, + {"b9f3086da92434da0bc1518f19f0ce86", "Assets/Settings/HDRPDefaultResources/HDRenderPipelineAsset.asset"}, + {"ac0316ca287ba459492b669ff1317a6f", "Assets/Settings/HDRPDefaultResources/HDRenderPipelineGlobalSettings.asset"}, + {"48e911a1e337b44e2b85dbc65b47a594", "Assets/Settings/HDRPDefaultResources/SkinDiffusionProfile.asset"}, + {"d03ed43fc9d8a4f2e9fa70c1c7916eb9", "Assets/Settings/Lit2DSceneTemplate.scenetemplate"}, + {"65bc7dbf4170f435aa868c779acfb082", "Assets/Settings/Mobile_Renderer.asset"}, + {"5e6cbd92db86f4b18aec3ed561671858", "Assets/Settings/Mobile_RPAsset.asset"}, + {"23cccccf13c3d4170a9b21e52a9bc86b", "Assets/Settings/Mobile/Mobile_High_ScreenRenderer.asset"}, + {"6e8f76111115f44e0a76c2bff3cec258", "Assets/Settings/Mobile/Mobile_Low_Renderer.asset"}, + {"aed30aee6a3ceae4090dadd1934d2ad0", "Assets/Settings/Mobile/Mobile_Low_ScreenRenderer.asset"}, + {"d7686b11d09df481bac3c76ecc5ea626", "Assets/Settings/Mobile/Mobile_Low.asset"}, + {"f288ae1f4751b564a96ac7587541f7a2", "Assets/Settings/PC_Renderer.asset"}, + {"4b83569d67af61e458304325a23e5dfd", "Assets/Settings/PC_RPAsset.asset"}, + {"42b230d443c6d6c4b89c47f97db59121", "Assets/Settings/PC/PC_High_ScreenRenderer.asset"}, + {"13ba41cd2fa191f43890b271bd110ed9", "Assets/Settings/PC/PC_Low_Renderer.asset"}, + {"a73f6fa069dd14a42b40cbb01bae63b4", "Assets/Settings/PC/PC_Low_ScreenRenderer.asset"}, + {"4eb9ff6b5314098428cfa0be7e36ccda", "Assets/Settings/PC/PC_Low.asset"}, + {"573ac53c334415945bf239de2c2f0511", "Assets/Settings/PlayerControllerFPS.prefab"}, + {"7ba2b06fb32e5274aad88925a5b8d3f5", "Assets/Settings/PostProcessVolumeProfile.asset"}, + {"424799608f7334c24bf367e4bbfa7f9a", "Assets/Settings/Renderer2D.asset"}, + {"183cbd347d25080429f42b520742bbd8", "Assets/Settings/SampleScenePostProcessingSettings.asset"}, + {"10fc4df2da32a41aaa32d77bc913491c", "Assets/Settings/SampleSceneProfile.asset"}, + {"a6560a915ef98420e9faacc1c7438823", "Assets/Settings/SampleSceneProfile.asset"}, + {"a123fc0ac58cb774e8592c925f167e7c", "Assets/Settings/SampleSceneSkyandFogSettings.asset"}, + {"26bdddf49760c61438938733f07fa2a2", "Assets/Settings/Skin.asset"}, + {"8ba92e2dd7f884a0f88b98fa2d235fe7", "Assets/Settings/SkyandFogSettingsProfile.asset"}, + {"4a8e21d5c33334b11b34a596161b9360", "Assets/Settings/UniversalRenderer.asset"}, + {"18dc0cd2c080841dea60987a38ce93fa", "Assets/Settings/UniversalRenderPipelineGlobalSettings.asset"}, + {"bdede76083021864d8ff8bf23b2f37f1", "Assets/Settings/UniversalRenderPipelineGlobalSettings.asset"}, + {"19ba41d7c0026c3459d37c2fe90c55a0", "Assets/Settings/UniversalRP-HighQuality.asset"}, + {"a31e9f9f9c9d4b9429ed0d1234e22103", "Assets/Settings/UniversalRP-LowQuality.asset"}, + {"d847b876476d3d6468f5dfcd34266f96", "Assets/Settings/UniversalRP-MediumQuality.asset"}, + {"681886c5eb7344803b6206f758bf0b1c", "Assets/Settings/UniversalRP.asset"}, + {"e634585d5c4544dd297acaee93dc2beb", "Assets/Settings/URP-Balanced-Renderer.asset"}, + {"e1260c1148f6143b28bae5ace5e9c5d1", "Assets/Settings/URP-Balanced.asset"}, + {"c40be3174f62c4acf8c1216858c64956", "Assets/Settings/URP-HighFidelity-Renderer.asset"}, + {"7b7fd9122c28c4d15b667c7040e3b3fd", "Assets/Settings/URP-HighFidelity.asset"}, + {"707360a9c581a4bd7aa53bfeb1429f71", "Assets/Settings/URP-Performant-Renderer.asset"}, + {"d0e2fc18fe036412f8223b3b3d9ad574", "Assets/Settings/URP-Performant.asset"}, + {"b62413aeefabaaa41a4b5a71dd7ae1ac", "Assets/Settings/VolumeProfiles/CinematicProfile.asset"}, + {"ac0c2cad5778d4544b6a690963e02fe3", "Assets/Settings/VolumeProfiles/DefaultVolumeProfile.asset"}, + {"f2d4d916a6612574cad220d125febbf2", "Assets/Settings/VolumeProfiles/LowQualityVolumeProfile.asset"}, + {"cef078630d63d0442a070f84d4f13735", "Assets/Settings/VolumeProfiles/MarketProfile.asset"}, + {"7ede9c9f109e5c442b7d29e54b4996fc", "Assets/Settings/VolumeProfiles/MediaOverrides.asset"}, + {"3532e98caae428047bcefe69a344f72c", "Assets/Settings/VolumeProfiles/OutlineEnabled.asset"}, + {"bfc08ba7e35de1a44bb84a32f1a693e1", "Assets/Settings/VolumeProfiles/ZenGardenProfile.asset"}, + {"59a34a3881431c246b3564a0f0ca5bb0", "Assets/Settings/Volumes/CinematicPhysicalCamera.asset"}, + {"03bc34b71695890468eb021c73b228db", "Assets/Settings/Volumes/ScreenshotsTimelineProfile.asset"}, + {"7f342610b85f4164f808a1f380dcc668", "Assets/Settings/Volumes/VolumeGlobal.asset"}, + {"bd6d234073408c44ca3828113aac655e", "Assets/Settings/Volumes/VolumeRoom1.asset"}, + {"d78a1b031ab26034eb6ec3cbc9fbcec3", "Assets/Settings/Volumes/VolumeRoom2.asset"}, + {"5727d3e07f75c3744b6cc8a1e55850a9", "Assets/Settings/Volumes/VolumeRoom2Skylight.asset"}, + {"06114ad16a0bc0a41957375ac3bf472e", "Assets/Settings/Volumes/VolumeRoom3.asset"}, + {"1584bf21cf81d5147aa00e8a2deaf2fb", "Assets/Settings/Volumes/VolumeRoom3Corridor.asset"}, + {"7bca3a07cdd522c4c8020832c20b3eae", "Assets/Settings/Volumes/VolumeRoom3Sitting.asset"}, + {"2872d90954412244a8b4a477b939c3ca", "Assets/Settings/XR/Loaders/Mock_HMD_Loader.asset"}, + {"f25758a0f79593d4a9b3ee30a17b4c2e", "Assets/Settings/XR/Loaders/Oculus_Loader.asset"}, + {"9e9f2958d1b4b4642ace1d0c7770650b", "Assets/Settings/XR/Settings/Mock_HMD_Build_Settings.asset"}, + {"290a6e6411d135049940bec2237b8938", "Assets/Settings/XR/Settings/Oculus_Settings.asset"}, + {"4c1640683c539c14080cfd43fbeffbda", "Assets/Settings/XR/XRGeneralSettings.asset"}, + {"93b439a37f63240aca3dd4e01d978a9f", "Assets/UniversalRenderPipelineGlobalSettings.asset"}, + {"38b35347542e5af4c9b140950c5b18db", "Assets/UniversalRenderPipelineGlobalSettings.asset"} + }; + } +} diff --git a/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckProjectTemplateAssets.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckProjectTemplateAssets.cs.meta new file mode 100644 index 00000000000..804f84b0512 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Test Methods/UnityPackage/CheckProjectTemplateAssets.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f02d52a702c712e4e8089f7c2e65bae7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI.meta b/.github/Asset Store Tools/Validator/Scripts/UI.meta new file mode 100644 index 00000000000..f401e9e8612 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0eed33a351c3c544ba6bf3cd29d24c26 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Data.meta new file mode 100644 index 00000000000..c3a170c9d1d --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 461bfd99d0923cd4a8dae2f440af1064 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions.meta new file mode 100644 index 00000000000..93d41554984 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d7d9c6cc805e072429392e7a378d2c9c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorResults.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorResults.cs new file mode 100644 index 00000000000..676b70cf420 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorResults.cs @@ -0,0 +1,15 @@ +using AssetStoreTools.Validator.Data; +using System; +using System.Collections.Generic; + +namespace AssetStoreTools.Validator.UI.Data +{ + internal interface IValidatorResults + { + event Action OnResultsChanged; + event Action OnRequireSerialize; + + void LoadResult(ValidationResult result); + IEnumerable GetSortedTestGroups(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorResults.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorResults.cs.meta new file mode 100644 index 00000000000..c914281e4e1 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorResults.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 91c62333b36d5ef47989289e8f90c056 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorSettings.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorSettings.cs new file mode 100644 index 00000000000..d439b0ddbbd --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorSettings.cs @@ -0,0 +1,31 @@ +using AssetStoreTools.Validator.Data; +using System; +using System.Collections.Generic; + +namespace AssetStoreTools.Validator.UI.Data +{ + internal interface IValidatorSettings + { + event Action OnCategoryChanged; + event Action OnValidationTypeChanged; + event Action OnValidationPathsChanged; + event Action OnRequireSerialize; + + void LoadSettings(ValidationSettings settings); + + string GetActiveCategory(); + void SetActiveCategory(string category); + List GetAvailableCategories(); + + ValidationType GetValidationType(); + void SetValidationType(ValidationType validationType); + + List GetValidationPaths(); + void AddValidationPath(string path); + void RemoveValidationPath(string path); + void ClearValidationPaths(); + bool IsValidationPathValid(string path, out string error); + + IValidator CreateValidator(); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorSettings.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorSettings.cs.meta new file mode 100644 index 00000000000..d9074404c6d --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cc6516196465ac6469258ef8950da607 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorTest.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorTest.cs new file mode 100644 index 00000000000..1dcafcec35b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorTest.cs @@ -0,0 +1,15 @@ +using AssetStoreTools.Validator.Data; + +namespace AssetStoreTools.Validator.UI.Data +{ + internal interface IValidatorTest + { + int Id { get; } + string Name { get; } + string Description { get; } + ValidationType ValidationType { get; } + TestResult Result { get; } + + void SetResult(TestResult result); + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorTest.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorTest.cs.meta new file mode 100644 index 00000000000..f1cbf2506cd --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d1e4d9ba8de8cfc4aa42786fbbc5037a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorTestGroup.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorTestGroup.cs new file mode 100644 index 00000000000..81b8d8f114a --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorTestGroup.cs @@ -0,0 +1,12 @@ +using AssetStoreTools.Validator.Data; +using System.Collections.Generic; + +namespace AssetStoreTools.Validator.UI.Data +{ + internal interface IValidatorTestGroup + { + string Name { get; } + TestResultStatus Status { get; } + IEnumerable Tests { get; } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorTestGroup.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorTestGroup.cs.meta new file mode 100644 index 00000000000..86c6b561ea6 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Abstractions/IValidatorTestGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fa8735b7eb65d3147ab8bdbf922f36cf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization.meta new file mode 100644 index 00000000000..4122afefc6a --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ec536685238584f41bd268edaaf0ad7d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateData.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateData.cs new file mode 100644 index 00000000000..4e75ed61f15 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateData.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace AssetStoreTools.Validator.UI.Data.Serialization +{ + internal class ValidatorStateData + { + [JsonProperty("validation_settings")] + private ValidatorStateSettings _settings; + [JsonProperty("validation_results")] + private ValidatorStateResults _results; + + public ValidatorStateData() + { + _settings = new ValidatorStateSettings(); + _results = new ValidatorStateResults(); + } + + public ValidatorStateSettings GetSettings() + { + return _settings; + } + + public ValidatorStateResults GetResults() + { + return _results; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateData.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateData.cs.meta new file mode 100644 index 00000000000..eea46eb9a32 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: da7a885e302cb6b43855b68f44f2c0fc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateDataContractResolver.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateDataContractResolver.cs new file mode 100644 index 00000000000..a72360abad0 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateDataContractResolver.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json.Serialization; + +namespace AssetStoreTools.Validator.UI.Data.Serialization +{ + internal class ValidatorStateDataContractResolver : DefaultContractResolver + { + private static ValidatorStateDataContractResolver _instance; + public static ValidatorStateDataContractResolver Instance => _instance ?? (_instance = new ValidatorStateDataContractResolver()); + + private NamingStrategy _namingStrategy; + + private ValidatorStateDataContractResolver() + { + _namingStrategy = new SnakeCaseNamingStrategy(); + } + + protected override string ResolvePropertyName(string propertyName) + { + var resolvedName = _namingStrategy.GetPropertyName(propertyName, false); + if (resolvedName.StartsWith("_")) + return resolvedName.Substring(1); + + return resolvedName; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateDataContractResolver.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateDataContractResolver.cs.meta new file mode 100644 index 00000000000..4187dca2083 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateDataContractResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 60f0e8d9b2ab86547a288c337fb2be0a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateResults.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateResults.cs new file mode 100644 index 00000000000..3558822c4a8 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateResults.cs @@ -0,0 +1,83 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.TestDefinitions; +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace AssetStoreTools.Validator.UI.Data.Serialization +{ + internal class ValidatorStateResults + { + // Primary data + [JsonProperty("validation_status")] + private ValidationStatus _status; + [JsonProperty("test_results")] + private SortedDictionary _results; + + // Secondary data + [JsonProperty("project_path")] + private string _projectPath; + [JsonProperty("had_compilation_errors")] + private bool _hadCompilationErrors; + + public ValidatorStateResults() + { + _projectPath = string.Empty; + _status = ValidationStatus.NotRun; + _hadCompilationErrors = false; + _results = new SortedDictionary(); + } + + public ValidationStatus GetStatus() + { + return _status; + } + + public void SetStatus(ValidationStatus status) + { + if (_status == status) + return; + + _status = status; + } + + public SortedDictionary GetResults() + { + return _results; + } + + public void SetResults(IEnumerable tests) + { + _results.Clear(); + foreach (var test in tests) + { + _results.Add(test.Id, test.Result); + } + } + + public string GetProjectPath() + { + return _projectPath; + } + + public void SetProjectPath(string projectPath) + { + if (_projectPath == projectPath) + return; + + _projectPath = projectPath; + } + + public bool GetHadCompilationErrors() + { + return _hadCompilationErrors; + } + + public void SetHadCompilationErrors(bool hadCompilationErrors) + { + if (_hadCompilationErrors == hadCompilationErrors) + return; + + _hadCompilationErrors = hadCompilationErrors; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateResults.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateResults.cs.meta new file mode 100644 index 00000000000..876eadbb45a --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateResults.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a90c3acfa50e8da4aa3da2b9c669502d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateSettings.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateSettings.cs new file mode 100644 index 00000000000..27cee0e758e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateSettings.cs @@ -0,0 +1,63 @@ +using AssetStoreTools.Validator.Data; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; + +namespace AssetStoreTools.Validator.UI.Data.Serialization +{ + internal class ValidatorStateSettings + { + [JsonProperty("category")] + private string _category; + [JsonProperty("validation_type")] + private ValidationType _validationType; + [JsonProperty("validation_paths")] + private List _validationPaths; + + public ValidatorStateSettings() + { + _category = string.Empty; + _validationType = ValidationType.UnityPackage; + _validationPaths = new List(); + } + + public string GetCategory() + { + return _category; + } + + public void SetCategory(string category) + { + if (_category == category) + return; + + _category = category; + } + + public ValidationType GetValidationType() + { + return _validationType; + } + + public void SetValidationType(ValidationType validationType) + { + if (validationType == _validationType) + return; + + _validationType = validationType; + } + + public List GetValidationPaths() + { + return _validationPaths; + } + + public void SetValidationPaths(List validationPaths) + { + if (_validationPaths.SequenceEqual(validationPaths)) + return; + + _validationPaths = validationPaths; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateSettings.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateSettings.cs.meta new file mode 100644 index 00000000000..0780a53017a --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/Serialization/ValidatorStateSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d39d56313ade8a8409aafe95dc84f79a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorResults.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorResults.cs new file mode 100644 index 00000000000..38244c8f50b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorResults.cs @@ -0,0 +1,137 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.TestDefinitions; +using AssetStoreTools.Validator.UI.Data.Serialization; +using AssetStoreTools.Validator.Utility; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AssetStoreTools.Validator.UI.Data +{ + internal class ValidatorResults : IValidatorResults + { + private ValidatorStateResults _stateData; + + private IValidatorSettings _settings; + private IEnumerable _tests; + + private readonly TestResultStatus[] _priorityGroups = new TestResultStatus[] + { + TestResultStatus.Undefined, + TestResultStatus.Fail, + TestResultStatus.Warning + }; + + public event Action OnResultsChanged; + public event Action OnRequireSerialize; + + public ValidatorResults(IValidatorSettings settings, ValidatorStateResults stateData) + { + _settings = settings; + _stateData = stateData; + + _tests = GetAllTests(); + + Deserialize(); + } + + private IEnumerable GetAllTests() + { + var tests = new List(); + var testObjects = ValidatorUtility.GetAutomatedTestCases(ValidatorUtility.SortType.Alphabetical); + + foreach (var testObject in testObjects) + { + var testSource = new AutomatedTest(testObject); + var test = new ValidatorTest(testSource); + tests.Add(test); + } + + return tests; + } + + public void LoadResult(ValidationResult result) + { + if (result == null) + return; + + foreach (var test in _tests) + { + if (!result.Tests.Any(x => x.Id == test.Id)) + continue; + + var matchingResult = result.Tests.First(x => x.Id == test.Id); + test.SetResult(matchingResult.Result); + } + + OnResultsChanged?.Invoke(); + + Serialize(result); + } + + public IEnumerable GetSortedTestGroups() + { + var groups = new List(); + var testsByStatus = _tests + .Where(x => x.ValidationType == ValidationType.Generic || x.ValidationType == _settings.GetValidationType()) + .GroupBy(x => x.Result.Status).ToDictionary(x => x.Key, x => x.ToList()); + + foreach (var kvp in testsByStatus) + { + var group = new ValidatorTestGroup(kvp.Key, kvp.Value); + groups.Add(group); + } + + return SortGroups(groups); + } + + private IEnumerable SortGroups(IEnumerable unsortedGroups) + { + var sortedGroups = new List(); + var groups = unsortedGroups.OrderBy(x => x.Status).ToList(); + + // Select priority groups first + foreach (var priority in _priorityGroups) + { + var priorityGroup = groups.FirstOrDefault(x => x.Status == priority); + if (priorityGroup == null) + continue; + + sortedGroups.Add(priorityGroup); + groups.Remove(priorityGroup); + } + + // Add the rest + sortedGroups.AddRange(groups); + + return sortedGroups; + } + + private void Serialize(ValidationResult result) + { + _stateData.SetStatus(result.Status); + _stateData.SetResults(result.Tests); + _stateData.SetProjectPath(result.ProjectPath); + _stateData.SetHadCompilationErrors(result.HadCompilationErrors); + OnRequireSerialize?.Invoke(); + } + + private void Deserialize() + { + if (_stateData == null) + return; + + var serializedResults = _stateData.GetResults(); + foreach (var test in _tests) + { + if (!serializedResults.Any(x => x.Key == test.Id)) + continue; + + var matchingResult = serializedResults.First(x => x.Key == test.Id); + test.SetResult(matchingResult.Value); + } + + OnResultsChanged?.Invoke(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorResults.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorResults.cs.meta new file mode 100644 index 00000000000..069816bf475 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorResults.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bf2839f8b2340294aae39c2965039d2a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorSettings.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorSettings.cs new file mode 100644 index 00000000000..d65ab0c1652 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorSettings.cs @@ -0,0 +1,236 @@ +using AssetStoreTools.Utility; +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.UI.Data.Serialization; +using AssetStoreTools.Validator.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; + +namespace AssetStoreTools.Validator.UI.Data +{ + internal class ValidatorSettings : IValidatorSettings + { + private ValidatorStateSettings _stateData; + + private string _category; + private ValidationType _validationType; + private List _validationPaths; + + public event Action OnCategoryChanged; + public event Action OnValidationTypeChanged; + public event Action OnValidationPathsChanged; + public event Action OnRequireSerialize; + + public ValidatorSettings(ValidatorStateSettings stateData) + { + _stateData = stateData; + + _category = string.Empty; + _validationType = ValidationType.UnityPackage; + _validationPaths = new List(); + + Deserialize(); + } + + public void LoadSettings(ValidationSettings settings) + { + if (settings == null) + return; + + var currentProjectValidationSettings = settings as CurrentProjectValidationSettings; + if (currentProjectValidationSettings == null) + throw new ArgumentException($"Only {nameof(CurrentProjectValidationSettings)} can be loaded"); + + _category = currentProjectValidationSettings.Category; + OnCategoryChanged?.Invoke(); + + _validationType = currentProjectValidationSettings.ValidationType; + OnValidationTypeChanged?.Invoke(); + + _validationPaths = currentProjectValidationSettings.ValidationPaths.ToList(); + OnValidationPathsChanged?.Invoke(); + + Serialize(); + } + + public string GetActiveCategory() + { + return _category; + } + + public void SetActiveCategory(string category) + { + if (category == _category) + return; + + _category = category; + Serialize(); + OnCategoryChanged?.Invoke(); + } + + public List GetAvailableCategories() + { + var categories = new HashSet(); + + var testData = ValidatorUtility.GetAutomatedTestCases(); + foreach (var test in testData) + { + if (test.CategoryInfo == null) + continue; + + foreach (var filter in test.CategoryInfo.Filter) + categories.Add(ConvertSlashToUnicodeSlash(filter)); + } + + return categories.OrderBy(x => x).ToList(); + } + + private string ConvertSlashToUnicodeSlash(string text) + { + return text.Replace('/', '\u2215'); + } + + public ValidationType GetValidationType() + { + return _validationType; + } + + public void SetValidationType(ValidationType validationType) + { + if (validationType == _validationType) + return; + + _validationType = validationType; + + Serialize(); + OnValidationTypeChanged?.Invoke(); + } + + public List GetValidationPaths() + { + return _validationPaths; + } + + public void AddValidationPath(string path) + { + if (string.IsNullOrEmpty(path)) + return; + + if (_validationPaths.Contains(path)) + return; + + // Prevent redundancy for new paths + var existingPath = _validationPaths.FirstOrDefault(x => path.StartsWith(x + "/")); + if (existingPath != null) + { + Debug.LogWarning($"Path '{path}' is already included with existing path: '{existingPath}'"); + return; + } + + // Prevent redundancy for already added paths + var redundantPaths = _validationPaths.Where(x => x.StartsWith(path + "/")).ToArray(); + foreach (var redundantPath in redundantPaths) + { + Debug.LogWarning($"Existing validation path '{redundantPath}' has been made redundant by the inclusion of new validation path: '{path}'"); + _validationPaths.Remove(redundantPath); + } + + _validationPaths.Add(path); + + Serialize(); + OnValidationPathsChanged?.Invoke(); + } + + public void RemoveValidationPath(string path) + { + if (!_validationPaths.Contains(path)) + return; + + _validationPaths.Remove(path); + + Serialize(); + OnValidationPathsChanged?.Invoke(); + } + + public void ClearValidationPaths() + { + if (_validationPaths.Count == 0) + return; + + _validationPaths.Clear(); + + Serialize(); + OnValidationPathsChanged?.Invoke(); + } + + public bool IsValidationPathValid(string path, out string error) + { + error = string.Empty; + + if (string.IsNullOrEmpty(path)) + { + error = "Path cannot be empty"; + return false; + } + + var isAssetsPath = path.StartsWith("Assets/") + || path.Equals("Assets"); + var isPackagePath = PackageUtility.GetPackageByManifestPath($"{path}/package.json", out _); + + if (!isAssetsPath && !isPackagePath) + { + error = "Selected path must be within the Assets folder or point to a root path of a package"; + return false; + } + + if (!Directory.Exists(path)) + { + error = "Path does not exist"; + return false; + } + + if (path.Split('/').Any(x => x.StartsWith(".") || x.EndsWith("~"))) + { + error = $"Path '{path}' cannot be validated as it is a hidden folder and not part of the Asset Database"; + return false; + } + + return true; + } + + public IValidator CreateValidator() + { + var settings = new CurrentProjectValidationSettings() + { + Category = _category, + ValidationType = _validationType, + ValidationPaths = _validationPaths + }; + + var validator = new CurrentProjectValidator(settings); + return validator; + } + + private void Serialize() + { + _stateData.SetCategory(_category); + _stateData.SetValidationType(_validationType); + _stateData.SetValidationPaths(_validationPaths); + + OnRequireSerialize?.Invoke(); + } + + private void Deserialize() + { + if (_stateData == null) + return; + + _category = _stateData.GetCategory(); + _validationType = _stateData.GetValidationType(); + foreach (var path in _stateData.GetValidationPaths()) + _validationPaths.Add(path); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorSettings.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorSettings.cs.meta new file mode 100644 index 00000000000..490590970a3 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 89504c2259614a743a164c5c162a197a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorTest.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorTest.cs new file mode 100644 index 00000000000..126cb4f1365 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorTest.cs @@ -0,0 +1,28 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.TestDefinitions; + +namespace AssetStoreTools.Validator.UI.Data +{ + internal class ValidatorTest : IValidatorTest + { + public int Id { get; private set; } + public string Name { get; private set; } + public string Description { get; private set; } + public ValidationType ValidationType { get; private set; } + public TestResult Result { get; private set; } + + public ValidatorTest(AutomatedTest source) + { + Id = source.Id; + Name = source.Title; + Description = source.Description; + ValidationType = source.ValidationType; + Result = source.Result; + } + + public void SetResult(TestResult result) + { + Result = result; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorTest.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorTest.cs.meta new file mode 100644 index 00000000000..eb0b8371003 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 838e8d45ce997d8489185bc194dfcf25 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorTestGroup.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorTestGroup.cs new file mode 100644 index 00000000000..b3620d86b62 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorTestGroup.cs @@ -0,0 +1,18 @@ +using AssetStoreTools.Validator.Data; +using System.Collections.Generic; + +namespace AssetStoreTools.Validator.UI.Data +{ + internal class ValidatorTestGroup : IValidatorTestGroup + { + public string Name => Status.ToString(); + public TestResultStatus Status { get; private set; } + public IEnumerable Tests { get; private set; } + + public ValidatorTestGroup(TestResultStatus status, IEnumerable tests) + { + Status = status; + Tests = tests; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorTestGroup.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorTestGroup.cs.meta new file mode 100644 index 00000000000..46de39802af --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Data/ValidatorTestGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f5d1fc9ff785904fb2e663e9232a7a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Elements.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Elements.meta new file mode 100644 index 00000000000..9a9f507c4a1 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Elements.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7ee7e5be29b8b184ba2abcd3ed38454e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorButtonElement.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorButtonElement.cs new file mode 100644 index 00000000000..93f8f89169e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorButtonElement.cs @@ -0,0 +1,50 @@ +using AssetStoreTools.Validator.UI.Data; +using System; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Validator.UI.Elements +{ + internal class ValidatorButtonElement : VisualElement + { + // Data + private IValidatorSettings _settings; + + // UI + private Button _validateButton; + + public event Action OnValidate; + + public ValidatorButtonElement(IValidatorSettings settings) + { + _settings = settings; + _settings.OnValidationPathsChanged += ValidationPathsChanged; + + Create(); + Deserialize(); + } + + private void Create() + { + _validateButton = new Button(Validate) { text = "Validate" }; + _validateButton.AddToClassList("validator-validate-button"); + + Add(_validateButton); + } + + private void Validate() + { + OnValidate?.Invoke(); + } + + private void ValidationPathsChanged() + { + var validationPathsPresent = _settings.GetValidationPaths().Count > 0; + _validateButton.SetEnabled(validationPathsPresent); + } + + private void Deserialize() + { + ValidationPathsChanged(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorButtonElement.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorButtonElement.cs.meta new file mode 100644 index 00000000000..f1360532c4e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorButtonElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 44fac105314df6341bf6a70fb3200baf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorDescriptionElement.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorDescriptionElement.cs new file mode 100644 index 00000000000..73d6e70df2e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorDescriptionElement.cs @@ -0,0 +1,114 @@ +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Validator.UI.Elements +{ + internal class ValidatorDescriptionElement : VisualElement + { + private const string DescriptionFoldoutText = "Validate your package to ensure your content follows the chosen submission guidelines."; + private const string DescriptionFoldoutContentText = "The validations below do not cover all of the content standards, and passing all validations does not " + + "guarantee that your package will be accepted to the Asset Store.\n\n" + + "The tests are not obligatory for submitting your assets, but they can help avoid instant rejection by the " + + "automated vetting system, or clarify reasons of rejection communicated by the vetting team.\n\n" + + "For more information about the validations, view the message by expanding the tests or contact our support team."; + + private VisualElement _descriptionSimpleContainer; + private Label _descriptionSimpleLabel; + private Button _showMoreButton; + + private VisualElement _descriptionFullContainer; + private Button _showLessButton; + + public ValidatorDescriptionElement() + { + AddToClassList("validator-description"); + Create(); + } + + private void Create() + { + CreateSimpleDescription(); + CreateFullDescription(); + } + + private void CreateSimpleDescription() + { + _descriptionSimpleContainer = new VisualElement(); + _descriptionSimpleContainer.AddToClassList("validator-description-simple-container"); + + _descriptionSimpleLabel = new Label(DescriptionFoldoutText); + _descriptionSimpleLabel.AddToClassList("validator-description-simple-label"); + + _showMoreButton = new Button(ToggleFullDescription) { text = "Show more..." }; + _showMoreButton.AddToClassList("validator-description-show-button"); + _showMoreButton.AddToClassList("validator-description-hyperlink-button"); + + _descriptionSimpleContainer.Add(_descriptionSimpleLabel); + _descriptionSimpleContainer.Add(_showMoreButton); + + Add(_descriptionSimpleContainer); + } + + private void CreateFullDescription() + { + _descriptionFullContainer = new VisualElement(); + _descriptionFullContainer.AddToClassList("validator-description-full-container"); + + var validatorDescription = new Label() + { + text = DescriptionFoldoutContentText + }; + validatorDescription.AddToClassList("validator-description-full-label"); + + var submissionGuidelinesButton = new Button(OpenSubmissionGuidelinesUrl) + { + text = "Submission Guidelines" + }; + submissionGuidelinesButton.AddToClassList("validator-description-hyperlink-button"); + + var supportTicketButton = new Button(OpenSupportTicketUrl) + { + text = "Contact our Support Team" + }; + supportTicketButton.AddToClassList("validator-description-hyperlink-button"); + + _showLessButton = new Button(ToggleFullDescription) { text = "Show less..." }; + _showLessButton.AddToClassList("validator-description-hide-button"); + _showLessButton.AddToClassList("validator-description-hyperlink-button"); + + _descriptionFullContainer.Add(validatorDescription); + _descriptionFullContainer.Add(submissionGuidelinesButton); + _descriptionFullContainer.Add(supportTicketButton); + _descriptionFullContainer.Add(_showLessButton); + + _descriptionFullContainer.style.display = DisplayStyle.None; + Add(_descriptionFullContainer); + } + + private void ToggleFullDescription() + { + var displayFullDescription = _descriptionFullContainer.style.display == DisplayStyle.None; + + if (displayFullDescription) + { + _showMoreButton.style.display = DisplayStyle.None; + _descriptionFullContainer.style.display = DisplayStyle.Flex; + } + else + { + _showMoreButton.style.display = DisplayStyle.Flex; + _descriptionFullContainer.style.display = DisplayStyle.None; + } + } + + private void OpenSubmissionGuidelinesUrl() + { + Application.OpenURL(Constants.Validator.SubmissionGuidelinesUrl); + } + + private void OpenSupportTicketUrl() + { + Application.OpenURL(Constants.Validator.SupportTicketUrl); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorDescriptionElement.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorDescriptionElement.cs.meta new file mode 100644 index 00000000000..898b2cebbbf --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorDescriptionElement.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9866d77420d947ba852055eed2bac895 +timeCreated: 1653383883 \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorPathsElement.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorPathsElement.cs new file mode 100644 index 00000000000..6e4fe6dcda7 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorPathsElement.cs @@ -0,0 +1,128 @@ +using AssetStoreTools.Utility; +using AssetStoreTools.Validator.UI.Data; +using UnityEditor; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Validator.UI.Elements +{ + internal class ValidatorPathsElement : VisualElement + { + // Data + private IValidatorSettings _settings; + + // UI + private ScrollView _pathBoxScrollView; + + public ValidatorPathsElement(IValidatorSettings settings) + { + AddToClassList("validator-paths"); + + _settings = settings; + _settings.OnValidationPathsChanged += ValidationPathsChanged; + + Create(); + Deserialize(); + } + + private void Create() + { + var pathSelectionRow = new VisualElement(); + pathSelectionRow.AddToClassList("validator-settings-selection-row"); + + VisualElement labelHelpRow = new VisualElement(); + labelHelpRow.AddToClassList("validator-settings-selection-label-help-row"); + labelHelpRow.style.alignSelf = Align.FlexStart; + + Label pathLabel = new Label { text = "Validation paths" }; + Image pathLabelTooltip = new Image + { + tooltip = "Select the folder (or multiple folders) that your package consists of." + + "\n\nAll files and folders of your package should be contained within " + + "a single root folder that is named after your package " + + "(e.g. 'Assets/[MyPackageName]' or 'Packages/[MyPackageName]')" + + "\n\nIf your package includes special folders that cannot be nested within " + + "the root package folder (e.g. 'WebGLTemplates'), they should be added to this list " + + "together with the root package folder" + }; + + labelHelpRow.Add(pathLabel); + labelHelpRow.Add(pathLabelTooltip); + + var fullPathBox = new VisualElement() { name = "ValidationPaths" }; + fullPathBox.AddToClassList("validator-paths-box"); + + _pathBoxScrollView = new ScrollView { name = "ValidationPathsScrollView" }; + _pathBoxScrollView.AddToClassList("validator-paths-scroll-view"); + + VisualElement scrollViewBottomRow = new VisualElement(); + scrollViewBottomRow.AddToClassList("validator-paths-scroll-view-bottom-row"); + + var addExtraPathsButton = new Button(BrowsePath) { text = "Add a path" }; + addExtraPathsButton.AddToClassList("validator-paths-add-button"); + scrollViewBottomRow.Add(addExtraPathsButton); + + fullPathBox.Add(_pathBoxScrollView); + fullPathBox.Add(scrollViewBottomRow); + + pathSelectionRow.Add(labelHelpRow); + pathSelectionRow.Add(fullPathBox); + + Add(pathSelectionRow); + } + + private VisualElement CreateSinglePathElement(string path) + { + var validationPath = new VisualElement(); + validationPath.AddToClassList("validator-paths-path-row"); + + var folderPathLabel = new Label(path); + folderPathLabel.AddToClassList("validator-paths-path-row-input-field"); + + var removeButton = new Button(() => + { + _settings.RemoveValidationPath(path); + }); + removeButton.text = "X"; + removeButton.AddToClassList("validator-paths-path-row-remove-button"); + + validationPath.Add(folderPathLabel); + validationPath.Add(removeButton); + + return validationPath; + } + + private void BrowsePath() + { + string absolutePath = EditorUtility.OpenFolderPanel("Select a directory", "Assets", ""); + + if (string.IsNullOrEmpty(absolutePath)) + return; + + var relativePath = FileUtility.AbsolutePathToRelativePath(absolutePath, ASToolsPreferences.Instance.EnableSymlinkSupport); + + if (!_settings.IsValidationPathValid(relativePath, out var error)) + { + EditorUtility.DisplayDialog("Invalid path", error, "OK"); + return; + } + + _settings.AddValidationPath(relativePath); + } + + private void ValidationPathsChanged() + { + var validationPaths = _settings.GetValidationPaths(); + + _pathBoxScrollView.Clear(); + foreach (var path in validationPaths) + { + _pathBoxScrollView.Add(CreateSinglePathElement(path)); + } + } + + private void Deserialize() + { + ValidationPathsChanged(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorPathsElement.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorPathsElement.cs.meta new file mode 100644 index 00000000000..93c9b6be0f8 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorPathsElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 370dcd3bc87ace647940b4b07147bf93 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorResultsElement.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorResultsElement.cs new file mode 100644 index 00000000000..f8e5113f70d --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorResultsElement.cs @@ -0,0 +1,47 @@ +using AssetStoreTools.Validator.UI.Data; +using System.Linq; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Validator.UI.Elements +{ + internal class ValidatorResultsElement : ScrollView + { + private IValidatorResults _results; + + public ValidatorResultsElement(IValidatorResults results) + { + AddToClassList("validator-test-list"); + + _results = results; + _results.OnResultsChanged += ResultsChanged; + + Create(); + } + + private void Create() + { + var groups = _results.GetSortedTestGroups().ToList(); + for (int i = 0; i < groups.Count; i++) + { + var groupElement = new ValidatorTestGroupElement(groups[i]); + Add(groupElement); + if (i != groups.Count - 1) + Add(CreateSeparator()); + } + } + + private void ResultsChanged() + { + Clear(); + Create(); + } + + private VisualElement CreateSeparator() + { + var groupSeparator = new VisualElement { name = "GroupSeparator" }; + groupSeparator.AddToClassList("validator-test-list-group-separator"); + + return groupSeparator; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorResultsElement.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorResultsElement.cs.meta new file mode 100644 index 00000000000..d84970a90c9 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorResultsElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 12f80f9088944a149a34b3f078ca859a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorSettingsElement.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorSettingsElement.cs new file mode 100644 index 00000000000..b3f69d75d06 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorSettingsElement.cs @@ -0,0 +1,96 @@ +using AssetStoreTools.Validator.UI.Data; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Validator.UI.Elements +{ + internal class ValidatorSettingsElement : VisualElement + { + // Data + private IValidatorSettings _settings; + + // UI + private ToolbarMenu _categoryMenu; + private ValidatorPathsElement _validationPathsElement; + + public ValidatorSettingsElement(IValidatorSettings settings) + { + AddToClassList("validator-settings"); + + _settings = settings; + _settings.OnCategoryChanged += CategoryChanged; + + Create(); + Deserialize(); + } + + public void Create() + { + CreateCategorySelection(); + CreateValidationPathSelection(); + } + + private void CreateCategorySelection() + { + var categorySelectionBox = new VisualElement(); + categorySelectionBox.AddToClassList("validator-settings-selection-row"); + + VisualElement labelHelpRow = new VisualElement(); + labelHelpRow.AddToClassList("validator-settings-selection-label-help-row"); + + Label categoryLabel = new Label { text = "Category" }; + Image categoryLabelTooltip = new Image + { + tooltip = "Choose a base category of your package" + + "\n\nThis can be found in the Publishing Portal when creating the package listing or just " + + "selecting a planned one." + + "\n\nNote: Different categories could have different severities of several test cases." + }; + + labelHelpRow.Add(categoryLabel); + labelHelpRow.Add(categoryLabelTooltip); + + _categoryMenu = new ToolbarMenu { name = "CategoryMenu" }; + _categoryMenu.AddToClassList("validator-settings-selection-dropdown"); + + categorySelectionBox.Add(labelHelpRow); + categorySelectionBox.Add(_categoryMenu); + + // Append available categories + var categories = _settings.GetAvailableCategories(); + foreach (var category in categories) + { + _categoryMenu.menu.AppendAction(category, _ => _settings.SetActiveCategory(category)); + } + + // Append misc category + _categoryMenu.menu.AppendAction("Other", _ => _settings.SetActiveCategory(string.Empty)); + + Add(categorySelectionBox); + } + + private void CreateValidationPathSelection() + { + _validationPathsElement = new ValidatorPathsElement(_settings); + Add(_validationPathsElement); + } + + private void CategoryChanged() + { + var category = _settings.GetActiveCategory(); + if (!string.IsNullOrEmpty(category)) + _categoryMenu.text = category; + else + _categoryMenu.text = "Other"; + } + + private void Deserialize() + { + if (_settings == null) + return; + + // Set initial category + CategoryChanged(); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorSettingsElement.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorSettingsElement.cs.meta new file mode 100644 index 00000000000..0522b1bc191 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorSettingsElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 760d25556d755d544bece3a605adea09 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorTestElement.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorTestElement.cs new file mode 100644 index 00000000000..c477c72b7b3 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorTestElement.cs @@ -0,0 +1,239 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.UI.Data; +using AssetStoreTools.Validator.Utility; +using System.Linq; +using UnityEditor.SceneManagement; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.SceneManagement; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Validator.UI.Elements +{ + internal class ValidatorTestElement : VisualElement + { + // Data + private IValidatorTest _test; + private bool _isExpanded; + + // UI + private Button _testFoldoutButton; + private Label _testFoldoutExpandStateLabel; + private Label _testFoldoutLabel; + private Image _testStatusImage; + + private VisualElement _testContent; + private VisualElement _resultMessagesBox; + + public ValidatorTestElement(IValidatorTest test) + { + AddToClassList("validator-test"); + + _test = test; + + Create(); + Unexpand(); + + SubscribeToSceneChanges(); + } + + private void Create() + { + CreateFoldoutButton(); + CreateTestContent(); + CreateTestDescription(); + CreateTestMessages(); + } + + private void CreateFoldoutButton() + { + _testFoldoutButton = new Button(ToggleExpand) { name = _test.Name }; + _testFoldoutButton.AddToClassList("validator-test-foldout"); + + // Expander and Asset Label + VisualElement labelExpanderRow = new VisualElement { name = "labelExpanderRow" }; + labelExpanderRow.AddToClassList("validator-test-expander"); + + _testFoldoutExpandStateLabel = new Label { name = "ExpanderLabel", text = "â–º" }; + _testFoldoutExpandStateLabel.AddToClassList("validator-test-expander-arrow"); + + _testFoldoutLabel = new Label { name = "TestLabel", text = _test.Name }; + _testFoldoutLabel.AddToClassList("validator-text-expander-label"); + + labelExpanderRow.Add(_testFoldoutExpandStateLabel); + labelExpanderRow.Add(_testFoldoutLabel); + + _testStatusImage = new Image + { + name = "TestImage", + image = ValidatorUtility.GetStatusTexture(_test.Result.Status) + }; + + _testStatusImage.AddToClassList("validator-test-expander-image"); + + _testFoldoutButton.Add(labelExpanderRow); + _testFoldoutButton.Add(_testStatusImage); + + Add(_testFoldoutButton); + } + + private void CreateTestContent() + { + _testContent = new VisualElement(); + _testContent.AddToClassList("validator-test-content"); + Add(_testContent); + } + + private void CreateTestDescription() + { + var testCaseDescription = new TextField + { + name = "Description", + value = _test.Description, + isReadOnly = true, + multiline = true, + focusable = false, + doubleClickSelectsWord = false, + tripleClickSelectsLine = false + }; + testCaseDescription.AddToClassList("validator-test-content-textfield"); + +#if UNITY_2022_1_OR_NEWER + testCaseDescription.focusable = true; + testCaseDescription.selectAllOnFocus = false; + testCaseDescription.selectAllOnMouseUp = false; +#endif + + _testContent.Add(testCaseDescription); + } + + private void CreateTestMessages() + { + if (_test.Result.MessageCount == 0) + return; + + _resultMessagesBox = new VisualElement(); + _resultMessagesBox.AddToClassList("validator-test-content-result-messages"); + + switch (_test.Result.Status) + { + case TestResultStatus.Pass: + _resultMessagesBox.AddToClassList("validator-test-content-result-messages-pass"); + break; + case TestResultStatus.Warning: + _resultMessagesBox.AddToClassList("validator-test-content-result-messages-warning"); + break; + case TestResultStatus.Fail: + _resultMessagesBox.AddToClassList("validator-test-content-result-messages-fail"); + break; + } + + for (int i = 0; i < _test.Result.MessageCount; i++) + { + _resultMessagesBox.Add(CreateMessage(_test.Result.GetMessage(i))); + + if (i == _test.Result.MessageCount - 1) + continue; + + var separator = new VisualElement() { name = "Separator" }; + separator.AddToClassList("message-separator"); + _resultMessagesBox.Add(separator); + } + + _testContent.Add(_resultMessagesBox); + } + + private VisualElement CreateMessage(TestResultMessage message) + { + var resultText = message.GetText(); + var clickAction = message.GetClickAction(); + + var resultMessage = new VisualElement { name = "ResultMessageElement" }; + resultMessage.AddToClassList("validator-test-content-result-messages-content"); + + var informationButton = new Button(); + informationButton.AddToClassList("validator-test-content-result-messages-content-button"); + + if (clickAction != null) + { + informationButton.tooltip = clickAction.Tooltip; + informationButton.clicked += clickAction.Execute; + informationButton.SetEnabled(true); + } + + var informationDescription = new Label { name = "InfoDesc", text = resultText }; + informationDescription.AddToClassList("validator-test-content-result-messages-content-label"); + + informationButton.Add(informationDescription); + resultMessage.Add(informationButton); + + for (int i = 0; i < message.MessageObjectCount; i++) + { + var obj = message.GetMessageObject(i); + if (obj == null) + continue; + + if (obj.GetObject() == null) + continue; + + var objectField = new ObjectField() { objectType = obj.GetType(), value = obj.GetObject() }; + objectField.RegisterCallback>((evt) => + objectField.SetValueWithoutNotify(evt.previousValue)); + resultMessage.Add(objectField); + } + + return resultMessage; + } + + private void ToggleExpand() + { + if (!_isExpanded) + Expand(); + else + Unexpand(); + } + + private void Expand() + { + _testFoldoutExpandStateLabel.text = "â–¼"; + _testFoldoutButton.AddToClassList("validator-test-foldout-expanded"); + _testContent.style.display = DisplayStyle.Flex; + _isExpanded = true; + } + + private void Unexpand() + { + _testFoldoutExpandStateLabel.text = "â–º"; + _testFoldoutButton.RemoveFromClassList("validator-test-foldout-expanded"); + _testContent.style.display = DisplayStyle.None; + _isExpanded = false; + } + + private void SubscribeToSceneChanges() + { + // Some result message objects only exist in specific scenes, + // therefore the UI must be refreshed on scene change + var windowToSubscribeTo = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + UnityAction sceneChanged = null; + sceneChanged = new UnityAction((_, __) => RefreshObjects(windowToSubscribeTo)); + EditorSceneManager.activeSceneChangedInEditMode += sceneChanged; + + void RefreshObjects(ValidatorWindow subscribedWindow) + { + // Remove callback if validator window instance changed + var activeWindow = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + if (subscribedWindow == null || subscribedWindow != activeWindow) + { + EditorSceneManager.activeSceneChangedInEditMode -= sceneChanged; + return; + } + + if (_resultMessagesBox != null) + _testContent.Remove(_resultMessagesBox); + + CreateTestMessages(); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorTestElement.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorTestElement.cs.meta new file mode 100644 index 00000000000..17794ffec49 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorTestElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 56c93e6f23ba5724da8cc38f832be4e0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorTestGroupElement.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorTestGroupElement.cs new file mode 100644 index 00000000000..7906a0dd5f0 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorTestGroupElement.cs @@ -0,0 +1,102 @@ +using AssetStoreTools.Validator.UI.Data; +using AssetStoreTools.Validator.Utility; +using System.Collections.Generic; +using System.Linq; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Validator.UI.Elements +{ + internal class ValidatorTestGroupElement : VisualElement + { + // Data + private IValidatorTestGroup _group; + private bool _isExpanded; + + // UI + private Button _groupFoldoutButton; + private Label _groupExpandStateLabel; + private Label _groupFoldoutLabel; + private Image _groupStatusImage; + + private VisualElement _groupContent; + private List _testElements; + + public ValidatorTestGroupElement(IValidatorTestGroup group) + { + AddToClassList("validator-test-list-group"); + + _group = group; + + Create(); + } + + private void Create() + { + CreateGroupFoldout(); + CreateGroupContent(); + } + + private void CreateGroupFoldout() + { + _groupFoldoutButton = new Button(ToggleExpand); + _groupFoldoutButton.AddToClassList("validator-test-list-group-expander"); + + _groupExpandStateLabel = new Label { name = "ExpanderLabel", text = "â–º" }; + _groupExpandStateLabel.AddToClassList("validator-test-list-group-expander-arrow"); + + _groupStatusImage = new Image + { + name = "TestImage", + image = ValidatorUtility.GetStatusTexture(_group.Status) + }; + _groupStatusImage.AddToClassList("validator-test-list-group-expander-image"); + + _groupFoldoutLabel = new Label() { text = $"{_group.Name} ({_group.Tests.Count()})" }; + _groupFoldoutLabel.AddToClassList("validator-test-list-group-expander-label"); + + _groupFoldoutButton.Add(_groupExpandStateLabel); + _groupFoldoutButton.Add(_groupStatusImage); + _groupFoldoutButton.Add(_groupFoldoutLabel); + + Add(_groupFoldoutButton); + } + + private void CreateGroupContent() + { + _groupContent = new VisualElement(); + _groupContent.AddToClassList("validator-test-list-group-content"); + + Add(_groupContent); + + _testElements = new List(); + foreach (var test in _group.Tests) + { + var testElement = new ValidatorTestElement(test); + _testElements.Add(testElement); + _groupContent.Add(testElement); + } + } + + private void ToggleExpand() + { + if (!_isExpanded) + Expand(); + else + Unexpand(); + } + + private void Expand() + { + _groupExpandStateLabel.text = "â–¼"; + _groupContent.style.display = DisplayStyle.Flex; + _isExpanded = true; + } + + private void Unexpand() + { + _groupExpandStateLabel.text = "â–º"; + _groupContent.style.display = DisplayStyle.None; + _isExpanded = false; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorTestGroupElement.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorTestGroupElement.cs.meta new file mode 100644 index 00000000000..eea4279eb7d --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Elements/ValidatorTestGroupElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d7c7a8788d0ea324e843a475244d8e18 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/ValidatorWindow.cs b/.github/Asset Store Tools/Validator/Scripts/UI/ValidatorWindow.cs new file mode 100644 index 00000000000..cea8163d3f4 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/ValidatorWindow.cs @@ -0,0 +1,56 @@ +using AssetStoreTools.Utility; +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services; +using AssetStoreTools.Validator.UI.Views; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Validator.UI +{ + + internal class ValidatorWindow : AssetStoreToolsWindow + { + protected override string WindowTitle => "Asset Store Validator"; + + private ICachingService _cachingService; + + private ValidatorTestsView _validationTestsView; + + protected override void Init() + { + minSize = new Vector2(350, 350); + + this.SetAntiAliasing(4); + + VisualElement root = rootVisualElement; + + // Clean it out, in case the window gets initialized again + root.Clear(); + + // Getting a reference to the USS Document and adding stylesheet to the root + root.styleSheets.Add(StyleSelector.ValidatorWindow.ValidatorWindowStyle); + root.styleSheets.Add(StyleSelector.ValidatorWindow.ValidatorWindowTheme); + + GetServices(); + ConstructWindow(); + } + + private void GetServices() + { + _cachingService = ValidatorServiceProvider.Instance.GetService(); + } + + private void ConstructWindow() + { + _validationTestsView = new ValidatorTestsView(_cachingService); + rootVisualElement.Add(_validationTestsView); + } + + public void Load(ValidationSettings settings, ValidationResult result) + { + _validationTestsView.LoadSettings(settings); + _validationTestsView.LoadResult(result); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/ValidatorWindow.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/ValidatorWindow.cs.meta new file mode 100644 index 00000000000..378aa82111b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/ValidatorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a0dc99b826513dd4f868f1cf405c3923 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Views.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Views.meta new file mode 100644 index 00000000000..e90db603f1b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Views.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8a973656ad14b8941b790ed83c874e97 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Views/ValidatorTestsView.cs b/.github/Asset Store Tools/Validator/Scripts/UI/Views/ValidatorTestsView.cs new file mode 100644 index 00000000000..b25a8b4439c --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Views/ValidatorTestsView.cs @@ -0,0 +1,103 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services; +using AssetStoreTools.Validator.UI.Data; +using AssetStoreTools.Validator.UI.Data.Serialization; +using AssetStoreTools.Validator.UI.Elements; +using UnityEditor; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Validator.UI.Views +{ + internal class ValidatorTestsView : VisualElement + { + // Data + private ValidatorStateData _stateData; + private IValidatorSettings _settings; + private IValidatorResults _results; + + private ICachingService _cachingService; + + // UI + private ValidatorSettingsElement _validatorSettingsElement; + private ValidatorButtonElement _validatorButtonElement; + private ValidatorResultsElement _validationTestListElement; + + public ValidatorTestsView(ICachingService cachingService) + { + _cachingService = cachingService; + + if (!_cachingService.GetCachedValidatorStateData(out _stateData)) + _stateData = new ValidatorStateData(); + + _settings = new ValidatorSettings(_stateData.GetSettings()); + _settings.OnRequireSerialize += Serialize; + + _results = new ValidatorResults(_settings, _stateData.GetResults()); + _results.OnRequireSerialize += Serialize; + + Create(); + } + + private void Create() + { + CreateValidatorDescription(); + CreateValidationSettings(); + CreateValidationButton(); + CreateValidatorResults(); + } + + private void CreateValidatorDescription() + { + var validationInfoElement = new ValidatorDescriptionElement(); + Add(validationInfoElement); + } + + private void CreateValidationSettings() + { + _validatorSettingsElement = new ValidatorSettingsElement(_settings); + Add(_validatorSettingsElement); + } + + private void CreateValidationButton() + { + _validatorButtonElement = new ValidatorButtonElement(_settings); + _validatorButtonElement.OnValidate += Validate; + Add(_validatorButtonElement); + } + + private void CreateValidatorResults() + { + _validationTestListElement = new ValidatorResultsElement(_results); + Add(_validationTestListElement); + } + + private void Validate() + { + var validator = _settings.CreateValidator(); + var result = validator.Validate(); + + if (result.Status == ValidationStatus.Failed) + { + EditorUtility.DisplayDialog("Validation failed", result.Exception.Message, "OK"); + return; + } + + LoadResult(result); + } + + public void LoadSettings(ValidationSettings settings) + { + _settings.LoadSettings(settings); + } + + public void LoadResult(ValidationResult result) + { + _results.LoadResult(result); + } + + private void Serialize() + { + _cachingService.CacheValidatorStateData(_stateData); + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/UI/Views/ValidatorTestsView.cs.meta b/.github/Asset Store Tools/Validator/Scripts/UI/Views/ValidatorTestsView.cs.meta new file mode 100644 index 00000000000..90c2cdd5502 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/UI/Views/ValidatorTestsView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c5e0da39c6638684c9d3faf8e62c60d3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Utility.meta b/.github/Asset Store Tools/Validator/Scripts/Utility.meta new file mode 100644 index 00000000000..148dca9d9e2 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Utility.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3bc3a78a4b494e44b75268ad1444ab81 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/Utility/ValidatorUtility.cs b/.github/Asset Store Tools/Validator/Scripts/Utility/ValidatorUtility.cs new file mode 100644 index 00000000000..0d58db5c2bb --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Utility/ValidatorUtility.cs @@ -0,0 +1,142 @@ +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services; +using AssetStoreTools.Validator.TestDefinitions; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; +using static AssetStoreTools.Constants; +using ValidatorConstants = AssetStoreTools.Constants.Validator; + +namespace AssetStoreTools.Validator.Utility +{ + internal static class ValidatorUtility + { + public enum SortType + { + Id, + Alphabetical + } + + public static ValidationTestScriptableObject[] GetAutomatedTestCases() => GetAutomatedTestCases(SortType.Id); + + public static ValidationTestScriptableObject[] GetAutomatedTestCases(SortType sortType) + { + string[] guids = AssetDatabase.FindAssets("t:AutomatedTestScriptableObject", new[] { ValidatorConstants.Tests.TestDefinitionsPath }); + ValidationTestScriptableObject[] tests = new ValidationTestScriptableObject[guids.Length]; + for (int i = 0; i < tests.Length; i++) + { + string testPath = AssetDatabase.GUIDToAssetPath(guids[i]); + AutomatedTestScriptableObject test = AssetDatabase.LoadAssetAtPath(testPath); + + tests[i] = test; + } + + switch (sortType) + { + default: + case SortType.Id: + tests = tests.Where(x => x != null).OrderBy(x => x.Id).ToArray(); + break; + case SortType.Alphabetical: + tests = tests.Where(x => x != null).OrderBy(x => x.Title).ToArray(); + break; + } + + return tests; + } + + public static MonoScript GenerateTestScript(string testName, ValidationType validationType) + { + var derivedType = nameof(ITestScript); + var configType = string.Empty; + var scriptPath = string.Empty; + switch (validationType) + { + case ValidationType.Generic: + configType = nameof(GenericTestConfig); + scriptPath = ValidatorConstants.Tests.GenericTestMethodsPath; + break; + case ValidationType.UnityPackage: + configType = nameof(GenericTestConfig); + scriptPath = ValidatorConstants.Tests.UnityPackageTestMethodsPath; + break; + default: + throw new System.Exception("Undefined validation type"); + } + + var newScriptPath = $"{scriptPath}/{testName}"; + if (!newScriptPath.EndsWith(".cs")) + newScriptPath += ".cs"; + + var existingScript = AssetDatabase.LoadAssetAtPath(newScriptPath); + if (existingScript != null) + return existingScript; + + var scriptContent = + $"using AssetStoreTools.Validator.Data;\n" + + $"using AssetStoreTools.Validator.TestDefinitions;\n\n" + + $"namespace AssetStoreTools.Validator.TestMethods\n" + + $"{{\n" + + $" internal class {testName} : {derivedType}\n" + + $" {{\n" + + $" private {configType} _config;\n\n" + + $" // Constructor also accepts dependency injection of registered {nameof(IValidatorService)} types\n" + + $" public {testName}({configType} config)\n" + + $" {{\n" + + $" _config = config;\n" + + $" }}\n\n" + + $" public {nameof(TestResult)} {nameof(ITestScript.Run)}()\n" + + $" {{\n" + + $" var result = new {nameof(TestResult)}() {{ {nameof(TestResult.Status)} = {nameof(TestResultStatus)}.{nameof(TestResultStatus.Undefined)} }};\n" + + $" return result;\n" + + $" }}\n" + + $" }}\n" + + $"}}\n"; + + File.WriteAllText(newScriptPath, scriptContent); + AssetDatabase.Refresh(); + return AssetDatabase.LoadAssetAtPath(newScriptPath); + } + + public static string GetLongestProjectPath() + { + var longPaths = GetProjectPaths(new string[] { "Assets", "Packages" }); + return longPaths.Aggregate("", (max, cur) => max.Length > cur.Length ? max : cur); + } + + public static IEnumerable GetProjectPaths(string[] rootPaths) + { + var longPaths = new List(); + var guids = AssetDatabase.FindAssets("*", rootPaths); + + foreach (var guid in guids) + { + var path = AssetDatabase.GUIDToAssetPath(guid); + longPaths.Add(path); + } + + return longPaths; + } + + public static Texture GetStatusTexture(TestResultStatus status) + { + var iconTheme = ""; + if (!EditorGUIUtility.isProSkin) + iconTheme = "_d"; + + switch (status) + { + case TestResultStatus.Pass: + return (Texture)EditorGUIUtility.Load($"{WindowStyles.ValidatorIconsPath}/success{iconTheme}.png"); + case TestResultStatus.Warning: + return (Texture)EditorGUIUtility.Load($"{WindowStyles.ValidatorIconsPath}/warning{iconTheme}.png"); + case TestResultStatus.Fail: + return (Texture)EditorGUIUtility.Load($"{WindowStyles.ValidatorIconsPath}/error{iconTheme}.png"); + default: + return (Texture)EditorGUIUtility.Load($"{WindowStyles.ValidatorIconsPath}/undefined{iconTheme}.png"); + } + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/Utility/ValidatorUtility.cs.meta b/.github/Asset Store Tools/Validator/Scripts/Utility/ValidatorUtility.cs.meta new file mode 100644 index 00000000000..b89dc0e274b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/Utility/ValidatorUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 24792af98b4d87746a4b945e2a45dc2d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Scripts/ValidatorBase.cs b/.github/Asset Store Tools/Validator/Scripts/ValidatorBase.cs new file mode 100644 index 00000000000..e65f137fb74 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/ValidatorBase.cs @@ -0,0 +1,108 @@ +using AssetStoreTools.Validator.Categories; +using AssetStoreTools.Validator.Data; +using AssetStoreTools.Validator.Services; +using AssetStoreTools.Validator.TestDefinitions; +using AssetStoreTools.Validator.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Validator +{ + internal abstract class ValidatorBase : IValidator + { + public ValidationSettings Settings { get; private set; } + + private CategoryEvaluator _categoryEvaluator; + private List _automatedTests; + + protected ICachingService CachingService; + + public ValidatorBase(ValidationSettings settings) + { + Settings = settings; + _categoryEvaluator = new CategoryEvaluator(settings?.Category); + + CachingService = ValidatorServiceProvider.Instance.GetService(); + + CreateAutomatedTestCases(); + } + + private void CreateAutomatedTestCases() + { + var testData = ValidatorUtility.GetAutomatedTestCases(ValidatorUtility.SortType.Alphabetical); + _automatedTests = new List(); + + foreach (var t in testData) + { + var test = new AutomatedTest(t); + _automatedTests.Add(test); + } + } + + protected abstract void ValidateSettings(); + protected abstract ValidationResult GenerateValidationResult(); + + public ValidationResult Validate() + { + try + { + ValidateSettings(); + } + catch (Exception e) + { + return new ValidationResult() { Status = ValidationStatus.Failed, Exception = e }; + } + + var result = GenerateValidationResult(); + return result; + } + + protected List GetApplicableTests(params ValidationType[] validationTypes) + { + return _automatedTests.Where(x => validationTypes.Any(y => y == x.ValidationType)).ToList(); + } + + protected ValidationResult RunTests(List tests, ITestConfig config) + { + var completedTests = new List(); + + for (int i = 0; i < tests.Count; i++) + { + var test = tests[i]; + + EditorUtility.DisplayProgressBar("Validating", $"Running validation: {i + 1} - {test.Title}", (float)i / _automatedTests.Count); + + test.Run(config); + + // Adjust result based on categories + var updatedStatus = _categoryEvaluator.Evaluate(test); + test.Result.Status = updatedStatus; + + // Add the result + completedTests.Add(test); + +#if AB_BUILDER + EditorUtility.UnloadUnusedAssetsImmediate(); +#endif + } + + EditorUtility.UnloadUnusedAssetsImmediate(); + EditorUtility.ClearProgressBar(); + + var projectPath = Application.dataPath.Substring(0, Application.dataPath.Length - "/Assets".Length); + var hasCompilationErrors = EditorUtility.scriptCompilationFailed; + var result = new ValidationResult() + { + Status = ValidationStatus.RanToCompletion, + Tests = completedTests, + ProjectPath = projectPath, + HadCompilationErrors = hasCompilationErrors + }; + + return result; + } + } +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Scripts/ValidatorBase.cs.meta b/.github/Asset Store Tools/Validator/Scripts/ValidatorBase.cs.meta new file mode 100644 index 00000000000..c7fbc8e42be --- /dev/null +++ b/.github/Asset Store Tools/Validator/Scripts/ValidatorBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2360246050affaa458413c6569c1f925 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Styles.meta b/.github/Asset Store Tools/Validator/Styles.meta new file mode 100644 index 00000000000..a5cbef6e4e2 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Styles.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 21f473cb130d5f0458b2823b3a67f789 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Styles/Style.uss b/.github/Asset Store Tools/Validator/Styles/Style.uss new file mode 100644 index 00000000000..4a79ecdd7ed --- /dev/null +++ b/.github/Asset Store Tools/Validator/Styles/Style.uss @@ -0,0 +1,337 @@ +/* Validator Description */ + +.validator-description { + flex-direction: column; + flex-shrink: 0; + + margin: 10px 5px 2px 5px; + padding: 2px 4px; +} + +.validator-description-simple-container { + flex-direction: column; + flex-wrap: wrap; +} + +.validator-description-simple-label { + white-space: normal; +} + +.validator-description-full-container { + margin-top: 12px; +} + +.validator-description-full-label { + white-space: normal; + margin-bottom: 10px; +} + +.validator-description-hyperlink-button { + margin: 0; + padding: 0; + + align-self: flex-start; + cursor: link; +} + +.validator-description-show-button { + margin-top: 12px; +} + +.validator-description-hide-button { + margin-top: 12px; +} + +/* Validator Settings */ + +.validator-settings { + flex-direction: column; + flex-shrink: 0; + + margin: 0px 5px 2px 5px; + padding: 2px 4px; +} + +.validator-settings-selection-row { + flex-direction: row; + flex-grow: 1; + + margin-top: 10px; + padding: 0 3px 0 2px; +} + +.validator-settings-selection-label-help-row { + flex-direction: row; + flex-shrink: 1; + flex-grow: 0; + + align-self: center; + align-items: center; + justify-content: flex-start; + + width: 120px; +} + +.validator-settings-selection-label-help-row > Label { + -unity-font-style: bold; +} + +.validator-settings-selection-label-help-row > Image { + height: 16px; + width: 16px; +} + +.validator-settings-selection-dropdown { + flex-grow: 1; + flex-shrink: 1; + + align-self: stretch; + + margin-right: 0; + margin-left: 3px; + padding: 1px 4px; +} + +/* Validate Button */ + +.validator-validate-button { + align-self: stretch; + + height: 25px; + margin-left: 2px; +} + +/* Validation Paths */ + +.validator-paths { + flex-direction: column; + flex-grow: 1; + flex-shrink: 0; + + margin-bottom: 10px; + padding: 0; +} + +.validator-paths-box { + flex-grow: 1; + flex-direction: column; +} + +.validator-paths-scroll-view { + flex-grow: 1; + height: 100px; + margin-left: 3px; +} + +.validator-paths-scroll-view > .unity-scroll-view__content-viewport +{ + margin-left: 1px; +} + +.validator-paths-scroll-view > * > .unity-scroll-view__content-container +{ + padding: 0 0 0 0; +} + +.validator-paths-scroll-view > * > .unity-scroll-view__vertical-scroller +{ + margin: -1px 0; +} + +.validator-paths-scroll-view-bottom-row { + flex-direction: row-reverse; + margin: -1px 0 0 3px; +} + +.validator-paths-add-button { + margin: 3px 0 0 0; + align-self: center; +} + +.validator-paths-path-row { + flex-direction: row; + flex-grow: 1; + + margin-top: 2px; + padding: 0 5px 0 2px; +} + +.validator-paths-path-row-input-field { + flex-grow: 1; + flex-shrink: 1; + + padding-left: 5px; + + white-space: normal; + -unity-text-align: middle-left; +} + +.validator-paths-path-row-remove-button { + width: 20px; + height: 20px; + margin-left: 2px; + margin-right: 1px; + padding: 1px 0 0 0; +} + +/* Tests List & Groups */ + +.validator-test-list { + flex-grow: 1; + flex-shrink: 1; +} + +.validator-test-list-group-separator { + height: 2px; + margin: 5px 15px; +} + +.validator-test-list-group { + overflow: hidden; +} + +.validator-test-list-group-expander { + flex-direction: row; + flex-grow: 0; + flex-shrink: 0; + + align-items: center; + + min-width: 200px; + min-height: 30px; + + margin: 10px -1px 2px -1px; +} + +.validator-test-list-group-expander-arrow { + align-self: center; + + width: 30px; + height: 30px; + + margin: 0; + padding: 0; +} + +.validator-test-list-group-expander-image { + flex-shrink: 0; + flex-grow: 0; + + width: 17px; + height: 17px; + + margin: 0 7px 0 2px; +} + +.validator-test-list-group-expander-label { + font-size: 14px; +} + +.validator-test-list-group-content { + margin: -2px -2px -2px -2px; +} + +/* Validation Test */ + +.validator-test { + flex-direction: column; + flex-shrink: 0; + flex-grow: 0; + + padding: 2px 0; +} + +.validator-test-foldout { + flex-direction: row; + flex-grow: 1; + flex-shrink: 1; + + align-items: center; + justify-content: space-between; + + min-width: 200px; + min-height: 35px; + + margin: 0; + padding: 5px 10px; +} + +.validator-test-expander { + flex-direction: row; + flex-grow: 1; +} + +.validator-test-expander-arrow { + font-size: 11px; + align-self: center; + + width: 30px; + height: 30px; + + margin: 0; + padding: 0; +} + +.validator-text-expander-label { + flex-grow: 1; + flex-shrink: 1; + + -unity-text-align: middle-left; + -unity-font-style: bold; + white-space: normal; +} + +.validator-test-expander-image { + flex-shrink: 0; + + width: 14px; + height: 14px; + + margin: 0 10px; +} + +.validator-test-content { + flex-grow: 1; + flex-shrink: 0; + + margin: 0; + padding: 5px 30px; +} + +.validator-test-content-textfield { + white-space: normal; +} + +.validator-test-content-result-messages { + flex-direction: column; + flex-shrink: 0; + flex-grow: 0; + + margin: 10px 0 5px 0; + padding: 0 3px 3px 3px; +} + +.validator-test-content-result-messages-content { + flex-basis: auto; + flex-direction: column; + + margin-top: 3px; +} + +.validator-test-content-result-messages-content-button { + align-self: stretch; + + -unity-font-style: normal; + -unity-text-align: middle-left; + + margin: 0; +} + +.validator-test-content-result-messages-content-label { + white-space: normal; +} + +.validator-test-content-result-messages-separator { + height: 3px; + margin: 5px -3px 0 -3px; +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Styles/Style.uss.meta b/.github/Asset Store Tools/Validator/Styles/Style.uss.meta new file mode 100644 index 00000000000..f933a2f4f94 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Styles/Style.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c67a10c292c653428af654599fc15aa +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Validator/Styles/ThemeDark.uss b/.github/Asset Store Tools/Validator/Styles/ThemeDark.uss new file mode 100644 index 00000000000..47c13ea1de4 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Styles/ThemeDark.uss @@ -0,0 +1,166 @@ +.primary-colors +{ + /* Light - lighter */ + background-color: rgb(220, 220, 220); + /* Light - middle */ + background-color: rgb(200, 200, 200); + /* Light - darker */ + background-color: rgb(180, 180, 180); + + /* Dark - lighter */ + background-color: rgb(78, 78, 78); + /* Dark - middle */ + background-color: rgb(68, 68, 68); + /* Dark - darker */ + background-color: rgb(58, 58, 58); + + /* Border color - light */ + border-color: rgb(200, 200, 200); + /* Border color - dark */ + border-color: rgb(33, 33, 33); +} + +/* Validator Description */ + +.validator-description-hyperlink-button { + color: rgb(68, 113, 229); + border-width: 0; + background-color: rgba(0, 0, 0, 0); +} + +.validator-description-hyperlink-button:hover { + color: rgb(68, 133, 229); +} + +.validator-description-hyperlink-button:active { + color: rgb(68, 93, 229); +} + +/* Validator Settings */ + +.validator-settings-selection-label-help-row > Image { + --unity-image: resource("d__Help@2x"); +} + +.validator-settings-selection-dropdown { + color: rgb(238, 238, 238); + background-color: rgb(88, 88, 88); + + border-width: 1px; + border-radius: 3px; + border-color: rgb(36, 36, 36); +} + +/* Validation Paths */ + +.validator-paths-scroll-view { + border-width: 1px; + border-color: rgb(33, 33, 33); + background-color: rgb(58, 58, 58); +} + +.validator-paths-scroll-view > * > .unity-scroll-view__vertical-scroller { + border-right-width: 0; +} + +.validator-paths-path-row-input-field:hover { + background-color: rgb(78, 78, 78); +} + +/* Tests List & Groups */ + +.validator-test-list { + flex-grow: 1; + flex-shrink: 1; +} + +.validator-test-list-group-separator { + background-color: rgb(104, 104, 104); +} + +.validator-test-list-group-expander { + border-width: 0; + border-color: rgba(0, 0, 0, 0); + + background-color: rgba(0, 0, 0, 0); +} + +.validator-test-list-group-expander-arrow { + color: rgb(104, 104, 104); +} + +.validator-test-list-group-expander-label { + color: rgb(255, 255, 255); + -unity-font-style: bold; +} + +/* Validation Test */ + +.validator-test-foldout { + border-width: 0; + border-radius: 0; + background-color: rgb(56, 56, 56); +} + +.validator-test-foldout:hover { + background-color: rgb(68, 68, 68); +} + +.validator-test-foldout:active { + background-color: rgb(48, 48, 48); +} + +.validator-test-foldout-expanded { + background-color: rgb(68, 68, 68); +} + +.validator-test-expander-arrow { + color: rgb(104, 104, 104); +} + +.validator-test-content { + background-color: rgb(68, 68, 68); +} + +.validator-test-content-textfield > .unity-base-field__input { + border-width: 0; + border-color: rgba(0, 0, 0, 0); + background-color: rgba(0, 0, 0, 0); +} + +.validator-test-content-result-messages { + border-left-width: 2px; + border-color: rgb(33, 33, 33); + background-color: rgb(58, 58, 58); +} + +.validator-test-content-result-messages-pass { + border-color: rgb(40, 200, 40); +} + +.validator-test-content-result-messages-warning { + border-color: rgb(200, 140, 40); +} + +.validator-test-content-result-messages-fail { + border-color: rgb(200, 40, 40); +} + +.validator-test-content-result-messages-content-button { + border-width: 0; + border-radius: 0; + + background-color: rgba(0, 0, 0, 0); +} + +.validator-test-content-result-messages-content-button:hover { + background-color: rgb(78, 78, 78); +} + +.validator-test-content-result-messages-content-button:active { + background-color: rgba(0, 0, 0, 0); +} + +.validator-test-content-result-messages-separator { + background-color: rgb(68, 68, 68); +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Styles/ThemeDark.uss.meta b/.github/Asset Store Tools/Validator/Styles/ThemeDark.uss.meta new file mode 100644 index 00000000000..fc5c6496077 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Styles/ThemeDark.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d09164f0be2befd40aac764571737ff7 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Validator/Styles/ThemeLight.uss b/.github/Asset Store Tools/Validator/Styles/ThemeLight.uss new file mode 100644 index 00000000000..2c29c721371 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Styles/ThemeLight.uss @@ -0,0 +1,166 @@ +.primary-colors +{ + /* Light - lighter */ + background-color: rgb(220, 220, 220); + /* Light - middle */ + background-color: rgb(200, 200, 200); + /* Light - darker */ + background-color: rgb(180, 180, 180); + + /* Dark - lighter */ + background-color: rgb(50, 50, 50); + /* Dark - middle */ + background-color: rgb(28, 28, 28); + /* Dark - darker */ + background-color: rgb(0, 0, 0); + + /* Border color - light */ + border-color: rgb(200, 200, 200); + /* Border color - dark */ + border-color: rgb(33, 33, 33); +} + +/* Validator Description */ + +.validator-description-hyperlink-button { + color: rgb(68, 113, 229); + border-width: 0; + background-color: rgba(0, 0, 0, 0); +} + +.validator-description-hyperlink-button:hover { + color: rgb(68, 133, 229); +} + +.validator-description-hyperlink-button:active { + color: rgb(68, 93, 229); +} + +/* Validator Settings */ + +.validator-settings-selection-label-help-row > Image { + --unity-image: resource("_Help@2x"); +} + +.validator-settings-selection-dropdown { + color: rgb(9, 9, 9); + background-color: rgb(228, 228, 228); + + border-width: 1px; + border-radius: 3px; + border-color: rgb(178, 178, 178); +} + +/* Validation Paths */ + +.validator-paths-scroll-view { + border-width: 1px; + border-color: rgb(33, 33, 33); + background-color: rgb(180, 180, 180); +} + +.validator-paths-scroll-view > * > .unity-scroll-view__vertical-scroller { + border-right-width: 0; +} + +.validator-paths-path-row-input-field:hover { + background-color: rgb(200, 200, 200); +} + +/* Tests List & Groups */ + +.validator-test-list { + flex-grow: 1; + flex-shrink: 1; +} + +.validator-test-list-group-separator { + background-color: rgb(77, 77, 77); +} + +.validator-test-list-group-expander { + border-width: 0; + border-color: rgba(0, 0, 0, 0); + + background-color: rgba(0, 0, 0, 0); +} + +.validator-test-list-group-expander-arrow { + color: rgb(77, 77, 77); +} + +.validator-test-list-group-expander-label { + color: rgb(48, 48, 48); + -unity-font-style: bold; +} + +/* Validation Test */ + +.validator-test-foldout { + border-width: 0; + border-radius: 0; + background-color: rgb(198, 198, 198); +} + +.validator-test-foldout:hover { + background-color: rgb(212, 212, 212); +} + +.validator-test-foldout:active { + background-color: rgb(180, 180, 180); +} + +.validator-test-foldout-expanded { + background-color: rgb(212, 212, 212); +} + +.validator-test-expander-arrow { + color: rgb(77, 77, 77); +} + +.validator-test-content { + background-color: rgb(212, 212, 212); +} + +.validator-test-content-textfield > .unity-base-field__input { + border-width: 0; + border-color: rgba(0, 0, 0, 0); + background-color: rgba(0, 0, 0, 0); +} + +.validator-test-content-result-messages { + border-left-width: 2px; + border-color: rgb(33, 33, 33); + background-color: rgb(198, 198, 198); +} + +.validator-test-content-result-messages-pass { + border-color: rgb(40, 200, 40); +} + +.validator-test-content-result-messages-warning { + border-color: rgb(200, 140, 40); +} + +.validator-test-content-result-messages-fail { + border-color: rgb(200, 40, 40); +} + +.validator-test-content-result-messages-content-button { + border-width: 0; + border-radius: 0; + + background-color: rgba(0, 0, 0, 0); +} + +.validator-test-content-result-messages-content-button:hover { + background-color: rgb(212, 212, 212); +} + +.validator-test-content-result-messages-content-button:active { + background-color: rgba(0, 0, 0, 0); +} + +.validator-test-content-result-messages-separator { + background-color: rgb(212, 212, 212); +} \ No newline at end of file diff --git a/.github/Asset Store Tools/Validator/Styles/ThemeLight.uss.meta b/.github/Asset Store Tools/Validator/Styles/ThemeLight.uss.meta new file mode 100644 index 00000000000..c3b2570fe48 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Styles/ThemeLight.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7404a65e6f9592846a20fd5190b12b1a +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/.github/Asset Store Tools/Validator/Tests.meta b/.github/Asset Store Tools/Validator/Tests.meta new file mode 100644 index 00000000000..45154436d3e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 82d68ee644bbbb44183019f731e9f205 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic.meta b/.github/Asset Store Tools/Validator/Tests/Generic.meta new file mode 100644 index 00000000000..e10e15e0f15 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 38036e7f211469848b7cf706e3a1febf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Animation Clips.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Animation Clips.asset new file mode 100644 index 00000000000..c9b418f876e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Animation Clips.asset @@ -0,0 +1,27 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Animation Clips + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 31 + Title: Check Animation Clips + Description: Animation Clips should not have the default name 'Take 001'. This + could lead to confusion when filtering assets by animation clips. + CategoryInfo: + IsFailFilter: 0 + IsInclusiveFilter: 0 + AppliesToSubCategories: 1 + Filter: + - Animation + ValidationType: 0 + TestScript: {fileID: 11500000, guid: 7a28985886f182c4bacc89a44777c742, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Animation Clips.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Animation Clips.asset.meta new file mode 100644 index 00000000000..e6479038148 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Animation Clips.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e0426dd01b5136a4ca1d42d312e12fad +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Audio Clipping.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Audio Clipping.asset new file mode 100644 index 00000000000..356c3aed841 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Audio Clipping.asset @@ -0,0 +1,32 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Audio Clipping + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 27 + Title: Check Audio Clipping + Description: 'Audio files should not peak above the defined dB threshold of -0.3db + + + Please + note that lossless audio files that are imported into Unity with their Compression + Format set to anything other than PCM can still fail the Validator. If such cases + arise, please export your audio files with extra headroom, or set the Compression + Format to PCM if applicable.' + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: + - Audio + TestScript: {fileID: 11500000, guid: f604db0353da0cb46bb048f5cd37186f, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Audio Clipping.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Audio Clipping.asset.meta new file mode 100644 index 00000000000..f7b5dcf3d17 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Audio Clipping.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 03c6cd398931b3e41b0784e8589e153f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Colliders.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Colliders.asset new file mode 100644 index 00000000000..079d75b016e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Colliders.asset @@ -0,0 +1,26 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Colliders + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 4 + Title: Check Colliders + Description: Prefabs with meshes inside them have to have colliders applied to + them, if the Prefabs are marked as Static. Please make sure you have appropriately + sized colliders applied to your prefabs. + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: 308b3d7b7a883b949a14f47cfd5c7ebe, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Colliders.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Colliders.asset.meta new file mode 100644 index 00000000000..9b1b8d4eacd --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Colliders.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 28ab5af444cf3c849800ed0d8f4a3102 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Compressed Files.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Compressed Files.asset new file mode 100644 index 00000000000..f8aa71f264f --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Compressed Files.asset @@ -0,0 +1,31 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Compressed Files + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 22 + Title: Check Compressed Files + Description: "Package should not contain nested .unitypackage or archive files + that obscure most of, or the entirety of the content.\n\n.unitypackage files + are acceptable for including setup preferences, settings, supplemental files + for other Asset Store products, or alternative render pipeline content\n\n.zip + files are acceptable if they are compressing files that do not natively function + in the Unity Editor. (For example, Blender, HTML Documentation, or Visual Studio + Projects). Such files should include 'source' in the file name (e.g. \"Blender_source.zip\" + or \"PSDSource.zip\")." + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: 84b23febe0d923842aef73b95da5f25b, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Compressed Files.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Compressed Files.asset.meta new file mode 100644 index 00000000000..dbf11647528 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Compressed Files.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 53189e6e51235b14192c4d5b3145dd27 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Empty Prefabs.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Empty Prefabs.asset new file mode 100644 index 00000000000..6154efa64d8 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Empty Prefabs.asset @@ -0,0 +1,31 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Empty Prefabs + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 5 + Title: Check Empty Prefabs + Description: Prefabs cannot be empty, please make sure that you set up your prefabs. + correctly. + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: + - 2D + - 3D + - Animation + - Essentials + - Templates + - VFX + TestScript: {fileID: 11500000, guid: 8055bed9373283e4793463b90b42f08f, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Empty Prefabs.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Empty Prefabs.asset.meta new file mode 100644 index 00000000000..d2f3da2be81 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Empty Prefabs.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 08790ea0ed0fd274fb1df75ccc32d415 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check File Menu Names.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check File Menu Names.asset new file mode 100644 index 00000000000..ff0f7a461ac --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check File Menu Names.asset @@ -0,0 +1,26 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check File Menu Names + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 21 + Title: Check File Menu Names + Description: File menus should be placed under an existing menu, such as "Window/". + If no existing menus are a good fit, they should be placed under a custom menu + called "Tools". + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: d8e3b12ecc1fcd74d9a9f8d2b549fc63, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check File Menu Names.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check File Menu Names.asset.meta new file mode 100644 index 00000000000..a5a922abbdf --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check File Menu Names.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eaf232919893db04b8e05e91f6815424 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check LODs.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check LODs.asset new file mode 100644 index 00000000000..2b332d74d9a --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check LODs.asset @@ -0,0 +1,33 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check LODs + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 17 + Title: Check LODs + Description: 'Prefabs containing meshes with ''LOD'' in their name must meet the + following requirements: + + - LOD Mesh must be referenced by an LOD Group Component + + - + LOD Mesh GameObject must be a child of an LOD Group Component.' + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: + - 3D + - Essentials + - Templates + TestScript: {fileID: 11500000, guid: 43b2158602f87704fa7b91561cfc8678, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check LODs.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check LODs.asset.meta new file mode 100644 index 00000000000..deb5c41ffeb --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check LODs.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ad52ffa05767e9d4bb4d92093ad68b03 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Line Endings.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Line Endings.asset new file mode 100644 index 00000000000..08e94263619 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Line Endings.asset @@ -0,0 +1,26 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Line Endings + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 20 + Title: Check Line Endings + Description: Inconsistent line endings in scripts might lead to incorrect line + numbers in stacktraces and compiler errors. Many text editors can fix this using + Convert Line Endings menu commands. + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: 85885005d1c594f42826de3555e98365, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Line Endings.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Line Endings.asset.meta new file mode 100644 index 00000000000..699185ff204 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Line Endings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1e7b5480c1d8bda43ab4fa945939e243 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Mesh Prefabs.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Mesh Prefabs.asset new file mode 100644 index 00000000000..3d8a99ab5a8 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Mesh Prefabs.asset @@ -0,0 +1,26 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Mesh Prefabs + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 2 + Title: Check Mesh Prefabs + Description: Each mesh should have a corresponding prefab set up with all variations + of the texture/mesh/material that you are providing. Please create prefabs for + all of your imported objects. + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: 3c3d0d642ac6a6a48aa124a93dae3734, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Mesh Prefabs.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Mesh Prefabs.asset.meta new file mode 100644 index 00000000000..cff122c4871 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Mesh Prefabs.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 03b362b67028eb443b7ba8b84aedd5f2 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Missing Components in Assets.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Missing Components in Assets.asset new file mode 100644 index 00000000000..0a69e354aae --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Missing Components in Assets.asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Missing Components in Assets + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 9 + Title: Check Missing Components in Assets + Description: We do not allow missing or broken material/texture/prefab/script connections + in your package. Please make sure none of your assets have missing components. + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: 22d8f814e2363e34ea220736a4042728, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Missing Components in Assets.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Missing Components in Assets.asset.meta new file mode 100644 index 00000000000..6ba4103ab02 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Missing Components in Assets.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1a3d0b3827fc16347867bee335e8f4ea +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Missing Components in Scenes.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Missing Components in Scenes.asset new file mode 100644 index 00000000000..f5ecb1adf27 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Missing Components in Scenes.asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Missing Components in Scenes + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 10 + Title: Check Missing Components in Scenes + Description: We do not allow missing or broken material/texture/prefab/script connections + in your package. Please make sure none of your scene objects have missing components. + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: 511e76d0ebcb23d40a7b49dda0e2980f, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Missing Components in Scenes.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Missing Components in Scenes.asset.meta new file mode 100644 index 00000000000..42d6127f278 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Missing Components in Scenes.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bc2cb4e6635aa334ea4a52e2e3ce57c8 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Import Logs.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Import Logs.asset new file mode 100644 index 00000000000..f5d9b0f616b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Import Logs.asset @@ -0,0 +1,26 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Model Import Logs + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 32 + Title: Check Model Import Logs + Description: Model assets should work without issues. Please make sure that there + are no errors or warnings when these models are imported. + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + ValidationType: 0 + TestScript: {fileID: 11500000, guid: 98f3ec209166855408eaf4abe5bff591, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Import Logs.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Import Logs.asset.meta new file mode 100644 index 00000000000..ba55c59e6e3 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Import Logs.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c889cdd91c2f41941a14363dad7a1a38 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Orientation.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Orientation.asset new file mode 100644 index 00000000000..dfd8345ece4 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Orientation.asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Model Orientation + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 7 + Title: Check Model Orientation + Description: 'Meshes should be facing the correct way. The proper facing is: Z + vector is forward, Y vector is up, X vector is right.' + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: 56cdcdc41a80fbc46b5b2b83ec8d66d7, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Orientation.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Orientation.asset.meta new file mode 100644 index 00000000000..4f493136ec7 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Orientation.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 45b2b11da67e8864aacc62d928524b4c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Types.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Types.asset new file mode 100644 index 00000000000..e9c9aacd416 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Types.asset @@ -0,0 +1,24 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Model Types + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 23 + Title: Check Model Types + Description: Mesh assets must be either .fbx, .dae, .abc, or .obj file types. + CategoryInfo: + IsFailFilter: 0 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: 428b1fb838e6f5a469bbfd26ca3fbfd2, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Types.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Types.asset.meta new file mode 100644 index 00000000000..c4aef071435 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Model Types.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ffef800a102b0e04cae1a3b98549ef1b +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Normal Map Textures.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Normal Map Textures.asset new file mode 100644 index 00000000000..2f65269ea4b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Normal Map Textures.asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Normal Map Textures + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 26 + Title: Check Normal Map Textures + Description: Textures that are assigned to Materials as Normal Maps should have + their import Texture Type set to 'Normal Map' + CategoryInfo: + IsFailFilter: 0 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: d55cea510248f814eb2194c2b53f88d2, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Normal Map Textures.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Normal Map Textures.asset.meta new file mode 100644 index 00000000000..66ff0da6b0d --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Normal Map Textures.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 241ad0174fcadb64da867011d196acbb +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Package Naming.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Package Naming.asset new file mode 100644 index 00000000000..5c34623f8b3 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Package Naming.asset @@ -0,0 +1,30 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Package Naming + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 33 + Title: Check Package Naming + Description: Your package and its contents should be named appropriately and not + artificially force themselves up the hierarchy within commonly used project folders. + This can happen when a file or a folder name starts with a special character + (such as an underscore). + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 0 + AppliesToSubCategories: 1 + Filter: + - Essentials + - Templates + ValidationType: 0 + TestScript: {fileID: 11500000, guid: afe9e04825c7d904981a54404b222290, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Package Naming.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Package Naming.asset.meta new file mode 100644 index 00000000000..f5e0feab8c5 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Package Naming.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 04098aa074d151b4a908dfa79dfddec3 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Particle Systems.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Particle Systems.asset new file mode 100644 index 00000000000..62d55cdebdf --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Particle Systems.asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Particle Systems + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 25 + Title: Check Particle Systems + Description: All Particle Systems should be saved as Prefabs + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: + - VFX + TestScript: {fileID: 11500000, guid: 6a623f7988c75884bb17b169ccd3e993, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Particle Systems.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Particle Systems.asset.meta new file mode 100644 index 00000000000..5404fd491fb --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Particle Systems.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 87da7eaed3cee0d4b8ada0b500e3a958 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Path Lengths.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Path Lengths.asset new file mode 100644 index 00000000000..676ae48f61b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Path Lengths.asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Path Lengths + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 28 + Title: Check Path Lengths + Description: Package content file paths should not be excessively lengthened. File + paths for assets must be under 140 characters + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: ae379305e9165e84584373a8272c09e7, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Path Lengths.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Path Lengths.asset.meta new file mode 100644 index 00000000000..4ebd5aceed8 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Path Lengths.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 21f8ec0602ffac045b1f4a93f8a9b555 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Prefab Transforms.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Prefab Transforms.asset new file mode 100644 index 00000000000..2cf336b9d90 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Prefab Transforms.asset @@ -0,0 +1,28 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Prefab Transforms + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 3 + Title: Check Prefab Transforms + Description: Prefabs must have their position/rotation set to 0, and their scale + set to 1. + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: + - 3D + - Essentials + - Templates + TestScript: {fileID: 11500000, guid: f712c17a60bf2d049a4e61c8f79e56c2, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Prefab Transforms.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Prefab Transforms.asset.meta new file mode 100644 index 00000000000..713d9087b80 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Prefab Transforms.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 700026f446833f649a3c63b33a90a295 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Script Compilation.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Script Compilation.asset new file mode 100644 index 00000000000..fae309f1c00 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Script Compilation.asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Script Compilation + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 29 + Title: Check Script Compilation + Description: Scripts in the package must compile successfully and not result in + compilation errors + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: 59db88f43969db8499299bce7f4fb967, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Script Compilation.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Script Compilation.asset.meta new file mode 100644 index 00000000000..3a026fb4d03 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Script Compilation.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 339e21c955642a04289482aa923e10b6 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Shader Compilation.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Shader Compilation.asset new file mode 100644 index 00000000000..dcbd6093f72 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Shader Compilation.asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Shader Compilation + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 18 + Title: Check Shader Compilation + Description: Please make sure the shaders inside your package do not have errors + and compile successfully. + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: 7abb278a6082bde4391e0779394cb85b, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Shader Compilation.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Shader Compilation.asset.meta new file mode 100644 index 00000000000..c9ebccf60c5 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Shader Compilation.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1450037453608204a989ff95dca62fae +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Texture Dimensions.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Texture Dimensions.asset new file mode 100644 index 00000000000..d676f146deb --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Texture Dimensions.asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Texture Dimensions + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 24 + Title: Check Texture Dimensions + Description: Dimensions of textures, where appropriate, should have pixel counts + that are a power of 2 + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: 073f1dacf3da34d4783140ae9d485d5f, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Texture Dimensions.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Texture Dimensions.asset.meta new file mode 100644 index 00000000000..d0318d49fe3 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Texture Dimensions.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c23253393b8e28846b8e02aeaee7e152 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Type Namespaces.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Check Type Namespaces.asset new file mode 100644 index 00000000000..8331d60684e --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Type Namespaces.asset @@ -0,0 +1,31 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Type Namespaces + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 19 + Title: Check Type Namespaces + Description: 'Types in your scripts (classes, interfaces, structs, enums) should + be nested under a namespace block to prevent them from being mistaken for other + potential types and for better organization as a whole. + + + It is not allowed + to nest your code under a Unity namespace.' + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + ValidationType: 0 + TestScript: {fileID: 11500000, guid: 279249fa7ef8c2446b3a9f013eeedbf0, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Check Type Namespaces.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Check Type Namespaces.asset.meta new file mode 100644 index 00000000000..2aa250d34e4 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Check Type Namespaces.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dd110ee16e8de4d48a602349ed7a0b25 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Remove Executable Files.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Executable Files.asset new file mode 100644 index 00000000000..1a00c2fa6dd --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Executable Files.asset @@ -0,0 +1,27 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Remove Executable Files + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 14 + Title: Remove Executable Files + Description: Your package must not contain an .exe, installer programs or applications. + If your plugin requires an external program to run, please remove the installer + program from your package and write the instructions on how to download and install + the installer program in your documentation. + CategoryInfo: + IsFailFilter: 0 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: 8e4450592cc60e54286ad089b66db94d, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Remove Executable Files.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Executable Files.asset.meta new file mode 100644 index 00000000000..b27033d7d23 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Executable Files.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e996c53186de96e49a742d414648a809 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Remove JPG Files.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Remove JPG Files.asset new file mode 100644 index 00000000000..cd2900fc6fc --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Remove JPG Files.asset @@ -0,0 +1,30 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Remove JPG Files + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 8 + Title: Remove JPG Files + Description: We do not allow texture images that are saved in lossy formats. Please + save all of your images as lossless format file types, such as PNG, TGA, or PSD. + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: + - 2D + - 3D + - Animation + - Essentials + - VFX/Shaders + TestScript: {fileID: 11500000, guid: 5634a12b3a8544c4585bbc280ae59ce2, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Remove JPG Files.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Remove JPG Files.asset.meta new file mode 100644 index 00000000000..45000c99714 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Remove JPG Files.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 781021ae3aa6570468e08d78e3195127 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Remove JavaScript Files.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Remove JavaScript Files.asset new file mode 100644 index 00000000000..bc5102e00f9 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Remove JavaScript Files.asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Remove JavaScript Files + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 11 + Title: Remove JavaScript Files + Description: JavaScript / UnityScript files are not allowed, as they are no longer + supported. + CategoryInfo: + IsFailFilter: 0 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: ab1676bde9afba442b35fd3319c18063, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Remove JavaScript Files.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Remove JavaScript Files.asset.meta new file mode 100644 index 00000000000..d41b9e65071 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Remove JavaScript Files.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bf01c18b66907f54c99517f6a877e3e0 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Remove Lossy Audio Files.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Lossy Audio Files.asset new file mode 100644 index 00000000000..954f6fd6b6b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Lossy Audio Files.asset @@ -0,0 +1,32 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Remove Lossy Audio Files + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 12 + Title: Remove Lossy Audio Files + Description: 'We do not recommend audio files that are saved as .mp3 or .ogg. Please + save all of your audio as lossless format file types, such as .wav. If you have + non-lossy alternative files to your lossy audio files, please make sure to name + them identically to avoid rejection. For example: ''shout.mp3'', ''shout.wav''. + Listing of the format in the file name is also allowed and accounted for by the + validator. For example: ''taunt MP3.mp3'', ''taunt WAV.wav''.' + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: + - Audio + - Essentials + - Templates + TestScript: {fileID: 11500000, guid: b7205a85061273a4eb50586f13f35d35, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Remove Lossy Audio Files.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Lossy Audio Files.asset.meta new file mode 100644 index 00000000000..2026425caae --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Lossy Audio Files.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a48657926de5cfb47ac559a7108d03ee +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Remove Mixamo Files.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Mixamo Files.asset new file mode 100644 index 00000000000..8d3cc14aefd --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Mixamo Files.asset @@ -0,0 +1,26 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Remove Mixamo Files + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 15 + Title: Remove Mixamo Files + Description: We do not allow or accept packages files that were made with third-party + software, such as Mixamo, Fuse, etc. because these files are under licensing + that does not agree with the Asset Store End User License Agreement (EULA). + CategoryInfo: + IsFailFilter: 0 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: 9df432e52aa958b44bb5e20c13d16552, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Remove Mixamo Files.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Mixamo Files.asset.meta new file mode 100644 index 00000000000..84abdb1b321 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Mixamo Files.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a0a44055f786ec64f86a07a214d5f831 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Remove SpeedTree Files.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Remove SpeedTree Files.asset new file mode 100644 index 00000000000..14ff13fee65 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Remove SpeedTree Files.asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Remove SpeedTree Files + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 16 + Title: Remove SpeedTree Files + Description: You cannot redistribute SpeedTree files on other marketplaces. Please + remove all SpeedTree files that are in this package. + CategoryInfo: + IsFailFilter: 0 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: e06bb7e0aa4f9944abc18281c002dff4, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Remove SpeedTree Files.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Remove SpeedTree Files.asset.meta new file mode 100644 index 00000000000..ffc10af27b1 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Remove SpeedTree Files.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 305bbe67f7c644d18bc8a5b2273aa6a4 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Remove Video Files.asset b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Video Files.asset new file mode 100644 index 00000000000..1df19b9dcd0 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Video Files.asset @@ -0,0 +1,26 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Remove Video Files + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 13 + Title: Remove Video Files + Description: You cannot include a video file in your package. Please upload your + video file to an online video hosting website (Youtube, Vimeo, etc.) and include + the link to the video in your written documentation. + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + TestScript: {fileID: 11500000, guid: f99724c71b0de66419b5d6e8e9bfcc2d, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/Generic/Remove Video Files.asset.meta b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Video Files.asset.meta new file mode 100644 index 00000000000..e62946cea75 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/Generic/Remove Video Files.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 893a0df188c2026438be48eed39b301f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/UnityPackage.meta b/.github/Asset Store Tools/Validator/Tests/UnityPackage.meta new file mode 100644 index 00000000000..0a50cef4a08 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/UnityPackage.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8e978e836f2fb224fa11de94e913da49 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Demo Scenes.asset b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Demo Scenes.asset new file mode 100644 index 00000000000..af6ecc2b4ee --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Demo Scenes.asset @@ -0,0 +1,34 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Demo Scenes + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 1 + Title: Check Demo Scenes + Description: If your product has content to show off, it should be displayed in + a demo scene. Please provide a practical demo with all of your assets set up. + If your asset is based on scripting or Editor extensions, please consider adding + a demo scene showcasing the asset or showing setup steps in the scene. + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: + - 3D + - 2D + - Animation + - Essentials + - Templates + - VFX + ValidationType: 1 + TestScript: {fileID: 11500000, guid: f844c2dfa4669ff4eacf5591b544edaf, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Demo Scenes.asset.meta b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Demo Scenes.asset.meta new file mode 100644 index 00000000000..d58914fe8c6 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Demo Scenes.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f108107be07f69045813d69eff580078 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Documentation.asset b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Documentation.asset new file mode 100644 index 00000000000..6ce011e342b --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Documentation.asset @@ -0,0 +1,32 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Documentation + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 6 + Title: Check Documentation + Description: If your asset contains any code (scripts, shaders) - we ask that you + include offline documentation in the format of pdf or rtf with your submission, + as it is mandatory for all packages that include scripts or other components + that require set up. Your documentation must be organized with a table of contents + and numbered, written in English and have no grammar mistakes. Create a setup + guide with a step-by-step tutorial (pdf or video), as well as a script reference + if users will need to do any coding. If your asset contains art (3D models, sprites) + and you used code to set up a demo scene, you may skip this step. + CategoryInfo: + IsFailFilter: 1 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: [] + ValidationType: 1 + TestScript: {fileID: 11500000, guid: 3c8425198983eda4c9b35aa0d59ea33c, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Documentation.asset.meta b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Documentation.asset.meta new file mode 100644 index 00000000000..4ee63356a86 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Documentation.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b03433f7977b29e4ca7e8d76393a6c26 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Package Size.asset b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Package Size.asset new file mode 100644 index 00000000000..e80b2c457a4 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Package Size.asset @@ -0,0 +1,25 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Package Size + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 30 + Title: Check Package Size + Description: Package submissions should not be more than 6 GB in size + CategoryInfo: + IsFailFilter: 0 + IsInclusiveFilter: 0 + AppliesToSubCategories: 1 + Filter: [] + ValidationType: 1 + TestScript: {fileID: 11500000, guid: a8601b99f4afa5049954f3a2dd5996d6, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Package Size.asset.meta b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Package Size.asset.meta new file mode 100644 index 00000000000..63433dd52d0 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Package Size.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 25721b2d7384e5b4f936cf3b33b80a02 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Project Template Assets.asset b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Project Template Assets.asset new file mode 100644 index 00000000000..645117c3845 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Project Template Assets.asset @@ -0,0 +1,36 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d813ff809ae82f643bf975031305d541, type: 3} + m_Name: Check Project Template Assets + m_EditorClassIdentifier: + HasBeenInitialized: 1 + Id: 34 + Title: Check Project Template Assets + Description: 'Assets in your package derived from project template assets should + be assigned a new GUID and be located under a different path. + + + This is to + minimize the risk of users accidentally overwriting their own assets when importing + your package. + + + Example commonly used asset: ''Assets/Scenes/SampleScene.unity''' + CategoryInfo: + IsFailFilter: 0 + IsInclusiveFilter: 1 + AppliesToSubCategories: 1 + Filter: + - Essentials + - Templates + ValidationType: 1 + TestScript: {fileID: 11500000, guid: f02d52a702c712e4e8089f7c2e65bae7, type: 3} diff --git a/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Project Template Assets.asset.meta b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Project Template Assets.asset.meta new file mode 100644 index 00000000000..a00554e8f90 --- /dev/null +++ b/.github/Asset Store Tools/Validator/Tests/UnityPackage/Check Project Template Assets.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5392e9de0549574419ff76897d1e0fa1 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/.github/UnityPublish.csx b/.github/UnityPublish.csx new file mode 100644 index 00000000000..8b4a0ee72b5 --- /dev/null +++ b/.github/UnityPublish.csx @@ -0,0 +1,137 @@ +#!/usr/bin/env dotnet-script +using System; +using System.Net.Http; +using System.Threading.Tasks; +using System.Text.Json; + +const string LOGIN_URL = "https://api.assetstore.unity3d.com/publisher/v1/session"; // From Constants.Api.SessionUrl +const string UPLOAD_URL = "https://api.assetstore.unity3d.com/publisher/v1/package/upload"; // From Constants.Api.UploadUnityPackageUrl +const string LOG_PREFIX = "UnityPublish: "; + +const string ApiVersion = "V12.0.1"; // This should be the version of your API +const string UnityVersion = "2021.3.35f1"; // This should be the version of Unity you are using + +var username = Environment.GetEnvironmentVariable("UNITY_USERNAME"); +var password = Environment.GetEnvironmentVariable("UNITY_PASSWORD"); +var packagePath = Environment.GetEnvironmentVariable("PACKAGE_PATH"); +var licenseHash = Environment.GetEnvironmentVariable("PUBLISH_LICENSE_HASH"); +var hardwareHash = Environment.GetEnvironmentVariable("PUBLISH_HARDWARE_HASH"); +var version = Environment.GetEnvironmentVariable("RELEASE_VERSION"); + +if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(packagePath)) +{ + Console.WriteLine($"{LOG_PREFIX}Missing required environment variables (username, password, or package path)."); + Environment.Exit(1); +} +if (string.IsNullOrEmpty(licenseHash) || string.IsNullOrEmpty(hardwareHash)) +{ + Console.WriteLine($"{LOG_PREFIX}Missing required environment variables (license_hash or hardware_hash)."); + Environment.Exit(1); +} +if (string.IsNullOrEmpty(version)) +{ + Console.WriteLine($"{LOG_PREFIX}Missing required environment variable (RELEASE_VERSION)."); + Environment.Exit(1); +} + +using (var client = new HttpClient()) +{ + client.DefaultRequestHeaders.ConnectionClose = false; + client.DefaultRequestHeaders.Add("Accept", "application/json"); + client.Timeout = TimeSpan.FromSeconds(1320); + await Main(client); +} + +async Task Main(HttpClient client) +{ + Console.WriteLine($"{LOG_PREFIX}Starting Unity publishing process..."); + + // Step 1: Login + Console.WriteLine($"{LOG_PREFIX}Attempting login to Unity Publisher..."); + string sessionId = await LoginAsync(client, username, password, licenseHash, hardwareHash); + if (string.IsNullOrEmpty(sessionId)) + { + Console.WriteLine($"{LOG_PREFIX}Login failed. Aborting."); + Environment.Exit(1); + } + Console.WriteLine($"{LOG_PREFIX}Login successful: {sessionId}"); + + // Set session ID in headers (mimicking SetSessionId) + client.DefaultRequestHeaders.Add("X-Unity-Session", sessionId); + + // Step 2: Upload Package + //Console.WriteLine($"{LOG_PREFIX}Uploading package: {packagePath} (version: {version})"); + //bool uploadSuccess = await UploadPackageAsync(client, packagePath, version); + //if (uploadSuccess) + //{ + // Console.WriteLine($"{LOG_PREFIX}Package uploaded successfully!"); + //} + //else + //{ + // Console.WriteLine($"{LOG_PREFIX}Upload failed."); + // Environment.Exit(1); + //} +} + +async Task LoginAsync(HttpClient client, string user, string pass, string license, string hardware) +{ + try + { + var loginPayload = new FormUrlEncodedContent(new[] + { + new KeyValuePair("unityversion", UnityVersion), + new KeyValuePair("toolversion", ApiVersion), + new KeyValuePair("license_hash", license), + new KeyValuePair("hardware_hash", hardware), + new KeyValuePair("user", user), + new KeyValuePair("pass", pass) + }); + + var response = await client.PostAsync(LOGIN_URL, loginPayload); + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + Console.WriteLine($"{LOG_PREFIX}Login request failed with status: {response.StatusCode}"); + Console.WriteLine($"-- {errorContent}"); + return null; + } + + var responseContent = await response.Content.ReadAsStringAsync(); + Console.WriteLine($"{LOG_PREFIX}Login response: {responseContent}"); + + var json = JsonSerializer.Deserialize>(responseContent); + return json.TryGetValue("sessionId", out var sessionId) ? sessionId : null; // Updated to sessionId + } + catch (Exception ex) + { + Console.WriteLine($"{LOG_PREFIX}Login error: {ex.Message}"); + return null; + } +} + +//async Task UploadPackageAsync(HttpClient client, string packagePath, string version) +//{ +// try +// { +// using var fileStream = File.OpenRead(packagePath); +// using var content = new MultipartFormDataContent(); +// content.Add(new StreamContent(fileStream), "file", Path.GetFileName(packagePath)); +// content.Add(new StringContent(version), "version"); + +// var response = await client.PostAsync(UPLOAD_URL, content); +// if (response.IsSuccessStatusCode) +// { +// return true; +// } +// else +// { +// Console.WriteLine($"{LOG_PREFIX}Upload failed with status: {response.StatusCode}"); +// return false; +// } +// } +// catch (Exception ex) +// { +// Console.WriteLine($"{LOG_PREFIX}Upload error: {ex.Message}"); +// return false; +// } +//} diff --git a/.github/workflows/RunUnityTests.yml b/.github/workflows/RunUnityTests.yml index 8bf19fb5fa6..f5c8daf72f3 100644 --- a/.github/workflows/RunUnityTests.yml +++ b/.github/workflows/RunUnityTests.yml @@ -16,7 +16,7 @@ jobs: - 2021.3.45f1 - 2022.3.60f1 - 2023.2.20f1 - - 6000.0.44f1 + - 6000.0.46f1 steps: - name: Checkout repository diff --git a/.github/workflows/UnityPublish.yml b/.github/workflows/UnityPublish.yml new file mode 100644 index 00000000000..71bd2d9be11 --- /dev/null +++ b/.github/workflows/UnityPublish.yml @@ -0,0 +1,67 @@ +name: Unity Publish + +on: + workflow_dispatch: + inputs: + unity_username: + description: 'Unity Publishing Username' + required: true + unity_password: + description: 'Unity Publishing Password' + required: true + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Mask sensitive inputs securely + run: | + UNITY_USERNAME=$(jq -r '.inputs.unity_username' $GITHUB_EVENT_PATH) + UNITY_PASSWORD=$(jq -r '.inputs.unity_password' $GITHUB_EVENT_PATH) + echo "::add-mask::$UNITY_USERNAME" + echo "::add-mask::$UNITY_PASSWORD" + + - name: Install dotnet-script + run: | + dotnet tool install -g dotnet-script + dotnet script --version + + - name: Get Latest Release + id: get_release + uses: actions/github-script@v6 + with: + script: | + const latestRelease = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo + }); + + const packageAsset = latestRelease.data.assets.find(asset => asset.name.endsWith('.unitypackage')); + if (!packageAsset) { + throw new Error('No .unitypackage asset found in the latest release.'); + } + + const version = latestRelease.data.tag_name.replace(/^v/, ''); + + // Set individual outputs + core.setOutput('url', packageAsset.browser_download_url); + core.setOutput('name', packageAsset.name); + core.setOutput('version', version); + + - name: Download Unity Package + run: | + curl -L -o "${{ steps.get_release.outputs.name }}" "${{ steps.get_release.outputs.url }}" + + - name: Run UnityPublish.csx + env: + UNITY_USERNAME: ${{ github.event.inputs.unity_username }} + UNITY_PASSWORD: ${{ github.event.inputs.unity_password }} + PACKAGE_PATH: ${{ steps.get_release.outputs.name }} + PUBLISH_LICENSE_HASH: ${{ secrets.PUBLISH_LICENSE_HASH }} + PUBLISH_HARDWARE_HASH: ${{ secrets.PUBLISH_HARDWARE_HASH }} + RELEASE_VERSION: ${{ steps.get_release.outputs.version }} + run: | + dotnet script .github/UnityPublish.csx diff --git a/.releaserc.yml b/.releaserc.yml index c40a5daadaa..270a4780de4 100644 --- a/.releaserc.yml +++ b/.releaserc.yml @@ -11,12 +11,14 @@ plugins: - - '@semantic-release/commit-analyzer' - preset: "angular" releaseRules: - - type: "breaking" - release: "major" - type: "release" release: "major" + - type: "breaking" + release: "minor" - type: "feature" release: "minor" + - type: "fix" + release: "patch" # Extends the behavior of semantic-release to generate release notes. # 'fix' and 'feat' are built in and don't need to be defined here. diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs index 29bd6ce6176..1b40e3e41c2 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs @@ -36,13 +36,13 @@ internal HexGrid2D(ushort visRange) // Precomputed array of neighbor offsets as Cell2D structs (center + 6 neighbors) static readonly Cell2D[] neighborCellsBase = new Cell2D[] { - new Cell2D(0, 0), // Center - new Cell2D(1, -1), // Top-right - new Cell2D(1, 0), // Right - new Cell2D(0, 1), // Bottom-right - new Cell2D(-1, 1), // Bottom-left - new Cell2D(-1, 0), // Left - new Cell2D(0, -1) // Top-left + new Cell2D(0, 0), // Center + new Cell2D(1, -1), // Top-right + new Cell2D(1, 0), // Right + new Cell2D(0, 1), // Bottom-right + new Cell2D(-1, 1), // Bottom-left + new Cell2D(-1, 0), // Left + new Cell2D(0, -1) // Top-left }; // Converts a grid cell (q, r) to a world position (x, z) diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs index b7a0fd36243..407de42c713 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs @@ -43,19 +43,19 @@ internal HexGrid3D(ushort visRange, ushort height) // Precomputed array of neighbor offsets as Cell3D structs (center + 6 per layer x 3 layers) static readonly Cell3D[] neighborCellsBase = new Cell3D[] { - // Center - new Cell3D(0, 0, 0), - // Upper layer (1) and its 6 neighbors - new Cell3D(0, 0, 1), - new Cell3D(1, -1, 1), new Cell3D(1, 0, 1), new Cell3D(0, 1, 1), - new Cell3D(-1, 1, 1), new Cell3D(-1, 0, 1), new Cell3D(0, -1, 1), - // Same layer (0) - 6 neighbors - new Cell3D(1, -1, 0), new Cell3D(1, 0, 0), new Cell3D(0, 1, 0), - new Cell3D(-1, 1, 0), new Cell3D(-1, 0, 0), new Cell3D(0, -1, 0), - // Lower layer (-1) and its 6 neighbors - new Cell3D(0, 0, -1), - new Cell3D(1, -1, -1), new Cell3D(1, 0, -1), new Cell3D(0, 1, -1), - new Cell3D(-1, 1, -1), new Cell3D(-1, 0, -1), new Cell3D(0, -1, -1) + // Center + new Cell3D(0, 0, 0), + // Upper layer (1) and its 6 neighbors + new Cell3D(0, 0, 1), + new Cell3D(1, -1, 1), new Cell3D(1, 0, 1), new Cell3D(0, 1, 1), + new Cell3D(-1, 1, 1), new Cell3D(-1, 0, 1), new Cell3D(0, -1, 1), + // Same layer (0) - 6 neighbors + new Cell3D(1, -1, 0), new Cell3D(1, 0, 0), new Cell3D(0, 1, 0), + new Cell3D(-1, 1, 0), new Cell3D(-1, 0, 0), new Cell3D(0, -1, 0), + // Lower layer (-1) and its 6 neighbors + new Cell3D(0, 0, -1), + new Cell3D(1, -1, -1), new Cell3D(1, 0, -1), new Cell3D(0, 1, -1), + new Cell3D(-1, 1, -1), new Cell3D(-1, 0, -1), new Cell3D(0, -1, -1) }; // Converts a grid cell (q, r, layer) to a world position (x, y, z) diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs index a394ce0f8c0..e5d8aa5795b 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs @@ -207,9 +207,8 @@ void AddToCell(Cell2D cell, NetworkIdentity identity) // If the cell doesn't exist in the array yet, fetch or create a new set from the pool if (cells[index] == null) - { cells[index] = cellPool.Count > 0 ? cellPool.Pop() : new HashSet(); - } + cells[index].Add(identity); } @@ -257,14 +256,13 @@ public override void ResetState() lastRebuildTime = 0; // Clear and return all cell sets to the pool for (int i = 0; i < cells.Count; i++) - { if (cells[i] != null) { cells[i].Clear(); cellPool.Push(cells[i]); cells[i] = null; } - } + lastIdentityPositions.Clear(); lastConnectionPositions.Clear(); connectionObservers.Clear(); @@ -306,38 +304,37 @@ int GetCellIndex(Cell2D cell) // Draws debug gizmos in the Unity Editor to visualize the 2D grid void OnDrawGizmos() { + // Only draw if there’s a local player to base the visualization on + if (NetworkClient.localPlayer == null) return; + // Initialize the grid if it hasn’t been created yet (e.g., before Awake) if (grid == null) grid = new HexGrid2D(visRange); - // Only draw if there’s a local player to base the visualization on - if (NetworkClient.localPlayer != null) - { - Vector3 playerPosition = NetworkClient.localPlayer.transform.position; + Vector3 playerPosition = NetworkClient.localPlayer.transform.position; - // Convert to grid cell using the full Vector3 for proper plane projection - Vector2 projectedPos = ProjectToGrid(playerPosition); - Cell2D playerCell = grid.WorldToCell(projectedPos); + // Convert to grid cell using the full Vector3 for proper plane projection + Vector2 projectedPos = ProjectToGrid(playerPosition); + Cell2D playerCell = grid.WorldToCell(projectedPos); - // Get all visible cells around the player into the pre-allocated array - grid.GetNeighborCells(playerCell, neighborCells); + // Get all visible cells around the player into the pre-allocated array + grid.GetNeighborCells(playerCell, neighborCells); - // Set gizmo color for visibility - Gizmos.color = Color.cyan; + // Set gizmo color for visibility + Gizmos.color = Color.cyan; - // Draw each visible cell as a 2D hexagon, oriented based on checkMethod - for (int i = 0; i < neighborCells.Length; i++) - { - // Convert cell to world coordinates (2D) - Vector2 worldPos2D = grid.CellToWorld(neighborCells[i]); + // Draw each visible cell as a 2D hexagon, oriented based on checkMethod + for (int i = 0; i < neighborCells.Length; i++) + { + // Convert cell to world coordinates (2D) + Vector2 worldPos2D = grid.CellToWorld(neighborCells[i]); - // Convert to 3D position based on checkMethod - Vector3 worldPos = checkMethod == CheckMethod.XZ_FOR_3D - ? new Vector3(worldPos2D.x, 0, worldPos2D.y) // XZ plane, flat - : new Vector3(worldPos2D.x, worldPos2D.y, 0); // XY plane, vertical + // Convert to 3D position based on checkMethod + Vector3 worldPos = checkMethod == CheckMethod.XZ_FOR_3D + ? new Vector3(worldPos2D.x, 0, worldPos2D.y) // XZ plane, flat + : new Vector3(worldPos2D.x, worldPos2D.y, 0); // XY plane, vertical - grid.DrawHexGizmo(worldPos, grid.cellRadius, checkMethod); - } + grid.DrawHexGizmo(worldPos, grid.cellRadius, checkMethod); } } #endif diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs index 64415da74c5..03d00b6c188 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs @@ -54,10 +54,10 @@ public class HexSpatialHash3DInterestManagement : InterestManagement readonly HashSet staticObjects = new HashSet(); // Scene bounds: ±9 km (18 km total) in each dimension - const int MAX_Q = 19; // Covers -9 to 9 (~18 km) - const int MAX_R = 23; // Covers -11 to 11 (~18 km) + const int MAX_Q = 19; // Covers -9 to 9 (~18km) + const int MAX_R = 23; // Covers -11 to 11 (~18km) const int LAYER_OFFSET = 18; // Offset for -18 to 17 layers - const int MAX_LAYERS = 36; // Total layers for ±9 km (18 km) + const int MAX_LAYERS = 36; // Total layers for +/-9km (~18km total) const ushort MAX_AREA = 9000; // Maximum area in meters void Awake() @@ -197,9 +197,8 @@ void AddToCell(Cell3D cell, NetworkIdentity identity) // If the cell doesn't exist in the array yet, fetch or create a new set from the pool if (cells[index] == null) - { cells[index] = cellPool.Count > 0 ? cellPool.Pop() : new HashSet(); - } + cells[index].Add(identity); } @@ -247,14 +246,13 @@ public override void ResetState() lastRebuildTime = 0; // Clear and return all cell sets to the pool for (int i = 0; i < cells.Count; i++) - { if (cells[i] != null) { cells[i].Clear(); cellPool.Push(cells[i]); cells[i] = null; } - } + lastIdentityPositions.Clear(); lastConnectionPositions.Clear(); connectionObservers.Clear(); @@ -298,36 +296,35 @@ int GetCellIndex(Cell3D cell) // Draws debug gizmos in the Unity Editor to visualize the grid void OnDrawGizmos() { + // Only draw if there's a local player to base the visualization on + if (NetworkClient.localPlayer == null) return; + // Initialize the grid if it hasn't been created yet (e.g., before Awake) if (grid == null) grid = new HexGrid3D(visRange, cellHeight); - // Only draw if there's a local player to base the visualization on - if (NetworkClient.localPlayer != null) - { - Vector3 playerPosition = NetworkClient.localPlayer.transform.position; + Vector3 playerPosition = NetworkClient.localPlayer.transform.position; - // Convert to grid cell - Cell3D playerCell = grid.WorldToCell(playerPosition); + // Convert to grid cell + Cell3D playerCell = grid.WorldToCell(playerPosition); - // Get all visible cells around the player into the pre-allocated array - grid.GetNeighborCells(playerCell, neighborCells); + // Get all visible cells around the player into the pre-allocated array + grid.GetNeighborCells(playerCell, neighborCells); - // Set default gizmo color (though overridden per cell) - Gizmos.color = Color.cyan; + // Set default gizmo color (though overridden per cell) + Gizmos.color = Color.cyan; - // Draw each visible cell as a hexagonal prism - for (int i = 0; i < neighborCells.Length; i++) - { - // Convert cell to world coordinates - Vector3 worldPos = grid.CellToWorld(neighborCells[i]); + // Draw each visible cell as a hexagonal prism + for (int i = 0; i < neighborCells.Length; i++) + { + // Convert cell to world coordinates + Vector3 worldPos = grid.CellToWorld(neighborCells[i]); - // Determine the layer relative to the player's cell for color coding - int relativeLayer = neighborCells[i].layer - playerCell.layer; + // Determine the layer relative to the player's cell for color coding + int relativeLayer = neighborCells[i].layer - playerCell.layer; - // Draw the hexagonal cell with appropriate color based on layer - grid.DrawHexGizmo(worldPos, grid.cellRadius, grid.cellHeight, relativeLayer); - } + // Draw the hexagonal cell with appropriate color based on layer + grid.DrawHexGizmo(worldPos, grid.cellRadius, grid.cellHeight, relativeLayer); } } diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs index 49b1e687715..ee4b277e018 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs @@ -11,19 +11,19 @@ namespace Mirror [AddComponentMenu("Network/ Interest Management/ Spatial Hash/Spatial Hashing Interest Management")] public class SpatialHashing3DInterestManagement : InterestManagement { - [Tooltip("The maximum range that objects will be visible at.")] - public int visRange = 30; + [Tooltip("The maximum range that objects will be visible.\nSet to 10-20% larger than camera far clip plane")] + public int visRange = 1200; // we use a 9 neighbour grid. // so we always see in a distance of 2 grids. // for example, our own grid and then one on top / below / left / right. // // this means that grid resolution needs to be distance / 2. - // so for example, for distance = 30 we see 2 cells = 15 * 2 distance. + // so for example, for distance = 1200 we see 2 cells = 600 * 2 distance. // // on first sight, it seems we need distance / 3 (we see left/us/right). // but that's not the case. - // resolution would be 10, and we only see 1 cell far, so 10+10=20. + // resolution would be 400, and we only see 1 cell far, so 400+400=800. public int resolution => visRange / 2; // same as XY because if XY is rotated 90 degree for 3D, it's still the same distance [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] @@ -77,7 +77,7 @@ internal void Update() // NOTE: unlike Scene/MatchInterestManagement, this rebuilds ALL // entities every INTERVAL. consider the other approach later. - // IMPORTANT: refresh grid every update! + // Old Notes: refresh grid every update! // => newly spawned entities get observers assigned via // OnCheckObservers. this can happen any time and we don't want // them broadcast to old (moved or destroyed) connections. @@ -85,14 +85,33 @@ internal void Update() // correct grid position. // => note that the actual 'rebuildall' doesn't need to happen all // the time. - // NOTE: consider refreshing grid only every 'interval' too. but not - // for now. stability & correctness matter. + + // Updated Notes: refresh grid and RebuildAll every interval. + // Real world application would have visRange larger than camera + // far clip plane, e.g. 1200, and a player movement speed of ~10m/sec + // so they typically won't cross cell boundaries quickly. If users notice + // flickering or mis-positioning, they can decrease the interval. // clear old grid results before we update everyone's position. // (this way we get rid of destroyed connections automatically) // // NOTE: keeps allocated HashSets internally. // clearing & populating every frame works without allocations + + // rebuild all spawned entities' observers every 'interval' + // this will call OnRebuildObservers which then returns the + // observers at grid[position] for each entity. + if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval) + { + RefreshGrid(); + RebuildAll(); + lastRebuildTime = NetworkTime.localTime; + } + } + + // (internal so we can update from tests) + internal void RefreshGrid() + { grid.ClearNonAlloc(); // put every connection into the grid at it's main player's position @@ -109,18 +128,9 @@ internal void Update() grid.Add(position, connection); } } - - // rebuild all spawned entities' observers every 'interval' - // this will call OnRebuildObservers which then returns the - // observers at grid[position] for each entity. - if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval) - { - RebuildAll(); - lastRebuildTime = NetworkTime.localTime; - } } -#if !UNITY_SERVER && (UNITY_EDITOR || DEVELOPMENT_BUILD) +#if !UNITY_SERVER && DEBUG // OnGUI allocates even if it does nothing. avoid in release. // slider from dotsnet. it's nice to play around with in the benchmark // demo. diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs index 0930112ca35..9631f6ed904 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs @@ -11,19 +11,19 @@ namespace Mirror [AddComponentMenu("Network/ Interest Management/ Spatial Hash/Spatial Hashing Interest Management")] public class SpatialHashingInterestManagement : InterestManagement { - [Tooltip("The maximum range that objects will be visible at.")] - public int visRange = 30; + [Tooltip("The maximum range that objects will be visible.\nSet to 10-20% larger than camera far clip plane")] + public int visRange = 1200; // we use a 9 neighbour grid. // so we always see in a distance of 2 grids. // for example, our own grid and then one on top / below / left / right. // // this means that grid resolution needs to be distance / 2. - // so for example, for distance = 30 we see 2 cells = 15 * 2 distance. + // so for example, for distance = 1200 we see 2 cells = 600 * 2 distance. // // on first sight, it seems we need distance / 3 (we see left/us/right). // but that's not the case. - // resolution would be 10, and we only see 1 cell far, so 10+10=20. + // resolution would be 400, and we only see 1 cell far, so 400+400=800. public int resolution => visRange / 2; [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] @@ -87,7 +87,7 @@ internal void Update() // NOTE: unlike Scene/MatchInterestManagement, this rebuilds ALL // entities every INTERVAL. consider the other approach later. - // IMPORTANT: refresh grid every update! + // Old Notes: refresh grid every update! // => newly spawned entities get observers assigned via // OnCheckObservers. this can happen any time and we don't want // them broadcast to old (moved or destroyed) connections. @@ -95,14 +95,33 @@ internal void Update() // correct grid position. // => note that the actual 'rebuildall' doesn't need to happen all // the time. - // NOTE: consider refreshing grid only every 'interval' too. but not - // for now. stability & correctness matter. + + // Updated Notes: refresh grid and RebuildAll every interval. + // Real world application would have visRange larger than camera + // far clip plane, e.g. 1200, and a player movement speed of ~10m/sec + // so they typically won't cross cell boundaries quickly. If users notice + // flickering or mis-positioning, they can decrease the interval. // clear old grid results before we update everyone's position. // (this way we get rid of destroyed connections automatically) // // NOTE: keeps allocated HashSets internally. // clearing & populating every frame works without allocations + + // rebuild all spawned entities' observers every 'interval' + // this will call OnRebuildObservers which then returns the + // observers at grid[position] for each entity. + if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval) + { + RefreshGrid(); + RebuildAll(); + lastRebuildTime = NetworkTime.localTime; + } + } + + // (internal so we can update from tests) + internal void RefreshGrid() + { grid.ClearNonAlloc(); // put every connection into the grid at it's main player's position @@ -119,18 +138,9 @@ internal void Update() grid.Add(position, connection); } } - - // rebuild all spawned entities' observers every 'interval' - // this will call OnRebuildObservers which then returns the - // observers at grid[position] for each entity. - if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval) - { - RebuildAll(); - lastRebuildTime = NetworkTime.localTime; - } } -#if !UNITY_SERVER && (UNITY_EDITOR || DEVELOPMENT_BUILD) +#if !UNITY_SERVER && DEBUG // OnGUI allocates even if it does nothing. avoid in release. // slider from dotsnet. it's nice to play around with in the benchmark // demo. diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs index cff87f3f67d..94a50330bf3 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs @@ -23,6 +23,7 @@ namespace Mirror { public enum CoordinateSpace { Local, World } + public enum UpdateMethod { Update, FixedUpdate, LateUpdate } public abstract class NetworkTransformBase : NetworkBehaviour { @@ -32,6 +33,10 @@ public abstract class NetworkTransformBase : NetworkBehaviour [Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")] public Transform target; + [Header("Base Settings")] + [Tooltip("Select which Update method to use.\nSelect FixedUpdate for non-kinematic rigidbodies.")] + public UpdateMethod updateMethod = UpdateMethod.Update; + // Is this a client with authority over this transform? // This component could be on the player object or any object that has been assigned authority to this client. protected bool IsClientWithAuthority => isClient && authority; @@ -476,7 +481,7 @@ void OnClientAuthorityChanged(NetworkConnectionToClient conn, NetworkIdentity id } } -#if !UNITY_SERVER && (UNITY_EDITOR || DEVELOPMENT_BUILD) +#if !UNITY_SERVER && DEBUG // OnGUI allocates even if it does nothing. avoid in release. // debug /////////////////////////////////////////////////////////////// protected virtual void OnGUI() diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs index f66a67cbe29..91298876e3a 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs @@ -9,8 +9,6 @@ namespace Mirror [AddComponentMenu("Network/Network Transform Hybrid")] public class NetworkTransformHybrid : NetworkTransformBase { - // FixedUpdate support to fix: https://github.com/MirrorNetworking/Mirror/pull/3989 - public bool useFixedUpdate; TransformSnapshot? pendingSnapshot; [Header("Additional Settings")] @@ -70,16 +68,14 @@ protected override void Configure() // update ////////////////////////////////////////////////////////////// void Update() { - // if server then always sync to others. - if (isServer) UpdateServer(); - // 'else if' because host mode shouldn't send anything to server. - // it is the server. don't overwrite anything there. - else if (isClient) UpdateClient(); + if (updateMethod == UpdateMethod.Update) + DoUpdate(); } void FixedUpdate() { - if (!useFixedUpdate) return; + if (updateMethod == UpdateMethod.FixedUpdate) + DoUpdate(); if (pendingSnapshot.HasValue && !IsClientWithAuthority) { @@ -91,6 +87,9 @@ void FixedUpdate() void LateUpdate() { + if (updateMethod == UpdateMethod.LateUpdate) + DoUpdate(); + // set dirty to trigger OnSerialize. either always, or only if changed. // It has to be checked in LateUpdate() for onlySyncOnChange to avoid // the possibility of Update() running first before the object's movement @@ -103,6 +102,15 @@ void LateUpdate() } } + void DoUpdate() + { + // if server then always sync to others. + if (isServer) UpdateServer(); + // 'else if' because host mode shouldn't send anything to server. + // it is the server. don't overwrite anything there. + else if (isClient) UpdateClient(); + } + protected virtual void UpdateServer() { // apply buffered snapshots IF client authority @@ -131,7 +139,7 @@ protected virtual void UpdateServer() // interpolate & apply TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); - if (useFixedUpdate) + if (updateMethod == UpdateMethod.FixedUpdate) pendingSnapshot = computed; else Apply(computed, to); @@ -159,7 +167,7 @@ protected virtual void UpdateClient() // interpolate & apply TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); - if (useFixedUpdate) + if (updateMethod == UpdateMethod.FixedUpdate) pendingSnapshot = computed; else Apply(computed, to); diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs index 87eac32cfa1..4eb30155ad7 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs @@ -15,7 +15,6 @@ public class NetworkTransformReliable : NetworkTransformBase [Header("Additional Settings")] [Tooltip("If we only sync on change, then we need to correct old snapshots if more time than sendInterval * multiplier has elapsed.\n\nOtherwise the first move will always start interpolating from the last move sequence's time, which will make it stutter when starting every time.")] public float onlySyncOnChangeCorrectionMultiplier = 2; - public bool useFixedUpdate; [Header("Rotation")] [Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] @@ -57,16 +56,14 @@ protected override void Configure() // update ////////////////////////////////////////////////////////////// void Update() { - // if server then always sync to others. - if (isServer) UpdateServer(); - // 'else if' because host mode shouldn't send anything to server. - // it is the server. don't overwrite anything there. - else if (isClient) UpdateClient(); + if (updateMethod == UpdateMethod.Update) + DoUpdate(); } void FixedUpdate() { - if (!useFixedUpdate) return; + if (updateMethod == UpdateMethod.FixedUpdate) + DoUpdate(); if (pendingSnapshot.HasValue && !IsClientWithAuthority) { @@ -78,6 +75,9 @@ void FixedUpdate() void LateUpdate() { + if (updateMethod == UpdateMethod.LateUpdate) + DoUpdate(); + // set dirty to trigger OnSerialize. either always, or only if changed. // It has to be checked in LateUpdate() for onlySyncOnChange to avoid // the possibility of Update() running first before the object's movement @@ -85,13 +85,22 @@ void LateUpdate() // instead. if (isServer || (IsClientWithAuthority && NetworkClient.ready)) { - if (sendIntervalCounter == sendIntervalMultiplier && (!onlySyncOnChange || Changed(Construct()))) + if (sendIntervalCounter >= sendIntervalMultiplier && (!onlySyncOnChange || Changed(Construct()))) SetDirty(); CheckLastSendTime(); } } + void DoUpdate() + { + // if server then always sync to others. + if (isServer) UpdateServer(); + // 'else if' because host mode shouldn't send anything to server. + // it is the server. don't overwrite anything there. + else if (isClient) UpdateClient(); + } + protected virtual void UpdateServer() { // apply buffered snapshots IF client authority @@ -126,7 +135,7 @@ protected virtual void UpdateServer() protected virtual void UpdateClient() { - if (useFixedUpdate) + if (updateMethod == UpdateMethod.FixedUpdate) { if (!IsClientWithAuthority && clientSnapshots.Count > 0) { @@ -170,7 +179,7 @@ protected virtual void CheckLastSendTime() // timeAsDouble not available in older Unity versions. if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime)) { - if (sendIntervalCounter == sendIntervalMultiplier) + if (sendIntervalCounter >= sendIntervalMultiplier) sendIntervalCounter = 0; sendIntervalCounter++; } diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs index c3c84f88712..4dd7adf3e82 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs @@ -15,7 +15,6 @@ public class NetworkTransformUnreliable : NetworkTransformBase // Testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching, however this should not be the default as it is a rare case Developers may want to cover. [Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.\nA larger buffer means more delay, but results in smoother movement.\nExample: 1 for faster responses minimal smoothing, 5 covers bad pings but has noticable delay, 3 is recommended for balanced results.")] public float bufferResetMultiplier = 3; - public bool useFixedUpdate; [Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] public float positionSensitivity = 0.01f; @@ -41,19 +40,19 @@ protected override void Configure() // Update applies interpolation void Update() { - if (isServer) UpdateServerInterpolation(); - // for all other clients (and for local player if !authority), - // we need to apply snapshots from the buffer. - // 'else if' because host mode shouldn't interpolate client - else if (isClient && !IsClientWithAuthority) UpdateClientInterpolation(); + if (updateMethod == UpdateMethod.Update) + DoUpdate(); } + void FixedUpdate() { - if (!useFixedUpdate) return; + if (updateMethod == UpdateMethod.FixedUpdate) + DoUpdate(); if (pendingSnapshot.HasValue) { + // Apply via base method, but in FixedUpdate Apply(pendingSnapshot.Value, pendingSnapshot.Value); pendingSnapshot = null; } @@ -66,6 +65,9 @@ void FixedUpdate() // this could cause visible jitter. void LateUpdate() { + if (updateMethod == UpdateMethod.LateUpdate) + DoUpdate(); + // if server then always sync to others. if (isServer) UpdateServerBroadcast(); // client authority, and local player (= allowed to move myself)? @@ -74,6 +76,15 @@ void LateUpdate() else if (isClient && IsClientWithAuthority) UpdateClientBroadcast(); } + void DoUpdate() + { + if (isServer) UpdateServerInterpolation(); + // for all other clients (and for local player if !authority), + // we need to apply snapshots from the buffer. + // 'else if' because host mode shouldn't interpolate client + else if (isClient && !IsClientWithAuthority) UpdateClientInterpolation(); + } + protected virtual void CheckLastSendTime() { // We check interval every frame, and then send if interval is reached. @@ -180,7 +191,7 @@ void UpdateServerInterpolation() // interpolate & apply TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); - if (useFixedUpdate) + if (updateMethod == UpdateMethod.FixedUpdate) pendingSnapshot = computed; else Apply(computed, to); @@ -255,7 +266,7 @@ void UpdateClientInterpolation() // interpolate & apply TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); - if (useFixedUpdate) + if (updateMethod == UpdateMethod.FixedUpdate) pendingSnapshot = computed; else Apply(computed, to); diff --git a/Assets/Mirror/Components/RemoteStatistics.cs b/Assets/Mirror/Components/RemoteStatistics.cs index 8ea0df4f6b2..705cc9d5ed1 100644 --- a/Assets/Mirror/Components/RemoteStatistics.cs +++ b/Assets/Mirror/Components/RemoteStatistics.cs @@ -99,6 +99,8 @@ public class RemoteStatistics : NetworkBehaviour // this is not perfectly secure. that's why RemoteStatistics is read-only. [Header("Authentication")] public string passwordFile = "remote_statistics.txt"; + [Tooltip("Set to false to skip password input and authentication on client and server.")] + public bool requiresPasswordAuth = true; // false to skip password input and checks protected bool serverAuthenticated; // client needs to authenticate protected bool clientAuthenticated; // show GUI until authenticated protected string serverPassword = null; // null means not found, auth impossible @@ -146,8 +148,16 @@ public override void OnStartServer() NetworkStatistics = NetworkManager.singleton.GetComponent(); if (NetworkStatistics == null) throw new Exception($"RemoteStatistics requires a NetworkStatistics component on {NetworkManager.singleton.name}!"); - // server needs to load the password - LoadPassword(); + if (!requiresPasswordAuth) + { + // auto authenticate if requiring password is false + serverAuthenticated = true; + } + else + { + // server needs to load the password + LoadPassword(); + } } public override void OnStartLocalPlayer() @@ -155,6 +165,12 @@ public override void OnStartLocalPlayer() // center the window initially windowRect.x = Screen.width / 2 - windowRect.width / 2; windowRect.y = Screen.height / 2 - windowRect.height / 2; + + if (!requiresPasswordAuth) + { + // auto authenticate if requiring password is false + clientAuthenticated = true; + } } [TargetRpc] diff --git a/Assets/Mirror/Core/InterestManagementBase.cs b/Assets/Mirror/Core/InterestManagementBase.cs index 1a3cc1b3563..12e33bcf438 100644 --- a/Assets/Mirror/Core/InterestManagementBase.cs +++ b/Assets/Mirror/Core/InterestManagementBase.cs @@ -61,6 +61,10 @@ public virtual void SetHostVisibility(NetworkIdentity identity, bool visible) foreach (AudioSource audio in identity.GetComponentsInChildren()) audio.enabled = visible; + foreach (Canvas canvas in identity.GetComponentsInChildren()) + if (canvas.renderMode == RenderMode.WorldSpace) + canvas.enabled = visible; + foreach (Terrain terrain in identity.GetComponentsInChildren()) { terrain.drawHeightmap = visible; diff --git a/Assets/Mirror/Core/NetworkManager.cs b/Assets/Mirror/Core/NetworkManager.cs index ca9f9582de9..7ac9e431521 100644 --- a/Assets/Mirror/Core/NetworkManager.cs +++ b/Assets/Mirror/Core/NetworkManager.cs @@ -794,6 +794,7 @@ public static void ResetStatics() public virtual void OnDestroy() { //Debug.Log("NetworkManager destroyed"); + SceneManager.sceneLoaded -= OnSceneLoaded; } /// The name of the current network scene. diff --git a/Assets/Mirror/Examples/PlayerTest/CharacterController/PlayerHybrid.prefab b/Assets/Mirror/Examples/PlayerTest/CharacterController/PlayerHybrid.prefab index f7382fc13da..526ab9d72db 100644 --- a/Assets/Mirror/Examples/PlayerTest/CharacterController/PlayerHybrid.prefab +++ b/Assets/Mirror/Examples/PlayerTest/CharacterController/PlayerHybrid.prefab @@ -464,6 +464,7 @@ MonoBehaviour: syncDirection: 1 syncMode: 0 syncInterval: 0 + updateMethod: 0 target: {fileID: 4659514702152478195} syncPosition: 1 syncRotation: 1 @@ -478,7 +479,6 @@ MonoBehaviour: showGizmos: 0 showOverlay: 0 overlayColor: {r: 0, g: 0, b: 0, a: 0.5} - useFixedUpdate: 0 onlySyncOnChangeCorrectionMultiplier: 2 rotationSensitivity: 0.01 positionPrecision: 0.01 diff --git a/Assets/Mirror/Examples/PlayerTest/CharacterController/PlayerReliable.prefab b/Assets/Mirror/Examples/PlayerTest/CharacterController/PlayerReliable.prefab index cfffa219a14..2b232c9b4a0 100644 --- a/Assets/Mirror/Examples/PlayerTest/CharacterController/PlayerReliable.prefab +++ b/Assets/Mirror/Examples/PlayerTest/CharacterController/PlayerReliable.prefab @@ -464,6 +464,7 @@ MonoBehaviour: syncDirection: 1 syncMode: 0 syncInterval: 0.05 + updateMethod: 0 target: {fileID: 4659514702152478195} syncPosition: 1 syncRotation: 1 @@ -479,7 +480,6 @@ MonoBehaviour: showOverlay: 0 overlayColor: {r: 0, g: 0, b: 0, a: 0.5} onlySyncOnChangeCorrectionMultiplier: 2 - useFixedUpdate: 0 rotationSensitivity: 0.01 positionPrecision: 0.01 scalePrecision: 0.01 diff --git a/Assets/Mirror/Examples/PlayerTest/CharacterController/PlayerUnreliable.prefab b/Assets/Mirror/Examples/PlayerTest/CharacterController/PlayerUnreliable.prefab index 5b6250f2e19..7c0c43ca8b9 100644 --- a/Assets/Mirror/Examples/PlayerTest/CharacterController/PlayerUnreliable.prefab +++ b/Assets/Mirror/Examples/PlayerTest/CharacterController/PlayerUnreliable.prefab @@ -464,6 +464,7 @@ MonoBehaviour: syncDirection: 1 syncMode: 0 syncInterval: 0.05 + updateMethod: 0 target: {fileID: 4659514702152478195} syncPosition: 1 syncRotation: 1 diff --git a/Assets/Mirror/Examples/PlayerTest/PlayerTestScene.unity b/Assets/Mirror/Examples/PlayerTest/PlayerTestScene.unity index 1b9026b60e5..40845735073 100644 --- a/Assets/Mirror/Examples/PlayerTest/PlayerTestScene.unity +++ b/Assets/Mirror/Examples/PlayerTest/PlayerTestScene.unity @@ -270,6 +270,7 @@ MonoBehaviour: syncDirection: 0 syncMode: 0 syncInterval: 0 + updateMethod: 1 target: {fileID: 856494741} syncPosition: 1 syncRotation: 1 @@ -285,7 +286,6 @@ MonoBehaviour: showOverlay: 0 overlayColor: {r: 0, g: 0, b: 0, a: 0.5} onlySyncOnChangeCorrectionMultiplier: 2 - useFixedUpdate: 0 rotationSensitivity: 0.01 positionPrecision: 0.01 scalePrecision: 0.01 @@ -457,7 +457,7 @@ Transform: m_GameObject: {fileID: 890531170} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: -325, y: 11.02, z: -6} + m_LocalPosition: {x: -340, y: 11.02, z: -6} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -501,7 +501,7 @@ Transform: m_GameObject: {fileID: 980545347} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: -325, y: 11.02, z: -10} + m_LocalPosition: {x: -335, y: 11.02, z: -10} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -545,7 +545,7 @@ Transform: m_GameObject: {fileID: 1175360880} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: -325, y: 11.02, z: -14} + m_LocalPosition: {x: -330, y: 11.02, z: -14} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] diff --git a/Assets/Mirror/Examples/PlayerTest/Rigidbody/PlayerRBHybrid.prefab b/Assets/Mirror/Examples/PlayerTest/Rigidbody/PlayerRBHybrid.prefab index 02e02ba592c..d24150f3172 100644 --- a/Assets/Mirror/Examples/PlayerTest/Rigidbody/PlayerRBHybrid.prefab +++ b/Assets/Mirror/Examples/PlayerTest/Rigidbody/PlayerRBHybrid.prefab @@ -437,6 +437,7 @@ MonoBehaviour: syncDirection: 1 syncMode: 0 syncInterval: 0 + updateMethod: 1 target: {fileID: 5650773562400175449} syncPosition: 1 syncRotation: 1 @@ -451,7 +452,6 @@ MonoBehaviour: showGizmos: 0 showOverlay: 0 overlayColor: {r: 0, g: 0, b: 0, a: 0.5} - useFixedUpdate: 1 onlySyncOnChangeCorrectionMultiplier: 2 rotationSensitivity: 0.01 positionPrecision: 0.01 @@ -488,6 +488,7 @@ MonoBehaviour: optionsKeys: MouseSteer: 109 AutoRun: 114 + Sliding: 103 ToggleUI: 117 controlOptions: 9 maxMoveSpeed: 8 diff --git a/Assets/Mirror/Examples/PlayerTest/Rigidbody/PlayerRBReliable.prefab b/Assets/Mirror/Examples/PlayerTest/Rigidbody/PlayerRBReliable.prefab index acb47a48117..7b97caa4437 100644 --- a/Assets/Mirror/Examples/PlayerTest/Rigidbody/PlayerRBReliable.prefab +++ b/Assets/Mirror/Examples/PlayerTest/Rigidbody/PlayerRBReliable.prefab @@ -26,12 +26,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2505838817581327622} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 1.4999999, z: 0} m_LocalScale: {x: 0.1, y: 0.1, z: 0.1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 5650773562400175449} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!23 &9066655185906928051 MeshRenderer: @@ -44,10 +45,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -72,6 +75,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!102 &4760233786622638652 TextMesh: serializedVersion: 3 @@ -131,12 +135,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 3564599214188516024} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0.39999998, z: 0.5} m_LocalScale: {x: 0.5, y: 0.1, z: 0.2} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 4320229852458648655} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!33 &7612349963074522185 MeshFilter: @@ -157,10 +162,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 4294967295 m_RendererPriority: 0 m_Materials: @@ -185,6 +192,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!1 &6908139674379992817 GameObject: m_ObjectHideFlags: 0 @@ -210,13 +218,14 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 6908139674379992817} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 5048467161036537608} m_Father: {fileID: 5650773562400175449} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!33 &7269450925790386019 MeshFilter: @@ -237,10 +246,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -265,6 +276,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!1 &7197623925735895267 GameObject: m_ObjectHideFlags: 0 @@ -295,14 +307,15 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 7197623925735895267} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1.1, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 4320229852458648655} - {fileID: 8070823805368028938} m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!136 &3870420104013059684 CapsuleCollider: @@ -312,8 +325,17 @@ CapsuleCollider: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 7197623925735895267} m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 m_IsTrigger: 0 + m_ProvidesContacts: 0 m_Enabled: 1 + serializedVersion: 2 m_Radius: 0.5 m_Height: 2 m_Direction: 1 @@ -325,10 +347,21 @@ Rigidbody: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 7197623925735895267} - serializedVersion: 2 + serializedVersion: 4 m_Mass: 1 m_Drag: 0 m_AngularDrag: 0.05 + m_CenterOfMass: {x: 0, y: 0, z: 0} + m_InertiaTensor: {x: 1, y: 1, z: 1} + m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ImplicitCom: 1 + m_ImplicitTensor: 1 m_UseGravity: 1 m_IsKinematic: 1 m_Interpolate: 0 @@ -347,7 +380,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 - _assetId: 3568813205 + _assetId: 3273196566 serverOnly: 0 visibility: 0 hasSpawned: 0 @@ -363,6 +396,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: a91a718a70d01b347b75cb768a6f1a92, type: 3} m_Name: m_EditorClassIdentifier: + syncMethod: 0 syncDirection: 0 syncMode: 0 syncInterval: 0 @@ -381,6 +415,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 71ac1e35462ffad469e77d1c2fe6c9f3, type: 3} m_Name: m_EditorClassIdentifier: + syncMethod: 0 syncDirection: 0 syncMode: 0 syncInterval: 0 @@ -398,9 +433,11 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8ff3ba0becae47b8b9381191598957c8, type: 3} m_Name: m_EditorClassIdentifier: + syncMethod: 0 syncDirection: 1 syncMode: 0 syncInterval: 0 + updateMethod: 1 target: {fileID: 5650773562400175449} syncPosition: 1 syncRotation: 1 @@ -416,7 +453,6 @@ MonoBehaviour: showOverlay: 0 overlayColor: {r: 0, g: 0, b: 0, a: 0.5} onlySyncOnChangeCorrectionMultiplier: 2 - useFixedUpdate: 1 rotationSensitivity: 0.01 positionPrecision: 0.01 scalePrecision: 0.01 @@ -432,13 +468,13 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 0336d1bd689de45418c08c76ae66e503, type: 3} m_Name: m_EditorClassIdentifier: + syncMethod: 0 syncDirection: 0 syncMode: 0 syncInterval: 0 rigidBody: {fileID: 4272311354144086857} capsuleCollider: {fileID: 3870420104013059684} - ControllerUIPrefab: {fileID: 1328287033780775043, guid: 5caaf0d5754a64f4080f0c8b55c0b03d, - type: 3} + ControllerUIPrefab: {fileID: 1328287033780775043, guid: 5caaf0d5754a64f4080f0c8b55c0b03d, type: 3} moveKeys: Forward: 119 Back: 115 @@ -450,6 +486,7 @@ MonoBehaviour: optionsKeys: MouseSteer: 109 AutoRun: 114 + Sliding: 103 ToggleUI: 117 controlOptions: 9 maxMoveSpeed: 8 diff --git a/Assets/Mirror/Examples/PlayerTest/Rigidbody/PlayerRBUnreliable.prefab b/Assets/Mirror/Examples/PlayerTest/Rigidbody/PlayerRBUnreliable.prefab index 15d5c2daf16..e28001a21f9 100644 --- a/Assets/Mirror/Examples/PlayerTest/Rigidbody/PlayerRBUnreliable.prefab +++ b/Assets/Mirror/Examples/PlayerTest/Rigidbody/PlayerRBUnreliable.prefab @@ -26,12 +26,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1900176203039934355} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 1.4999999, z: 0} m_LocalScale: {x: 0.1, y: 0.1, z: 0.1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 6814142693731383418} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!23 &6572775962608386051 MeshRenderer: @@ -44,10 +45,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -72,6 +75,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!102 &3912971638250293984 TextMesh: serializedVersion: 3 @@ -131,12 +135,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2414815785185615771} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0.39999998, z: 0.5} m_LocalScale: {x: 0.5, y: 0.1, z: 0.2} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 3102887894851733868} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!33 &8745807705742672234 MeshFilter: @@ -157,10 +162,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 4294967295 m_RendererPriority: 0 m_Materials: @@ -185,6 +192,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!1 &5703107844171822034 GameObject: m_ObjectHideFlags: 0 @@ -210,13 +218,14 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 5703107844171822034} + serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 6265883749291389483} m_Father: {fileID: 6814142693731383418} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!33 &8365824619968961088 MeshFilter: @@ -237,10 +246,12 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -265,6 +276,7 @@ MeshRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!1 &8293447860930641344 GameObject: m_ObjectHideFlags: 0 @@ -295,14 +307,15 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 8293447860930641344} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1.1, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 3102887894851733868} - {fileID: 4129545300081926521} m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!136 &2687902018279037767 CapsuleCollider: @@ -312,8 +325,17 @@ CapsuleCollider: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 8293447860930641344} m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 m_IsTrigger: 0 + m_ProvidesContacts: 0 m_Enabled: 1 + serializedVersion: 2 m_Radius: 0.5 m_Height: 2 m_Direction: 1 @@ -325,10 +347,21 @@ Rigidbody: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 8293447860930641344} - serializedVersion: 2 + serializedVersion: 4 m_Mass: 1 m_Drag: 0 m_AngularDrag: 0.05 + m_CenterOfMass: {x: 0, y: 0, z: 0} + m_InertiaTensor: {x: 1, y: 1, z: 1} + m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ImplicitCom: 1 + m_ImplicitTensor: 1 m_UseGravity: 1 m_IsKinematic: 1 m_Interpolate: 0 @@ -347,7 +380,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 - _assetId: 1425781377 + _assetId: 3428531582 serverOnly: 0 visibility: 0 hasSpawned: 0 @@ -363,6 +396,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: a91a718a70d01b347b75cb768a6f1a92, type: 3} m_Name: m_EditorClassIdentifier: + syncMethod: 0 syncDirection: 0 syncMode: 0 syncInterval: 0 @@ -381,6 +415,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 71ac1e35462ffad469e77d1c2fe6c9f3, type: 3} m_Name: m_EditorClassIdentifier: + syncMethod: 0 syncDirection: 0 syncMode: 0 syncInterval: 0 @@ -398,9 +433,11 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, type: 3} m_Name: m_EditorClassIdentifier: + syncMethod: 1 syncDirection: 1 syncMode: 0 syncInterval: 0 + updateMethod: 1 target: {fileID: 6814142693731383418} syncPosition: 1 syncRotation: 1 @@ -432,13 +469,13 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 60cbbdae6551c7b4395e1bd09e2ff3ea, type: 3} m_Name: m_EditorClassIdentifier: + syncMethod: 0 syncDirection: 0 syncMode: 0 syncInterval: 0 rigidBody: {fileID: 3148414963753227882} capsuleCollider: {fileID: 2687902018279037767} - ControllerUIPrefab: {fileID: 1328287033780775043, guid: 5caaf0d5754a64f4080f0c8b55c0b03d, - type: 3} + ControllerUIPrefab: {fileID: 1328287033780775043, guid: 5caaf0d5754a64f4080f0c8b55c0b03d, type: 3} moveKeys: Forward: 119 Back: 115 @@ -450,6 +487,7 @@ MonoBehaviour: optionsKeys: MouseSteer: 109 AutoRun: 114 + Sliding: 103 ToggleUI: 117 controlOptions: 9 maxMoveSpeed: 8 diff --git a/Assets/Mirror/Examples/PlayerTest/Tank/TankHybrid.prefab b/Assets/Mirror/Examples/PlayerTest/Tank/TankHybrid.prefab index 9f12917dc94..dffb3ab4123 100644 --- a/Assets/Mirror/Examples/PlayerTest/Tank/TankHybrid.prefab +++ b/Assets/Mirror/Examples/PlayerTest/Tank/TankHybrid.prefab @@ -169,6 +169,7 @@ MonoBehaviour: syncDirection: 1 syncMode: 0 syncInterval: 0 + updateMethod: 0 target: {fileID: 4659514702152478195} syncPosition: 1 syncRotation: 1 @@ -183,7 +184,6 @@ MonoBehaviour: showGizmos: 0 showOverlay: 0 overlayColor: {r: 0, g: 0, b: 0, a: 0.5} - useFixedUpdate: 0 onlySyncOnChangeCorrectionMultiplier: 2 rotationSensitivity: 0.01 positionPrecision: 0.01 @@ -304,6 +304,7 @@ MonoBehaviour: syncDirection: 1 syncMode: 0 syncInterval: 0 + updateMethod: 0 target: {fileID: 702109291309605222} syncPosition: 0 syncRotation: 1 @@ -318,7 +319,6 @@ MonoBehaviour: showGizmos: 0 showOverlay: 0 overlayColor: {r: 0, g: 0, b: 0, a: 0.5} - useFixedUpdate: 0 onlySyncOnChangeCorrectionMultiplier: 2 rotationSensitivity: 0.01 positionPrecision: 0.01 @@ -341,6 +341,7 @@ MonoBehaviour: syncDirection: 1 syncMode: 0 syncInterval: 0 + updateMethod: 0 target: {fileID: 702109291309605224} syncPosition: 0 syncRotation: 1 @@ -355,7 +356,6 @@ MonoBehaviour: showGizmos: 0 showOverlay: 0 overlayColor: {r: 0, g: 0, b: 0, a: 0.5} - useFixedUpdate: 0 onlySyncOnChangeCorrectionMultiplier: 2 rotationSensitivity: 0.01 positionPrecision: 0.01 diff --git a/Assets/Mirror/Examples/PlayerTest/Tank/TankReliable.prefab b/Assets/Mirror/Examples/PlayerTest/Tank/TankReliable.prefab index a7a4d0b2547..10dbcab96c1 100644 --- a/Assets/Mirror/Examples/PlayerTest/Tank/TankReliable.prefab +++ b/Assets/Mirror/Examples/PlayerTest/Tank/TankReliable.prefab @@ -169,6 +169,7 @@ MonoBehaviour: syncDirection: 1 syncMode: 0 syncInterval: 0.05 + updateMethod: 0 target: {fileID: 4659514702152478195} syncPosition: 1 syncRotation: 1 @@ -184,7 +185,6 @@ MonoBehaviour: showOverlay: 0 overlayColor: {r: 0, g: 0, b: 0, a: 0.5} onlySyncOnChangeCorrectionMultiplier: 2 - useFixedUpdate: 0 rotationSensitivity: 0.01 positionPrecision: 0.01 scalePrecision: 0.01 @@ -302,6 +302,7 @@ MonoBehaviour: syncDirection: 1 syncMode: 0 syncInterval: 0.05 + updateMethod: 0 target: {fileID: 702109291309605222} syncPosition: 0 syncRotation: 1 @@ -317,7 +318,6 @@ MonoBehaviour: showOverlay: 0 overlayColor: {r: 0, g: 0, b: 0, a: 0.5} onlySyncOnChangeCorrectionMultiplier: 2 - useFixedUpdate: 0 rotationSensitivity: 0.01 positionPrecision: 0.01 scalePrecision: 0.01 @@ -337,6 +337,7 @@ MonoBehaviour: syncDirection: 1 syncMode: 0 syncInterval: 0.05 + updateMethod: 0 target: {fileID: 702109291309605224} syncPosition: 0 syncRotation: 1 @@ -352,7 +353,6 @@ MonoBehaviour: showOverlay: 0 overlayColor: {r: 0, g: 0, b: 0, a: 0.5} onlySyncOnChangeCorrectionMultiplier: 2 - useFixedUpdate: 0 rotationSensitivity: 0.01 positionPrecision: 0.01 scalePrecision: 0.01 diff --git a/Assets/Mirror/Examples/PlayerTest/Tank/TankUnreliable.prefab b/Assets/Mirror/Examples/PlayerTest/Tank/TankUnreliable.prefab index 47d3e311a56..7c0157fc833 100644 --- a/Assets/Mirror/Examples/PlayerTest/Tank/TankUnreliable.prefab +++ b/Assets/Mirror/Examples/PlayerTest/Tank/TankUnreliable.prefab @@ -169,6 +169,7 @@ MonoBehaviour: syncDirection: 1 syncMode: 0 syncInterval: 0 + updateMethod: 0 target: {fileID: 4659514702152478195} syncPosition: 1 syncRotation: 1 @@ -302,6 +303,7 @@ MonoBehaviour: syncDirection: 1 syncMode: 0 syncInterval: 0 + updateMethod: 0 target: {fileID: 702109291309605222} syncPosition: 0 syncRotation: 1 @@ -337,6 +339,7 @@ MonoBehaviour: syncDirection: 1 syncMode: 0 syncInterval: 0 + updateMethod: 0 target: {fileID: 702109291309605224} syncPosition: 0 syncRotation: 1 diff --git a/Assets/Mirror/Examples/Room/Scripts/NetworkRoomManagerExt.cs b/Assets/Mirror/Examples/Room/Scripts/NetworkRoomManagerExt.cs index 2fffd944df7..588387b3d38 100644 --- a/Assets/Mirror/Examples/Room/Scripts/NetworkRoomManagerExt.cs +++ b/Assets/Mirror/Examples/Room/Scripts/NetworkRoomManagerExt.cs @@ -79,19 +79,19 @@ Setting showStartButton false when the button is pressed hides it in the game sc is set as DontDestroyOnLoad = true. */ +#if !UNITY_SERVER bool showStartButton; +#endif public override void OnRoomServerPlayersReady() { // calling the base method calls ServerChangeScene as soon as all players are in Ready state. if (Utils.IsHeadless()) - { base.OnRoomServerPlayersReady(); - } +#if !UNITY_SERVER else - { showStartButton = true; - } +#endif } #if !UNITY_SERVER diff --git a/Assets/Mirror/Examples/_Common/Controllers/PlayerControllerRB/PlayerControllerRBHybrid.cs b/Assets/Mirror/Examples/_Common/Controllers/PlayerControllerRB/PlayerControllerRBHybrid.cs index ed3f0b0bcdb..9cd720e27df 100644 --- a/Assets/Mirror/Examples/_Common/Controllers/PlayerControllerRB/PlayerControllerRBHybrid.cs +++ b/Assets/Mirror/Examples/_Common/Controllers/PlayerControllerRB/PlayerControllerRBHybrid.cs @@ -17,7 +17,7 @@ protected override void OnValidate() public override void Reset() { base.Reset(); - GetComponent().useFixedUpdate = true; + GetComponent().updateMethod = UpdateMethod.FixedUpdate; } } } diff --git a/Assets/Mirror/Examples/_Common/Controllers/PlayerControllerRB/PlayerControllerRBReliable.cs b/Assets/Mirror/Examples/_Common/Controllers/PlayerControllerRB/PlayerControllerRBReliable.cs index af791470577..96d98e0f830 100644 --- a/Assets/Mirror/Examples/_Common/Controllers/PlayerControllerRB/PlayerControllerRBReliable.cs +++ b/Assets/Mirror/Examples/_Common/Controllers/PlayerControllerRB/PlayerControllerRBReliable.cs @@ -17,7 +17,7 @@ protected override void OnValidate() public override void Reset() { base.Reset(); - GetComponent().useFixedUpdate = true; + GetComponent().updateMethod = UpdateMethod.FixedUpdate; } } } diff --git a/Assets/Mirror/Examples/_Common/Controllers/PlayerControllerRB/PlayerControllerRBUnreliable.cs b/Assets/Mirror/Examples/_Common/Controllers/PlayerControllerRB/PlayerControllerRBUnreliable.cs index 5a007d930e6..fa7c23984a6 100644 --- a/Assets/Mirror/Examples/_Common/Controllers/PlayerControllerRB/PlayerControllerRBUnreliable.cs +++ b/Assets/Mirror/Examples/_Common/Controllers/PlayerControllerRB/PlayerControllerRBUnreliable.cs @@ -17,7 +17,7 @@ protected override void OnValidate() public override void Reset() { base.Reset(); - GetComponent().useFixedUpdate = true; + GetComponent().updateMethod = UpdateMethod.FixedUpdate; } } } diff --git a/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_SpatialHashing.cs b/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_SpatialHashing.cs index 9a708ed211e..7d5501c8a86 100644 --- a/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_SpatialHashing.cs +++ b/Assets/Mirror/Tests/Editor/InterestManagement/InterestManagementTests_SpatialHashing.cs @@ -117,8 +117,8 @@ public void OutOfRange_Initial() // A and B are too far from each other identityB.transform.position = Vector3.right * (aoi.visRange + 1); - // update grid now that positions were changed - aoi.Update(); + // Refresh the grid + aoi.RefreshGrid(); // rebuild for boths NetworkServer.RebuildObservers(identityA, true); @@ -137,8 +137,8 @@ public void OutOfRange_NotInitial() // A and B are too far from each other identityB.transform.position = Vector3.right * (aoi.visRange + 1); - // update grid now that positions were changed - aoi.Update(); + // Refresh the grid + aoi.RefreshGrid(); // rebuild for boths NetworkServer.RebuildObservers(identityA, false); diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs index 7c07bf3d68d..53eaa0936d6 100644 --- a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs @@ -144,14 +144,17 @@ void OnGUIRelay() GUILayout.EndArea(); } -#if !UNITY_SERVER +#if !UNITY_SERVER && DEBUG + protected override void OnGUI() + { + base.OnGUI(); + if (relayGUI) OnGUIRelay(); + } +#elif !UNITY_SERVER && !DEBUG // base OnGUI only shows in editor & development builds. // here we always show it because we need the sessionid & userid buttons. - new void OnGUI() + void OnGUI() { -#if UNITY_EDITOR || DEVELOPMENT_BUILD - base.OnGUI(); -#endif if (relayGUI) OnGUIRelay(); } #endif diff --git a/Assets/Mirror/Transports/KCP/KcpTransport.cs b/Assets/Mirror/Transports/KCP/KcpTransport.cs index d3f21f23b4c..dc52218a13c 100644 --- a/Assets/Mirror/Transports/KCP/KcpTransport.cs +++ b/Assets/Mirror/Transports/KCP/KcpTransport.cs @@ -324,7 +324,7 @@ protected virtual void OnGUIStatistics() } // OnGUI allocates even if it does nothing. avoid in release. -#if !UNITY_SERVER && (UNITY_EDITOR || DEVELOPMENT_BUILD) +#if !UNITY_SERVER && DEBUG protected virtual void OnGUI() { if (statisticsGUI) OnGUIStatistics(); diff --git a/Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs b/Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs index 38db368bd13..6854374a10b 100644 --- a/Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs +++ b/Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs @@ -283,7 +283,7 @@ protected virtual void OnGUIStatistics() } // OnGUI allocates even if it does nothing. avoid in release. -#if !UNITY_SERVER && (UNITY_EDITOR || DEVELOPMENT_BUILD) +#if !UNITY_SERVER && DEBUG protected virtual void OnGUI() { if (statisticsGUI) OnGUIStatistics(); diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs index 9f25028d650..dbb8f7581ab 100644 --- a/Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs @@ -121,7 +121,7 @@ void Awake() Log.minLogLevel = minimumLogLevel; } - public override string ToString() => $"SWT [{port}]"; + public override string ToString() => $"SWT [{Port}]"; void OnValidate() {