diff --git a/Assets/Mirror/Hosting/Edgegap/Dockerfile b/Assets/Mirror/Hosting/Edgegap/Dockerfile new file mode 100644 index 000000000..258a141dc --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Dockerfile @@ -0,0 +1,16 @@ +FROM ubuntu:bionic + +ARG DEBIAN_FRONTEND=noninteractive + +COPY Builds/EdgegapServer /root/build/ + +WORKDIR /root/ + +RUN chmod +x /root/build/ServerBuild + +RUN apt-get update && \ + apt-get install -y ca-certificates && \ + apt-get clean && \ + update-ca-certificates + +ENTRYPOINT [ "/root/build/ServerBuild", "-batchmode", "-nographics"] diff --git a/Assets/Mirror/Hosting/Edgegap/Dockerfile.meta b/Assets/Mirror/Hosting/Edgegap/Dockerfile.meta new file mode 100644 index 000000000..654f29901 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Dockerfile.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 78b80371aabba1d48aac39ec7ccfe7c5 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Dockerfile b/Assets/Mirror/Hosting/Edgegap/Editor/Dockerfile index a621ec396..3d9e3d02d 100644 --- a/Assets/Mirror/Hosting/Edgegap/Editor/Dockerfile +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Dockerfile @@ -1,9 +1,8 @@ FROM ubuntu:22.04 ARG DEBIAN_FRONTEND=noninteractive -ARG SERVER_BUILD_PATH=Builds/EdgegapServer -COPY ${SERVER_BUILD_PATH} /root/build/ +COPY Builds/EdgegapServer /root/build/ WORKDIR /root/ @@ -14,4 +13,4 @@ RUN apt-get update && \ apt-get clean && \ update-ca-certificates -CMD [ "/root/build/ServerBuild", "-batchmode", "-nographics", "$UNITY_COMMANDLINE_ARGS"] +ENTRYPOINT [ "/root/build/ServerBuild", "-batchmode", "-nographics"] diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowMetadata.cs b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowMetadata.cs index 9943d0919..fdeeede78 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowMetadata.cs +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowMetadata.cs @@ -63,7 +63,7 @@ public enum LogLevel public const string EDGEGAP_GET_A_TOKEN_URL = "https://app.edgegap.com/?oneClick=true"; public const string EDGEGAP_ADD_MORE_GAME_SERVERS_URL = "https://edgegap.com/resources/contact"; public const string EDGEGAP_DOC_BTN_HOW_TO_LOGIN_VIA_CLI_URL = "https://docs.edgegap.com/docs/container/edgegap-container-registry/#getting-your-credentials"; - private const string DEFAULT_UTM_SOURCE_TAG = "partner_mirror_source_unity"; + private const string DEFAULT_UTM_SOURCE_TAG = "partner_mirror_assetstore_unity"; private const string DEFAULT_UTM_MEDIUM_TAG = "servers_quickstart_plugin"; private const string DEFAULT_UTM_CONTENT_TAG = "plugin_button"; public const string DEFAULT_UTM_TAGS = "utm_source=" + DEFAULT_UTM_SOURCE_TAG + diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowV2.cs b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowV2.cs index bc1006161..d425131b4 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowV2.cs +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindowV2.cs @@ -112,8 +112,7 @@ public class EdgegapWindowV2 : EditorWindow Path.GetDirectoryName(AssetDatabase.GetAssetPath(MonoScript.FromScriptableObject(this))); // END MIRROR CHANGE internal string ProjectRootPath => Directory.GetCurrentDirectory(); - internal string ThisScriptPath => Directory.GetFiles(ProjectRootPath, GetType().Name + ".cs", SearchOption.AllDirectories)[0]; - internal string DockerFilePath => $"{Directory.GetParent(ThisScriptPath).FullName}{Path.DirectorySeparatorChar}Dockerfile"; + internal string DockerFilePath => $"{Directory.GetParent(Directory.GetFiles(ProjectRootPath, GetType().Name + ".cs", SearchOption.AllDirectories)[0]).FullName}{Path.DirectorySeparatorChar}Dockerfile"; [MenuItem("Tools/Edgegap Hosting")] // MIRROR CHANGE: more obvious title public static void ShowEdgegapToolWindow() @@ -546,11 +545,7 @@ private void debugEnableAllGroups() _containerCustomRegistryWrapper.SetEnabled(true); } - private void onApiTokenVerifyBtnClick() - { - _ = verifyApiTokenGetRegistryCredsAsync(); - _ = checkForUpdates(); - } + private void onApiTokenVerifyBtnClick() => _ = verifyApiTokenGetRegistryCredsAsync(); private void onApiTokenGetBtnClick() => openGetApiTokenWebsite(); /// Process UI + validation before/after API logic @@ -920,24 +915,6 @@ private string getBase64StrFromSprite(Sprite sprite, int maxKbSize = 200) } } - /// - /// Fetch latest github release and compare with local package.json version - /// - private async Task checkForUpdates() - { - // get local package.json version - DirectoryInfo thisScriptDir = new DirectoryInfo(ThisScriptPath); - PackageJSON local = PackageJSON.PackageJSONFromJSON($"{thisScriptDir.Parent.Parent.ToString()}{Path.DirectorySeparatorChar}package.json"); - - // get latest release from github repository - string releaseJSON = await GithubRelease.GithubReleaseFromAPI(); - GithubRelease latest = GithubRelease.GithubReleaseFromJSON(releaseJSON); - - if (local.version != latest.name) { - Debug.LogWarning($"Please update your Edgegap Quickstart plugin - local version `{local.version}` < latest version `{latest.name}`. See https://github.com/edgegap/edgegap-unity-plugin."); - } - } - /// /// Verifies token => apps/container groups -> gets registry creds (if any). /// TODO: UX - Show loading spinner. diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/GithubRelease.cs b/Assets/Mirror/Hosting/Edgegap/Editor/GithubRelease.cs deleted file mode 100644 index 2efad1738..000000000 --- a/Assets/Mirror/Hosting/Edgegap/Editor/GithubRelease.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using UnityEngine; - -namespace Edgegap.Editor -{ - [Serializable] - public class GithubRelease - { - public string name; - - public static async Task GithubReleaseFromAPI() - { - HttpClient http = new HttpClient(); - http.DefaultRequestHeaders.Add("Accept", "application/vnd.github+json"); - http.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28"); - http.DefaultRequestHeaders.Add("User-Agent", "Unity"); - http.Timeout = TimeSpan.FromSeconds(30); - - HttpResponseMessage response = await http.GetAsync("https://api.github.com/repos/edgegap/edgegap-unity-plugin/releases/latest").ConfigureAwait(false); - - return response.IsSuccessStatusCode ? await response.Content.ReadAsStringAsync() : "{}"; - } - - public static GithubRelease GithubReleaseFromJSON(string json) - { - return JsonUtility.FromJson(json); - } - } -} \ No newline at end of file diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/PackageJSON.cs b/Assets/Mirror/Hosting/Edgegap/Editor/PackageJSON.cs deleted file mode 100644 index 9b0e06534..000000000 --- a/Assets/Mirror/Hosting/Edgegap/Editor/PackageJSON.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.IO; -using UnityEngine; - -namespace Edgegap.Editor -{ - [Serializable] - public class PackageJSON - { - [Serializable] - public struct Author - { - public string name; - public string email; - public string url; - } - - public string name; - public string version; - public string displayName; - public string description; - public string unity; - public string unityRelease; - public string documentationUrl; - public string changelogUrl; - public string licensesUrl; - - public Author author; - - // dependencies omitted since JsonUtility doesn't support dictionaries - - public static PackageJSON PackageJSONFromJSON(string path) - { - return JsonUtility.FromJson(File.ReadAllText(path)); - } - } -} \ No newline at end of file diff --git a/Assets/Mirror/Hosting/Edgegap/README.md b/Assets/Mirror/Hosting/Edgegap/README.md index 638be6ea2..129829ea6 100644 --- a/Assets/Mirror/Hosting/Edgegap/README.md +++ b/Assets/Mirror/Hosting/Edgegap/README.md @@ -42,25 +42,15 @@ This plugin does not need to be included in your builds, as it's only a developm ## Other sources The only other official distribution channels for this plugin are: -- [Unity Asset Store package](https://assetstore.unity.com/packages/tools/network/edgegap-game-server-hosting-212563) -- [Mirror Networking source](https://github.com/MirrorNetworking/Mirror) -- [Mirror Networking free package](https://assetstore.unity.com/packages/tools/network/mirror-129321) -- [Mirror Networking LTS package](https://assetstore.unity.com/packages/tools/network/mirror-lts-102631) -- [Fish Networking source](https://github.com/FirstGearGames/FishNet) -- [Fish Networking free package](https://assetstore.unity.com/packages/tools/network/fishnet-networking-evolved-207815) -- [Fish Networking Pro package](https://assetstore.unity.com/packages/tools/network/fishnet-pro-networking-evolved-287711) +- [Mirror Networking samples](https://mirror-networking.gitbook.io/docs/hosting/edgegap-hosting-plugin-guide) +- [Fish Networking samples](https://fish-networking.gitbook.io/docs/manual/server-hosting/edgegap-official-partner) + +**WARNING!** The [Edgegap plugin published on Unity Asset Store](https://assetstore.unity.com/packages/tools/network/edgegap-game-server-hosting-212563) is outdated and not supported anymore. If you've previously installed our plugin by another method than described above, please remove any Edgegap files or dependencies related before updating your plugin using the git URL. ## Next Steps Once you have it, check for **Tools** -> **Edgegap Hosting** in Unity's top menu. -### Usage requirements - -To take full advantage of our Unity plugin's build features, you will need to: -- [Create an Edgegap Free Tier account](https://app.edgegap.com/auth/register), -- [Install Docker Desktop](https://www.docker.com/products/docker-desktop/) (or Docker CLI), -- Install Unity Linux Build Support modules for Unity. - From here, we recommend following our [Unity Plugin Guide](https://docs.edgegap.com/docs/tools-and-integrations/unity-plugin-guide) to get your first dedicated server deployed. ### Update the Plugin in Unity diff --git a/Assets/Mirror/Hosting/Edgegap/package.json b/Assets/Mirror/Hosting/Edgegap/package.json index 2863cc656..baf5e4bc4 100644 --- a/Assets/Mirror/Hosting/Edgegap/package.json +++ b/Assets/Mirror/Hosting/Edgegap/package.json @@ -1,6 +1,6 @@ { "name": "com.edgegap.unity-servers-plugin", - "version": "2.3.1", + "version": "1.0.8", "displayName": "Edgegap Servers Quickstart", "description": "Get started quickly with Edgegap Dedicated Server hosting.", "unity": "2021.3", @@ -18,4 +18,4 @@ "email": "contact@edgegap.com", "url": "https://www.edgegap.com" } -} \ No newline at end of file +} diff --git a/Assets/Mirror/Notice.txt b/Assets/Mirror/Notice.txt new file mode 100644 index 000000000..5eeaf888f --- /dev/null +++ b/Assets/Mirror/Notice.txt @@ -0,0 +1,123 @@ +This asset is governed by the Asset Store EULA; however, the following +components are governed by the licenses indicated below: + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- + +Mirror +MIT License + +Copyright (c) 2015, Unity Technologies +Copyright (c) 2019, vis2k, Paul and Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- + +kcp2k +MIT License + +Copyright (c) 2016 limpo1989 +Copyright (c) 2020 Paul Pacheco +Copyright (c) 2020 Lymdun +Copyright (c) 2020 vis2k + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- + +Mono.Cecil +MIT License + +Copyright (c) 2008 - 2015 Jb Evain +Copyright (c) 2008 - 2011 Novell, Inc. + +https://github.com/jbevain/cecil + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- + +Telepathy +MIT License + +Copyright (c) 2018, vis2k + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- \ No newline at end of file diff --git a/Assets/Mirror/Notice.txt.meta b/Assets/Mirror/Notice.txt.meta new file mode 100644 index 000000000..3281bd822 --- /dev/null +++ b/Assets/Mirror/Notice.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1a7b49ad188074707b004e7bb8824857 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Version.txt b/Assets/Mirror/Version.txt new file mode 100644 index 000000000..d4d062d43 --- /dev/null +++ b/Assets/Mirror/Version.txt @@ -0,0 +1 @@ +93.0.0 \ No newline at end of file diff --git a/Assets/Mirror/Version.txt.meta b/Assets/Mirror/Version.txt.meta new file mode 100644 index 000000000..097f25955 --- /dev/null +++ b/Assets/Mirror/Version.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c6b1f72568a9340178b4c34608fbdbc3 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/CHANGELOG.md b/Packages/com.unity.asset-store-tools/CHANGELOG.md new file mode 100644 index 000000000..4871dfedf --- /dev/null +++ b/Packages/com.unity.asset-store-tools/CHANGELOG.md @@ -0,0 +1,229 @@ +# Changelog +All notable changes to this package will be documented in this file. + +## [11.3.0] - 2023-07-04 + +### Uploader Changes + +- Added the option to validate a pre-exported package +- Added the option to export a .unitypackage file without uploading +- Updated the dependency selection UI + +### Validator Changes + +- Added the option to validate several asset paths at once + - Note: when validating package that is comprised of several folders (e.g. Assets/MyPackage + + Assets/StreamingAssets + Assets/WebGLTemplates), please select all applicable paths that would be included in the package +- Added several new validation tests for: + - File Menu Names + - Compressed files + - Model Types + - Texture Dimensions + - Particle Systems + - Normal Map Textures + - Audio Clipping + - Path Lengths + - Script Compilation +- Updated validation test severities based on package category +- Updated validation tests to each have their own test logic class +- Updated validation tests to be displayed in alphabetical order +- Fixed several issues with the namespace check test +- Fixed scenes in Samples~ folders not being taken into account for the sample scene check test +- Other internal changes + +### Exporter Changes + +- Package exporter is now a separate module (similar to Uploader and Validator) +- Fixed hidden folders being included when exporting package content + - Note: this prevents an issue with the Unity Editor, where exported hidden folders would appear in the Project window + as empty folders when imported, despite having content on disk. Content nested within hidden folders is still collected, + provided it contains unique .meta files + +## [11.2.2] - 2023-02-23 + +### Validator Changes + +- Updated the 'LOD Setup' test to address some issues + - Added additional checks for LOD renderers (inactive renderer check, LOD Group reference check, relative hierarchy position to LOD Group check) + - LOD Group Component is no longer required to be on the root of the Prefab + - Updated the test result message interface when invalid Prefabs are found + +## [11.2.1] - 2023-01-17 + +### Uploader Changes + +- Added a more informative error when exporting content with clashing guid meta files in hidden folders +- Fixed a compilation issue for Unity 2020.1 and 2020.2 +- Fixed a rare error condition when queueing multiple package uploads in quick succession +- Fixed Asset Store Uploader state not being properly reset if the uploading process fails + +### Validator Changes + +- Updated the Asset Store Validator description +- Fixed a rare memory overflow issue when performing package validation + +## [11.2.0] - 2022-11-03 + +### Uploader Changes + +- Uploader will now use the custom package exporter by default + - An option to use the legacy (native) exporter can be found in the Asset Store Publishing Tools' settings window +- When exporting from the Assets folder, package dependencies can now be selected individually instead of being a choice between 'All' or 'None' + - This option is only available with the custom exporter +- Changed the way the Uploader reports completed uploading tasks + - Modal pop-up has been replaced by a new UI view state + - Added an option to the Asset Store Publishing Tools' Settings to display the pop-up after a completed upload +- Changed exported .unitypackage files to have distinguishable file names +- Fixed the Uploader window indefinitely stalling at 100% upload progress when a response from the Asset Store server is not received +- Fixed native package exporter producing broken packages when the export path contained hidden folders +- Fixed an issue with high CPU usage when uploading packages +- Fixed Asset Store Publishing Tools' settings not being saved between Editor sessions on macOS +- Other minor changes and tweaks + +### Validator Changes + +- Added two new tests: + - 'Types have namespaces': checks whether scripts and native libraries under the validated path are nested under a namespace + - 'Consistent line endings': checks whether scripts under the validated path have consistent line endings. This is similar to the warning from the Unity Editor compilation pipeline when a script contains both Windows and UNIX line endings. +- Improved 'Reset Prefabs' test to display and be more informative about prefabs with unusually low transform values +- Improved 'SpeedTree asset inclusion' test to search for '.st' files +- Improved 'Documentation inclusion' test to treat '.md' files as valid documentation files +- Improved 'Lossy audio file inclusion' test to treat '.aif' and '.aiff' files as valid non-lossy audio files +- Improved 'Lossy audio file inclusion' test to search the project for non-lossy variants of existing lossy audio files +- Removed 'Duplicate animation names' test +- Tweaked validation severities for several tests +- Other minor changes and tweaks + +## [11.1.0] - 2022-09-14 + +### Uploader Changes + +- Package Publisher Portal links can now be opened for all packages regardless of package status +- External Dependency Manager can now be selected as a 'Special Folder' if found in the root Assets folder + +### Validator Changes + +- Added category selection for the Validator + - Categories help determine the outcome of package validation more accurately. For example, documentation is not crucial for art packages, but is required for tooling packages. +- Added a list of prefabs with missing mesh references to 'Meshes have Prefabs' test when the test fails +- Corrected the message for a passing 'Shader compilation errors' test +- Improved the floating point precision accuracy of 'Reset Prefabs' test +- Fixed 'Missing Components in Assets' test checking all project folders instead of only the set path +- Fixed 'Prefabs for meshes' test not checking meshes in certain paths +- Fixed 'Reset Prefabs' test failing because of Prefabs with a Rect Transform Component +- Fixed 'Reset Prefabs' test ignoring Transform rotation +- Fixed test description text overlapping in some cases +- Other minor changes and tweaks + +## [11.0.2] - 2022-08-09 + +- Corrected some namespaces which were causing issues when deriving classes from Editor class + +## [11.0.1] - 2022-08-05 + +### Uploader Changes + +- Added Settings window (Asset Store Tools > Settings) +- Added Soft/Junction Symlink support (enable through Settings) +- Added workflow and path selection serialization (workflow saved locally, paths locally and online) +- No more logs when using the `-nullable` compiler option (thanks @alfish) +- Some API refactoring in preparation for CLI support +- Other minor fixes/improvements + +**Note:** when updating Asset Store Tools from the Package Manager, don't forget to remove the old version from the project (V11.0.0) before importing the new one (V11.0.1) + + +## [11.0.0] - 2022-07-20 + +### Uploader changes + +- UI has been reworked using UI Toolkit +- New login window, allowing to login using Unity Cloud Services +- Improved top bar, including search and sorting +- Draft packages moved to the top +- Added category, size, and last modified date next to the package +- Added a link to the publishing portal next to the package +- New uploading flow: “Pre-exported .unitypackage” +- Previous uploading flow (folder selection) has been renamed to “From Assets Folder” +- Dependencies check has been renamed to “Include Package Manifest” for clarity +- Special Folders can now be selected and uploaded together with the package’s main folder (i.e. StreamingAssets, Plugins) +- You can now upload to multiple packages at the same time without waiting for the first one to finish +- Package can now be validated in the Uploading window by pressing the “Validate” button +- Added refresh and logout buttons to the bottom toolbar for easier access +- Packages caching - package information will no longer be redownloaded every time you open the Uploader window during the same Editor session +- (Experimental) Custom exporter - will export your package ~2 times faster, but may miss some asset previews in the final product. To enable it - click three dots on the top left side of the window and enable “Use Custom Exporting” + + +### Validator changes + +- UI has been reworked using UI Toolkit +- New tests based on the new guidelines +- Updated tests’ titles, descriptions, and error reporting + +## [5.0.5] - 2021-11-04 + +- Fixed namespace issues + +## [5.0.4] - 2020-07-28 + +- Fixed issues with Unity 2020.1 + +## [5.0.3] - 2020-05-07 + +- Remove "Remove Standard Assets" check + +## [5.0.2] - 2020-04-21 + +- Enable auto login with Unity account +- Upload package with thread + +## [5.0.1] - 2020-03-23 + +- Fix domain resolve issue + +## [5.0.0] - 2019-10-09 + +- Added "Package Validator" tool +- Added Help window +- Added logout confirmation popup +- Updated toolbar menu layout +- Removed "Mass Labeler" tool +- Updated layout of Login and Package Upload windows +- Error messages are now more elaborate and user-friendly +- Removed deprecated "Main Assets" step from the Package Upload window +- Package Upload window now has a step for including package manager dependencies +- Tooltips are now added to each upload process step + + +## [4.1.0] - 2018-05-14 + +- Made Tool compatible with 2017.1 + +## [4.0.7] - 2017-07-10 + +- Tweaked menu items. + +## [4.0.6] - 2016-07-15 + +- Improved error messages. + +## [4.0.5] - 2016-03-17 + +- Enabling upload of fbm files. + +## [4.0.4] - 2015-11-16 + +- Login improvements + +## [4.0.3] - 2015-11-16 + +- Prepare the Tools for Unity 5.3 + +## [4.0.2] - 2015-10-23 + +- Fixed issue where Upload button would not work for some projects. +- Fixed issues for publishers that only had one package. + +## [4.0.0] - 2015-09-01 + +- Replaced Package Manager with Package Upload. Package management is now handled by Publisher Administration \ No newline at end of file diff --git a/Packages/com.unity.asset-store-tools/CHANGELOG.md.meta b/Packages/com.unity.asset-store-tools/CHANGELOG.md.meta new file mode 100644 index 000000000..0db1ee3e5 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 06607220dbd46414e8f66bf9c5e3eb79 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: 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 000000000..8521ad694 --- /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/AssemblyInfo.cs b/Packages/com.unity.asset-store-tools/Editor/AssemblyInfo.cs new file mode 100644 index 000000000..60e79b670 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Unity.AssetStoreTools.Editor.Tests.asmdef")] +[assembly: InternalsVisibleTo("ab-builder")] \ No newline at end of file diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/GithubRelease.cs.meta b/Packages/com.unity.asset-store-tools/Editor/AssemblyInfo.cs.meta similarity index 83% rename from Assets/Mirror/Hosting/Edgegap/Editor/GithubRelease.cs.meta rename to Packages/com.unity.asset-store-tools/Editor/AssemblyInfo.cs.meta index 77d150c38..972ae52b3 100644 --- a/Assets/Mirror/Hosting/Edgegap/Editor/GithubRelease.cs.meta +++ b/Packages/com.unity.asset-store-tools/Editor/AssemblyInfo.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 6ef365e2acd5a5044a4a14fa83c7d9b9 +guid: ccfd7faf75ab3c74a98015e772288d86 MonoImporter: externalObjects: {} serializedVersion: 2 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 000000000..25a0a83ba --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreTools.cs @@ -0,0 +1,53 @@ +using UnityEditor; +using UnityEngine; +using System; +using AssetStoreTools.Uploader; +using AssetStoreTools.Validator; +using AssetStoreTools.Utility; + +namespace AssetStoreTools +{ + internal class AssetStoreTools : EditorWindow + { + [MenuItem("Asset Store Tools/Asset Store Uploader", false, 0)] + public static void ShowAssetStoreToolsUploader() + { + Type inspectorType = Type.GetType("UnityEditor.InspectorWindow,UnityEditor.dll"); + var wnd = GetWindow(inspectorType); + wnd.Show(); + } + + + [MenuItem("Asset Store Tools/Asset Store Validator", false, 1)] + public static void ShowAssetStoreToolsValidator() + { + Type inspectorType = Type.GetType("UnityEditor.InspectorWindow,UnityEditor.dll"); + var wnd = GetWindow(typeof(AssetStoreUploader), inspectorType); + wnd.Show(); + } + + [MenuItem("Asset Store Tools/Publisher Portal", false, 20)] + public static void OpenPublisherPortal() + { + Application.OpenURL("https://publisher.unity.com/"); + } + + [MenuItem("Asset Store Tools/Submission Guidelines", false, 21)] + public static void OpenSubmissionGuidelines() + { + Application.OpenURL("https://assetstore.unity.com/publishing/submission-guidelines/"); + } + + [MenuItem("Asset Store Tools/Provide Feedback", false, 22)] + public static void OpenFeedback() + { + Application.OpenURL("https://forum.unity.com/threads/new-asset-store-tools-version-coming-july-20th-2022.1310939/"); + } + + [MenuItem("Asset Store Tools/Settings", false, 50)] + public static void OpenSettings() + { + ASToolsPreferencesProvider.OpenSettings(); + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/PackageJSON.cs.meta b/Packages/com.unity.asset-store-tools/Editor/AssetStoreTools.cs.meta similarity index 83% rename from Assets/Mirror/Hosting/Edgegap/Editor/PackageJSON.cs.meta rename to Packages/com.unity.asset-store-tools/Editor/AssetStoreTools.cs.meta index f0382d94b..9452bb059 100644 --- a/Assets/Mirror/Hosting/Edgegap/Editor/PackageJSON.cs.meta +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreTools.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: e8abfa078881a944a81ac3bcc8e5e0f4 +guid: 6060eef206afc844caaa1732538e8890 MonoImporter: externalObjects: {} serializedVersion: 2 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 000000000..aa69025ae --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/AssetStoreToolsWindow.cs @@ -0,0 +1,21 @@ +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools +{ + internal 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 000000000..2fe87e578 --- /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/Exporter.meta b/Packages/com.unity.asset-store-tools/Editor/Exporter.meta new file mode 100644 index 000000000..4e1143b83 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Exporter.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5f5ca981958937a43997a9f365759edf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/Exporter/ExportResult.cs b/Packages/com.unity.asset-store-tools/Editor/Exporter/ExportResult.cs new file mode 100644 index 000000000..0ce4a73bc --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Exporter/ExportResult.cs @@ -0,0 +1,16 @@ +using AssetStoreTools.Utility; + +namespace AssetStoreTools.Exporter +{ + internal class ExportResult + { + public bool Success; + public string ExportedPath; + public ASError Error; + + public static implicit operator bool(ExportResult value) + { + return value != null && value.Success; + } + } +} \ No newline at end of file diff --git a/Packages/com.unity.asset-store-tools/Editor/Exporter/ExportResult.cs.meta b/Packages/com.unity.asset-store-tools/Editor/Exporter/ExportResult.cs.meta new file mode 100644 index 000000000..a990a5098 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Exporter/ExportResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ce99a618d1e211444b53f18bb3444f75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/Exporter/ExporterSettings.cs b/Packages/com.unity.asset-store-tools/Editor/Exporter/ExporterSettings.cs new file mode 100644 index 000000000..0992bc886 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Exporter/ExporterSettings.cs @@ -0,0 +1,18 @@ +namespace AssetStoreTools.Exporter +{ + public abstract class ExporterSettings + { + public string[] ExportPaths; + public string OutputFilename; + } + + public class DefaultExporterSettings : ExporterSettings + { + public string[] Dependencies; + } + + public class LegacyExporterSettings : ExporterSettings + { + public bool IncludeDependencies; + } +} \ No newline at end of file diff --git a/Packages/com.unity.asset-store-tools/Editor/Exporter/ExporterSettings.cs.meta b/Packages/com.unity.asset-store-tools/Editor/Exporter/ExporterSettings.cs.meta new file mode 100644 index 000000000..24d41f792 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Exporter/ExporterSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 399b115514c617d47a00b8c0a5e430fd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporter.cs b/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporter.cs new file mode 100644 index 000000000..720757002 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporter.cs @@ -0,0 +1,120 @@ +using AssetStoreTools.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using UnityEditor; + +namespace AssetStoreTools.Exporter +{ + internal abstract class PackageExporter + { + protected const string ProgressBarTitle = "Exporting Package"; + protected const string ProgressBarStepSavingAssets = "Saving Assets..."; + protected const string ProgressBarStepGatheringFiles = "Gathering files..."; + protected const string ProgressBarStepCompressingPackage = "Compressing package..."; + + public static async Task ExportPackage(ExporterSettings exportSettings) + { + if (!IsSettingsValid(exportSettings, out Exception e)) + return new ExportResult() { Success = false, Error = ASError.GetGenericError(e) }; + + switch (exportSettings) + { + case LegacyExporterSettings legacySettings: + return await PackageExporterLegacy.ExportPackage(legacySettings); + case DefaultExporterSettings defaultSettings: + return PackageExporterDefault.ExportPackage(defaultSettings); + default: + return new ExportResult() { Success = false, Error = ASError.GetGenericError(new ArgumentException("Unrecognized ExportSettings type was provided")) }; + } + } + + private static bool IsSettingsValid(ExporterSettings settings, out Exception e) + { + e = null; + + if (settings == null) + e = new ArgumentException("Package Exporting failed: ExporterSettings cannot be null"); + else if (settings.ExportPaths == null || settings.ExportPaths.Length == 0) + e = new ArgumentException("Package Exporting failed: received an invalid export paths array"); + else if (string.IsNullOrEmpty(settings.OutputFilename)) + e = new ArgumentException("Package Exporting failed: received an invalid output path"); + else if (settings.OutputFilename.EndsWith("/") || settings.OutputFilename.EndsWith("\\")) + e = new ArgumentException("Package Exporting failed: output path must be a valid filename and not end with a directory separator character"); + + return e == null; + } + + protected string[] GetAssetPaths(string rootPath) + { + // To-do: slight optimization is possible in the future by having a list of excluded folders/file extensions + List paths = new List(); + + // Add files within given directory + var filePaths = Directory.GetFiles(rootPath).Select(p => p.Replace('\\', '/')).ToArray(); + paths.AddRange(filePaths); + + // Add directories within given directory + var directoryPaths = Directory.GetDirectories(rootPath).Select(p => p.Replace('\\', '/')).ToArray(); + foreach (var nestedDirectory in directoryPaths) + paths.AddRange(GetAssetPaths(nestedDirectory)); + + // Add the given directory itself if it is not empty + if (filePaths.Length > 0 || directoryPaths.Length > 0) + paths.Add(rootPath); + + return paths.ToArray(); + } + + protected string GetAssetGuid(string assetPath, bool hiddenSearch) + { + // Skip meta files as they do not have guids + if (assetPath.EndsWith(".meta")) + return string.Empty; + + // Skip hidden assets. They normally do not have meta files, but + // have been observed to retain them in the past due to a Unity bug + if (assetPath.EndsWith("~")) + return string.Empty; + + // Attempt retrieving guid from the Asset Database first + var guid = AssetDatabase.AssetPathToGUID(assetPath); + if (guid != string.Empty) + return guid; + + // Files in hidden folders (e.g. Samples~) are not part of the Asset Database, + // therefore GUIDs need to be scraped from the .meta file. + // Note: only do this for custom exporter since the native exporter + // will not be able to retrieve the asset path from a hidden folder + if (hiddenSearch) + { + // To-do: handle hidden folders without meta files + var metaPath = $"{assetPath}.meta"; + + if (!File.Exists(metaPath)) + return string.Empty; + + using (StreamReader reader = new StreamReader(metaPath)) + { + string line; + while ((line = reader.ReadLine()) != string.Empty) + { + if (!line.StartsWith("guid:")) + continue; + var metaGuid = line.Substring("guid:".Length).Trim(); + return metaGuid; + } + } + } + + return string.Empty; + } + + protected virtual void PostExportCleanup() + { + EditorUtility.ClearProgressBar(); + } + } +} \ No newline at end of file diff --git a/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporter.cs.meta b/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporter.cs.meta new file mode 100644 index 000000000..66906b4dc --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 52ef11a59e545544fafaa99a5fa6cce9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporterDefault.cs b/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporterDefault.cs new file mode 100644 index 000000000..1dfe7b1c4 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporterDefault.cs @@ -0,0 +1,336 @@ +using AssetStoreTools.Uploader.Utility; +using AssetStoreTools.Utility; +using AssetStoreTools.Utility.Json; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using UnityEditor; +using UnityEngine; + +namespace AssetStoreTools.Exporter +{ + internal class PackageExporterDefault : PackageExporter + { + private const string TemporaryExportPathName = "CustomExport"; + private const string ManifestJsonPath = "Packages/manifest.json"; + + private DefaultExporterSettings _exportSettings; + + private PackageExporterDefault(DefaultExporterSettings exportSettings) + { + _exportSettings = exportSettings; + } + + public static ExportResult ExportPackage(DefaultExporterSettings exportSettings) + { + var exporter = new PackageExporterDefault(exportSettings); + return exporter.ExportPackage(); + } + + private ExportResult ExportPackage() + { + ASDebug.Log("Using custom package exporter"); + + // Save assets before exporting + EditorUtility.DisplayProgressBar(ProgressBarTitle, ProgressBarStepSavingAssets, 0.1f); + AssetDatabase.SaveAssets(); + + try + { + // Create a temporary export path + var temporaryExportPath = GetTemporaryExportPath(); + if (!Directory.Exists(temporaryExportPath)) + Directory.CreateDirectory(temporaryExportPath); + + // Construct an unzipped package structure + CreateTempPackageStructure(temporaryExportPath); + + // Build a .unitypackage file from the temporary folder + CreateUnityPackage(temporaryExportPath, _exportSettings.OutputFilename); + + EditorUtility.RevealInFinder(_exportSettings.OutputFilename); + + ASDebug.Log($"Package file has been created at {_exportSettings.OutputFilename}"); + return new ExportResult() { Success = true, ExportedPath = _exportSettings.OutputFilename }; + } + catch (Exception e) + { + return new ExportResult() { Success = false, Error = ASError.GetGenericError(e) }; + } + finally + { + PostExportCleanup(); + } + } + + private string GetTemporaryExportPath() + { + return $"{AssetStoreCache.TempCachePath}/{TemporaryExportPathName}"; + } + + private void CreateTempPackageStructure(string tempOutputPath) + { + EditorUtility.DisplayProgressBar(ProgressBarTitle, ProgressBarStepGatheringFiles, 0.4f); + var pathGuidPairs = GetPathGuidPairs(_exportSettings.ExportPaths); + + // Caching asset previews takes time, so we'll start doing it as we + // iterate through assets and only retrieve them after generating the rest + // of the package structure + AssetPreview.SetPreviewTextureCacheSize(pathGuidPairs.Count + 100); + var pathObjectPairs = new Dictionary(); + + foreach (var pair in pathGuidPairs) + { + var originalAssetPath = pair.Key; + var outputAssetPath = $"{tempOutputPath}/{pair.Value}"; + + if (Directory.Exists(outputAssetPath)) + { + var path1 = File.ReadAllText($"{outputAssetPath}/pathname"); + var path2 = originalAssetPath; + throw new InvalidOperationException($"Multiple assets with guid {pair.Value} have been detected " + + $"when exporting the package. Please resolve the guid conflicts and try again:\n{path1}\n{path2}"); + } + + Directory.CreateDirectory(outputAssetPath); + + // Every exported asset has a pathname file + using (StreamWriter writer = new StreamWriter($"{outputAssetPath}/pathname")) + writer.Write(originalAssetPath); + + // Only files (not folders) have an asset file + if (File.Exists(originalAssetPath)) + File.Copy(originalAssetPath, $"{outputAssetPath}/asset"); + + // Most files and folders have an asset.meta file (but ProjectSettings folder assets do not) + if (File.Exists($"{originalAssetPath}.meta")) + File.Copy($"{originalAssetPath}.meta", $"{outputAssetPath}/asset.meta"); + + // To-do: handle previews in hidden folders as they are not part of the AssetDatabase + var previewObject = AssetDatabase.LoadAssetAtPath(originalAssetPath); + if (previewObject == null) + continue; + // Start caching the asset preview + AssetPreview.GetAssetPreview(previewObject); + pathObjectPairs.Add(outputAssetPath, previewObject); + } + + WritePreviewTextures(pathObjectPairs); + + if (_exportSettings.Dependencies == null || _exportSettings.Dependencies.Length == 0) + return; + + var manifestJson = GetPackageManifestJson(); + var allDependenciesDict = manifestJson["dependencies"].AsDict(); + + var allLocalPackages = PackageUtility.GetAllLocalPackages(); + List allPackagesList = new List(allDependenciesDict.Keys); + + foreach (var package in allPackagesList) + { + if (!_exportSettings.Dependencies.Any(x => x == package)) + { + allDependenciesDict.Remove(package); + continue; + } + + if (!allLocalPackages.Select(x => x.name).Contains(package)) + continue; + + allDependenciesDict.Remove(package); + UnityEngine.Debug.LogWarning($"Found an unsupported Package Manager dependency: {package}.\n" + + "This dependency is not supported in the project's manifest.json and will be skipped."); + } + + if (allDependenciesDict.Count == 0) + return; + + var tempManifestDirectoryPath = $"{tempOutputPath}/packagemanagermanifest"; + Directory.CreateDirectory(tempManifestDirectoryPath); + var tempManifestFilePath = $"{tempManifestDirectoryPath}/asset"; + + File.WriteAllText(tempManifestFilePath, manifestJson.ToString()); + } + + private Dictionary GetPathGuidPairs(string[] exportPaths) + { + var pathGuidPairs = new Dictionary(); + + foreach (var exportPath in exportPaths) + { + var assetPaths = GetAssetPaths(exportPath); + + foreach (var assetPath in assetPaths) + { + var guid = GetAssetGuid(assetPath, true); + if (string.IsNullOrEmpty(guid)) + continue; + + pathGuidPairs.Add(assetPath, guid); + } + } + + return pathGuidPairs; + } + + private void WritePreviewTextures(Dictionary pathObjectPairs) + { + foreach (var kvp in pathObjectPairs) + { + var obj = kvp.Value; + var queuePreview = false; + + switch (obj) + { + case Material _: + case TerrainLayer _: + case AudioClip _: + case Mesh _: + case Texture _: + case UnityEngine.Tilemaps.Tile _: + case GameObject _: + queuePreview = true; + break; + } + + if (!queuePreview) + continue; + + AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out var guid, out long _); + var preview = GetAssetPreviewFromGuid(guid); + + if (!preview) + continue; + + var thumbnailWidth = Mathf.Min(preview.width, 128); + var thumbnailHeight = Mathf.Min(preview.height, 128); + var rt = RenderTexture.GetTemporary(thumbnailWidth, thumbnailHeight, 0, RenderTextureFormat.Default, RenderTextureReadWrite.sRGB); + + var copy = new Texture2D(rt.width, rt.height, TextureFormat.ARGB32, false); + + RenderTexture.active = rt; + GL.Clear(true, true, new Color(0, 0, 0, 0)); + Graphics.Blit(preview, rt); + copy.ReadPixels(new Rect(0, 0, copy.width, copy.height), 0, 0, false); + copy.Apply(); + RenderTexture.active = null; + + var bytes = copy.EncodeToPNG(); + if (bytes != null && bytes.Length > 0) + { + File.WriteAllBytes(kvp.Key + "/preview.png", bytes); + } + + RenderTexture.ReleaseTemporary(rt); + } + } + + private Texture2D GetAssetPreviewFromGuid(string guid) + { + var method = typeof(AssetPreview).GetMethod("GetAssetPreviewFromGUID", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, new[] { typeof(string) }, null); + var args = new object[] { guid }; + + return method?.Invoke(null, args) as Texture2D; + } + + private void CreateUnityPackage(string pathToArchive, string outputPath) + { + if (Directory.GetDirectories(pathToArchive).Length == 0) + throw new InvalidOperationException("Unable to export package. The specified path is empty"); + + EditorUtility.DisplayProgressBar(ProgressBarTitle, ProgressBarStepCompressingPackage, 0.5f); + + // Archiving process working path will be set to the + // temporary package path so adjust the output path accordingly + if (!Path.IsPathRooted(outputPath)) + outputPath = $"{Application.dataPath.Substring(0, Application.dataPath.Length - "/Assets".Length)}/{outputPath}"; + +#if UNITY_EDITOR_WIN + CreateUnityPackageUniversal(pathToArchive, outputPath); +#elif UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX + CreateUnityPackageOsxLinux(pathToArchive, outputPath); +#endif + } + + private void CreateUnityPackageUniversal(string pathToArchive, string outputPath) + { + var _7zPath = EditorApplication.applicationContentsPath; +#if UNITY_EDITOR_WIN + _7zPath = Path.Combine(_7zPath, "Tools", "7z.exe"); +#elif UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX + _7zPath = Path.Combine(_7zPath, "Tools", "7za"); +#endif + if (!File.Exists(_7zPath)) + throw new FileNotFoundException("Archiving utility was not found in your Unity installation directory"); + + var argumentsTar = $"a -r -ttar -y -bd archtemp.tar ."; + var result = StartProcess(_7zPath, argumentsTar, pathToArchive); + if (result != 0) + throw new Exception("Failed to compress the package"); + + // Create a GZIP archive + var argumentsGzip = $"a -tgzip -bd -y \"{outputPath}\" archtemp.tar"; + result = StartProcess(_7zPath, argumentsGzip, pathToArchive); + if (result != 0) + throw new Exception("Failed to compress the package"); + } + + private void CreateUnityPackageOsxLinux(string pathToArchive, string outputPath) + { + var tarPath = "/usr/bin/tar"; + + if (!File.Exists(tarPath)) + { + // Fallback to the universal export method + ASDebug.LogWarning("'/usr/bin/tar' executable not found. Falling back to 7za"); + CreateUnityPackageUniversal(pathToArchive, outputPath); + return; + } + + // Create a TAR archive + var arguments = $"-czpf \"{outputPath}\" ."; + var result = StartProcess(tarPath, arguments, pathToArchive); + if (result != 0) + throw new Exception("Failed to compress the package"); + } + + private int StartProcess(string processPath, string arguments, string workingDirectory) + { + var info = new ProcessStartInfo() + { + FileName = processPath, + Arguments = arguments, + WorkingDirectory = workingDirectory, + CreateNoWindow = true, + UseShellExecute = false + }; + + using (Process process = Process.Start(info)) + { + process.WaitForExit(); + return process.ExitCode; + } + } + + private JsonValue GetPackageManifestJson() + { + string manifestJsonString = File.ReadAllText(ManifestJsonPath); + JSONParser parser = new JSONParser(manifestJsonString); + var manifestJson = parser.Parse(); + + return manifestJson; + } + + protected override void PostExportCleanup() + { + base.PostExportCleanup(); + + var tempExportPath = GetTemporaryExportPath(); + if (Directory.Exists(tempExportPath)) + Directory.Delete(tempExportPath, true); + } + } +} \ No newline at end of file diff --git a/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporterDefault.cs.meta b/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporterDefault.cs.meta new file mode 100644 index 000000000..fa1bd636a --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporterDefault.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 32f50122a1b2bc2428cf8fba321e2414 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporterLegacy.cs b/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporterLegacy.cs new file mode 100644 index 000000000..9a39cfbbf --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporterLegacy.cs @@ -0,0 +1,102 @@ +using AssetStoreTools.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace AssetStoreTools.Exporter +{ + internal class PackageExporterLegacy : PackageExporter + { + private const string ExportMethodWithoutDependencies = "UnityEditor.PackageUtility.ExportPackage"; + private const string ExportMethodWithDependencies = "UnityEditor.PackageUtility.ExportPackageAndPackageManagerManifest"; + + private LegacyExporterSettings _exportSettings; + + private PackageExporterLegacy(LegacyExporterSettings exportSettings) + { + _exportSettings = exportSettings; + } + + public static async Task ExportPackage(LegacyExporterSettings exportSettings) + { + var exporter = new PackageExporterLegacy(exportSettings); + return await exporter.ExportPackage(); + } + + private async Task ExportPackage() + { + ASDebug.Log("Using native package exporter"); + + try + { + var guids = GetGuids(_exportSettings.ExportPaths, out bool onlyFolders); + + if (guids.Length == 0 || onlyFolders) + throw new ArgumentException("Package Exporting failed: provided export paths are empty or only contain empty folders"); + + string exportMethod = ExportMethodWithoutDependencies; + if (_exportSettings.IncludeDependencies) + exportMethod = ExportMethodWithDependencies; + + var split = exportMethod.Split('.'); + var assembly = Assembly.Load(split[0]); // UnityEditor + var typeName = $"{split[0]}.{split[1]}"; // UnityEditor.PackageUtility + var methodName = split[2]; // ExportPackage or ExportPackageAndPackageManagerManifest + + var type = assembly.GetType(typeName); + var method = type.GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public, + null, new Type[] { typeof(string[]), typeof(string) }, null); + + ASDebug.Log("Invoking native export method"); + + method?.Invoke(null, new object[] { guids, _exportSettings.OutputFilename }); + + // The internal exporter methods are asynchronous, therefore + // we need to wait for exporting to finish before returning + await Task.Run(() => + { + while (!File.Exists(_exportSettings.OutputFilename)) + Thread.Sleep(100); + }); + + ASDebug.Log($"Package file has been created at {_exportSettings.OutputFilename}"); + return new ExportResult() { Success = true, ExportedPath = _exportSettings.OutputFilename }; + } + catch (Exception e) + { + return new ExportResult() { Success = false, Error = ASError.GetGenericError(e) }; + } + finally + { + PostExportCleanup(); + } + } + + private string[] GetGuids(string[] exportPaths, out bool onlyFolders) + { + var guids = new List(); + onlyFolders = true; + + foreach (var exportPath in exportPaths) + { + var assetPaths = GetAssetPaths(exportPath); + + foreach (var assetPath in assetPaths) + { + var guid = GetAssetGuid(assetPath, false); + if (string.IsNullOrEmpty(guid)) + continue; + + guids.Add(guid); + if (onlyFolders == true && (File.Exists(assetPath))) + onlyFolders = false; + } + } + + return guids.ToArray(); + } + } +} \ No newline at end of file diff --git a/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporterLegacy.cs.meta b/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporterLegacy.cs.meta new file mode 100644 index 000000000..853e472fe --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Exporter/PackageExporterLegacy.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3200380dff2de104aa79620e4b41dc70 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/Unity.AssetStoreTools.Editor.asmdef b/Packages/com.unity.asset-store-tools/Editor/Unity.AssetStoreTools.Editor.asmdef new file mode 100644 index 000000000..eb298a867 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Unity.AssetStoreTools.Editor.asmdef @@ -0,0 +1,15 @@ +{ + "name": "asset-store-tools-editor", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Packages/com.unity.asset-store-tools/Editor/Unity.AssetStoreTools.Editor.asmdef.meta b/Packages/com.unity.asset-store-tools/Editor/Unity.AssetStoreTools.Editor.asmdef.meta new file mode 100644 index 000000000..2f67bb9e0 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Unity.AssetStoreTools.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c183be512f4485d40a3437fabd6c81cf +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/Uploader.meta b/Packages/com.unity.asset-store-tools/Editor/Uploader.meta new file mode 100644 index 000000000..b4b866142 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader.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/Uploader/AssetStoreUploader.cs b/Packages/com.unity.asset-store-tools/Editor/Uploader/AssetStoreUploader.cs new file mode 100644 index 000000000..f40f3a8cc --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/AssetStoreUploader.cs @@ -0,0 +1,226 @@ +using AssetStoreTools.Utility.Json; +using System; +using System.Collections.Generic; +using AssetStoreTools.Utility; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; +using AssetStoreTools.Uploader.Utility; +using AssetStoreTools.Uploader.UIElements; + +namespace AssetStoreTools.Uploader +{ + internal class AssetStoreUploader : AssetStoreToolsWindow + { + public const string MinRequiredPackageVersion = "2021.3"; + + private const string MainWindowVisualTree = "Packages/com.unity.asset-store-tools/Editor/Uploader/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 ShowPackageVersionDialog + { + get => string.Compare(Application.unityVersion, MinRequiredPackageVersion, StringComparison.Ordinal) == -1 && ASToolsPreferences.Instance.UploadVersionCheck; + set { ASToolsPreferences.Instance.UploadVersionCheck = value; ASToolsPreferences.Instance.Save(); } + } + + 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); + + root.styleSheets.Add(StyleSelector.UploaderWindow.BaseWindowStyle); + root.styleSheets.Add(StyleSelector.UploaderWindow.BaseWindowTheme); + + + // 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); + } + + #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/Uploader/AssetStoreUploader.cs.meta b/Packages/com.unity.asset-store-tools/Editor/Uploader/AssetStoreUploader.cs.meta new file mode 100644 index 000000000..ef78266e2 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/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/Uploader/Icons.meta b/Packages/com.unity.asset-store-tools/Editor/Uploader/Icons.meta new file mode 100644 index 000000000..7026063d3 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/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/Uploader/Icons/open-in-browser.png b/Packages/com.unity.asset-store-tools/Editor/Uploader/Icons/open-in-browser.png new file mode 100644 index 000000000..245875b4d Binary files /dev/null and b/Packages/com.unity.asset-store-tools/Editor/Uploader/Icons/open-in-browser.png differ diff --git a/Packages/com.unity.asset-store-tools/Editor/Uploader/Icons/open-in-browser.png.meta b/Packages/com.unity.asset-store-tools/Editor/Uploader/Icons/open-in-browser.png.meta new file mode 100644 index 000000000..26ccaa5a2 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/Icons/open-in-browser.png.meta @@ -0,0 +1,147 @@ +fileFormatVersion: 2 +guid: e7df43612bbf44d4692de879c751902a +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 2 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/Uploader/Icons/publisher_portal_black.png b/Packages/com.unity.asset-store-tools/Editor/Uploader/Icons/publisher_portal_black.png new file mode 100644 index 000000000..621e906e3 Binary files /dev/null and b/Packages/com.unity.asset-store-tools/Editor/Uploader/Icons/publisher_portal_black.png differ diff --git a/Packages/com.unity.asset-store-tools/Editor/Uploader/Icons/publisher_portal_black.png.meta b/Packages/com.unity.asset-store-tools/Editor/Uploader/Icons/publisher_portal_black.png.meta new file mode 100644 index 000000000..305aa317d --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/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/Uploader/Icons/publisher_portal_white.png b/Packages/com.unity.asset-store-tools/Editor/Uploader/Icons/publisher_portal_white.png new file mode 100644 index 000000000..70f470378 Binary files /dev/null and b/Packages/com.unity.asset-store-tools/Editor/Uploader/Icons/publisher_portal_white.png differ diff --git a/Packages/com.unity.asset-store-tools/Editor/Uploader/Icons/publisher_portal_white.png.meta b/Packages/com.unity.asset-store-tools/Editor/Uploader/Icons/publisher_portal_white.png.meta new file mode 100644 index 000000000..a0f13697c --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/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/Uploader/Scripts.meta b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts.meta new file mode 100644 index 000000000..63c6efc3d --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/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/Uploader/Scripts/AssetStoreAPI.cs b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/AssetStoreAPI.cs new file mode 100644 index 000000000..c3c5d07de --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/AssetStoreAPI.cs @@ -0,0 +1,774 @@ +using AssetStoreTools.Uploader.Data; +using AssetStoreTools.Uploader.Utility; +using AssetStoreTools.Utility; +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.3.0"; + + 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/Uploader/Scripts/AssetStoreAPI.cs.meta b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/AssetStoreAPI.cs.meta new file mode 100644 index 000000000..d248bfef2 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/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/Uploader/Scripts/Data.meta b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data.meta new file mode 100644 index 000000000..be3554a6d --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9e3cae7082463da41b807724242fd617 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/ASAnalytics.cs b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/ASAnalytics.cs new file mode 100644 index 000000000..a91b18e6c --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/ASAnalytics.cs @@ -0,0 +1,46 @@ +using UnityEditor; +using UnityEngine.Analytics; + +namespace AssetStoreTools.Uploader.Data +{ + 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/Uploader/Scripts/Data/ASAnalytics.cs.meta b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/ASAnalytics.cs.meta new file mode 100644 index 000000000..22a47b189 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/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/Uploader/Scripts/Data/OngoingUpload.cs b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/OngoingUpload.cs new file mode 100644 index 000000000..585e4fc98 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/OngoingUpload.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading; + +namespace AssetStoreTools.Uploader.Data +{ + internal class OngoingUpload : IDisposable + { + private CancellationTokenSource _cancellationTokenSource; + + public string VersionId { get; } + public string PackageName { get; } + public float Progress { get; private set; } + public CancellationToken CancellationToken => _cancellationTokenSource.Token; + + public OngoingUpload(string versionId, string packageName) + { + VersionId = versionId; + PackageName = packageName; + Progress = 0f; + _cancellationTokenSource = new CancellationTokenSource(); + } + + public void Cancel() + { + _cancellationTokenSource?.Cancel(); + } + + public void Dispose() + { + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + } + + public void UpdateProgress(float newProgress) + { + Progress = newProgress; + } + } +} \ No newline at end of file diff --git a/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/OngoingUpload.cs.meta b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/OngoingUpload.cs.meta new file mode 100644 index 000000000..82a24a3dd --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/OngoingUpload.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 601fdada4edc5b94eb83a21d1a01ed26 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/PackageData.cs b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/PackageData.cs new file mode 100644 index 000000000..ab69968cb --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/PackageData.cs @@ -0,0 +1,36 @@ +namespace AssetStoreTools.Uploader.Data +{ + internal class PackageData + { + public string Id { get; } + public string Name { get; } + public string VersionId { get; } + public string Status { get; } + public string Category { get; } + public bool IsCompleteProject { get; } + public string LastUploadedPath { get; } + public string LastUploadedGuid { get; } + + public string LastDate { get; } + public string LastSize { get; } + + public PackageData(string id, string name, string versionId, string status, string category, bool isCompleteProject, string lastUploadedPath, string lastUploadedGuid, string lastDate, string lastSize) + { + Id = id; + Name = name; + VersionId = versionId; + Status = status; + Category = category; + IsCompleteProject = isCompleteProject; + LastUploadedPath = lastUploadedPath; + LastUploadedGuid = lastUploadedGuid; + LastDate = lastDate; + LastSize = lastSize; + } + + public override string ToString() + { + return $"{Id} {Name} {VersionId} {Status} {Category} {LastUploadedPath} {LastUploadedGuid} {IsCompleteProject} {LastDate} {LastSize}"; + } + } +} \ No newline at end of file diff --git a/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/PackageData.cs.meta b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/PackageData.cs.meta new file mode 100644 index 000000000..c9c1b2113 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/PackageData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8157930875be4972a48c870a3d1e8ff1 +timeCreated: 1658919930 \ No newline at end of file diff --git a/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/PackageUploadResult.cs b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/PackageUploadResult.cs new file mode 100644 index 000000000..367ef2190 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/PackageUploadResult.cs @@ -0,0 +1,46 @@ +using AssetStoreTools.Utility; +using UnityEngine; + +namespace AssetStoreTools.Uploader.Data +{ + internal class PackageUploadResult + { + public enum UploadStatus + { + Default = 0, + Success = 1, + Fail = 2, + Cancelled = 3, + ResponseTimeout = 4 + } + + public UploadStatus Status; + public ASError Error; + + private PackageUploadResult() { } + + public static PackageUploadResult PackageUploadSuccess() => new PackageUploadResult() { Status = UploadStatus.Success }; + + public static PackageUploadResult PackageUploadFail(ASError e) => new PackageUploadResult() { Status = UploadStatus.Fail, Error = e }; + + public static PackageUploadResult PackageUploadCancelled() => new PackageUploadResult() { Status = UploadStatus.Cancelled }; + + public static PackageUploadResult PackageUploadResponseTimeout() => new PackageUploadResult() { Status = UploadStatus.ResponseTimeout }; + + public static Color GetColorByStatus(UploadStatus status) + { + switch (status) + { + default: + case UploadStatus.Default: + return new Color(0.13f, 0.59f, 0.95f); + case UploadStatus.Success: + return new Color(0f, 0.50f, 0.14f); + case UploadStatus.Cancelled: + return new Color(0.78f, 0.59f, 0f); + case UploadStatus.Fail: + return new Color(0.69f, 0.04f, 0.04f); + } + } + } +} diff --git a/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/PackageUploadResult.cs.meta b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/PackageUploadResult.cs.meta new file mode 100644 index 000000000..c69572269 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/Data/PackageUploadResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 188361b01a1450145a6fc2a7aa0a3a3c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/UI Elements.meta b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/UI Elements.meta new file mode 100644 index 000000000..87a66a2d5 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/UI Elements.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3eb6991a3db8cc34dad63504bc6c3c0e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/UI Elements/Login.meta b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/UI Elements/Login.meta new file mode 100644 index 000000000..0a7ccff81 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/UI Elements/Login.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d6e2d6bcfe000764e9330d78017e32bc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/UI Elements/Login/LoginWindow.cs b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/UI Elements/Login/LoginWindow.cs new file mode 100644 index 000000000..e711f60a4 --- /dev/null +++ b/Packages/com.unity.asset-store-tools/Editor/Uploader/Scripts/UI Elements/Login/LoginWindow.cs @@ -0,0 +1,233 @@ +using AssetStoreTools.Utility; +using AssetStoreTools.Utility.Json; +using System; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AssetStoreTools.Uploader.UIElements +{ + 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() + { + styleSheets.Add(StyleSelector.UploaderWindow.LoginWindowStyle); + styleSheets.Add(StyleSelector.UploaderWindow.LoginWindowTheme); + ConstructLoginWindow(); + EditorApplication.update += UpdateCloudLoginButton; + } + + public void SetupLoginElements(Action onSuccess, Action onFail) + { + this.SetEnabled(true); + + _cloudLoginLabel = _cloudLoginButton.Q