diff --git a/Assets/Scripts/Tracks/TrackManager.cs b/Assets/Scripts/Tracks/TrackManager.cs index 949fa81a..29365dcb 100644 --- a/Assets/Scripts/Tracks/TrackManager.cs +++ b/Assets/Scripts/Tracks/TrackManager.cs @@ -128,7 +128,7 @@ public class TrackManager : MonoBehaviour protected const int k_DesiredSegmentCount = 10; protected const float k_SegmentRemovalDistance = -30f; protected const float k_Acceleration = 0.2f; - + protected void Awake() { m_ScoreAccum = 0.0f; @@ -562,7 +562,7 @@ public void SpawnObstacle(TrackSegment segment) private IEnumerator SpawnFromAssetReference(AssetReference reference, TrackSegment segment, int posIndex) { - AsyncOperationHandle op = reference.LoadAssetAsync(); + AsyncOperationHandle op = Addressables.LoadAssetAsync(reference); yield return op; GameObject obj = op.Result as GameObject; if (obj != null) diff --git a/Packages/com.unity.asset-store-tools/Editor.meta b/Packages/com.unity.asset-store-tools/Editor.meta new file mode 100644 index 00000000..8521ad69 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 166da5c6fc70e814a8262463903b2714 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreTools.cs b/Packages/com.unity.asset-store-tools/Editor/AssetStoreTools.cs new file mode 100644 index 00000000..f754f134 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreTools.cs @@ -0,0 +1,44 @@ +using UnityEditor; +using UnityEngine; +using System; +using AssetStoreTools.Uploader; +using AssetStoreTools.Validator; + +namespace AssetStoreTools +{ + internal class AssetStoreTools : EditorWindow + { + [MenuItem("Asset Store Tools v2/Asset Store Uploader", false, 0)] + public static void ShowAssetStoreToolsUploader() + { + Type inspectorType = Type.GetType("UnityEditor.InspectorWindow,UnityEditor.dll"); + GetWindow(inspectorType); + } + + + [MenuItem("Asset Store Tools v2/Asset Store Validator", false, 1)] + public static void ShowAssetStoreToolsValidator() + { + Type inspectorType = Type.GetType("UnityEditor.InspectorWindow,UnityEditor.dll"); + GetWindow(typeof(AssetStoreUploader), inspectorType); + } + + [MenuItem("Asset Store Tools v2/Publisher Portal", false, 20)] + public static void OpenPublisherPortal() + { + Application.OpenURL("https://publisher.unity.com/"); + } + + [MenuItem("Asset Store Tools v2/Submission Guidelines", false, 21)] + public static void OpenSubmissionGuidelines() + { + Application.OpenURL("https://assetstore.unity.com/publishing/submission-guidelines/"); + } + + [MenuItem("Asset Store Tools v2/Provide Feedback", false, 50)] + public static void OpenFeedback() + { + Application.OpenURL("https://forum.unity.com/threads/new-asset-store-tools-version-coming-july-20th-2022.1310939/"); + } + } +} \ No newline at end of file diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreTools.cs.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreTools.cs.meta new file mode 100644 index 00000000..9452bb05 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/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/Packages/com.unity.asset-store-tools/Editor/AssetStoreToolsWindow.cs b/Packages/com.unity.asset-store-tools/Editor/AssetStoreToolsWindow.cs new file mode 100644 index 00000000..5f173a43 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreToolsWindow.cs @@ -0,0 +1,22 @@ +using UnityEditor; +using UnityEngine; +using System; + +namespace AssetStoreTools +{ + public abstract class AssetStoreToolsWindow : EditorWindow + { + protected abstract string WindowTitle { get; } + + protected virtual void Init() + { + titleContent = new GUIContent(WindowTitle); + } + + private void OnEnable() + { + Init(); + } + + } +} \ No newline at end of file diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreToolsWindow.cs.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreToolsWindow.cs.meta new file mode 100644 index 00000000..2fe87e57 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/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/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader.meta new file mode 100644 index 00000000..b4b86614 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9722d52df16aab742b26fe301782c74c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/AssetStoreUploader.cs b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/AssetStoreUploader.cs new file mode 100644 index 00000000..0d4ee1e6 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/AssetStoreUploader.cs @@ -0,0 +1,238 @@ +using AssetStoreTools.Utility.Json; +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader +{ + public class AssetStoreUploader : AssetStoreToolsWindow, IHasCustomMenu + { + public const string MinRequiredPackageVersion = "2020.3"; + + private const string MainWindowVisualTree = "Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Styles/Base/BaseWindow_Main"; + private const string DebugPhrase = "debug"; + + // UI Windows + private LoginWindow _loginWindow; + private UploadWindow _uploadWindow; + + private readonly List _debugBuffer = new List(); + + public static bool EnableCustomExporter + { + get => EditorPrefs.GetBool("ASTCustomExporter", false); + set => EditorPrefs.SetBool("ASTCustomExporter", value); + } + + public static bool ShowPackageVersionDialog + { + get => Application.unityVersion.CompareTo(MinRequiredPackageVersion) == 1 ? false : EditorPrefs.GetBool("ASTPreUploadVersionCheck", true); + set => EditorPrefs.SetBool("ASTPreUploadVersionCheck", value); + } + + protected override string WindowTitle => "Asset Store Uploader"; + + protected override void Init() + { + if (_loginWindow != null && _uploadWindow != null) + return; + + minSize = new Vector2(400, 430); + this.SetAntiAliasing(4); + + base.Init(); + + VisualElement root = rootVisualElement; + root.AddToClassList("root"); + + // Getting a reference to the UXML Document and adding to the root + var visualTree = AssetDatabase.LoadAssetAtPath($"{MainWindowVisualTree}.uxml"); + VisualElement uxmlRoot = visualTree.CloneTree(); + uxmlRoot.style.flexGrow = 1; + root.Add(uxmlRoot); + + StyleSelector.SetStyle(root, StyleSelector.Style.Base, !EditorGUIUtility.isProSkin); + + // Find necessary windows / views and sets up appropriate functionality + SetupCoreElements(); + + if (!AssetStoreAPI.IsUploading) + { + // Should only authenticate if the session is available. Other authentications are only available + // in the login window. See "SetupLoginElements". + HideElement(_uploadWindow); + Authenticate(); + } + else + { + ShowUploadWindow(); + } + } + + private void OnGUI() + { + CheckForDebugMode(); + } + + private void OnDestroy() + { + if (AssetStoreAPI.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 void SetupCoreElements() + { + _loginWindow = rootVisualElement.Q("LoginWindow"); + _uploadWindow = rootVisualElement.Q("UploadWindow"); + + _loginWindow.SetupLoginElements(OnLoginSuccess, OnLoginFail); + _uploadWindow.SetupWindows(OnLogout, OnPackageDownloadFail); + } + + public void AddItemsToMenu(GenericMenu menu) + { + menu.AddItem(new GUIContent("(Experimental) Enable Custom Exporter"), + EnableCustomExporter, + () => + { + if (!EnableCustomExporter && !EditorUtility.DisplayDialog("Notice", "Custom exporter is an experimental feature. " + + "It packs selected Assets without using the native Unity API and is observed to be slightly faster.\n\n" + + "Please note that Asset preview images used to showcase specific asset types (Textures, Materials, Prefabs) before importing the package " + + "might not be generated consistently at this time. This does not affect functionality of the package after it gets imported.", + "OK")) + return; + EnableCustomExporter = !EnableCustomExporter; + ASDebug.Log($"Custom exporter set to {EnableCustomExporter}"); + }); + } + + #region Login Interface + + private void Authenticate() + { + ShowLoginWindow(); + + // 1 - Check if there's an active session + // 2 - Check if there's a saved session + // 3 - Attempt to login via Cloud session token + // 4 - Prompt manual login + EnableLoginWindow(false); + AssetStoreAPI.LoginWithSession(OnLoginSuccess, OnLoginFail, OnLoginFailSession); + } + + private void OnLoginFail(ASError error) + { + Debug.LogError(error.Message); + + _loginWindow.EnableErrorBox(true, error.Message); + EnableLoginWindow(true); + } + + private void OnLoginFailSession() + { + // All previous login methods are unavailable + EnableLoginWindow(true); + } + + private void OnLoginSuccess(JsonValue json) + { + ASDebug.Log($"Login json\n{json}"); + + if (!AssetStoreAPI.IsPublisherValid(json, out var error)) + { + EnableLoginWindow(true); + _loginWindow.EnableErrorBox(true, error.Message); + ASDebug.Log($"Publisher {json["name"]} is invalid."); + return; + } + + ASDebug.Log($"Publisher {json["name"]} is valid."); + AssetStoreAPI.SavedSessionId = json["xunitysession"].AsString(); + AssetStoreAPI.LastLoggedInUser = json["username"].AsString(); + + ShowUploadWindow(); + } + + private void OnPackageDownloadFail(ASError error) + { + _loginWindow.EnableErrorBox(true, error.Message); + EnableLoginWindow(true); + ShowLoginWindow(); + } + + private void OnLogout() + { + AssetStoreAPI.SavedSessionId = String.Empty; + AssetStoreCache.ClearTempCache(); + + _loginWindow.ClearLoginBoxes(); + ShowLoginWindow(); + EnableLoginWindow(true); + } + + #endregion + + #region UI Window Utils + private void ShowLoginWindow() + { + HideElement(_uploadWindow); + ShowElement(_loginWindow); + } + + private void ShowUploadWindow() + { + HideElement(_loginWindow); + ShowElement(_uploadWindow); + + _uploadWindow.ShowAllPackagesView(); + _uploadWindow.ShowPublisherEmail(AssetStoreAPI.LastLoggedInUser); + _uploadWindow.LoadPackages(true, OnPackageDownloadFail); + } + + 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 EnableLoginWindow(bool enable) + { + _loginWindow.SetEnabled(enable); + } + + #endregion + + #region Debug Utility + + 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/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/AssetStoreUploader.cs.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/AssetStoreUploader.cs.meta new file mode 100644 index 00000000..c803d540 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/AssetStoreUploader.cs.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +<<<<<<< HEAD:Tests/Editor/ScreenshotTest.cs.meta +guid: 7b5319699cc84194a9a768ad33b86c21 +======= +guid: b3da785da3e541c4181e955bbf25187c +>>>>>>> development:Editor/AssetStoreUploader/AssetStoreUploader.cs.meta +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons.meta new file mode 100644 index 00000000..7026063d --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ab9d0e254817f4f4589a6a378d77babc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/open-in-browser.png b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/open-in-browser.png new file mode 100644 index 00000000..1556ba0b --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/open-in-browser.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:569ce9bfc3665e9506df567eb9e5b66689ddb0f5fb0dfba56573e4f6be49ca8e +size 878 diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/open-in-browser.png.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/open-in-browser.png.meta new file mode 100644 index 00000000..26ccaa5a --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/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/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/publisher_portal_black.png b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/publisher_portal_black.png new file mode 100644 index 00000000..f3acfbe2 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/publisher_portal_black.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:376cffa3a224b6c60f0386119dada94b9f24428cba700a4c4b99a7d1132efb07 +size 33281 diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/publisher_portal_black.png.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/publisher_portal_black.png.meta new file mode 100644 index 00000000..305aa317 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/publisher_portal_black.png.meta @@ -0,0 +1,128 @@ +fileFormatVersion: 2 +guid: 8e0749dce5b14cc46b73b0303375c162 +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: 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: 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: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/publisher_portal_white.png b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/publisher_portal_white.png new file mode 100644 index 00000000..3af51e71 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/publisher_portal_white.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8d73ac86c29673df144abe18114eb06c9cd91dc336d7532dc92b685d7383ff1 +size 33588 diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/publisher_portal_white.png.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/publisher_portal_white.png.meta new file mode 100644 index 00000000..a0f13697 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/publisher_portal_white.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/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts.meta new file mode 100644 index 00000000..63c6efc3 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 15b24ad8f9d236249910fb8eef1e30ea +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASDebug.cs b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASDebug.cs new file mode 100644 index 00000000..42c53562 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASDebug.cs @@ -0,0 +1,64 @@ +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Uploader +{ + internal static class ASDebug + { + private enum LogType + { + Log, + Warning, + Error + } + + private static bool s_debugModeEnabled = EditorPrefs.GetBool("ASTDebugMode"); + + public static bool DebugModeEnabled + { + get => s_debugModeEnabled; + set + { + s_debugModeEnabled = value; + EditorPrefs.SetBool("ASTDebugMode", value); + } + } + + public static void Log(object message) + { + LogMessage(message, LogType.Log); + } + + 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.Log: + Debug.Log(message); + break; + case LogType.Warning: + Debug.LogWarning(message); + break; + case LogType.Error: + Debug.LogError(message); + break; + default: + Debug.Log(message); + break; + } + } + } +} diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASDebug.cs.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASDebug.cs.meta new file mode 100644 index 00000000..2f4aab78 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/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/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASError.cs b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASError.cs new file mode 100644 index 00000000..cf20b5d8 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASError.cs @@ -0,0 +1,90 @@ +using System; +using System.Net; +using System.Net.Http; + +namespace AssetStoreTools.Uploader +{ + public class ASError + { + public string Message { get; private set; } + public Exception Exception { get; private set; } + + public ASError() { } + + public static ASError GetGenericError(Exception ex) + { + ASError error = new ASError() + { + Message = ex.Message, + Exception = ex + }; + + return error; + } + + public static ASError GetLoginError(HttpResponseMessage response) => GetLoginError(response, null); + + public static ASError GetLoginError(HttpResponseMessage response, HttpRequestException ex) + { + ASError error = new ASError() { Exception = ex }; + + switch (response.StatusCode) + { + // Add common error codes here + case HttpStatusCode.Unauthorized: + error.Message = "Incorrect email and/or password. Please try again."; + break; + case HttpStatusCode.InternalServerError: + error.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: + ParseHtmlMessage(response, out string message); + error.Message = message; + break; + } + + return error; + } + + public static ASError GetPublisherNullError(string publisherName) + { + ASError error = new ASError + { + Message = $"Your Unity ID {publisherName} is not currently connected to a publisher account. " + + $"Please create a publisher profile." + }; + + return error; + } + + private static bool ParseHtmlMessage(HttpResponseMessage response, out string message) + { + message = "An undefined error has been encountered"; + string html = response.Content.ReadAsStringAsync().Result; + + if (!html.Contains("", StringComparison.Ordinal) + "

".Length; + var endIndex = html.IndexOf("

", StringComparison.Ordinal); + + if (startIndex == -1 || endIndex == -1) + return false; + + string htmlBodyMessage = html.Substring(startIndex, (endIndex - startIndex)); + htmlBodyMessage = htmlBodyMessage.Replace("\n", " "); + + message += htmlBodyMessage; + message += "\n\nIf this error message is not very informative, please report this to Unity"; + + return true; + } + + public override string ToString() + { + return Message; + } + } +} \ No newline at end of file diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASError.cs.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASError.cs.meta new file mode 100644 index 00000000..3e3eee6e --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASError.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 265ad6f65404f8c42aec35d3b8e6f970 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/AssetStoreAPI.cs b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/AssetStoreAPI.cs new file mode 100644 index 00000000..e39d23ea --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/AssetStoreAPI.cs @@ -0,0 +1,649 @@ +using AssetStoreTools.Utility.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Uploader +{ + public static class AssetStoreAPI + { + private const string ToolVersion = "V6.0.0"; + private const string AssetStoreProdUrl = "https://kharma.unity3d.com"; + private const string UnauthSessionId = "26c4202eb475d02864b40827dfff11a14657aa41"; + private const string KharmaSessionId = "kharma.sessionid"; + + private static string s_sessionId = EditorPrefs.GetString(KharmaSessionId); + private static HttpClient httpClient = new HttpClient(); + private static CancellationTokenSource s_downloadCancellationSource; + + public static string SavedSessionId + { + get => s_sessionId; + set + { + s_sessionId = value; + EditorPrefs.SetString(KharmaSessionId, value); + } + } + + public static bool IsCloudUserAvailable => CloudProjectSettings.userName != "anonymous"; + public static string LastLoggedInUser = ""; + public static Dictionary ActiveUploads = new Dictionary(); + public static bool IsUploading => (ActiveUploads.Count > 0); + + static AssetStoreAPI() + { + ServicePointManager.DefaultConnectionLimit = 500; + httpClient.DefaultRequestHeaders.ConnectionClose = false; + httpClient.Timeout = TimeSpan.FromMinutes(1320); + } + + #region Login API + + public static void Login(string email, string password, Action onSuccess, Action onFail) + { + FormUrlEncodedContent data = GetLoginContent(new Dictionary { { "user", email }, { "pass", password } }); + Login(data, onSuccess, onFail); + } + + public static void LoginWithSession(Action onSuccess, Action onFail, Action onFailNoSession) + { + if (string.IsNullOrEmpty(SavedSessionId)) + { + onFailNoSession?.Invoke(); + return; + } + + FormUrlEncodedContent data = GetLoginContent(new Dictionary { { "reuse_session", SavedSessionId }, { "xunitysession", UnauthSessionId } }); + Login(data, onSuccess, onFail); + } + + public static void LoginWithToken(string token, Action onSuccess, Action onFail) + { + FormUrlEncodedContent data = GetLoginContent(new Dictionary { { "user_access_token", token } }); + Login(data, onSuccess, onFail); + } + + private static async void Login(FormUrlEncodedContent data, Action onSuccess, Action onFail) + { + Uri uri = new Uri($"{AssetStoreProdUrl}/login"); + + httpClient.DefaultRequestHeaders.Clear(); + httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); + + try + { + var response = await httpClient.PostAsync(uri, data); + UploadValuesCompletedLogin(response, onSuccess, onFail); + } + catch (Exception e) + { + onFail?.Invoke(ASError.GetGenericError(e)); + } + } + + private static void UploadValuesCompletedLogin(HttpResponseMessage response, Action onSuccess, Action onFail) + { + ASDebug.Log($"Upload Values Complete {response.ReasonPhrase}"); + ASDebug.Log($"Login success? {response.IsSuccessStatusCode}"); + try + { + response.EnsureSuccessStatusCode(); + var responseResult = response.Content.ReadAsStringAsync().Result; + var success = JSONParser.AssetStoreResponseParse(responseResult, out ASError error, out JsonValue jsonResult); + if (success) + onSuccess?.Invoke(jsonResult); + else + onFail?.Invoke(error); + } + catch (HttpRequestException ex) + { + onFail?.Invoke(ASError.GetLoginError(response, ex)); + } + } + + #endregion + + #region Package Metadata API + + private static async Task GetPackageDataMain() + { + return await GetAssetStoreData(APIUri("asset-store-tools", "metadata/0", SavedSessionId)); + } + + private static async Task GetPackageDataExtra() + { + return await GetAssetStoreData(APIUri("management", "packages", SavedSessionId)); + } + + private static async Task GetCategories(bool useCached) + { + if(useCached) + { + if (AssetStoreCache.GetCachedCategories(out JsonValue cachedCategoryJson)) + return cachedCategoryJson; + + ASDebug.LogWarning("Failed to retrieve cached category data. Proceeding to download"); + } + var categoryJson = await GetAssetStoreData(APIUri("management", "categories", SavedSessionId)); + AssetStoreCache.CacheCategories(categoryJson); + + return categoryJson; + } + + public static async void GetPackageDataFull(bool useCached, Action onSuccess, Action onFail) + { + if (useCached) + { + if (AssetStoreCache.GetCachedPackageMetadata(out JsonValue cachedData)) + { + onSuccess?.Invoke(cachedData); + return; + } + + ASDebug.LogWarning("Failed to retrieve cached package metadata. Proceeding to download"); + } + + try + { + var jsonMainData = await GetPackageDataMain(); + var jsonExtraData = await GetPackageDataExtra(); + var jsonCategoryData = await GetCategories(useCached); + + var joinedData = MergePackageData(jsonMainData, jsonExtraData, jsonCategoryData); + AssetStoreCache.CachePackageMetadata(joinedData); + + onSuccess?.Invoke(joinedData); + } + catch (OperationCanceledException) + { + ASDebug.Log("Package metadata download operation cancelled"); + DisposeDownloadCancellation(); + } + catch (Exception e) + { + onFail?.Invoke(ASError.GetGenericError(e)); + } + } + + public static async void GetPackageThumbnails(JsonValue packageJson, bool useCached, Action onSuccess, Action onFail) + { + SetupDownloadCancellation(); + var packageDict = packageJson["packages"].AsDict(); + var packageEnum = packageDict.GetEnumerator(); + + for (int i = 0; i < packageDict.Count; i++) + { + packageEnum.MoveNext(); + var package = packageEnum.Current; + + try + { + s_downloadCancellationSource.Token.ThrowIfCancellationRequested(); + + if (package.Value["icon_url"] + .IsNull()) // If no URL is found in the package metadata, use the default image + { + Texture2D fallbackTexture = null; + ASDebug.Log($"Package {package.Key} has no thumbnail. Returning default image"); + onSuccess?.Invoke(package.Key, fallbackTexture); + continue; + } + + if (useCached && + AssetStoreCache.GetCachedTexture(package.Key, + out Texture2D texture)) // Try returning cached thumbnails first + { + ASDebug.Log($"Returning cached thumbnail for package {package.Key}"); + onSuccess?.Invoke(package.Key, texture); + continue; + } + + var textureBytes = + await DownloadPackageThumbnail(package.Value["icon_url"].AsString()); + Texture2D tex = new Texture2D(1, 1, TextureFormat.RGBA32, false); + tex.LoadImage(textureBytes); + AssetStoreCache.CacheTexture(package.Key, tex); + ASDebug.Log($"Returning downloaded thumbnail for package {package.Key}"); + onSuccess?.Invoke(package.Key, tex); + } + catch (OperationCanceledException) + { + DisposeDownloadCancellation(); + ASDebug.Log("Package thumbnail download operation cancelled"); + return; + } + catch (Exception e) + { + onFail?.Invoke(package.Key, ASError.GetGenericError(e)); + } + finally + { + packageEnum.Dispose(); + } + } + } + + private static async Task DownloadPackageThumbnail(string url) + { + // icon_url is presented without http/https + Uri uri = new Uri($"https:{url}"); + + var textureBytes = await httpClient.GetAsync(uri, s_downloadCancellationSource.Token). + ContinueWith((response) => response.Result.Content.ReadAsByteArrayAsync().Result, s_downloadCancellationSource.Token); + s_downloadCancellationSource.Token.ThrowIfCancellationRequested(); + return textureBytes; + } + + public static async void GetRefreshedPackageData(string packageId, Action onSuccess, Action onFail) + { + try + { + var refreshedDataJson = await GetPackageDataExtra(); + var refreshedPackage = default(JsonValue); + + // Find the updated package data in the latest data json + foreach (var p in refreshedDataJson["packages"].AsList()) + { + if (p["id"] == packageId) + { + refreshedPackage = p["versions"].AsList()[p["versions"].AsList().Count - 1]; + break; + } + } + + if (refreshedPackage.Equals(default(JsonValue))) + { + onFail?.Invoke(ASError.GetGenericError(new MissingMemberException($"Unable to find downloaded package data for package id {packageId}"))); + return; + } + + // Check if the supplied package id data has been cached and if it contains the corresponding package + if (!AssetStoreCache.GetCachedPackageMetadata(out JsonValue cachedData) || + !cachedData["packages"].AsDict().ContainsKey(packageId)) + { + onFail?.Invoke(ASError.GetGenericError(new MissingMemberException($"Unable to find cached package id {packageId}"))); + return; + } + + var cachedPackage = cachedData["packages"].AsDict()[packageId]; + + // Retrieve the category map + var categoryJson = await GetCategories(true); + var categories = CreateCategoryDictionary(categoryJson); + + // Update the package data + cachedPackage["name"] = refreshedPackage["name"].AsString(); + cachedPackage["status"] = refreshedPackage["status"].AsString(); + cachedPackage["extra_info"].AsDict()["category_info"].AsDict()["id"] = refreshedPackage["category_id"].AsString(); + cachedPackage["extra_info"].AsDict()["category_info"].AsDict()["name"] = + categories.ContainsKey(refreshedPackage["category_id"]) ? categories[refreshedPackage["category_id"].AsString()] : "Unknown"; + cachedPackage["extra_info"].AsDict()["modified"] = refreshedPackage["modified"].AsString(); + cachedPackage["extra_info"].AsDict()["size"] = refreshedPackage["size"].AsString(); + + AssetStoreCache.CachePackageMetadata(cachedData); + onSuccess?.Invoke(cachedPackage); + } + catch (OperationCanceledException) + { + ASDebug.Log("Package metadata download operation cancelled"); + DisposeDownloadCancellation(); + } + catch (Exception e) + { + onFail?.Invoke(ASError.GetGenericError(e)); + } + } + + public static List GetPackageUploadedVersions(string packageId, string versionId) + { + var versions = new List(); + try + { + // Retrieve the data for already uploaded versions (should prevent interaction with Uploader) + var versionsTask = Task.Run(() => GetAssetStoreData(APIUri("content", $"preview/{packageId}/{versionId}", SavedSessionId))); + if (!versionsTask.Wait(5000)) + throw new TimeoutException("Could not retrieve uploaded versions within a reasonable time interval"); + + var versionsJson = versionsTask.Result; + foreach (var version in versionsJson["content"].AsDict()["unity_versions"].AsList()) + versions.Add(version.AsString()); + } + catch (OperationCanceledException) + { + ASDebug.Log("Package version download operation cancelled"); + DisposeDownloadCancellation(); + } + catch (Exception e) + { + ASDebug.LogError(e); + } + + return versions; + } + + #endregion + + #region Package Upload API + + public static async Task UploadPackage(string packageId, string packageName, string filePath, + string localPackageGuid, string localPackagePath, string localProjectPath) + { + try + { + ASDebug.Log("Upload task starting"); + // Reloading assemblies or entering Play Mode may cancel the upload as static variables are reset + EditorApplication.LockReloadAssemblies(); + if (!IsUploading) // Only subscribe before the first upload + EditorApplication.playModeStateChanged += EditorPlayModeStateChangeHandler; + + var progressData = new OngoingUpload(packageId, packageName); + ActiveUploads.Add(packageId, progressData); + + var result = await Task.Run(() => UploadPackageTask(progressData, filePath, localPackageGuid, localPackagePath, localProjectPath)); + + ActiveUploads.Remove(packageId); + + ASDebug.Log("Upload task finished"); + return result; + } + catch (Exception e) + { + return PackageUploadResult.PackageUploadFail(ASError.GetGenericError(e)); + } + finally + { + if (!IsUploading) // Only unsubscribe after the last upload + EditorApplication.playModeStateChanged -= EditorPlayModeStateChangeHandler; + EditorApplication.UnlockReloadAssemblies(); + } + } + + private static PackageUploadResult UploadPackageTask(OngoingUpload currentUpload, string filePath, + string localPackageGuid, string localPackagePath, string localProjectPath) + { + ASDebug.Log("Preparing to upload package within API"); + string api = "asset-store-tools"; + string uri = $"package/{currentUpload.VersionId}/unitypackage"; + + Dictionary packageParams = new Dictionary + { + {"root_guid", localPackageGuid}, // NOTE: prepackaged uploads will not pass these parameters. + {"root_path", localPackagePath}, // We need to make sure that the backend validation + {"project_path", localProjectPath} // service accepts such use-cases without failure + }; + + ASDebug.Log($"Creating upload request for {currentUpload.VersionId} {currentUpload.PackageName}"); + + FileStream requestFileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + httpClient.DefaultRequestHeaders.Clear(); + httpClient.DefaultRequestHeaders.Add("X-Unity-Session", SavedSessionId); + + long chunkSize = 32768; + try + { + ASDebug.Log("Starting upload process..."); + var watch = System.Diagnostics.Stopwatch.StartNew(); // Debugging + + var content = new StreamContent(requestFileStream, (int)chunkSize); + var response = httpClient.PutAsync(APIUri(api, uri, SavedSessionId, packageParams), content, currentUpload.CancellationToken); + + // Progress tracking + int updateIntervalMs = 100; + DateTime previousTime = DateTime.Now; + while (!response.IsCompleted) + { + var currentTime = DateTime.Now; + if (DateTime.Now.Subtract(previousTime).Milliseconds < updateIntervalMs) + continue; + previousTime = currentTime; + + float uploadProgress = (float)requestFileStream.Position / requestFileStream.Length * 100; + currentUpload.UpdateProgress(uploadProgress); + } + + // 2020.3 - although cancellation token shows a requested cancellation, the HttpClient + // tends to return a false 'IsCanceled' value, thus yielding an exception when attempting to read the response. + // For now we'll just check the token as well, but this needs to be investigated later on. + if (response.IsCanceled || currentUpload.CancellationToken.IsCancellationRequested) + currentUpload.CancellationToken.ThrowIfCancellationRequested(); + + watch.Stop(); + + ASDebug.Log($"Finished uploading, time taken: {watch.Elapsed.Seconds} seconds"); + var responseString = response.Result.Content.ReadAsStringAsync().Result; + + var success = JSONParser.AssetStoreResponseParse(responseString, out ASError error, out JsonValue json); + ASDebug.Log("Upload response JSON: " + json.ToString()); + if (success) + return PackageUploadResult.PackageUploadSuccess(); + else + return PackageUploadResult.PackageUploadFail(error); + } + catch (OperationCanceledException) + { + // Uploading is canceled + ASDebug.Log("Upload operation cancelled"); + return PackageUploadResult.PackageUploadCancelled(); + } + catch (Exception e) + { + ASDebug.LogError("Upload operation encountered an undefined exception: " + e); + var error = ASError.GetGenericError(e); + return PackageUploadResult.PackageUploadFail(error); + } + finally + { + requestFileStream.Dispose(); + currentUpload.Dispose(); + } + } + + public static void AbortPackageUpload(string packageId) + { + ActiveUploads[packageId]?.Cancel(); + } + + #endregion + + #region Utility Methods + + private static string GetLicenseHash() + { + return UnityEditorInternal.InternalEditorUtility.GetAuthToken().Substring(0, 40); + } + + private static string GetHardwareHash() + { + return UnityEditorInternal.InternalEditorUtility.GetAuthToken().Substring(40, 40); + } + + private static FormUrlEncodedContent GetLoginContent(Dictionary loginData) + { + loginData.Add("unityversion", Application.unityVersion); + loginData.Add("toolversion", ToolVersion); + loginData.Add("license_hash", GetLicenseHash()); + loginData.Add("hardware_hash", GetHardwareHash()); + + return new FormUrlEncodedContent(loginData); + } + + private static async Task GetAssetStoreData(Uri uri) + { + SetupDownloadCancellation(); + + httpClient.DefaultRequestHeaders.Clear(); + httpClient.DefaultRequestHeaders.Add("X-Unity-Session", SavedSessionId); + + var response = await httpClient.GetAsync(uri, s_downloadCancellationSource.Token) + .ContinueWith((x) => x.Result.Content.ReadAsStringAsync().Result, s_downloadCancellationSource.Token); + s_downloadCancellationSource.Token.ThrowIfCancellationRequested(); + + if (!JSONParser.AssetStoreResponseParse(response, out var error, out var jsonMainData)) + throw error.Exception; + + return jsonMainData; + } + + private static Uri APIUri(string apiPath, string endPointPath, string sessionId) + { + return APIUri(apiPath, endPointPath, sessionId, null); + } + + // Method borrowed from A$ tools, could maybe be simplified to only retain what is necessary? + private static Uri APIUri(string apiPath, string endPointPath, string sessionId, IDictionary extraQuery) + { + Dictionary extraQueryMerged; + + if (extraQuery == null) + extraQueryMerged = new Dictionary(); + else + extraQueryMerged = new Dictionary(extraQuery); + + extraQueryMerged.Add("unityversion", Application.unityVersion); + extraQueryMerged.Add("toolversion", ToolVersion); + extraQueryMerged.Add("xunitysession", sessionId); + + string uriPath = $"{AssetStoreProdUrl}/api/{apiPath}/{endPointPath}.json"; + UriBuilder uriBuilder = new UriBuilder(uriPath); + + StringBuilder queryToAppend = new StringBuilder(); + foreach (KeyValuePair queryPair in extraQueryMerged) + { + string queryName = queryPair.Key; + string queryValue = Uri.EscapeDataString(queryPair.Value); + + queryToAppend.AppendFormat("&{0}={1}", queryName, queryValue); + } + if (!string.IsNullOrEmpty(uriBuilder.Query)) + uriBuilder.Query = uriBuilder.Query.Substring(1) + queryToAppend; + else + uriBuilder.Query = queryToAppend.Remove(0, 1).ToString(); + + return uriBuilder.Uri; + } + + private static JsonValue MergePackageData(JsonValue mainPackageData, JsonValue extraPackageData, JsonValue categoryData) + { + ASDebug.Log($"Main package data\n{mainPackageData}"); + var mainDataDict = mainPackageData["packages"].AsDict(); + + // Most likely both of them will be true at the same time, but better to be safe + if (mainDataDict.Count == 0 || !extraPackageData.ContainsKey("packages")) + return new JsonValue(); + + ASDebug.Log($"Extra package data\n{extraPackageData}"); + var extraDataDict = extraPackageData["packages"].AsList(); + + var categories = CreateCategoryDictionary(categoryData); + + foreach (var md in mainDataDict) + { + foreach (var ed in extraDataDict) + { + if (ed["id"].AsString() != md.Key) + continue; + + // Create a field for extra data + var extraData = JsonValue.NewDict(); + + // Add category field + var categoryEntry = JsonValue.NewDict(); + + var categoryId = ed["category_id"].AsString(); + var categoryName = categories.ContainsKey(categoryId) ? categories[categoryId] : "Unknown"; + + categoryEntry["id"] = categoryId; + categoryEntry["name"] = categoryName; + + extraData["category_info"] = categoryEntry; + + // Add modified time and size + var versions = ed["versions"].AsList(); + extraData["modified"] = versions[versions.Count - 1]["modified"]; + extraData["size"] = versions[versions.Count - 1]["size"]; + + md.Value.AsDict()["extra_info"] = extraData; + } + } + + mainPackageData.AsDict()["packages"] = new JsonValue(mainDataDict); + return mainPackageData; + } + + private static Dictionary CreateCategoryDictionary(JsonValue json) + { + var categories = new Dictionary(); + + var list = json.AsList(); + + for (int i = 0; i < list.Count; i++) + { + var category = list[i].AsDict(); + if (category["status"].AsString() == "deprecated") + continue; + categories.Add(category["id"].AsString(), category["assetstore_name"].AsString()); + } + + return categories; + } + + public static bool IsPublisherValid(JsonValue json, out ASError error) + { + error = ASError.GetPublisherNullError(json["name"]); + + if (!json.ContainsKey("publisher")) + return false; + + // If publisher account is not created - let them know + return !json["publisher"].IsNull(); + } + + public static void AbortDownloadTasks() + { + s_downloadCancellationSource?.Cancel(); + } + + public static void AbortUploadTasks() + { + foreach(var upload in ActiveUploads) + { + AbortPackageUpload(upload.Key); + } + } + + private static void SetupDownloadCancellation() + { + if (s_downloadCancellationSource != null && s_downloadCancellationSource.IsCancellationRequested) + DisposeDownloadCancellation(); + + if (s_downloadCancellationSource == null) + s_downloadCancellationSource = new CancellationTokenSource(); + } + + private static void DisposeDownloadCancellation() + { + s_downloadCancellationSource?.Dispose(); + s_downloadCancellationSource = null; + } + + private static void EditorPlayModeStateChangeHandler(PlayModeStateChange state) + { + if (state != 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"); + } + + #endregion + } +} \ No newline at end of file diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/AssetStoreAPI.cs.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/AssetStoreAPI.cs.meta new file mode 100644 index 00000000..d248bfef --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/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/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/AssetStoreCache.cs b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/AssetStoreCache.cs new file mode 100644 index 00000000..8bb11fa0 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/AssetStoreCache.cs @@ -0,0 +1,128 @@ +using AssetStoreTools.Utility.Json; +using System.IO; +using System.Text; +using UnityEngine; + +namespace AssetStoreTools.Uploader +{ + public static class AssetStoreCache + { + public const string TempCachePath = "Temp/AssetStoreToolsCache"; + public const string PersistentCachePath = "Library/AssetStoreToolsCache"; + + private const string PackageDataFile = "PackageMetadata.json"; + private const string CategoryDataFile = "Categories.json"; + + private static void CreateFileInTempCache(string fileName, object content, bool overwrite) + { + CreateCacheFile(TempCachePath, fileName, content, overwrite); + } + + private static void CreateFileInPersistentCache(string fileName, object content, bool overwrite) + { + CreateCacheFile(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; + } + } + + public static void ClearTempCache() + { + if (!File.Exists(Path.Combine(TempCachePath, 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) + File.Delete(Path.Combine(TempCachePath, PackageDataFile)); + } + + public static void CacheCategories(JsonValue data) + { + CreateFileInTempCache(CategoryDataFile, data, true); + } + + public static bool GetCachedCategories(out JsonValue data) + { + data = new JsonValue(); + var path = Path.Combine(TempCachePath, CategoryDataFile); + if (!File.Exists(path)) + return false; + + data = JSONParser.SimpleParse(File.ReadAllText(path, Encoding.UTF8)); + return true; + } + + public static void CachePackageMetadata(JsonValue data) + { + CreateFileInTempCache(PackageDataFile, data.ToString(), true); + } + + public static bool GetCachedPackageMetadata(out JsonValue data) + { + data = new JsonValue(); + var path = Path.Combine(TempCachePath, PackageDataFile); + if (!File.Exists(path)) + return false; + + data = JSONParser.SimpleParse(File.ReadAllText(path, Encoding.UTF8)); + return true; + } + + 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(TempCachePath, $"{packageId}.png"); + if (!File.Exists(path)) + return false; + + texture.LoadImage(File.ReadAllBytes(path)); + return true; + } + + public static void CacheUploadSelections(string packageId, string versionId, JsonValue json) + { + var fileName = $"{packageId}-{versionId}-uploadselection.asset"; + CreateFileInPersistentCache(fileName, json.ToString(), true); + } + + public static bool GetCachedUploadSelections(string packageId, string versionId, out JsonValue json) + { + json = new JsonValue(); + var path = Path.Combine(PersistentCachePath, $"{packageId}-{versionId}-uploadselection.asset"); + if (!File.Exists(path)) + return false; + + json = JSONParser.SimpleParse(File.ReadAllText(path, Encoding.UTF8)); + return true; + } + } +} diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/AssetStoreCache.cs.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/AssetStoreCache.cs.meta new file mode 100644 index 00000000..fca787fb --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/AssetStoreCache.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/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/LoginWindow.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/LoginWindow.meta new file mode 100644 index 00000000..3a2c655e --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/LoginWindow.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6390238ed687a564cb0236e8d6ba8cd9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/LoginWindow/LoginWindow.cs b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/LoginWindow/LoginWindow.cs new file mode 100644 index 00000000..7688d0e7 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/LoginWindow/LoginWindow.cs @@ -0,0 +1,221 @@ +using AssetStoreTools.Utility.Json; +using System; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader +{ + public class LoginWindow : VisualElement + { + private readonly string REGISTER_URL = "https://publisher.unity.com/access"; + private readonly string FORGOT_PASSWORD_URL = "https://id.unity.com/password/new"; + + private Button _cloudLoginButton; + private Button _credentialsLoginButton; + + private Label _cloudLoginLabel; + + private TextField _emailField; + private TextField _passwordField; + + private Box _errorBox; + private Label _errorLabel; + + private double _cloudLoginRefreshTime = 1d; + private double _lastRefreshTime; + + public new class UxmlFactory : UxmlFactory { } + + public LoginWindow() + { + StyleSelector.SetStyle(this, StyleSelector.Style.Login, !EditorGUIUtility.isProSkin); + ConstructLoginWindow(); + EditorApplication.update += UpdateCloudLoginButton; + } + + public void SetupLoginElements(Action onSuccess, Action onFail) + { + this.SetEnabled(true); + + _cloudLoginLabel = _cloudLoginButton.Q