("UploadWindow");
+
+ _loginWindow.SetupLoginElements(OnLoginSuccess, OnLoginFail);
+ _uploadWindow.SetupWindows(OnLogout, OnPackageDownloadFail);
+ }
+
+ #region Login Interface
+
+ private async 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);
+ var result = await AssetStoreAPI.LoginWithSessionAsync();
+ if (result.Success)
+ OnLoginSuccess(result.Response);
+ else if (result.SilentFail)
+ OnLoginFailSession();
+ else
+ OnLoginFail(result.Error);
+ }
+
+ 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 0000000..ef78266
--- /dev/null
+++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/AssetStoreUploader.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/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 0000000..7026063
--- /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 0000000..245875b
Binary files /dev/null and b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/open-in-browser.png differ
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 0000000..26ccaa5
--- /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 0000000..621e906
Binary files /dev/null and b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/publisher_portal_black.png differ
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 0000000..305aa31
--- /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 0000000..70f4703
Binary files /dev/null and b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Icons/publisher_portal_white.png differ
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 0000000..a0f1369
--- /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 0000000..63c6efc
--- /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/ASAnalytics.cs b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASAnalytics.cs
new file mode 100644
index 0000000..a0feb73
--- /dev/null
+++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASAnalytics.cs
@@ -0,0 +1,47 @@
+using UnityEditor;
+using UnityEngine.Analytics;
+
+namespace AssetStoreTools.Uploader
+{
+ internal static class ASAnalytics
+ {
+ private const int VersionId = 3;
+ private const int MaxEventsPerHour = 20;
+ private const int MaxNumberOfElements = 1000;
+
+ private const string VendorKey = "unity.assetStoreTools";
+ private const string EventName = "assetStoreTools";
+
+ static bool EnableAnalytics()
+ {
+ var result = EditorAnalytics.RegisterEventWithLimit(EventName, MaxEventsPerHour, MaxNumberOfElements, VendorKey, VersionId);
+ return result == AnalyticsResult.Ok;
+ }
+
+ [System.Serializable]
+ public struct AnalyticsData
+ {
+ public string ToolVersion;
+ 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 static void SendUploadingEvent(AnalyticsData data)
+ {
+ if (!EditorAnalytics.enabled)
+ return;
+
+ if (!EnableAnalytics())
+ return;
+
+ EditorAnalytics.SendEventWithLimit(EventName, data, VersionId);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASAnalytics.cs.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASAnalytics.cs.meta
new file mode 100644
index 0000000..22a47b1
--- /dev/null
+++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASAnalytics.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 1095145789a64767a6add837eea19786
+timeCreated: 1658832954
\ No newline at end of file
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 0000000..ed116ab
--- /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 0000000..2f4aab7
--- /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 0000000..142d5d5
--- /dev/null
+++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/ASError.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Net;
+using System.Net.Http;
+
+namespace AssetStoreTools.Uploader
+{
+ ///
+ /// A structure for retrieving and converting errors from Asset Store Tools class methods
+ ///
+ internal 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 0000000..3e3eee6
--- /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 0000000..367fa24
--- /dev/null
+++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/AssetStoreAPI.cs
@@ -0,0 +1,770 @@
+using AssetStoreTools.Utility.Json;
+using System;
+using System.Collections.Concurrent;
+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
+{
+ ///
+ /// A class for retrieving data from the Asset Store backend
+ /// Note: most data retrieval methods require to be set
+ ///
+ internal static class AssetStoreAPI
+ {
+ public const string ToolVersion = "V6.2.1";
+ private const string UnauthSessionId = "26c4202eb475d02864b40827dfff11a14657aa41";
+ private const string KharmaSessionId = "kharma.sessionid";
+ private const int UploadResponseTimeoutMs = 10000;
+
+ public static string AssetStoreProdUrl = "https://kharma.unity3d.com";
+ 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);
+ httpClient.DefaultRequestHeaders.Clear();
+ if (!string.IsNullOrEmpty(value))
+ httpClient.DefaultRequestHeaders.Add("X-Unity-Session", SavedSessionId);
+ }
+ }
+
+ public static bool IsCloudUserAvailable => CloudProjectSettings.userName != "anonymous";
+ public static string LastLoggedInUser = "";
+ public static ConcurrentDictionary ActiveUploads = new ConcurrentDictionary();
+ public static bool IsUploading => (ActiveUploads.Count > 0);
+
+ static AssetStoreAPI()
+ {
+ ServicePointManager.DefaultConnectionLimit = 500;
+ httpClient.DefaultRequestHeaders.ConnectionClose = false;
+ httpClient.Timeout = TimeSpan.FromMinutes(1320);
+ }
+
+ ///
+ /// A structure used to return the success outcome and the result of Asset Store API calls
+ ///
+ internal class APIResult
+ {
+ public JsonValue Response;
+ public bool Success;
+ public bool SilentFail;
+ public ASError Error;
+
+ public static implicit operator bool(APIResult value)
+ {
+ return value != null && value.Success != false;
+ }
+ }
+
+ #region Login API
+
+ ///
+ /// A login API call that uses the email and password credentials
+ ///
+ ///
+ /// Note: this method only returns a response from the server and does not set the itself
+ ///
+ public static async Task LoginWithCredentialsAsync(string email, string password)
+ {
+ FormUrlEncodedContent data = GetLoginContent(new Dictionary { { "user", email }, { "pass", password } });
+ return await LoginAsync(data);
+ }
+
+ ///
+ /// A login API call that uses the
+ ///
+ ///
+ /// Note: this method only returns a response from the server and does not set the itself
+ ///
+ public static async Task LoginWithSessionAsync()
+ {
+ if (string.IsNullOrEmpty(SavedSessionId))
+ return new APIResult() { Success = false, SilentFail = true, Error = ASError.GetGenericError(new Exception("No active session available")) };
+
+ FormUrlEncodedContent data = GetLoginContent(new Dictionary { { "reuse_session", SavedSessionId }, { "xunitysession", UnauthSessionId } });
+ return await LoginAsync(data);
+ }
+
+ ///
+ /// A login API call that uses the
+ ///
+ ///
+ /// Note: this method only returns a response from the server and does not set the itself
+ ///
+ /// Cloud access token. Can be retrieved by calling
+ public static async Task LoginWithTokenAsync(string token)
+ {
+ FormUrlEncodedContent data = GetLoginContent(new Dictionary { { "user_access_token", token } });
+ return await LoginAsync(data);
+ }
+
+ private static async Task LoginAsync(FormUrlEncodedContent data)
+ {
+ OverrideAssetStoreUrl();
+ Uri uri = new Uri($"{AssetStoreProdUrl}/login");
+
+ httpClient.DefaultRequestHeaders.Clear();
+ httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
+
+ try
+ {
+ var response = await httpClient.PostAsync(uri, data);
+ return UploadValuesCompletedLogin(response);
+ }
+ catch (Exception e)
+ {
+ return new APIResult() { Success = false, Error = ASError.GetGenericError(e) };
+ }
+ }
+
+ private static APIResult UploadValuesCompletedLogin(HttpResponseMessage response)
+ {
+ 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)
+ return new APIResult() { Success = true, Response = jsonResult };
+ else
+ return new APIResult() { Success = false, Error = error };
+ }
+ catch (HttpRequestException ex)
+ {
+ return new APIResult() { Success = false, Error = 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;
+ }
+
+ ///
+ /// Retrieve data for all packages associated with the currently logged in account (identified by )
+ ///
+ ///
+ ///
+ public static async Task GetFullPackageDataAsync(bool useCached)
+ {
+ if (useCached)
+ {
+ if (AssetStoreCache.GetCachedPackageMetadata(out JsonValue cachedData))
+ return new APIResult() { Success = true, Response = cachedData };
+
+ 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);
+
+ return new APIResult() { Success = true, Response = joinedData };
+ }
+ catch (OperationCanceledException e)
+ {
+ ASDebug.Log("Package metadata download operation cancelled");
+ DisposeDownloadCancellation();
+ return new APIResult() { Success = false, SilentFail = true, Error = ASError.GetGenericError(e) };
+ }
+ catch (Exception e)
+ {
+ return new APIResult() { Success = false, Error = ASError.GetGenericError(e) };
+ }
+ }
+
+ ///
+ /// Retrieve the thumbnail textures for all packages within the provided json structure and perform a given action after each retrieval
+ ///
+ /// A json file retrieved from
+ /// Return cached thumbnails if they are found
+ ///
+ /// Action to perform upon a successful thumbnail retrieval
+ /// - Package Id
+ /// - Associated Thumbnail
+ ///
+ ///
+ /// Action to perform upon a failed thumbnail retrieval
+ /// - Package Id
+ /// - Associated error
+ ///
+ 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;
+ }
+
+ ///
+ /// Retrieve, update the cache and return the updated data for a previously cached package
+ ///
+ public static async Task GetRefreshedPackageData(string packageId)
+ {
+ 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)))
+ return new APIResult() { Success = false, Error = ASError.GetGenericError(new MissingMemberException($"Unable to find downloaded package data for package id {packageId}")) };
+
+ // 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))
+ return new APIResult() { Success = false, Error = ASError.GetGenericError(new MissingMemberException($"Unable to find cached package id {packageId}")) };
+
+ 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);
+ return new APIResult() { Success = true, Response = cachedPackage };
+ }
+ catch (OperationCanceledException)
+ {
+ ASDebug.Log("Package metadata download operation cancelled");
+ DisposeDownloadCancellation();
+ return new APIResult() { Success = false, SilentFail = true };
+ }
+ catch (Exception e)
+ {
+ return new APIResult() { Success = false, Error = ASError.GetGenericError(e) };
+ }
+ }
+
+ ///
+ /// Retrieve all Unity versions that the given package has already had uploaded content with
+ ///
+ ///
+ ///
+ ///
+ 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
+
+ ///
+ /// Upload a content file (.unitypackage) to a provided package version
+ ///
+ ///
+ /// Name of the package. Only used for identifying the package in class
+ /// Path to the .unitypackage file
+ /// The value of the main content folder for the provided package
+ /// The local path (relative to the root project folder) of the main content folder for the provided package
+ /// The path to the project that this package was built from
+ ///
+ public static async Task UploadPackageAsync(string versionId, string packageName, string filePath,
+ string localPackageGuid, string localPackagePath, string localProjectPath)
+ {
+ try
+ {
+ ASDebug.Log("Upload task starting");
+ EditorApplication.LockReloadAssemblies();
+
+ if (!IsUploading) // Only subscribe before the first upload
+ EditorApplication.playModeStateChanged += EditorPlayModeStateChangeHandler;
+
+ var progressData = new OngoingUpload(versionId, packageName);
+ ActiveUploads.TryAdd(versionId, progressData);
+
+ var result = await Task.Run(() => UploadPackageTask(progressData, filePath, localPackageGuid, localPackagePath, localProjectPath));
+
+ ActiveUploads.TryRemove(versionId, out OngoingUpload _);
+
+ ASDebug.Log("Upload task finished");
+ return result;
+ }
+ catch (Exception e)
+ {
+ ASDebug.LogError("Upload task failed with an exception: " + e);
+ ActiveUploads.TryRemove(versionId, out OngoingUpload _);
+ 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
+ {
+ // Note: project_path is currently used to store UI selections
+ {"root_guid", localPackageGuid},
+ {"root_path", localPackagePath},
+ {"project_path", localProjectPath}
+ };
+
+ ASDebug.Log($"Creating upload request for {currentUpload.VersionId} {currentUpload.PackageName}");
+
+ FileStream requestFileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
+
+ bool responseTimedOut = false;
+ long chunkSize = 32768;
+ try
+ {
+ ASDebug.Log("Starting upload process...");
+
+ var content = new StreamContent(requestFileStream, (int)chunkSize);
+ var response = httpClient.PutAsync(APIUri(api, uri, SavedSessionId, packageParams), content, currentUpload.CancellationToken);
+
+ // Progress tracking
+ int updateIntervalMs = 100;
+ bool allBytesSent = false;
+ DateTime timeOfCompletion = default(DateTime);
+
+ while (!response.IsCompleted)
+ {
+ float uploadProgress = (float)requestFileStream.Position / requestFileStream.Length * 100;
+ currentUpload.UpdateProgress(uploadProgress);
+ Thread.Sleep(updateIntervalMs);
+
+ // A timeout for rare cases, when package uploading reaches 100%, but PutAsync 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)
+ {
+ responseTimedOut = true;
+ currentUpload.Cancel();
+ break;
+ }
+ }
+ }
+
+ // 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();
+
+ 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
+ if (!responseTimedOut)
+ {
+ ASDebug.Log("Upload operation cancelled");
+ return PackageUploadResult.PackageUploadCancelled();
+ }
+ else
+ {
+ ASDebug.LogWarning("All data has been uploaded, but waiting for the response timed out");
+ return PackageUploadResult.PackageUploadResponseTimeout();
+ }
+ }
+ catch (Exception e)
+ {
+ ASDebug.LogError("Upload operation encountered an undefined exception: " + e);
+ var fullError = e.InnerException != null ? ASError.GetGenericError(e.InnerException) : ASError.GetGenericError(e);
+ return PackageUploadResult.PackageUploadFail(fullError);
+ }
+ finally
+ {
+ requestFileStream.Dispose();
+ currentUpload.Dispose();
+ }
+ }
+
+ ///
+ /// Cancel the uploading task for a package with the provided package id
+ ///
+ 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();
+
+ 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;
+ }
+
+ ///
+ /// Check if the account data is for a valid publisher account
+ ///
+ /// Json structure retrieved from one of the API login methods
+ 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();
+ }
+
+ ///
+ /// Cancel all data retrieval tasks
+ ///
+ public static void AbortDownloadTasks()
+ {
+ s_downloadCancellationSource?.Cancel();
+ }
+
+ ///
+ /// Cancel all data uploading tasks
+ ///
+ 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");
+ }
+
+ private static void OverrideAssetStoreUrl()
+ {
+ var args = Environment.GetCommandLineArgs();
+ for (var i = 0; i < args.Length; i++)
+ {
+ if (!args[i].Equals("-assetStoreUrl"))
+ continue;
+
+ if (i + 1 >= args.Length)
+ return;
+
+ ASDebug.Log($"Overriding A$ URL to: {args[i + 1]}");
+ AssetStoreProdUrl = args[i + 1];
+ return;
+ }
+ }
+
+ #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 0000000..d248bfe
--- /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 0000000..856688e
--- /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
+{
+ internal 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, JsonValue json)
+ {
+ var fileName = $"{packageId}-uploadselection.asset";
+ CreateFileInPersistentCache(fileName, json.ToString(), true);
+ }
+
+ public static bool GetCachedUploadSelections(string packageId, out JsonValue json)
+ {
+ json = new JsonValue();
+ var path = Path.Combine(PersistentCachePath, $"{packageId}-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 0000000..fca787f
--- /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 0000000..3a2c655
--- /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 0000000..8afd509
--- /dev/null
+++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreUploader/Scripts/LoginWindow/LoginWindow.cs
@@ -0,0 +1,231 @@
+using AssetStoreTools.Utility.Json;
+using System;
+using UnityEditor;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace AssetStoreTools.Uploader
+{
+ internal 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