Asset Store Checklist:
- PRODUCTION GAME TEST (!)
- remove Tests
- remove StreamingAssets
- Edgegap asset store version
- try Unity 2020
- Add Preprocessordefines
- Update Version.txt
- Include Dependencies: newtonsoft.json
This commit is contained in:
vis2k 2021-03-06 14:26:03 +08:00 committed by mischa
parent c1f71dffe9
commit 370ed8c049
354 changed files with 18449 additions and 115 deletions

View File

@ -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"]

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 78b80371aabba1d48aac39ec7ccfe7c5
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,9 +1,8 @@
FROM ubuntu:22.04 FROM ubuntu:22.04
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG SERVER_BUILD_PATH=Builds/EdgegapServer
COPY ${SERVER_BUILD_PATH} /root/build/ COPY Builds/EdgegapServer /root/build/
WORKDIR /root/ WORKDIR /root/
@ -14,4 +13,4 @@ RUN apt-get update && \
apt-get clean && \ apt-get clean && \
update-ca-certificates update-ca-certificates
CMD [ "/root/build/ServerBuild", "-batchmode", "-nographics", "$UNITY_COMMANDLINE_ARGS"] ENTRYPOINT [ "/root/build/ServerBuild", "-batchmode", "-nographics"]

View File

@ -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_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_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"; 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_MEDIUM_TAG = "servers_quickstart_plugin";
private const string DEFAULT_UTM_CONTENT_TAG = "plugin_button"; private const string DEFAULT_UTM_CONTENT_TAG = "plugin_button";
public const string DEFAULT_UTM_TAGS = "utm_source=" + DEFAULT_UTM_SOURCE_TAG + public const string DEFAULT_UTM_TAGS = "utm_source=" + DEFAULT_UTM_SOURCE_TAG +

View File

@ -112,8 +112,7 @@ public class EdgegapWindowV2 : EditorWindow
Path.GetDirectoryName(AssetDatabase.GetAssetPath(MonoScript.FromScriptableObject(this))); Path.GetDirectoryName(AssetDatabase.GetAssetPath(MonoScript.FromScriptableObject(this)));
// END MIRROR CHANGE // END MIRROR CHANGE
internal string ProjectRootPath => Directory.GetCurrentDirectory(); internal string ProjectRootPath => Directory.GetCurrentDirectory();
internal string ThisScriptPath => Directory.GetFiles(ProjectRootPath, GetType().Name + ".cs", SearchOption.AllDirectories)[0]; internal string DockerFilePath => $"{Directory.GetParent(Directory.GetFiles(ProjectRootPath, GetType().Name + ".cs", SearchOption.AllDirectories)[0]).FullName}{Path.DirectorySeparatorChar}Dockerfile";
internal string DockerFilePath => $"{Directory.GetParent(ThisScriptPath).FullName}{Path.DirectorySeparatorChar}Dockerfile";
[MenuItem("Tools/Edgegap Hosting")] // MIRROR CHANGE: more obvious title [MenuItem("Tools/Edgegap Hosting")] // MIRROR CHANGE: more obvious title
public static void ShowEdgegapToolWindow() public static void ShowEdgegapToolWindow()
@ -546,11 +545,7 @@ private void debugEnableAllGroups()
_containerCustomRegistryWrapper.SetEnabled(true); _containerCustomRegistryWrapper.SetEnabled(true);
} }
private void onApiTokenVerifyBtnClick() private void onApiTokenVerifyBtnClick() => _ = verifyApiTokenGetRegistryCredsAsync();
{
_ = verifyApiTokenGetRegistryCredsAsync();
_ = checkForUpdates();
}
private void onApiTokenGetBtnClick() => openGetApiTokenWebsite(); private void onApiTokenGetBtnClick() => openGetApiTokenWebsite();
/// <summary>Process UI + validation before/after API logic</summary> /// <summary>Process UI + validation before/after API logic</summary>
@ -920,24 +915,6 @@ private string getBase64StrFromSprite(Sprite sprite, int maxKbSize = 200)
} }
} }
/// <summary>
/// Fetch latest github release and compare with local package.json version
/// </summary>
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.");
}
}
/// <summary> /// <summary>
/// Verifies token => apps/container groups -> gets registry creds (if any). /// Verifies token => apps/container groups -> gets registry creds (if any).
/// TODO: UX - Show loading spinner. /// TODO: UX - Show loading spinner.

View File

@ -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<string> 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<GithubRelease>(json);
}
}
}

View File

@ -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<PackageJSON>(File.ReadAllText(path));
}
}
}

View File

@ -42,25 +42,15 @@ This plugin does not need to be included in your builds, as it's only a developm
## Other sources ## Other sources
The only other official distribution channels for this plugin are: 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 samples](https://mirror-networking.gitbook.io/docs/hosting/edgegap-hosting-plugin-guide)
- [Mirror Networking source](https://github.com/MirrorNetworking/Mirror) - [Fish Networking samples](https://fish-networking.gitbook.io/docs/manual/server-hosting/edgegap-official-partner)
- [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) **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.
- [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)
## Next Steps ## Next Steps
Once you have it, check for **Tools** -> **Edgegap Hosting** in Unity's top menu. 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. 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 ### Update the Plugin in Unity

View File

@ -1,6 +1,6 @@
{ {
"name": "com.edgegap.unity-servers-plugin", "name": "com.edgegap.unity-servers-plugin",
"version": "2.3.1", "version": "1.0.8",
"displayName": "Edgegap Servers Quickstart", "displayName": "Edgegap Servers Quickstart",
"description": "Get started quickly with Edgegap Dedicated Server hosting.", "description": "Get started quickly with Edgegap Dedicated Server hosting.",
"unity": "2021.3", "unity": "2021.3",

123
Assets/Mirror/Notice.txt Normal file
View File

@ -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.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1a7b49ad188074707b004e7bb8824857
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1 @@
93.0.0

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c6b1f72568a9340178b4c34608fbdbc3
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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 packages 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

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 06607220dbd46414e8f66bf9c5e3eb79
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 166da5c6fc70e814a8262463903b2714
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Unity.AssetStoreTools.Editor.Tests.asmdef")]
[assembly: InternalsVisibleTo("ab-builder")]

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 6ef365e2acd5a5044a4a14fa83c7d9b9 guid: ccfd7faf75ab3c74a98015e772288d86
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

View File

@ -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<AssetStoreUploader>(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<AssetStoreValidator>(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();
}
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: e8abfa078881a944a81ac3bcc8e5e0f4 guid: 6060eef206afc844caaa1732538e8890
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

View File

@ -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();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c1057a05baaa45942808573065c02a03
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5f5ca981958937a43997a9f365759edf
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ce99a618d1e211444b53f18bb3444f75
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 399b115514c617d47a00b8c0a5e430fd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<ExportResult> 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<string> paths = new List<string>();
// 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();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 52ef11a59e545544fafaa99a5fa6cce9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<string, UnityEngine.Object>();
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<UnityEngine.Object>(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<string> allPackagesList = new List<string>(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<string, string> GetPathGuidPairs(string[] exportPaths)
{
var pathGuidPairs = new Dictionary<string, string>();
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<string, UnityEngine.Object> 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);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 32f50122a1b2bc2428cf8fba321e2414
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<ExportResult> ExportPackage(LegacyExporterSettings exportSettings)
{
var exporter = new PackageExporterLegacy(exportSettings);
return await exporter.ExportPackage();
}
private async Task<ExportResult> 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<string>();
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();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3200380dff2de104aa79620e4b41dc70
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,15 @@
{
"name": "asset-store-tools-editor",
"references": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c183be512f4485d40a3437fabd6c81cf
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9722d52df16aab742b26fe301782c74c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<char> _debugBuffer = new List<char>();
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<VisualTreeAsset>($"{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>("LoginWindow");
_uploadWindow = rootVisualElement.Q<UploadWindow>("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
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7b5319699cc84194a9a768ad33b86c21
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ab9d0e254817f4f4589a6a378d77babc
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

View File

@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -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:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 15b24ad8f9d236249910fb8eef1e30ea
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
{
/// <summary>
/// A class for retrieving data from the Asset Store backend <para/>
/// <b>Note:</b> most data retrieval methods require <see cref="SavedSessionId"/> to be set
/// </summary>
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<string, OngoingUpload> ActiveUploads = new ConcurrentDictionary<string, OngoingUpload>();
public static bool IsUploading => (ActiveUploads.Count > 0);
static AssetStoreAPI()
{
ServicePointManager.DefaultConnectionLimit = 500;
httpClient.DefaultRequestHeaders.ConnectionClose = false;
httpClient.Timeout = TimeSpan.FromMinutes(1320);
}
/// <summary>
/// A structure used to return the success outcome and the result of Asset Store API calls
/// </summary>
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
/// <summary>
/// A login API call that uses the email and password credentials
/// </summary>
/// <remarks>
/// <b>Note:</b> this method only returns a response from the server and does not set the <see cref="SavedSessionId"/> itself
/// </remarks>
public static async Task<APIResult> LoginWithCredentialsAsync(string email, string password)
{
FormUrlEncodedContent data = GetLoginContent(new Dictionary<string, string> { { "user", email }, { "pass", password } });
return await LoginAsync(data);
}
/// <summary>
/// A login API call that uses the <see cref="SavedSessionId"/>
/// </summary>
/// <remarks>
/// <b>Note:</b> this method only returns a response from the server and does not set the <see cref="SavedSessionId"/> itself
/// </remarks>
public static async Task<APIResult> 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<string, string> { { "reuse_session", SavedSessionId }, { "xunitysession", UnauthSessionId } });
return await LoginAsync(data);
}
/// <summary>
/// A login API call that uses the <see cref="CloudProjectSettings.accessToken"/><para/>
/// </summary>
/// <remarks>
/// <b>Note:</b> this method only returns a response from the server and does not set the <see cref="SavedSessionId"/> itself
/// </remarks>
/// <param name="token">Cloud access token. Can be retrieved by calling <see cref="CloudProjectSettings.accessToken"/></param>
public static async Task<APIResult> LoginWithTokenAsync(string token)
{
FormUrlEncodedContent data = GetLoginContent(new Dictionary<string, string> { { "user_access_token", token } });
return await LoginAsync(data);
}
private static async Task<APIResult> 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<JsonValue> GetPackageDataMain()
{
return await GetAssetStoreData(APIUri("asset-store-tools", "metadata/0", SavedSessionId));
}
private static async Task<JsonValue> GetPackageDataExtra()
{
return await GetAssetStoreData(APIUri("management", "packages", SavedSessionId));
}
private static async Task<JsonValue> 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;
}
/// <summary>
/// Retrieve data for all packages associated with the currently logged in account (identified by <see cref="SavedSessionId"/>)
/// </summary>
/// <param name="useCached"></param>
/// <returns></returns>
public static async Task<APIResult> 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) };
}
}
/// <summary>
/// Retrieve the thumbnail textures for all packages within the provided json structure and perform a given action after each retrieval
/// </summary>
/// <param name="packageJson">A json file retrieved from <see cref="GetFullPackageDataAsync(bool)"/></param>
/// <param name="useCached">Return cached thumbnails if they are found</param>
/// <param name="onSuccess">
/// Action to perform upon a successful thumbnail retrieval <para/>
/// <see cref="string"/> - Package Id <br/>
/// <see cref="Texture2D"/> - Associated Thumbnail
/// </param>
/// <param name="onFail">
/// Action to perform upon a failed thumbnail retrieval <para/>
/// <see cref="string"/> - Package Id <br/>
/// <see cref="ASError"/> - Associated error
/// </param>
public static async void GetPackageThumbnails(JsonValue packageJson, bool useCached, Action<string, Texture2D> onSuccess, Action<string, ASError> 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<byte[]> 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;
}
/// <summary>
/// Retrieve, update the cache and return the updated data for a previously cached package
/// </summary>
public static async Task<APIResult> 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) };
}
}
/// <summary>
/// Retrieve all Unity versions that the given package has already had uploaded content with
/// </summary>
/// <param name="packageId"></param>
/// <param name="versionId"></param>
/// <returns></returns>
public static List<string> GetPackageUploadedVersions(string packageId, string versionId)
{
var versions = new List<string>();
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
/// <summary>
/// Upload a content file (.unitypackage) to a provided package version
/// </summary>
/// <param name="versionId"></param>
/// <param name="packageName">Name of the package. Only used for identifying the package in <see cref="OngoingUpload"/> class</param>
/// <param name="filePath">Path to the .unitypackage file</param>
/// <param name="localPackageGuid">The <see cref="AssetDatabase.AssetPathToGUID(string)"/> value of the main content folder for the provided package</param>
/// <param name="localPackagePath">The local path (relative to the root project folder) of the main content folder for the provided package</param>
/// <param name="localProjectPath">The path to the project that this package was built from</param>
/// <returns></returns>
public static async Task<PackageUploadResult> 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<string, string> packageParams = new Dictionary<string, string>
{
// 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();
}
}
/// <summary>
/// Cancel the uploading task for a package with the provided package id
/// </summary>
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<string, string> 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<JsonValue> 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<string, string> extraQuery)
{
Dictionary<string, string> extraQueryMerged;
if (extraQuery == null)
extraQueryMerged = new Dictionary<string, string>();
else
extraQueryMerged = new Dictionary<string, string>(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<string, string> 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<string, string> CreateCategoryDictionary(JsonValue json)
{
var categories = new Dictionary<string, string>();
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;
}
/// <summary>
/// Check if the account data is for a valid publisher account
/// </summary>
/// <param name="json">Json structure retrieved from one of the API login methods</param>
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();
}
/// <summary>
/// Cancel all data retrieval tasks
/// </summary>
public static void AbortDownloadTasks()
{
s_downloadCancellationSource?.Cancel();
}
/// <summary>
/// Cancel all data uploading tasks
/// </summary>
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
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 684fca3fffd79d944a32d9b3adbfc007
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9e3cae7082463da41b807724242fd617
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1095145789a64767a6add837eea19786
timeCreated: 1658832954

View File

@ -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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 601fdada4edc5b94eb83a21d1a01ed26
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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}";
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8157930875be4972a48c870a3d1e8ff1
timeCreated: 1658919930

View File

@ -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);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 188361b01a1450145a6fc2a7aa0a3a3c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3eb6991a3db8cc34dad63504bc6c3c0e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d6e2d6bcfe000764e9330d78017e32bc
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<LoginWindow> { }
public LoginWindow()
{
styleSheets.Add(StyleSelector.UploaderWindow.LoginWindowStyle);
styleSheets.Add(StyleSelector.UploaderWindow.LoginWindowTheme);
ConstructLoginWindow();
EditorApplication.update += UpdateCloudLoginButton;
}
public void SetupLoginElements(Action<JsonValue> onSuccess, Action<ASError> onFail)
{
this.SetEnabled(true);
_cloudLoginLabel = _cloudLoginButton.Q<Label>(className: "login-description");
_cloudLoginLabel.text = "Cloud login unavailable.";
_cloudLoginButton.SetEnabled(false);
_cloudLoginButton.clicked += async () =>
{
EnableErrorBox(false);
this.SetEnabled(false);
var result = await AssetStoreAPI.LoginWithTokenAsync(CloudProjectSettings.accessToken);
if (result.Success)
onSuccess(result.Response);
else
onFail(result.Error);
};
// Normal login
_credentialsLoginButton.clicked += async () =>
{
EnableErrorBox(false);
var validatedFields = ValidateLoginFields(_emailField.text, _passwordField.value);
this.SetEnabled(!validatedFields);
if (validatedFields)
{
var result = await AssetStoreAPI.LoginWithCredentialsAsync(_emailField.text, _passwordField.text);
if (result.Success)
onSuccess(result.Response);
else
onFail(result.Error);
}
};
}
public void EnableErrorBox(bool enable, string message=null)
{
var displayStyle = enable ? DisplayStyle.Flex : DisplayStyle.None;
_errorBox.style.display = displayStyle;
if (!String.IsNullOrEmpty(message))
_errorLabel.text = message;
}
public void ClearLoginBoxes()
{
_emailField.value = String.Empty;
_passwordField.value = String.Empty;
}
private bool ValidateLoginFields(string email, string password)
{
if (string.IsNullOrEmpty(email))
{
EnableErrorBox(true, "Email field cannot be empty.");
return false;
}
if (string.IsNullOrEmpty(password))
{
EnableErrorBox(true, "Password field cannot be empty.");
return false;
}
return true;
}
private void ConstructLoginWindow()
{
// Asset Store logo
Image assetStoreLogo = new Image {name = "AssetStoreLogo"};
assetStoreLogo.AddToClassList("asset-store-logo");
Add(assetStoreLogo);
// Cloud login
VisualElement cloudLogin = new VisualElement {name = "CloudLogin"};
_cloudLoginButton = new Button {name = "LoginButtonCloud"};
_cloudLoginButton.AddToClassList("login-button-cloud");
Label loginDescription = new Label {text = "Cloud login unavailable"};
loginDescription.AddToClassList("login-description");
Label orLabel = new Label {text = "or"};
orLabel.AddToClassList("or-label");
_cloudLoginButton.Add(loginDescription);
cloudLogin.Add(_cloudLoginButton);
cloudLogin.Add(orLabel);
Add(cloudLogin);
_errorBox = new Box() { name = "LoginErrorBox" };
_errorBox.AddToClassList("login-error-box");
var errorImage = new Image();
_errorBox.Add(errorImage);
_errorLabel = new Label();
_errorBox.Add(_errorLabel);
Add(_errorBox);
EnableErrorBox(false);
// Manual login
VisualElement manualLoginBox = new VisualElement {name = "ManualLoginBox"};
manualLoginBox.AddToClassList("manual-login-box");
// Email input box
VisualElement inputBoxEmail = new VisualElement();
inputBoxEmail.AddToClassList("input-box-login");
Label emailTitle = new Label {text = "Email"};
_emailField = new TextField();
inputBoxEmail.Add(emailTitle);
inputBoxEmail.Add(_emailField);
manualLoginBox.Add(inputBoxEmail);
// Password input box
VisualElement inputBoxPassword = new VisualElement();
inputBoxPassword.AddToClassList("input-box-login");
Label passwordTitle = new Label {text = "Password"};
_passwordField = new TextField {isPasswordField = true};
inputBoxPassword.Add(passwordTitle);
inputBoxPassword.Add(_passwordField);
manualLoginBox.Add(inputBoxPassword);
// Login button
_credentialsLoginButton = new Button {name = "LoginButtonCredentials"};
_credentialsLoginButton.AddToClassList("login-button-cred");
Label loginDescriptionCredentials = new Label {text = "Login"};
loginDescriptionCredentials.AddToClassList("login-description");
_credentialsLoginButton.Add(loginDescriptionCredentials);
manualLoginBox.Add(_credentialsLoginButton);
Add(manualLoginBox);
// Helper buttons
VisualElement helperBox = new VisualElement {name = "HelperBox"};
helperBox.AddToClassList("helper-button-box");
Button createAccountButton = new Button {name = "CreateAccountButton", text = "Create Publisher ID"};
Button forgotPasswordButton = new Button {name = "ForgotPasswordButton", text = "Reset Password"};
createAccountButton.AddToClassList("hyperlink-button");
forgotPasswordButton.AddToClassList("hyperlink-button");
createAccountButton.clicked += () => Application.OpenURL(REGISTER_URL);
forgotPasswordButton.clicked += () => Application.OpenURL(FORGOT_PASSWORD_URL);
helperBox.Add(createAccountButton);
helperBox.Add(forgotPasswordButton);
Add(helperBox);
}
private void UpdateCloudLoginButton()
{
if (_cloudLoginLabel == null)
return;
if (_lastRefreshTime + _cloudLoginRefreshTime > EditorApplication.timeSinceStartup)
return;
_lastRefreshTime = EditorApplication.timeSinceStartup;
// Cloud login
if (AssetStoreAPI.IsCloudUserAvailable)
{
_cloudLoginLabel.text = $"Login with {CloudProjectSettings.userName}";
_cloudLoginButton.SetEnabled(true);
}
else
{
_cloudLoginLabel.text = "Cloud login unavailable";
_cloudLoginButton.SetEnabled(false);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d4a93170d5bda304895e5feaf6e34aa8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 854f6f9e93b37204eb2e6042138643bc
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,408 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AssetStoreTools.Uploader.Data;
using AssetStoreTools.Uploader.Utility;
using AssetStoreTools.Utility;
using AssetStoreTools.Validator.Data;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace AssetStoreTools.Uploader.UIElements
{
internal class AllPackageView : VisualElement
{
public new class UxmlFactory : UxmlFactory<AllPackageView> { }
private enum PackageSorting
{
Name,
Category,
Date
}
// Package Data
private readonly string[] _priorityGroups = { "Draft", "Published" };
private readonly List<PackageGroup> _packageGroups;
private List<PackageView> _allPackages;
// Visual Elements
private readonly ScrollView _packageScrollView;
private Scroller _packageViewScroller;
// Sorting data
private PackageSorting _activeSorting;
// Spinner
private VisualElement _spinnerBox;
private Image _loadingSpinner;
private int _spinIndex;
private double _spinTimer;
private double _spinThreshold = 0.1;
public Action<bool> RefreshingPackages;
public AllPackageView()
{
_packageScrollView = new ScrollView();
_allPackages = new List<PackageView>();
_packageGroups = new List<PackageGroup>();
_activeSorting = PackageSorting.Name; // Default sorting type returned by the metadata JSON
styleSheets.Add(StyleSelector.UploaderWindow.AllPackagesStyle);
styleSheets.Add(StyleSelector.UploaderWindow.AllPackagesTheme);
ConstructAllPackageView();
EditorApplication.playModeStateChanged -= PlayModeStateChanged;
EditorApplication.playModeStateChanged += PlayModeStateChanged;
ValidationState.Instance.OnJsonSave -= RepaintPackageIcons;
ValidationState.Instance.OnJsonSave += RepaintPackageIcons;
}
#region Element Setup
private void ConstructAllPackageView()
{
SetupFilteringTools();
SetupSpinner();
Add(_packageScrollView);
}
private void SetupFilteringTools()
{
// Top Toolbar
var topToolsRow = new VisualElement { name = "TopToolsRow" };
topToolsRow.AddToClassList("top-tools-row");
// Search
var searchField = new ToolbarSearchField { name = "SearchField" };
searchField.AddToClassList("package-search-field");
// Sorting menu button
var sortMenu = new ToolbarMenu() { text = "Sort by name, A→Z" };
sortMenu.menu.AppendAction("Sort by name, A→Z", (_) => { sortMenu.text = "Sort by name, A→Z"; Sort(PackageSorting.Name); });
sortMenu.menu.AppendAction("Sort by last updated", (_) => { sortMenu.text = "Sort by last updated"; Sort(PackageSorting.Date); });
sortMenu.menu.AppendAction("Sort by category, A→Z", (_) => { sortMenu.text = "Sort by category, A→Z"; Sort(PackageSorting.Category); });
sortMenu.AddToClassList("sort-menu");
// Finalize the bar
topToolsRow.Add(searchField);
topToolsRow.Add(sortMenu);
Add(topToolsRow);
// Add Callbacks and click events
searchField.RegisterCallback<ChangeEvent<string>>(evt =>
{
var searchString = evt.newValue.ToLower();
SearchFilter(searchString);
});
}
private void SearchFilter(string filter)
{
foreach (var g in _packageGroups)
g.SearchFilter(filter);
}
private void SetupReadOnlyInfoBox(string infoText)
{
var groupHeader = new Box { name = "GroupReadOnlyInfoBox" };
groupHeader.AddToClassList("group-info-box");
var infoImage = new Image();
groupHeader.Add(infoImage);
var infoLabel = new Label { text = infoText };
groupHeader.Add(infoLabel);
_packageScrollView.Add(groupHeader);
}
#endregion
#region Package Display
public async void ShowPackagesList(bool useCached, Action<ASError> onFail)
{
// Clear existing packages in the UI
ClearPackages();
// Enable spinner and disable refreshing
EnableSpinner();
RefreshingPackages?.Invoke(true);
// Read package metadata from the Publisher portal
PackageFetcher packageFetcher = new PackageFetcher();
var result = await packageFetcher.Fetch(useCached);
if (!result.Success)
{
if (result.SilentFail)
return;
ASDebug.LogError(result.Error.Message);
onFail?.Invoke(result.Error);
}
var packages = result.Packages;
var json = result.Json;
// Clear before appending as well
ClearPackages();
if (packages == null)
{
RefreshingPackages?.Invoke(false);
DisplayNoPackages();
return;
}
DisplayAllPackages(packages);
// Only performed after adding all packages to prevent slowdowns. Sorting also repaints the view
Sort(_activeSorting);
RefreshingPackages?.Invoke(false);
DisableSpinner();
AssetStoreAPI.GetPackageThumbnails(json, true, (id, texture) =>
{
var package = GetPackage(id);
var packageImage = package.Q<Image>();
packageImage.style.backgroundImage = texture;
if (texture == null)
packageImage.AddToClassList("package-image-not-found");
},
(id, error) =>
{
ASDebug.LogWarning($"Package {id} could not download thumbnail successfully\n{error.Exception}");
});
}
public void ClearPackages()
{
_allPackages.Clear();
_packageScrollView.Clear();
_packageGroups.Clear();
}
private void DisplayNoPackages()
{
SetupReadOnlyInfoBox("You don't have packages yet. Please visit Publishing Portal if you " +
"would like to create one.");
DisableSpinner();
}
private void DisplayAllPackages(ICollection<PackageData> packages)
{
// Each package has an identifier and a bunch of data (current version id, name, etc.)
foreach (var package in packages)
{
AddPackage(package);
}
}
private void AddPackage(PackageData packageData)
{
var newEntry = PackageViewStorer.GetPackage(packageData);
_allPackages.Add(newEntry);
}
private VisualElement GetPackage(string id)
{
return _allPackages.FirstOrDefault(package => package.PackageId == id);
}
private void Repaint()
{
_packageScrollView.Clear();
_packageGroups.Clear();
var groupedDict = new SortedDictionary<string, List<PackageView>>();
// Group packages by status into a dictionary
foreach (var p in _allPackages)
{
var status = char.ToUpper(p.Status.First()) + p.Status.Substring(1);
if (!groupedDict.ContainsKey(status))
groupedDict.Add(status, new List<PackageView>());
groupedDict[status].Add(p);
}
// Add prioritized status groups first
foreach (var group in _priorityGroups)
{
if (!groupedDict.ContainsKey(group))
continue;
AddGroup(group, groupedDict[group], true);
groupedDict.Remove(group);
// After adding the 'Draft' group, an infobox indicating that other groups are non-interactable is added
if (group == "Draft" && groupedDict.Count > 0)
SetupReadOnlyInfoBox("Only packages with a 'Draft' status can be selected for uploading Assets");
}
// Add any leftover status groups
foreach (var c in groupedDict.Keys)
{
AddGroup(c, groupedDict[c], false);
}
// Shared group adding method for priority and non-priority groups
void AddGroup(string groupName, List<PackageView> packages, bool createExpanded)
{
var group = new PackageGroup(groupName, createExpanded)
{
OnSliderChange = AdjustVerticalSliderPosition
};
foreach (var p in packages)
group.AddPackage(p);
_packageGroups.Add(group);
_packageScrollView.Add(group);
}
}
private void RepaintPackageIcons()
{
foreach (var package in _allPackages)
{
if (!AssetStoreCache.GetCachedTexture(package.PackageId, out Texture2D texture))
continue;
var packageImage = package.Q<Image>();
packageImage.style.backgroundImage = texture;
}
}
private void AdjustVerticalSliderPosition(float delta)
{
if (_packageViewScroller == null)
_packageViewScroller = this.Q<Scroller>(className: "unity-scroll-view__vertical-scroller");
_packageViewScroller.value += delta;
}
#endregion
#region Package View Sorting
private void Sort(PackageSorting sortBy)
{
if (sortBy == _activeSorting && _packageScrollView.childCount > 0)
return;
switch (sortBy)
{
case PackageSorting.Name:
SortByName(false);
break;
case PackageSorting.Date:
SortByDate(true);
break;
case PackageSorting.Category:
SortByCategory(false);
break;
}
_activeSorting = sortBy;
}
private void SortByName(bool descending)
{
if (!descending)
_allPackages = _allPackages.OrderBy(p => p.PackageName).ToList();
else
_allPackages = _allPackages.OrderByDescending(p => p.PackageName).ToList();
Repaint();
}
private void SortByCategory(bool descending)
{
if (!descending)
_allPackages = _allPackages.OrderBy(p => p.Category).ThenBy(p => p.PackageName).ToList();
else
_allPackages = _allPackages.OrderByDescending(p => p.Category).ThenBy(p => p.PackageName).ToList();
Repaint();
}
private void SortByDate(bool descending)
{
if (!descending)
_allPackages = _allPackages.OrderBy(p => p.LastUpdatedDate).ThenBy(p => p.PackageName).ToList();
else
_allPackages = _allPackages.OrderByDescending(p => p.LastUpdatedDate).ThenBy(p => p.PackageName).ToList();
Repaint();
}
private void PlayModeStateChanged(PlayModeStateChange playModeState)
{
if (playModeState == PlayModeStateChange.EnteredEditMode)
{
RepaintPackageIcons();
}
}
#endregion
#region Spinner
private void SetupSpinner()
{
_spinnerBox = new VisualElement {name = "SpinnerBox"};
_spinnerBox.AddToClassList("spinner-box");
_loadingSpinner = new Image {name = "SpinnerImage"};
_loadingSpinner.AddToClassList("spinner-image");
_spinnerBox.Add(_loadingSpinner);
Add(_spinnerBox);
}
private void UpdateSpinner()
{
if (_loadingSpinner == null)
return;
if (_spinTimer + _spinThreshold > EditorApplication.timeSinceStartup)
return;
_spinTimer = EditorApplication.timeSinceStartup;
_loadingSpinner.image = EditorGUIUtility.IconContent($"WaitSpin{_spinIndex:00}").image;
_spinIndex += 1;
if (_spinIndex > 11)
_spinIndex = 0;
}
private void EnableSpinner()
{
EditorApplication.update += UpdateSpinner;
_packageScrollView.style.display = DisplayStyle.None;
_spinnerBox.style.display = DisplayStyle.Flex;
}
private void DisableSpinner()
{
EditorApplication.update -= UpdateSpinner;
_packageScrollView.style.display = DisplayStyle.Flex;
_spinnerBox.style.display = DisplayStyle.None;
}
#endregion
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 607cf9e3fb4a49839f2e6a82e0d8d535
timeCreated: 1651220955

View File

@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using UnityEngine.UIElements;
namespace AssetStoreTools.Uploader.UIElements
{
internal class PackageGroup : VisualElement
{
// Category Data
private string GroupName { get; }
private readonly List<PackageView> _packages;
// Visual Elements
private Button _groupExpanderBox;
private VisualElement _groupContent;
private Label _expanderLabel;
private Label _groupLabel;
// Other
private PackageView _expandedPackageView;
private bool _expanded;
private bool? _expandingOverriden;
// Actions
public Action<float> OnSliderChange;
public PackageGroup(string groupName, bool createExpanded)
{
GroupName = groupName;
AddToClassList("package-group");
_packages = new List<PackageView>();
_expanded = createExpanded;
SetupSingleGroupElement();
HandleExpanding();
}
public void AddPackage(PackageView packageView)
{
_packages.Add(packageView);
_groupContent.Add(packageView);
UpdateGroupLabel();
packageView.OnPackageSelection = HandlePackageSelection;
packageView.ShowFunctions(false);
}
public void SearchFilter(string filter)
{
var foundPackageCount = 0;
foreach(var p in _packages)
{
if (p.SearchableText.Contains(filter))
{
foundPackageCount++;
p.style.display = DisplayStyle.Flex;
_groupContent.style.display = DisplayStyle.Flex;
}
else
p.style.display = DisplayStyle.None;
}
if (string.IsNullOrEmpty(filter))
{
_expandingOverriden = null;
UpdateGroupLabel();
SetEnabled(true);
HandleExpanding();
}
else
{
OverwriteGroupLabel($"{GroupName} ({foundPackageCount} found)");
SetEnabled(foundPackageCount > 0);
HandleExpanding(foundPackageCount > 0);
}
}
private void SetupSingleGroupElement()
{
_groupExpanderBox = new Button();
_groupExpanderBox.AddToClassList("group-expander-box");
_expanderLabel = new Label { name = "ExpanderLabel", text = "►" };
_expanderLabel.AddToClassList("expander");
_groupLabel = new Label {text = $"{GroupName} ({_packages.Count})"};
_groupLabel.AddToClassList("group-label");
_groupExpanderBox.Add(_expanderLabel);
_groupExpanderBox.Add(_groupLabel);
_groupContent = new VisualElement {name = "GroupContentBox"};
_groupContent.AddToClassList("group-content-box");
_groupExpanderBox.clicked += () =>
{
if (_expandingOverriden == null)
_expanded = !_expanded;
else
_expandingOverriden = !_expandingOverriden;
HandleExpanding();
};
var groupSeparator = new VisualElement {name = "GroupSeparator"};
groupSeparator.AddToClassList("group-separator");
if (GroupName.ToLower() != "draft")
{
_groupLabel.SetEnabled(false);
_groupContent.AddToClassList("unity-disabled");
groupSeparator.style.display = DisplayStyle.Flex;
}
Add(_groupExpanderBox);
Add(_groupContent);
Add(groupSeparator);
}
private void HandleExpanding(bool? overrideExpanding=null)
{
var expanded = _expanded;
if (overrideExpanding != null)
{
expanded = (bool) overrideExpanding;
_expandingOverriden = expanded;
}
else
{
if (_expandingOverriden != null)
expanded = (bool) _expandingOverriden;
}
_expanderLabel.text = !expanded ? "►" : "▼";
var displayStyle = expanded ? DisplayStyle.Flex : DisplayStyle.None;
_groupContent.style.display = displayStyle;
}
private void HandlePackageSelection(PackageView packageView)
{
if (_expandedPackageView == packageView)
{
_expandedPackageView = null;
return;
}
if (_expandedPackageView == null)
{
_expandedPackageView = packageView;
return;
}
// Always where it was
if (packageView.worldBound.y > _expandedPackageView.worldBound.y)
{
var sliderChangeDelta = -(_expandedPackageView.worldBound.height - packageView.worldBound.height);
OnSliderChange?.Invoke(sliderChangeDelta);
}
_expandedPackageView?.ShowFunctions(false);
_expandedPackageView = packageView;
}
private void UpdateGroupLabel()
{
_groupLabel.text = $"{GroupName} ({_packages.Count})";
}
private void OverwriteGroupLabel(string text)
{
_groupLabel.text = text;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dd683831688cd414f8cc9cd352689b4d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,699 @@
using AssetStoreTools.Utility.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.IO;
using System.Text.RegularExpressions;
using AssetStoreTools.Utility;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using AssetStoreTools.Exporter;
using AssetStoreTools.Uploader.Data;
using AssetStoreTools.Uploader.Utility;
namespace AssetStoreTools.Uploader.UIElements
{
internal class PackageView : VisualElement
{
public string PackageId => _packageData.Id;
public string VersionId => _packageData.VersionId;
public string PackageName => _packageData.Name;
public string Status => _packageData.Status;
public string Category => _packageData.Category;
public string LastUpdatedDate => FormatDate(_packageData.LastDate);
public string LastUpdatedSize => FormatSize(_packageData.LastSize);
public bool IsCompleteProject => _packageData.IsCompleteProject;
public string LastUploadedPath => _packageData.LastUploadedPath;
public string LastUploadedGuid => _packageData.LastUploadedGuid;
public string SearchableText { get; private set; }
private PackageData _packageData;
// Unexpanded state dynamic elements
private Button _foldoutBox;
private Label _expanderLabel;
private Label _assetLabel;
private Label _lastDateSizeLabel;
private Button _openInBrowserButton;
// Expanded state dynamic elements
private VisualElement _functionsBox;
private VisualElement _exportAndUploadContainer;
private VisualElement _uploadProgressContainer;
private Button _exportButton;
private Button _uploadButton;
private Button _cancelUploadButton;
private ProgressBar _uploadProgressBarFlow;
private ProgressBar _uploadProgressBarHeader;
private VisualElement _uploadProgressFlowBg;
private VisualElement _uploadProgressHeaderBg;
private bool _expanded;
public Action<PackageView> OnPackageSelection;
private VisualElement _workflowSelectionBox;
private UploadWorkflowView _activeWorkflowElement;
private Dictionary<string, UploadWorkflowView> _uploadWorkflows;
public PackageView(PackageData packageData)
{
UpdateDataValues(packageData);
SetupPackageElement();
}
public void UpdateDataValues(PackageData packageData)
{
_packageData = packageData;
SearchableText = $"{PackageName} {Category}".ToLower();
if (_foldoutBox == null)
return;
_assetLabel.text = PackageName;
_lastDateSizeLabel.text = $"{Category} | {LastUpdatedSize} | {LastUpdatedDate}";
if (_uploadWorkflows != null && _uploadWorkflows.ContainsKey(FolderUploadWorkflowView.WorkflowName))
((FolderUploadWorkflowView) _uploadWorkflows[FolderUploadWorkflowView.WorkflowName]).SetCompleteProject(packageData.IsCompleteProject);
if (Status == "draft")
return;
ResetPostUpload();
SetupExpander();
}
public void ShowFunctions(bool show)
{
if (_functionsBox == null)
{
if (show)
SetupFunctionsElement();
else
return;
}
if (show == _expanded)
return;
_expanded = show;
_expanderLabel.text = !_expanded ? "►" : "▼";
if (_expanded)
_foldoutBox.AddToClassList("foldout-box-expanded");
else
_foldoutBox.RemoveFromClassList("foldout-box-expanded");
if (_functionsBox != null)
_functionsBox.style.display = show ? DisplayStyle.Flex : DisplayStyle.None;
}
private void SetupPackageElement()
{
AddToClassList("full-package-box");
_foldoutBox = new Button {name = "Package"};
_foldoutBox.AddToClassList("foldout-box");
// Expander, Icon and Asset Label
VisualElement foldoutBoxInfo = new VisualElement { name = "foldoutBoxInfo" };
foldoutBoxInfo.AddToClassList("foldout-box-info");
VisualElement labelExpanderRow = new VisualElement { name = "labelExpanderRow" };
labelExpanderRow.AddToClassList("expander-label-row");
_expanderLabel = new Label { name = "ExpanderLabel", text = "►" };
_expanderLabel.AddToClassList("expander");
Image assetImage = new Image { name = "AssetImage" };
assetImage.AddToClassList("package-image");
VisualElement assetLabelInfoBox = new VisualElement { name = "assetLabelInfoBox" };
assetLabelInfoBox.AddToClassList("asset-label-info-box");
_assetLabel = new Label { name = "AssetLabel", text = PackageName };
_assetLabel.AddToClassList("asset-label");
_lastDateSizeLabel = new Label {name = "AssetInfoLabel", text = $"{Category} | {LastUpdatedSize} | {LastUpdatedDate}"};
_lastDateSizeLabel.AddToClassList("asset-info");
assetLabelInfoBox.Add(_assetLabel);
assetLabelInfoBox.Add(_lastDateSizeLabel);
labelExpanderRow.Add(_expanderLabel);
labelExpanderRow.Add(assetImage);
labelExpanderRow.Add(assetLabelInfoBox);
_openInBrowserButton = new Button
{
name = "OpenInBrowserButton",
tooltip = "View your package in the Publishing Portal."
};
_openInBrowserButton.AddToClassList("open-in-browser-button");
// Header Progress bar
_uploadProgressBarHeader = new ProgressBar { name = "HeaderProgressBar" };
_uploadProgressBarHeader.AddToClassList("header-progress-bar");
_uploadProgressBarHeader.style.display = DisplayStyle.None;
_uploadProgressHeaderBg = _uploadProgressBarHeader.Q<VisualElement>(className:"unity-progress-bar__progress");
// Connect it all
foldoutBoxInfo.Add(labelExpanderRow);
foldoutBoxInfo.Add(_openInBrowserButton);
_foldoutBox.Add(foldoutBoxInfo);
_foldoutBox.Add(_uploadProgressBarHeader);
Add(_foldoutBox);
SetupExpander();
}
private void SetupExpander()
{
if (_foldoutBox != null)
{
_foldoutBox.clickable = null;
// If not draft - hide expander, open a listing page on click
if (Status != "draft")
{
_expanderLabel.style.display = DisplayStyle.None;
_foldoutBox.clicked += () =>
{
Application.OpenURL($"https://publisher.unity.com/packages/{VersionId}/edit/upload");
};
}
else
{
// Else open functions box
_foldoutBox.clicked += () =>
{
OnPackageSelection?.Invoke(this);
ShowFunctions(!_expanded);
};
}
}
if (_openInBrowserButton != null)
{
_openInBrowserButton.clickable = null;
_openInBrowserButton.clicked += () =>
{
Application.OpenURL($"https://publisher.unity.com/packages/{VersionId}/edit/upload");
};
}
}
private void SetupFunctionsElement()
{
_functionsBox = new VisualElement { name = "FunctionalityBox" };
_functionsBox.AddToClassList("functionality-box");
_functionsBox.style.display = DisplayStyle.None;
// Validation and uploading boxes
var uploadingWorkflow = ConstructUploadingWorkflow();
_functionsBox.Add(uploadingWorkflow);
Add(_functionsBox);
}
private VisualElement ConstructUploadingWorkflow()
{
// Upload Box
VisualElement uploadBox = new VisualElement { name = "UploadBox" };
uploadBox.AddToClassList("upload-box");
var folderUploadWorkflow = FolderUploadWorkflowView.Create(Category, IsCompleteProject, SerializeWorkflowSelections);
var unitypackageUploadWorkflow = UnityPackageUploadWorkflowView.Create(Category, SerializeWorkflowSelections);
var hybridPackageUploadWorkflow = HybridPackageUploadWorkflowView.Create(Category, SerializeWorkflowSelections);
// Workflow selection
_workflowSelectionBox = new VisualElement();
_workflowSelectionBox.AddToClassList("selection-box-row");
VisualElement labelHelpRow = new VisualElement();
labelHelpRow.AddToClassList("label-help-row");
Label workflowLabel = new Label { text = "Upload type" };
Image workflowLabelTooltip = new Image
{
tooltip = "Select what content you are uploading to the Asset Store"
+ "\n\n• From Assets Folder - content located within the project's 'Assets' folder or one of its subfolders"
+ "\n\n• Pre-exported .unitypackage - content that has already been compressed into a .unitypackage file"
#if UNITY_ASTOOLS_EXPERIMENTAL
+ "\n\n• Local UPM Package - content that is located within the project's 'Packages' folder. Only embedded and local packages are supported"
#endif
};
labelHelpRow.Add(workflowLabel);
labelHelpRow.Add(workflowLabelTooltip);
var flowDrop = new ToolbarMenu();
flowDrop.menu.AppendAction(FolderUploadWorkflowView.WorkflowDisplayName, _ => { SetActiveWorkflowElement(folderUploadWorkflow, flowDrop); });
flowDrop.menu.AppendAction(UnityPackageUploadWorkflowView.WorkflowDisplayName, _ => { SetActiveWorkflowElement(unitypackageUploadWorkflow, flowDrop); });
#if UNITY_ASTOOLS_EXPERIMENTAL
flowDrop.menu.AppendAction(HybridPackageUploadWorkflowView.WorkflowDisplayName, _ => { SetActiveWorkflowElement(hybridPackageUploadWorkflow, flowDrop); });
#endif // UNITY_ASTOOLS_EXPERIMENTAL
flowDrop.AddToClassList("workflow-dropdown");
_workflowSelectionBox.Add(labelHelpRow);
_workflowSelectionBox.Add(flowDrop);
uploadBox.Add(_workflowSelectionBox);
_uploadWorkflows = new Dictionary<string, UploadWorkflowView>
{
{FolderUploadWorkflowView.WorkflowName, folderUploadWorkflow},
{UnityPackageUploadWorkflowView.WorkflowName, unitypackageUploadWorkflow},
{HybridPackageUploadWorkflowView.WorkflowName, hybridPackageUploadWorkflow}
};
foreach (var kvp in _uploadWorkflows)
uploadBox.Add(kvp.Value);
var progressUploadBox = SetupProgressUploadBox();
uploadBox.Add(progressUploadBox);
DeserializeWorkflowSelections(flowDrop);
return uploadBox;
}
private void SerializeWorkflowSelections()
{
ASDebug.Log("Serializing workflow selections");
var json = JsonValue.NewDict();
// Active workflow
var activeWorkflow = JsonValue.NewString(_activeWorkflowElement.Name);
json["ActiveWorkflow"] = activeWorkflow;
// Workflow Selections
foreach(var kvp in _uploadWorkflows)
json[kvp.Key] = kvp.Value.SerializeWorkflow();
AssetStoreCache.CacheUploadSelections(PackageId, json);
}
private void DeserializeWorkflowSelections(ToolbarMenu activeFlowMenu)
{
AssetStoreCache.GetCachedUploadSelections(PackageId, out JsonValue cachedSelections);
// Individual workflow selections
foreach (var kvp in _uploadWorkflows)
{
if (cachedSelections.ContainsKey(kvp.Key))
kvp.Value.LoadSerializedWorkflow(cachedSelections[kvp.Key], LastUploadedPath, LastUploadedGuid);
else
kvp.Value.LoadSerializedWorkflowFallback(LastUploadedPath, LastUploadedGuid);
}
// Active workflow selection
if (!cachedSelections.ContainsKey("ActiveWorkflow"))
{
// Set default to folder workflow
SetActiveWorkflowElement(_uploadWorkflows[FolderUploadWorkflowView.WorkflowName], activeFlowMenu);
return;
}
var serializedWorkflow = cachedSelections["ActiveWorkflow"].AsString();
SetActiveWorkflowElement(_uploadWorkflows[serializedWorkflow], activeFlowMenu);
}
private void SetActiveWorkflowElement(UploadWorkflowView newActiveWorkflowElement, ToolbarMenu activeFlowMenu)
{
if (_activeWorkflowElement != null)
_activeWorkflowElement.style.display = DisplayStyle.None;
_activeWorkflowElement = newActiveWorkflowElement;
_activeWorkflowElement.style.display = DisplayStyle.Flex;
activeFlowMenu.text = newActiveWorkflowElement.DisplayName;
UpdateActionButtons();
SerializeWorkflowSelections();
}
private void UpdateActionButtons()
{
switch (_activeWorkflowElement)
{
case UnityPackageUploadWorkflowView _:
_exportButton.style.display = DisplayStyle.None;
_uploadButton.style.marginLeft = 0f;
_uploadButton.text = "Upload";
break;
default:
_exportButton.style.display = DisplayStyle.Flex;
_uploadButton.style.marginLeft = 5f;
_uploadButton.text = "Export and Upload";
break;
}
}
private VisualElement SetupProgressUploadBox()
{
var progressUploadBox = new VisualElement();
progressUploadBox.AddToClassList("progress-upload-box");
_exportAndUploadContainer = new VisualElement();
_exportAndUploadContainer.AddToClassList("export-and-upload-container");
_exportButton = new Button(ExportWithoutUploading) { name = "ExportButton", text = "Export" };
_exportButton.AddToClassList("export-button");
_uploadButton = new Button(PreparePackageUpload) { name = "UploadButton", text = "Export and Upload" };
_uploadButton.AddToClassList("upload-button");
_exportAndUploadContainer.Add(_exportButton);
_exportAndUploadContainer.Add(_uploadButton);
_uploadProgressContainer = new VisualElement();
_uploadProgressContainer.AddToClassList("upload-progress-container");
_uploadProgressContainer.style.display = DisplayStyle.None;
_uploadProgressBarFlow = new ProgressBar { name = "UploadProgressBar" };
_uploadProgressBarFlow.AddToClassList("upload-progress-bar");
_uploadProgressFlowBg = _uploadProgressBarFlow.Q<VisualElement>(className: "unity-progress-bar__progress");
_cancelUploadButton = new Button() { name = "CancelButton", text = "Cancel" };
_cancelUploadButton.AddToClassList("cancel-button");
_uploadProgressContainer.Add(_uploadProgressBarFlow);
_uploadProgressContainer.Add(_cancelUploadButton);
progressUploadBox.Add(_exportAndUploadContainer);
progressUploadBox.Add(_uploadProgressContainer);
return progressUploadBox;
}
private string FormatSize(string size)
{
if (string.IsNullOrEmpty(size))
return "0.00 MB";
float.TryParse(size, out var sizeBytes);
return $"{sizeBytes / (1024f * 1024f):0.00} MB";
}
private string FormatDate(string date)
{
DateTime dt = DateTime.Parse(date);
return dt.Date.ToString("yyyy-MM-dd");
}
#region Package Uploading
private async void ExportWithoutUploading()
{
var paths = _activeWorkflowElement.GetAllExportPaths();
if(paths.Length == 0)
{
EditorUtility.DisplayDialog("Exporting failed", "No path was selected. Please " +
"select a path and try again.", "OK");
return;
}
var rootProjectPath = Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length);
var packageNameStripped = Regex.Replace(PackageName, "[^a-zA-Z0-9]", "");
var outputPath = EditorUtility.SaveFilePanel("Export Package", rootProjectPath,
$"{packageNameStripped}-{DateTime.Now:yyyy-dd-M--HH-mm-ss}", "unitypackage");
if (string.IsNullOrEmpty(outputPath))
return;
var exportResult = await ExportPackage(outputPath);
if (!exportResult.Success)
Debug.LogError($"Package exporting failed: {exportResult.Error}");
else
Debug.Log($"Package exported to '{Path.GetFullPath(exportResult.ExportedPath).Replace("\\", "/")}'");
}
private async Task<ExportResult> ExportPackage(string outputPath)
{
var exportResult = await _activeWorkflowElement.ExportPackage(outputPath, IsCompleteProject);
return exportResult;
}
private bool ValidateUnityVersionsForUpload()
{
if (!AssetStoreUploader.ShowPackageVersionDialog)
return true;
EditorUtility.DisplayProgressBar("Preparing...", "Checking version compatibility", 0.4f);
var versions = AssetStoreAPI.GetPackageUploadedVersions(PackageId, VersionId);
EditorUtility.ClearProgressBar();
if (versions.Any(x => string.Compare(x, AssetStoreUploader.MinRequiredPackageVersion, StringComparison.Ordinal) >= 0))
return true;
var result = EditorUtility.DisplayDialogComplex("Asset Store Tools", $"You may upload this package, but you will need to add a package using Unity version {AssetStoreUploader.MinRequiredPackageVersion} " +
"or higher to be able to submit a new asset", "Upload", "Cancel", "Upload and do not display this again");
switch (result)
{
case 1:
return false;
case 2:
AssetStoreUploader.ShowPackageVersionDialog = false;
break;
}
return true;
}
private async void PreparePackageUpload()
{
var paths = _activeWorkflowElement.GetAllExportPaths();
if (paths.Length == 0)
{
EditorUtility.DisplayDialog("Uploading failed", "No path was selected. Please " +
"select a path and try again.", "OK");
return;
}
var packageNameStripped = Regex.Replace(PackageName, "[^a-zA-Z0-9]", "");
var outputPath = $"Temp/{packageNameStripped}-{DateTime.Now:yyyy-dd-M--HH-mm-ss}.unitypackage";
var exportResult = await ExportPackage(outputPath);
if (!exportResult.Success)
{
Debug.LogError($"Package exporting failed: {exportResult.Error}");
return;
}
if (!ValidateUnityVersionsForUpload())
return;
var localPackageGuid = _activeWorkflowElement.GetLocalPackageGuid();
var localPackagePath = _activeWorkflowElement.GetLocalPackagePath();
var localProjectPath = _activeWorkflowElement.GetLocalProjectPath();
BeginPackageUpload(exportResult.ExportedPath, localPackageGuid, localPackagePath, localProjectPath);
}
private async void BeginPackageUpload(string exportedPackagePath, string packageGuid, string packagePath, string projectPath)
{
// Configure the UI
// Disable Active Workflow
EnableWorkflowElements(false);
// Progress bar
_exportAndUploadContainer.style.display = DisplayStyle.None;
_uploadProgressContainer.style.display = DisplayStyle.Flex;
// Configure the upload cancel button
_cancelUploadButton.clickable = null;
_cancelUploadButton.clicked += () => AssetStoreAPI.AbortPackageUpload(VersionId);
_cancelUploadButton.text = "Cancel";
// Set up upload progress tracking for the unexpanded package progress bar
EditorApplication.update += OnPackageUploadProgressHeader;
// Set up upload progress tracking for the expanded package progress bar
EditorApplication.update += OnPackageUploadProgressContent;
// Set up base analytics data
var analyticsData = ConstructAnalyticsData(exportedPackagePath);
// Start tracking uploading time
var watch = System.Diagnostics.Stopwatch.StartNew(); // Debugging
// Start uploading the package
var result = await AssetStoreAPI.UploadPackageAsync(VersionId, PackageName, exportedPackagePath, packageGuid, packagePath, projectPath);
watch.Stop();
analyticsData.TimeTaken = watch.Elapsed.TotalSeconds;
switch (result.Status)
{
case PackageUploadResult.UploadStatus.Success:
analyticsData.UploadFinishedReason = "Success";
ASDebug.Log($"Finished uploading, time taken: {watch.Elapsed.TotalSeconds} seconds");
await OnPackageUploadSuccess();
break;
case PackageUploadResult.UploadStatus.Cancelled:
analyticsData.UploadFinishedReason = "Cancelled";
ASDebug.Log($"Uploading cancelled, time taken: {watch.Elapsed.TotalSeconds} seconds");
break;
case PackageUploadResult.UploadStatus.Fail:
analyticsData.UploadFinishedReason = result.Error.Exception.ToString();
OnPackageUploadFail(result.Error);
break;
case PackageUploadResult.UploadStatus.ResponseTimeout:
analyticsData.UploadFinishedReason = "ResponseTimeout";
Debug.LogWarning($"All bytes for the package '{PackageName}' have been uploaded, but a response " +
$"from the server was not received. This can happen because of Firewall restrictions. " +
$"Please make sure that a new version of your package has reached the Publishing Portal.");
await OnPackageUploadSuccess();
break;
}
ASAnalytics.SendUploadingEvent(analyticsData);
PostUploadCleanup(result.Status);
}
private ASAnalytics.AnalyticsData ConstructAnalyticsData(string exportedPackagePath)
{
bool validated;
string validationResults;
validated = _activeWorkflowElement.GetValidationSummary(out validationResults);
FileInfo packageFileInfo = new FileInfo(exportedPackagePath);
string workflow = _activeWorkflowElement.Name;
ASAnalytics.AnalyticsData data = new ASAnalytics.AnalyticsData
{
ToolVersion = AssetStoreAPI.ToolVersion,
EndpointUrl = AssetStoreAPI.AssetStoreProdUrl,
PackageId = PackageId,
Category = Category,
UsedValidator = validated,
ValidatorResults = validationResults,
PackageSize = packageFileInfo.Length,
Workflow = workflow
};
return data;
}
private void OnPackageUploadProgressHeader()
{
// Header progress bar is only shown when the package is not expanded and has progress
if (_uploadProgressBarHeader.value > 0.0f)
_uploadProgressBarHeader.style.display = !_expanded ? DisplayStyle.Flex : DisplayStyle.None;
if (!AssetStoreAPI.ActiveUploads.ContainsKey(VersionId))
return;
_uploadProgressBarHeader.value = AssetStoreAPI.ActiveUploads[VersionId].Progress;
}
private void OnPackageUploadProgressContent()
{
if (!AssetStoreAPI.ActiveUploads.ContainsKey(VersionId))
return;
var progressValue = AssetStoreAPI.ActiveUploads[VersionId].Progress;
_uploadProgressBarFlow.value = progressValue;
_uploadProgressBarFlow.title = $"{progressValue:0.#}%";
if(progressValue == 100f && _cancelUploadButton.enabledInHierarchy)
_cancelUploadButton.SetEnabled(false);
}
private async Task OnPackageUploadSuccess()
{
if (ASToolsPreferences.Instance.DisplayUploadDialog)
EditorUtility.DisplayDialog("Success!", $"Package for '{PackageName}' has been uploaded successfully!", "OK");
SetEnabled(false);
PackageFetcher fetcher = new PackageFetcher();
var result = await fetcher.FetchRefreshedPackage(PackageId);
if(!result.Success)
{
ASDebug.LogError(result.Error);
SetEnabled(true);
return;
}
UpdateDataValues(result.Package);
ASDebug.Log($"Updated name, status, date and size values for package version id {VersionId}");
SetEnabled(true);
}
private void OnPackageUploadFail(ASError error)
{
if (ASToolsPreferences.Instance.DisplayUploadDialog)
EditorUtility.DisplayDialog("Upload failed", "Package uploading failed. See Console for details", "OK");
Debug.LogError(error);
}
private void PostUploadCleanup(PackageUploadResult.UploadStatus uploadStatus)
{
if (_activeWorkflowElement == null)
return;
SetProgressBarColorByStatus(uploadStatus);
_uploadProgressBarFlow.title = $"Upload: {uploadStatus.ToString()}";
_cancelUploadButton.clickable = null;
_cancelUploadButton.clicked += ResetPostUpload;
_cancelUploadButton.text = "Done";
// Re-enable the Cancel/Done button since it gets disabled at 100% progress
_cancelUploadButton.SetEnabled(true);
}
private void ResetPostUpload()
{
if (_activeWorkflowElement == null)
return;
// Cleanup the progress bars
EditorApplication.update -= OnPackageUploadProgressContent;
EditorApplication.update -= OnPackageUploadProgressHeader;
EnableWorkflowElements(true);
ResetProgressBar();
_exportAndUploadContainer.style.display = DisplayStyle.Flex;
_uploadProgressContainer.style.display = DisplayStyle.None;
}
private void ResetProgressBar()
{
SetProgressBarColorByStatus(PackageUploadResult.UploadStatus.Default);
_uploadProgressBarHeader.style.display = DisplayStyle.None;
_uploadProgressBarHeader.value = 0f;
_uploadProgressBarFlow.value = 0f;
_uploadProgressBarFlow.title = string.Empty;
}
private void EnableWorkflowElements(bool enable)
{
_workflowSelectionBox?.SetEnabled(enable);
_activeWorkflowElement?.SetEnabled(enable);
}
private void SetProgressBarColorByStatus(PackageUploadResult.UploadStatus status)
{
var color = PackageUploadResult.GetColorByStatus(status);
_uploadProgressFlowBg.style.backgroundColor = color;
_uploadProgressHeaderBg.style.backgroundColor = color;
}
#endregion
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7c2205d924ccc4a458abc3d370143740
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,105 @@
using System;
using AssetStoreTools.Uploader.Utility;
using AssetStoreTools.Utility;
using UnityEditor;
using UnityEngine.UIElements;
namespace AssetStoreTools.Uploader.UIElements
{
internal class UploadWindow : VisualElement
{
public new class UxmlFactory : UxmlFactory<UploadWindow> { }
// Views
private AllPackageView _allPackageView;
// Toolbar elements
private Label _accountEmailLabel;
private Button _refreshButton;
private Button _logoutButton;
public UploadWindow()
{
styleSheets.Add(StyleSelector.UploaderWindow.UploadWindowStyle);
styleSheets.Add(StyleSelector.UploaderWindow.UploadWindowTheme);
}
public void SetupWindows(Action onLogout, Action<ASError> onPackageDownloadFail)
{
_allPackageView = this.Q<AllPackageView>("AllPackageView");
SetupBottomToolbar(onLogout, onPackageDownloadFail);
}
private void SetupBottomToolbar(Action onLogout, Action<ASError> onPackageDownloadFail)
{
// Bottom Tools Row
VisualElement bottomToolsRow = new VisualElement { name = "BottomToolsRow" };
bottomToolsRow.AddToClassList("bottom-tools-row");
// Left side of the toolbar
VisualElement leftSideContainer = new VisualElement { name = "LeftSideContainer" };
leftSideContainer.AddToClassList("toolbar-left-side-container");
_accountEmailLabel = new Label { name = "AccountEmail" };
_accountEmailLabel.AddToClassList("account-name");
leftSideContainer.Add(_accountEmailLabel);
// Right side of the toolbar
VisualElement rightSideContainer = new VisualElement { name = "RightSideContainer" };
rightSideContainer.AddToClassList("toolbar-right-side-container");
// Refresh button
_refreshButton = new Button { name = "RefreshButton", text = "Refresh" };
_refreshButton.AddToClassList("refresh-button");
_refreshButton.clicked += () => _allPackageView.ShowPackagesList(false, onPackageDownloadFail);
_allPackageView.RefreshingPackages += (isRefreshing) => _refreshButton.SetEnabled(!isRefreshing);
// Logout button
_logoutButton = new Button { name = "LogoutButton", text = "Logout" };
_logoutButton.AddToClassList("logout-button");
_logoutButton.clicked += () => Logout(onLogout);
rightSideContainer.Add(_refreshButton);
rightSideContainer.Add(_logoutButton);
// Constructing the final toolbar
bottomToolsRow.Add(leftSideContainer);
bottomToolsRow.Add(rightSideContainer);
Add(bottomToolsRow);
}
public void LoadPackages(bool useCached, Action<ASError> onPackageDownloadFail)
{
_allPackageView.ShowPackagesList(useCached, onPackageDownloadFail);
}
public void ShowAllPackagesView()
{
_logoutButton.style.display = DisplayStyle.Flex;
_allPackageView.style.display = DisplayStyle.Flex;
}
public void ShowPublisherEmail(string publisherEmail)
{
_accountEmailLabel.text = publisherEmail;
}
private void Logout(Action onLogout)
{
if (AssetStoreAPI.IsUploading && !EditorUtility.DisplayDialog("Notice",
"Assets are still being uploaded to the Asset Store. Logging out will cancel all uploads in progress.\n\n" +
"Would you still like to log out?", "Yes", "No"))
return;
AssetStoreAPI.AbortDownloadTasks();
AssetStoreAPI.AbortUploadTasks();
PackageViewStorer.Reset();
_allPackageView.ClearPackages();
onLogout?.Invoke();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 52c6e85acbb00794686387cd876ffc81
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7562bae25c6218744a023670e3a2f06f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,116 @@
using AssetStoreTools.Validator;
using AssetStoreTools.Validator.Data;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine.UIElements;
namespace AssetStoreTools.Uploader.UIElements
{
internal class AssetValidationElement : ValidationElement
{
private Button _viewReportButton;
private string[] _validationPaths;
protected override void SetupInfoBox(string infoText)
{
InfoBox = new Box { name = "InfoBox" };
InfoBox.style.display = DisplayStyle.None;
InfoBox.AddToClassList("info-box");
InfoBoxImage = new Image();
InfoBoxLabel = new Label { name = "ValidationLabel", text = infoText };
_viewReportButton = new Button(ViewReport) { text = "View report" };
_viewReportButton.AddToClassList("hyperlink-button");
InfoBox.Add(InfoBoxImage);
InfoBox.Add(InfoBoxLabel);
InfoBox.Add(_viewReportButton);
Add(InfoBox);
}
public override void SetValidationPaths(params string[] paths)
{
_validationPaths = paths;
EnableValidation(true);
}
protected override async void RunValidation()
{
ValidateButton.SetEnabled(false);
// Make sure everything is collected and validation button is disabled
await Task.Delay(100);
var outcomeList = new List<TestResult>();
var validationSettings = new ValidationSettings()
{
ValidationPaths = _validationPaths.ToList(),
Category = Category
};
var validationResult = PackageValidator.ValidatePackage(validationSettings);
if(validationResult.Status != ValidationStatus.RanToCompletion)
{
EditorUtility.DisplayDialog("Validation failed", $"Package validation failed: {validationResult.Error}", "OK");
return;
}
foreach (var test in validationResult.AutomatedTests)
outcomeList.Add(test.Result);
EnableInfoBox(true, validationResult.HadCompilationErrors, outcomeList);
ValidateButton.SetEnabled(true);
}
private void ViewReport()
{
var validationStateData = ValidationState.Instance.ValidationStateData;
// Re-run validation if paths are out of sync
if (validationStateData.SerializedValidationPaths.Count != _validationPaths.Length ||
!validationStateData.SerializedValidationPaths.All(_validationPaths.Contains))
RunValidation();
// Re-run validation if category is out of sync
if (validationStateData.SerializedCategory != Category)
RunValidation();
// Show the Validator
AssetStoreTools.ShowAssetStoreToolsValidator();
}
private void EnableInfoBox(bool enable, bool hasCompilationErrors, List<TestResult> outcomeList)
{
if (!enable)
{
InfoBox.style.display = DisplayStyle.None;
return;
}
var errorCount = outcomeList.Count(x => x.Result == TestResult.ResultStatus.Fail);
var warningCount = outcomeList.Count(x => x.Result == TestResult.ResultStatus.Warning);
PopulateInfoBox(hasCompilationErrors, errorCount, warningCount);
ValidateButton.text = "Re-validate";
InfoBox.style.display = DisplayStyle.Flex;
}
public override bool GetValidationSummary(out string validationSummary)
{
validationSummary = string.Empty;
if (string.IsNullOrEmpty(InfoBoxLabel.text))
return false;
var data = ValidationState.Instance.ValidationStateData;
return ValidationState.GetValidationSummaryJson(data, out validationSummary);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 118376dcb318c4341b1df6773e3d8d4c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,190 @@
using AssetStoreTools.Utility;
using AssetStoreTools.Validator;
using AssetStoreTools.Validator.Data;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace AssetStoreTools.Uploader.UIElements
{
internal class PackageValidationElement : ValidationElement
{
private string _packagePath;
private ValidationStateData _packageValidationStateData;
private VisualElement _projectButtonContainer;
protected override void SetupInfoBox(string infoText)
{
InfoBox = new Box { name = "InfoBox" };
InfoBox.style.display = DisplayStyle.None;
InfoBox.AddToClassList("info-box");
InfoBoxImage = new Image();
InfoBoxLabel = new Label { name = "ValidationLabel", text = infoText };
_projectButtonContainer = new VisualElement() { name = "Button Container" };
_projectButtonContainer.AddToClassList("hyperlink-button-container");
InfoBox.Add(InfoBoxImage);
InfoBox.Add(InfoBoxLabel);
InfoBox.Add(_projectButtonContainer);
Add(InfoBox);
}
public override void SetValidationPaths(params string[] paths)
{
if (paths == null || paths.Length != 1)
throw new ArgumentException("Package Validation only accepts a single path argument");
_packagePath = paths[0];
EnableValidation(true);
}
protected override void RunValidation()
{
if (!EditorUtility.DisplayDialog("Notice", "Pre-exported package validation is performed in a separate temporary project. " +
"It may take some time for the temporary project to be created, which will halt any actions in the current project. " +
"The current project will resume work after the temporary project is exited.\n\nDo you wish to proceed?", "Yes", "No"))
return;
var validationSettings = new ValidationSettings()
{
ValidationPaths = new List<string> { _packagePath },
Category = Category
};
var validationResult = PackageValidator.ValidatePreExportedPackage(validationSettings, false);
switch (validationResult.Status)
{
case ValidationStatus.RanToCompletion:
DisplayValidationResult(validationResult.ProjectPath);
break;
case ValidationStatus.Failed:
EditorUtility.DisplayDialog("Error", validationResult.Error, "OK");
break;
case ValidationStatus.Cancelled:
ASDebug.Log("Validation cancelled");
break;
case ValidationStatus.NotRun:
throw new InvalidOperationException("Validation was not run. Please report this issue");
default:
throw new ArgumentException("Received an invalid validation status");
}
}
public override bool GetValidationSummary(out string validationSummary)
{
validationSummary = string.Empty;
if (_packageValidationStateData == null)
return false;
return ValidationState.GetValidationSummaryJson(_packageValidationStateData, out validationSummary);
}
private void DisplayValidationResult(string projectPath)
{
// Retrieve the validation state data from the validation project
var validationStateDataPath = $"{projectPath}/{ValidationState.PersistentDataLocation}/{ValidationState.ValidationDataFilename}";
if (File.Exists(validationStateDataPath))
_packageValidationStateData = JsonUtility.FromJson<ValidationStateData>(File.ReadAllText(validationStateDataPath));
var validationComplete = _packageValidationStateData != null;
EnableInfoBox(true, validationComplete, projectPath);
}
private void EnableInfoBox(bool enable, bool validationComplete, string projectPath)
{
if (!enable)
{
InfoBox.style.display = DisplayStyle.None;
return;
}
if (!validationComplete)
{
InfoBoxImage.image = EditorGUIUtility.IconContent("console.erroricon@2x").image;
InfoBoxLabel.text = "Validation status unknown. Please report this issue";
return;
}
InfoBox.style.display = DisplayStyle.Flex;
var compilationFailed = _packageValidationStateData.HasCompilationErrors;
var failCount = _packageValidationStateData.SerializedValues.Count(x => x.Result.Result == TestResult.ResultStatus.Fail);
var warningCount = _packageValidationStateData.SerializedValues.Count(x => x.Result.Result == TestResult.ResultStatus.Warning);
PopulateInfoBox(compilationFailed, failCount, warningCount);
_projectButtonContainer.Clear();
var openButton = new Button(() => OpenTemporaryProject(projectPath)) { text = "Open Project" };
openButton.AddToClassList("hyperlink-button");
var saveButton = new Button(() => SaveTemporaryProject(projectPath)) { text = "Save Project" };
saveButton.AddToClassList("hyperlink-button");
_projectButtonContainer.Add(openButton);
_projectButtonContainer.Add(saveButton);
}
private void OpenTemporaryProject(string projectPath)
{
try
{
EditorUtility.DisplayProgressBar("Waiting...", "Validation project is open. Waiting for it to exit...", 0.4f);
#if UNITY_EDITOR_OSX
var unityPath = Path.Combine(EditorApplication.applicationPath, "Contents", "MacOS", "Unity");
#else
var unityPath = EditorApplication.applicationPath;
#endif
var logFilePath = $"{projectPath}/editor.log";
var processInfo = new System.Diagnostics.ProcessStartInfo()
{
FileName = unityPath,
Arguments = $"-projectPath \"{projectPath}\" -logFile \"{logFilePath}\" -executeMethod AssetStoreTools.AssetStoreTools.ShowAssetStoreToolsValidator"
};
using (var process = System.Diagnostics.Process.Start(processInfo))
{
process.WaitForExit();
}
}
finally
{
EditorUtility.ClearProgressBar();
}
}
private void SaveTemporaryProject(string projectPath)
{
try
{
var savePath = EditorUtility.SaveFolderPanel("Select a folder", Environment.GetFolderPath(Environment.SpecialFolder.Desktop), string.Empty);
if (string.IsNullOrEmpty(savePath))
return;
var saveDir = new DirectoryInfo(savePath);
if(!saveDir.Exists || saveDir.GetFileSystemInfos().Length != 0)
{
EditorUtility.DisplayDialog("Saving project failed", "Selected directory must be an empty folder", "OK");
return;
}
EditorUtility.DisplayProgressBar("Saving...", "Saving project...", 0.4f);
FileUtility.CopyDirectory(projectPath, savePath, true);
}
finally
{
EditorUtility.ClearProgressBar();
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 33a72e7596565c749a495b4213579a67
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,99 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace AssetStoreTools.Uploader.UIElements
{
internal abstract class ValidationElement : VisualElement
{
protected Button ValidateButton;
protected VisualElement InfoBox;
protected Label InfoBoxLabel;
protected Image InfoBoxImage;
protected string Category;
public ValidationElement()
{
ConstructValidationElement();
SetupInfoBox(string.Empty);
EnableValidation(false);
}
public void SetCategory(string category)
{
Category = category;
}
private void ConstructValidationElement()
{
VisualElement validatorButtonRow = new VisualElement();
validatorButtonRow.AddToClassList("selection-box-row");
VisualElement validatorLabelHelpRow = new VisualElement();
validatorLabelHelpRow.AddToClassList("label-help-row");
Label validatorLabel = new Label { text = "Validation" };
Image validatorLabelTooltip = new Image
{
tooltip = "You can use the Asset Store Validator to check your package for common publishing issues"
};
ValidateButton = new Button(RunValidation) { name = "ValidateButton", text = "Validate" };
ValidateButton.AddToClassList("validation-button");
validatorLabelHelpRow.Add(validatorLabel);
validatorLabelHelpRow.Add(validatorLabelTooltip);
validatorButtonRow.Add(validatorLabelHelpRow);
validatorButtonRow.Add(ValidateButton);
Add(validatorButtonRow);
}
protected void EnableValidation(bool enable)
{
style.display = enable ? DisplayStyle.Flex : DisplayStyle.None;
}
protected void PopulateInfoBox(bool hasCompilationErrors, int errorCount, int warningCount)
{
Texture infoImage = null;
var infoText = string.Empty;
if (hasCompilationErrors)
{
infoImage = EditorGUIUtility.IconContent("console.erroricon@2x").image;
infoText += "• Package caused compilation errors\n";
}
if (errorCount > 0)
{
infoImage = EditorGUIUtility.IconContent("console.erroricon@2x").image;
infoText += $"• Validation reported {errorCount} error(s)\n";
}
if (warningCount > 0)
{
if (infoImage == null)
infoImage = EditorGUIUtility.IconContent("console.warnicon@2x").image;
infoText += $"• Validation reported {warningCount} warning(s)\n";
}
if (string.IsNullOrEmpty(infoText))
{
infoText = "No issues were found!";
infoImage = InfoBoxImage.image = EditorGUIUtility.IconContent("console.infoicon@2x").image;
}
else
infoText = infoText.Substring(0, infoText.Length - "\n".Length);
InfoBoxImage.image = infoImage;
InfoBoxLabel.text = infoText;
}
protected abstract void SetupInfoBox(string infoText);
public abstract void SetValidationPaths(params string[] paths);
protected abstract void RunValidation();
public abstract bool GetValidationSummary(out string validationSummary);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cee024b6c5a4407780a9f8677f7a6e97
timeCreated: 1654674025

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1c85e58f7d4786a40a140c67b0d124a0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,627 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AssetStoreTools.Exporter;
using AssetStoreTools.Utility;
using AssetStoreTools.Utility.Json;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace AssetStoreTools.Uploader.UIElements
{
internal class FolderUploadWorkflowView : UploadWorkflowView
{
public const string WorkflowName = "FolderWorkflow";
public const string WorkflowDisplayName = "From Assets Folder";
public override string Name => WorkflowName;
public override string DisplayName => WorkflowDisplayName;
private Toggle _dependenciesToggle;
private List<string> _includedDependencies = new List<string>();
private bool _isCompleteProject;
private VisualElement _specialFolderTogglesBox;
private VisualElement _specialFoldersElement;
private VisualElement _packageDependencyBox;
private ScrollView _packagesTogglesBox;
private ToolbarMenu _filteringDropdown;
private Label _noPackagesLabel;
private string _packagesFilter;
// Special folders that would not work if not placed directly in the 'Assets' folder
private readonly string[] _extraAssetFolderNames =
{
"Editor Default Resources", "Gizmos", "Plugins",
"StreamingAssets", "Standard Assets", "WebGLTemplates",
"ExternalDependencyManager", "XR"
};
private readonly List<string> _packageSelectionFilters = new List<string> { "All", "Selected", "Not Selected" };
private FolderUploadWorkflowView(string category, bool isCompleteProject, Action serializeSelection) : base(serializeSelection)
{
_isCompleteProject = isCompleteProject;
Category = category;
SetupWorkflow();
}
public static FolderUploadWorkflowView Create(string category, bool isCompleteProject, Action serializeAction)
{
return new FolderUploadWorkflowView(category, isCompleteProject, serializeAction);
}
public void SetCompleteProject(bool isCompleteProject)
{
_isCompleteProject = isCompleteProject;
}
private bool GetIncludeDependenciesToggle()
{
return _dependenciesToggle.value;
}
private List<string> GetIncludedDependencies()
{
return _includedDependencies;
}
protected sealed override void SetupWorkflow()
{
// Path selection
VisualElement folderPathSelectionRow = new VisualElement();
folderPathSelectionRow.AddToClassList("selection-box-row");
VisualElement labelHelpRow = new VisualElement();
labelHelpRow.AddToClassList("label-help-row");
Label folderPathLabel = new Label { text = "Folder path" };
Image folderPathLabelTooltip = new Image
{
tooltip = "Select the main folder of your package" +
"\n\nAll files and folders of your package should preferably be contained within a single root folder that is named after your package" +
"\n\nExample: 'Assets/[MyPackageName]'" +
"\n\nNote: If your content makes use of special folders that are required to be placed in the root Assets folder (e.g. 'StreamingAssets')," +
" you will be able to include them after selecting the main folder"
};
labelHelpRow.Add(folderPathLabel);
labelHelpRow.Add(folderPathLabelTooltip);
PathSelectionField = new TextField();
PathSelectionField.AddToClassList("path-selection-field");
PathSelectionField.isReadOnly = true;
Button browsePathButton = new Button(BrowsePath) { name = "BrowsePathButton", text = "Browse" };
browsePathButton.AddToClassList("browse-button");
folderPathSelectionRow.Add(labelHelpRow);
folderPathSelectionRow.Add(PathSelectionField);
folderPathSelectionRow.Add(browsePathButton);
Add(folderPathSelectionRow);
// Dependencies selection
VisualElement dependenciesSelectionRow = new VisualElement();
dependenciesSelectionRow.AddToClassList("selection-box-row");
VisualElement dependenciesLabelHelpRow = new VisualElement();
dependenciesLabelHelpRow.AddToClassList("label-help-row");
Label dependenciesLabel = new Label { text = "Dependencies" };
Image dependenciesLabelTooltip = new Image
{
tooltip = "Tick this checkbox if your package content has dependencies on Unity packages from the Package Manager"
};
_dependenciesToggle = new Toggle { name = "DependenciesToggle", text = "Include Package Manifest" };
_dependenciesToggle.AddToClassList("dependencies-toggle");
_dependenciesToggle.RegisterValueChangedCallback((_) => SerializeSelection?.Invoke());
_dependenciesToggle.RegisterValueChangedCallback(OnDependencyToggleValueChange);
RegisterCallback<AttachToPanelEvent>((_) => {ASToolsPreferences.OnSettingsChange += OnASTSettingsChange;});
RegisterCallback<DetachFromPanelEvent>((_) => {ASToolsPreferences.OnSettingsChange -= OnASTSettingsChange;});
// Dependencies selection
_packageDependencyBox = new VisualElement();
_packageDependencyBox.AddToClassList("selection-box-row");
_packageDependencyBox.style.display = DisplayStyle.None;
dependenciesLabelHelpRow.Add(dependenciesLabel);
dependenciesLabelHelpRow.Add(dependenciesLabelTooltip);
dependenciesSelectionRow.Add(dependenciesLabelHelpRow);
dependenciesSelectionRow.Add(_dependenciesToggle);
Add(dependenciesSelectionRow);
Add(_packageDependencyBox);
ValidationElement = new AssetValidationElement();
Add(ValidationElement);
ValidationElement.SetCategory(Category);
}
public override JsonValue SerializeWorkflow()
{
var workflowDict = base.SerializeWorkflow();
workflowDict["dependencies"] = GetIncludeDependenciesToggle();
workflowDict["dependenciesNames"] = GetIncludedDependencies().Select(JsonValue.NewString).ToList();
return workflowDict;
}
public override void LoadSerializedWorkflow(JsonValue json, string lastUploadedPath, string lastUploadedGuid)
{
if (!DeserializeMainExportPath(json, out string mainExportPath) || (!Directory.Exists(mainExportPath) && mainExportPath != String.Empty))
{
ASDebug.Log("Unable to restore Folder upload workflow paths from the local cache");
LoadSerializedWorkflowFallback(lastUploadedPath, lastUploadedGuid);
return;
}
DeserializeExtraExportPaths(json, out List<string> extraExportPaths);
DeserializeDependencies(json, out List<string> dependencies);
DeserializeDependenciesToggle(json, out var dependenciesToggle);
ASDebug.Log($"Restoring serialized Folder workflow values from local cache");
HandleFolderUploadPathSelection(mainExportPath, extraExportPaths, dependencies, false);
if (dependenciesToggle)
{
_dependenciesToggle.SetValueWithoutNotify(true);
FindAndPopulateDependencies(_includedDependencies);
}
}
public override void LoadSerializedWorkflowFallback(string lastUploadedPath, string lastUploadedGuid)
{
var mainExportPath = AssetDatabase.GUIDToAssetPath(lastUploadedGuid);
if (string.IsNullOrEmpty(mainExportPath))
mainExportPath = lastUploadedPath;
if ((!mainExportPath.StartsWith("Assets/") && mainExportPath != "Assets") || !Directory.Exists(mainExportPath))
{
ASDebug.Log("Unable to restore Folder workflow paths from previous upload values");
return;
}
ASDebug.Log($"Restoring serialized Folder workflow values from previous upload values");
HandleFolderUploadPathSelection(mainExportPath, null, null, false);
}
#region Folder Upload
protected override void BrowsePath()
{
// Path retrieval
var absoluteExportPath = string.Empty;
var relativeExportPath = string.Empty;
var rootProjectPath = Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length);
bool includeAllAssets = false;
if (_isCompleteProject)
{
includeAllAssets = EditorUtility.DisplayDialog("Notice",
"Your package draft is set to a category that is treated" +
" as a complete project. Project settings will be included automatically. Would you like everything in the " +
"'Assets' folder to be included?\n\nYou will still be able to change the selected assets before uploading",
"Yes, include all folders and assets",
"No, I'll select what to include manually");
if (includeAllAssets)
absoluteExportPath = Application.dataPath;
}
if (!includeAllAssets)
{
absoluteExportPath =
EditorUtility.OpenFolderPanel("Select folder to compress into a package", "Assets/", "");
if (string.IsNullOrEmpty(absoluteExportPath))
return;
}
if (absoluteExportPath.StartsWith(rootProjectPath))
{
relativeExportPath = absoluteExportPath.Substring(rootProjectPath.Length);
}
else
{
if (ASToolsPreferences.Instance.EnableSymlinkSupport)
SymlinkUtil.FindSymlinkFolderRelative(absoluteExportPath, out relativeExportPath);
}
if (!relativeExportPath.StartsWith("Assets/") && !(relativeExportPath == "Assets" && _isCompleteProject))
{
if (relativeExportPath.StartsWith("Assets") && !_isCompleteProject)
EditorUtility.DisplayDialog("Invalid selection",
"'Assets' folder is only available for packages tagged as a 'Complete Project'.", "OK");
else
EditorUtility.DisplayDialog("Invalid selection", "Selected folder path must be within the project.",
"OK");
return;
}
HandleFolderUploadPathSelection(relativeExportPath, null, _includedDependencies, true);
}
private void HandleFolderUploadPathSelection(string relativeExportPath, List<string> serializedToggles, List<string> dependencies, bool serializeValues)
{
if (relativeExportPath != String.Empty)
PathSelectionField.value = relativeExportPath + "/";
MainExportPath = relativeExportPath;
ExtraExportPaths = new List<string>();
_includedDependencies = new List<string>();
LocalPackageGuid = AssetDatabase.AssetPathToGUID(MainExportPath);
LocalPackagePath = MainExportPath;
LocalProjectPath = MainExportPath;
if (_specialFoldersElement != null)
{
_specialFoldersElement.Clear();
Remove(_specialFoldersElement);
_specialFoldersElement = null;
}
// Prompt additional path selection (e.g. StreamingAssets, WebGLTemplates, etc.)
List<string> specialFoldersFound = new List<string>();
foreach (var extraAssetFolderName in _extraAssetFolderNames)
{
var fullExtraPath = "Assets/" + extraAssetFolderName;
if (!Directory.Exists(fullExtraPath))
continue;
if (MainExportPath.ToLower().StartsWith(fullExtraPath.ToLower()))
continue;
// Don't include nested paths
if (!fullExtraPath.ToLower().StartsWith(MainExportPath.ToLower()))
specialFoldersFound.Add(fullExtraPath);
}
if (specialFoldersFound.Count != 0)
PopulateSpecialFoldersBox(specialFoldersFound, serializedToggles);
if (dependencies != null && dependencies.Count != 0)
FindAndPopulateDependencies(dependencies);
// After setting up the main and extra paths update validation paths
UpdateValidationPaths();
// Only serialize current selection when no serialized toggles were passed
if (serializeValues)
SerializeSelection?.Invoke();
}
private void InitializeSpecialFoldersSelection()
{
// Dependencies selection
_specialFoldersElement = new VisualElement();
_specialFoldersElement.AddToClassList("selection-box-row");
VisualElement specialFoldersHelpRow = new VisualElement();
specialFoldersHelpRow.AddToClassList("label-help-row");
Label specialFoldersLabel = new Label { text = "Special folders" };
Image specialFoldersLabelTooltip = new Image
{
tooltip =
"If your package content relies on Special Folders (e.g. StreamingAssets), please select which of these folders should be included in the package"
};
_specialFolderTogglesBox = new VisualElement { name = "SpecialFolderToggles" };
_specialFolderTogglesBox.AddToClassList("special-folders-toggles-box");
specialFoldersHelpRow.Add(specialFoldersLabel);
specialFoldersHelpRow.Add(specialFoldersLabelTooltip);
_specialFoldersElement.Add(specialFoldersHelpRow);
_specialFoldersElement.Add(_specialFolderTogglesBox);
Add(_specialFoldersElement);
}
private void PopulateSpecialFoldersBox(List<string> specialFoldersFound, List<string> checkedToggles)
{
InitializeSpecialFoldersSelection();
EventCallback<ChangeEvent<bool>, string> toggleChangeCallback = OnSpecialFolderPathToggledAsset;
foreach (var path in specialFoldersFound)
{
var toggle = new Toggle { value = false, text = path };
toggle.AddToClassList("special-folder-toggle");
if (checkedToggles != null && checkedToggles.Contains(toggle.text))
{
toggle.SetValueWithoutNotify(true);
ExtraExportPaths.Add(toggle.text);
}
toggle.RegisterCallback(toggleChangeCallback, toggle.text);
_specialFolderTogglesBox.Add(toggle);
}
}
private void OnSpecialFolderPathToggledAsset(ChangeEvent<bool> evt, string folderPath)
{
switch (evt.newValue)
{
case true when !ExtraExportPaths.Contains(folderPath):
ExtraExportPaths.Add(folderPath);
break;
case false when ExtraExportPaths.Contains(folderPath):
ExtraExportPaths.Remove(folderPath);
break;
}
UpdateValidationPaths();
SerializeSelection?.Invoke();
}
private void UpdateValidationPaths()
{
var validationPaths = new List<string>() { MainExportPath };
validationPaths.AddRange(ExtraExportPaths);
ValidationElement.SetValidationPaths(validationPaths.ToArray());
}
private void OnToggleDependency(ChangeEvent<bool> evt, string dependency)
{
switch (evt.newValue)
{
case true when !_includedDependencies.Contains(dependency):
_includedDependencies.Add(dependency);
break;
case false when _includedDependencies.Contains(dependency):
_includedDependencies.Remove(dependency);
break;
}
FilterPackageSelection(_packagesFilter);
SerializeSelection?.Invoke();
}
private void OnDependencyToggleValueChange(ChangeEvent<bool> evt)
{
CheckDependencyBoxState();
}
private void OnASTSettingsChange()
{
CheckDependencyBoxState();
}
private void CheckDependencyBoxState()
{
if (_dependenciesToggle.value && !ASToolsPreferences.Instance.UseLegacyExporting)
{
FindAndPopulateDependencies(_includedDependencies);
}
else
{
_packageDependencyBox.style.display = DisplayStyle.None;
}
}
private void FindAndPopulateDependencies(List<string> checkedToggles)
{
_packageDependencyBox?.Clear();
var registryPackages = PackageUtility.GetAllRegistryPackages();
if (registryPackages == null)
{
ASDebug.LogWarning("Package Manifest was not found or could not be parsed.");
return;
}
List<string> packagesFound = new List<string>(registryPackages.Select(x => x.name));
PopulatePackagesSelectionBox(packagesFound, checkedToggles);
}
private void PopulatePackagesSelectionBox(List<string> packagesFound, List<string> checkedToggles)
{
InitializePackageSelection();
EventCallback<ChangeEvent<bool>, string> toggleChangeCallback = OnToggleDependency;
if (packagesFound.Count == 0 || ASToolsPreferences.Instance.UseLegacyExporting)
{
_packageDependencyBox.style.display = DisplayStyle.None;
return;
}
_packageDependencyBox.style.display = DisplayStyle.Flex;
foreach (var path in packagesFound)
{
var toggle = new Toggle { value = false, text = path };
toggle.AddToClassList("extra-packages-toggle");
if (checkedToggles != null && checkedToggles.Contains(toggle.text))
{
toggle.SetValueWithoutNotify(true);
if (!_includedDependencies.Contains(toggle.text))
_includedDependencies.Add(toggle.text);
}
toggle.RegisterCallback(toggleChangeCallback, toggle.text);
_packagesTogglesBox.Add(toggle);
}
}
private void InitializePackageSelection()
{
VisualElement dependenciesHelpRow = new VisualElement();
dependenciesHelpRow.AddToClassList("label-help-row");
Label allPackagesLabel = new Label { text = "All Packages" };
Image allPackagesLabelTooltip = new Image
{
tooltip =
"Select UPM dependencies you would like to include with your package."
};
VisualElement fullPackageSelectionBox = new VisualElement();
fullPackageSelectionBox.AddToClassList("extra-packages-box");
_packagesTogglesBox = new ScrollView { name = "DependencyToggles" };
_packagesTogglesBox.AddToClassList("extra-packages-scroll-view");
_noPackagesLabel = new Label("No packages were found that match this criteria.");
_noPackagesLabel.AddToClassList("no-packages-label");
var scrollContainer = _packagesTogglesBox.Q<VisualElement>("unity-content-viewport");
scrollContainer.Add(_noPackagesLabel);
VisualElement packagesFilteringBox = new VisualElement();
packagesFilteringBox.AddToClassList("packages-filtering-box");
// Select - deselect buttons
VisualElement selectingPackagesBox = new VisualElement();
selectingPackagesBox.AddToClassList("filtering-packages-buttons-box");
Button selectAllButton = new Button(() => SelectAllPackages(true))
{
text = "Select All"
};
Button deSelectAllButton = new Button(() => SelectAllPackages(false))
{
text = "Deselect All"
};
selectAllButton.AddToClassList("filter-packages-button");
deSelectAllButton.AddToClassList("filter-packages-button");
selectingPackagesBox.Add(selectAllButton);
selectingPackagesBox.Add(deSelectAllButton);
// Filtering dropdown
VisualElement filteringDropdownBox = new VisualElement();
filteringDropdownBox.AddToClassList("filtering-packages-dropdown-box");
_filteringDropdown = new ToolbarMenu {text = _packagesFilter = _packageSelectionFilters[0]};
_filteringDropdown.AddToClassList("filter-packages-dropdown");
foreach (var filter in _packageSelectionFilters)
_filteringDropdown.menu.AppendAction(filter, delegate { FilterPackageSelection(filter);});
filteringDropdownBox.Add(_filteringDropdown);
VisualElement packageSelectionButtonsBox = new VisualElement();
packageSelectionButtonsBox.AddToClassList("extra-packages-buttons-box");
// Final adding
packagesFilteringBox.Add(filteringDropdownBox);
packagesFilteringBox.Add(selectingPackagesBox);
fullPackageSelectionBox.Add(_packagesTogglesBox);
fullPackageSelectionBox.Add(packagesFilteringBox);
dependenciesHelpRow.Add(allPackagesLabel);
dependenciesHelpRow.Add(allPackagesLabelTooltip);
_packageDependencyBox.Add(dependenciesHelpRow);
_packageDependencyBox.Add(fullPackageSelectionBox);
}
private void FilterPackageSelection(string filter)
{
var allToggles = _packagesTogglesBox.Children().Cast<Toggle>().ToArray();
var selectedIndex = _packageSelectionFilters.FindIndex(x => x == filter);
switch (selectedIndex)
{
case 0:
foreach (var toggle in allToggles)
toggle.style.display = DisplayStyle.Flex;
break;
case 1:
foreach (var toggle in allToggles)
toggle.style.display = toggle.value ? DisplayStyle.Flex : DisplayStyle.None;
break;
case 2:
foreach (var toggle in allToggles)
toggle.style.display = toggle.value ? DisplayStyle.None : DisplayStyle.Flex;
break;
}
// Check if any toggles are displayed
var count = allToggles.Count(toggle => toggle.style.display == DisplayStyle.Flex);
_noPackagesLabel.style.display = count > 0 ? DisplayStyle.None : DisplayStyle.Flex;
_packagesFilter = filter;
_filteringDropdown.text = filter;
}
private void SelectAllPackages(bool shouldSelect)
{
var allToggles = _packagesTogglesBox.Children().Cast<Toggle>();
foreach (var toggle in allToggles)
toggle.value = shouldSelect;
}
public override async Task<ExportResult> ExportPackage(string outputPath, bool isCompleteProject)
{
var paths = GetAllExportPaths();
if (isCompleteProject)
paths = IncludeProjectSettings(paths);
var includeDependencies = GetIncludeDependenciesToggle();
var dependenciesToInclude = Array.Empty<string>();
if (includeDependencies)
dependenciesToInclude = GetIncludedDependencies().ToArray();
ExporterSettings exportSettings;
if (ASToolsPreferences.Instance.UseLegacyExporting)
exportSettings = new LegacyExporterSettings()
{
ExportPaths = paths,
OutputFilename = outputPath,
IncludeDependencies = includeDependencies,
};
else
exportSettings = new DefaultExporterSettings()
{
ExportPaths = paths,
OutputFilename = outputPath,
Dependencies = dependenciesToInclude,
};
return await PackageExporter.ExportPackage(exportSettings);
}
private string[] IncludeProjectSettings(string[] exportPaths)
{
if (exportPaths.Contains("ProjectSettings"))
return exportPaths;
var updatedExportPaths = new string[exportPaths.Length + 1];
exportPaths.CopyTo(updatedExportPaths, 0);
updatedExportPaths[updatedExportPaths.Length - 1] = "ProjectSettings";
return updatedExportPaths;
}
#endregion
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f8bafd0b9c5b47bc985d17a18fc07978
timeCreated: 1654089523

View File

@ -0,0 +1,414 @@
using AssetStoreTools.Exporter;
using AssetStoreTools.Utility;
using AssetStoreTools.Utility.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
namespace AssetStoreTools.Uploader.UIElements
{
internal class HybridPackageUploadWorkflowView : UploadWorkflowView
{
public const string WorkflowName = "HybridPackageWorkflow";
public const string WorkflowDisplayName = "Local UPM Package";
public override string Name => WorkflowName;
public override string DisplayName => WorkflowDisplayName;
private VisualElement _extraPackagesBox;
private Label _noExtraPackagesLabel;
private ScrollView _extraPackagesTogglesBox;
private ToolbarMenu _filteringDropdown;
private string _extraPackagesFilter;
private readonly List<string> _extraPackageSelectionFilters = new List<string> { "All", "Selected", "Not Selected" };
private HybridPackageUploadWorkflowView(string category, Action serializeSelection) : base(serializeSelection)
{
Category = category;
SetupWorkflow();
}
public static HybridPackageUploadWorkflowView Create(string category, Action serializeAction)
{
return new HybridPackageUploadWorkflowView(category, serializeAction);
}
protected sealed override void SetupWorkflow()
{
// Path selection
VisualElement folderPathSelectionRow = new VisualElement();
folderPathSelectionRow.AddToClassList("selection-box-row");
VisualElement labelHelpRow = new VisualElement();
labelHelpRow.AddToClassList("label-help-row");
Label folderPathLabel = new Label { text = "Package path" };
Image folderPathLabelTooltip = new Image
{
tooltip = "Select a local Package you would like to export and upload to the Store."
};
labelHelpRow.Add(folderPathLabel);
labelHelpRow.Add(folderPathLabelTooltip);
PathSelectionField = new TextField();
PathSelectionField.AddToClassList("path-selection-field");
PathSelectionField.isReadOnly = true;
Button browsePathButton = new Button(BrowsePath) { name = "BrowsePathButton", text = "Browse" };
browsePathButton.AddToClassList("browse-button");
folderPathSelectionRow.Add(labelHelpRow);
folderPathSelectionRow.Add(PathSelectionField);
folderPathSelectionRow.Add(browsePathButton);
Add(folderPathSelectionRow);
ValidationElement = new AssetValidationElement();
Add(ValidationElement);
ValidationElement.SetCategory(Category);
}
public override void LoadSerializedWorkflow(JsonValue json, string lastUploadedPath, string lastUploadedGuid)
{
if(!DeserializeMainExportPath(json, out string mainExportPath) || !Directory.Exists(mainExportPath))
{
ASDebug.Log("Unable to restore Hybrid Package workflow paths from local cache");
LoadSerializedWorkflowFallback(lastUploadedGuid, lastUploadedGuid);
return;
}
DeserializeExtraExportPaths(json, out List<string> extraExportPaths);
ASDebug.Log($"Restoring serialized Hybrid Package workflow values from local cache");
LoadSerializedWorkflow(mainExportPath, extraExportPaths);
}
public override void LoadSerializedWorkflowFallback(string lastUploadedPath, string lastUploadedGuid)
{
var mainExportPath = AssetDatabase.GUIDToAssetPath(lastUploadedGuid);
if (string.IsNullOrEmpty(mainExportPath))
mainExportPath = lastUploadedPath;
if (!mainExportPath.StartsWith("Packages/") || !Directory.Exists(mainExportPath))
{
ASDebug.Log("Unable to restore Hybrid Package workflow paths from previous upload values");
return;
}
ASDebug.Log($"Restoring serialized Hybrid Package workflow values from previous upload values");
LoadSerializedWorkflow(mainExportPath, null);
}
private void LoadSerializedWorkflow(string relativeAssetDatabasePath, List<string> extraExportPaths)
{
// Expected path is in ADB form, so we need to reconstruct it first
var rootProjectPath = Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length);
var realPath = Path.GetFullPath(relativeAssetDatabasePath).Replace('\\', '/');
if (realPath.StartsWith(rootProjectPath))
realPath = realPath.Substring(rootProjectPath.Length);
if (!IsValidLocalPackage(realPath, out relativeAssetDatabasePath))
{
ASDebug.Log("Unable to restore Hybrid Package workflow path - package is not a valid UPM package");
return;
}
// Treat this as a manual selection
HandleHybridUploadPathSelection(realPath, relativeAssetDatabasePath, extraExportPaths, false);
}
protected override void BrowsePath()
{
// Path retrieval
string relativeExportPath = string.Empty;
string rootProjectPath = Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length);
var absoluteExportPath = EditorUtility.OpenFolderPanel("Select the Package", "Packages/", "");
if (string.IsNullOrEmpty(absoluteExportPath))
return;
if (absoluteExportPath.StartsWith(rootProjectPath))
relativeExportPath = absoluteExportPath.Substring(rootProjectPath.Length);
var workingPath = !string.IsNullOrEmpty(relativeExportPath) ? relativeExportPath : absoluteExportPath;
if (!IsValidLocalPackage(workingPath, out string relativeAssetDatabasePath))
{
EditorUtility.DisplayDialog("Invalid selection", "Selected export path must be a valid local package", "OK");
return;
}
HandleHybridUploadPathSelection(workingPath, relativeAssetDatabasePath, null, true);
}
private void HandleHybridUploadPathSelection(string relativeExportPath, string relativeAssetDatabasePath, List<string> serializedToggles, bool serializeValues)
{
PathSelectionField.value = relativeExportPath + "/";
// Reset and reinitialize the selected export path(s) array
MainExportPath = relativeAssetDatabasePath;
ExtraExportPaths = new List<string>();
// Set additional upload data for the Publisher Portal backend (GUID and Package Path).
// The backend workflow currently accepts only 1 package guid and path, so we'll use the main folder data
LocalPackageGuid = AssetDatabase.AssetPathToGUID(relativeAssetDatabasePath);
LocalPackagePath = relativeAssetDatabasePath;
LocalProjectPath = relativeAssetDatabasePath;
if (_extraPackagesBox != null)
{
_extraPackagesBox.Clear();
Remove(_extraPackagesBox);
_extraPackagesBox = null;
}
List<string> pathsToAdd = new List<string>();
foreach (var package in PackageUtility.GetAllLocalPackages())
{
// Exclude the Asset Store Tools themselves
if (package.name == "com.unity.asset-store-tools")
continue;
var localPackagePath = package.GetConvenientPath();
if (localPackagePath == relativeExportPath)
continue;
pathsToAdd.Add(package.assetPath);
}
pathsToAdd.Sort();
if (pathsToAdd.Count != 0)
PopulateExtraPackagesBox(pathsToAdd, serializedToggles);
// After setting up the main and extra paths update validation paths
UpdateValidationPaths();
if (serializeValues)
SerializeSelection?.Invoke();
}
private void PopulateExtraPackagesBox(List<string> otherPackagesFound, List<string> checkedToggles)
{
// Dependencies selection
_extraPackagesBox = new VisualElement();
_extraPackagesBox.AddToClassList("selection-box-row");
InitializeExtraPackageSelection();
EventCallback<ChangeEvent<bool>, string> toggleChangeCallback = OnToggledPackage;
foreach (var path in otherPackagesFound)
{
var toggle = new Toggle { value = false, text = path };
toggle.AddToClassList("extra-packages-toggle");
toggle.tooltip = path;
if (checkedToggles != null && checkedToggles.Contains(toggle.text))
{
toggle.SetValueWithoutNotify(true);
ExtraExportPaths.Add(toggle.text);
}
toggle.RegisterCallback(toggleChangeCallback, toggle.text);
_extraPackagesTogglesBox.Add(toggle);
}
Add(_extraPackagesBox);
}
private void InitializeExtraPackageSelection()
{
VisualElement extraPackagesHelpRow = new VisualElement();
extraPackagesHelpRow.AddToClassList("label-help-row");
Label extraPackagesLabel = new Label { text = "Extra Packages" };
Image extraPackagesLabelTooltip = new Image
{
tooltip = "If your package has dependencies on other local packages, please select which of these packages should also be included in the resulting package"
};
var fullPackageSelectionBox = new VisualElement();
fullPackageSelectionBox.AddToClassList("extra-packages-box");
_extraPackagesTogglesBox = new ScrollView { name = "ExtraPackageToggles" };
_extraPackagesTogglesBox.AddToClassList("extra-packages-scroll-view");
_noExtraPackagesLabel = new Label("No packages were found that match this criteria.");
_noExtraPackagesLabel.AddToClassList("no-packages-label");
var scrollContainer = _extraPackagesTogglesBox.Q<VisualElement>("unity-content-viewport");
scrollContainer.Add(_noExtraPackagesLabel);
VisualElement extraPackagesFilteringBox = new VisualElement();
extraPackagesFilteringBox.AddToClassList("packages-filtering-box");
// Select - deselect buttons
VisualElement selectingPackagesBox = new VisualElement();
selectingPackagesBox.AddToClassList("filtering-packages-buttons-box");
Button selectAllButton = new Button(() => SelectAllPackages(true))
{
text = "Select All"
};
Button deSelectAllButton = new Button(() => SelectAllPackages(false))
{
text = "Deselect All"
};
selectAllButton.AddToClassList("filter-packages-button");
deSelectAllButton.AddToClassList("filter-packages-button");
selectingPackagesBox.Add(selectAllButton);
selectingPackagesBox.Add(deSelectAllButton);
// Filtering dropdown
VisualElement filteringDropdownBox = new VisualElement();
filteringDropdownBox.AddToClassList("filtering-packages-dropdown-box");
_filteringDropdown = new ToolbarMenu { text = _extraPackagesFilter = _extraPackageSelectionFilters[0] };
_filteringDropdown.AddToClassList("filter-packages-dropdown");
foreach (var filter in _extraPackageSelectionFilters)
_filteringDropdown.menu.AppendAction(filter, delegate { FilterPackageSelection(filter); });
filteringDropdownBox.Add(_filteringDropdown);
VisualElement packageSelectionButtonsBox = new VisualElement();
packageSelectionButtonsBox.AddToClassList("extra-packages-buttons-box");
// Final adding
extraPackagesFilteringBox.Add(filteringDropdownBox);
extraPackagesFilteringBox.Add(selectingPackagesBox);
fullPackageSelectionBox.Add(_extraPackagesTogglesBox);
fullPackageSelectionBox.Add(extraPackagesFilteringBox);
extraPackagesHelpRow.Add(extraPackagesLabel);
extraPackagesHelpRow.Add(extraPackagesLabelTooltip);
_extraPackagesBox.Add(extraPackagesHelpRow);
_extraPackagesBox.Add(fullPackageSelectionBox);
}
private void SelectAllPackages(bool shouldSelect)
{
var allToggles = _extraPackagesTogglesBox.Children().Cast<Toggle>();
foreach (var toggle in allToggles)
toggle.value = shouldSelect;
}
private void FilterPackageSelection(string filter)
{
var allToggles = _extraPackagesTogglesBox.Children().Cast<Toggle>().ToArray();
var selectedIndex = _extraPackageSelectionFilters.FindIndex(x => x == filter);
switch (selectedIndex)
{
case 0:
foreach (var toggle in allToggles)
toggle.style.display = DisplayStyle.Flex;
break;
case 1:
foreach (var toggle in allToggles)
toggle.style.display = toggle.value ? DisplayStyle.Flex : DisplayStyle.None;
break;
case 2:
foreach (var toggle in allToggles)
toggle.style.display = toggle.value ? DisplayStyle.None : DisplayStyle.Flex;
break;
}
// Check if any toggles are displayed
var count = allToggles.Count(toggle => toggle.style.display == DisplayStyle.Flex);
_noExtraPackagesLabel.style.display = count > 0 ? DisplayStyle.None : DisplayStyle.Flex;
_extraPackagesFilter = filter;
_filteringDropdown.text = filter;
}
private void OnToggledPackage(ChangeEvent<bool> evt, string folderPath)
{
switch (evt.newValue)
{
case true when !ExtraExportPaths.Contains(folderPath):
ExtraExportPaths.Add(folderPath);
break;
case false when ExtraExportPaths.Contains(folderPath):
ExtraExportPaths.Remove(folderPath);
break;
}
FilterPackageSelection(_extraPackagesFilter);
UpdateValidationPaths();
SerializeSelection?.Invoke();
}
private void UpdateValidationPaths()
{
var validationPaths = new List<string>() { MainExportPath };
validationPaths.AddRange(ExtraExportPaths);
ValidationElement.SetValidationPaths(validationPaths.ToArray());
}
private bool IsValidLocalPackage(string packageFolderPath, out string assetDatabasePackagePath)
{
assetDatabasePackagePath = string.Empty;
string packageManifestPath = $"{packageFolderPath}/package.json";
if (!File.Exists(packageManifestPath))
return false;
try
{
var localPackages = PackageUtility.GetAllLocalPackages();
if (localPackages == null || localPackages.Length == 0)
return false;
foreach (var package in localPackages)
{
var localPackagePath = package.GetConvenientPath();
if (localPackagePath != packageFolderPath)
continue;
assetDatabasePackagePath = package.assetPath;
return true;
}
}
catch
{
return false;
}
return false;
}
public override async Task<ExportResult> ExportPackage(string outputPath, bool _)
{
var paths = GetAllExportPaths();
var exportSettings = new DefaultExporterSettings()
{
ExportPaths = paths,
OutputFilename = outputPath
};
return await PackageExporter.ExportPackage(exportSettings);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b5472a1f8e8745779ee82d18b63ec19c
timeCreated: 1654112492

View File

@ -0,0 +1,140 @@
using AssetStoreTools.Exporter;
using AssetStoreTools.Utility;
using AssetStoreTools.Utility.Json;
using System;
using System.IO;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace AssetStoreTools.Uploader.UIElements
{
internal class UnityPackageUploadWorkflowView : UploadWorkflowView
{
public const string WorkflowName = "UnitypackageWorkflow";
public const string WorkflowDisplayName = "Pre-exported .unitypackage";
public override string Name => WorkflowName;
public override string DisplayName => WorkflowDisplayName;
private UnityPackageUploadWorkflowView(string category, Action serializeSelection) : base(serializeSelection)
{
Category = category;
SetupWorkflow();
}
public static UnityPackageUploadWorkflowView Create(string category, Action serializeAction)
{
return new UnityPackageUploadWorkflowView(category, serializeAction);
}
protected sealed override void SetupWorkflow()
{
// Path selection
VisualElement folderPathSelectionRow = new VisualElement();
folderPathSelectionRow.AddToClassList("selection-box-row");
VisualElement labelHelpRow = new VisualElement();
labelHelpRow.AddToClassList("label-help-row");
Label folderPathLabel = new Label { text = "Package path" };
Image folderPathLabelTooltip = new Image
{
tooltip = "Select the .unitypackage file you would like to upload."
};
labelHelpRow.Add(folderPathLabel);
labelHelpRow.Add(folderPathLabelTooltip);
PathSelectionField = new TextField();
PathSelectionField.AddToClassList("path-selection-field");
PathSelectionField.isReadOnly = true;
Button browsePathButton = new Button(BrowsePath) { name = "BrowsePathButton", text = "Browse" };
browsePathButton.AddToClassList("browse-button");
folderPathSelectionRow.Add(labelHelpRow);
folderPathSelectionRow.Add(PathSelectionField);
folderPathSelectionRow.Add(browsePathButton);
Add(folderPathSelectionRow);
ValidationElement = new PackageValidationElement();
Add(ValidationElement);
ValidationElement.SetCategory(Category);
}
public override void LoadSerializedWorkflow(JsonValue json, string lastUploadedPath, string lastUploadedGuid)
{
if (!DeserializeMainExportPath(json, out string mainPackagePath) || !File.Exists(mainPackagePath))
{
ASDebug.Log("Unable to restore .unitypackage upload workflow path from the local cache");
LoadSerializedWorkflowFallback(lastUploadedPath, lastUploadedGuid);
return;
}
ASDebug.Log($"Restoring serialized .unitypackage workflow values from local cache");
HandleUnityPackageUploadPathSelection(mainPackagePath, false);
}
public override void LoadSerializedWorkflowFallback(string lastUploadedPath, string lastUploadedGuid)
{
var packagePath = AssetDatabase.GUIDToAssetPath(lastUploadedGuid);
if (string.IsNullOrEmpty(packagePath))
packagePath = lastUploadedPath;
if (!packagePath.EndsWith(".unitypackage") || !File.Exists(packagePath))
{
ASDebug.Log("Unable to restore .unitypackage workflow path from previous upload values");
return;
}
ASDebug.Log($"Restoring serialized .unitypackage workflow values from previous upload values");
HandleUnityPackageUploadPathSelection(packagePath, false);
}
protected override void BrowsePath()
{
// Path retrieval
var rootProjectPath = Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length);
var absolutePackagePath = EditorUtility.OpenFilePanel("Select a .unitypackage file", rootProjectPath, "unitypackage");
if (string.IsNullOrEmpty(absolutePackagePath))
return;
var relativeExportPath = string.Empty;
if (absolutePackagePath.StartsWith(rootProjectPath))
relativeExportPath = absolutePackagePath.Substring(rootProjectPath.Length);
var selectedPackagePath = !string.IsNullOrEmpty(relativeExportPath) ? relativeExportPath : absolutePackagePath;
HandleUnityPackageUploadPathSelection(selectedPackagePath, true);
}
private void HandleUnityPackageUploadPathSelection(string selectedPackagePath, bool serializeValues)
{
// Main upload path
PathSelectionField.value = selectedPackagePath;
// Export data
MainExportPath = selectedPackagePath;
LocalPackageGuid = string.Empty;
LocalPackagePath = string.Empty;
LocalProjectPath = selectedPackagePath;
if (serializeValues)
SerializeSelection?.Invoke();
ValidationElement.SetValidationPaths(MainExportPath);
}
public override Task<ExportResult> ExportPackage(string __, bool _)
{
if (string.IsNullOrEmpty(MainExportPath))
return Task.FromResult(new ExportResult() { Success = false, Error = ASError.GetGenericError(new ArgumentException("Package export path is empty or null")) });
return Task.FromResult(new ExportResult() { Success = true, ExportedPath = MainExportPath });
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a9e30749a5784d18a1a2644cc44dda29
timeCreated: 1654112475

View File

@ -0,0 +1,180 @@
using AssetStoreTools.Exporter;
using AssetStoreTools.Utility;
using AssetStoreTools.Utility.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine.UIElements;
namespace AssetStoreTools.Uploader.UIElements
{
internal abstract class UploadWorkflowView : VisualElement
{
protected TextField PathSelectionField;
// Upload data
protected List<string> ExtraExportPaths = new List<string>();
protected string MainExportPath = String.Empty;
protected string LocalPackageGuid;
protected string LocalPackagePath;
protected string LocalProjectPath;
protected string Category;
protected ValidationElement ValidationElement;
protected readonly Action SerializeSelection;
public abstract string Name { get; }
public abstract string DisplayName { get; }
protected UploadWorkflowView(Action serializeSelection)
{
SerializeSelection = serializeSelection;
style.display = DisplayStyle.None;
}
public string[] GetAllExportPaths()
{
var allPaths = new List<string>(ExtraExportPaths);
if (!string.IsNullOrEmpty(MainExportPath))
allPaths.Insert(0, MainExportPath);
return allPaths.ToArray();
}
public string GetLocalPackageGuid()
{
return LocalPackageGuid;
}
public string GetLocalPackagePath()
{
return LocalPackagePath;
}
public string GetLocalProjectPath()
{
return LocalProjectPath;
}
protected abstract void SetupWorkflow();
public virtual JsonValue SerializeWorkflow()
{
var workflowDict = JsonValue.NewDict();
var mainExportPathDict = JsonValue.NewDict();
mainExportPathDict["path"] = MainExportPath;
if (MainExportPath != null && !MainExportPath.StartsWith("Assets/") && !MainExportPath.StartsWith("Packages/"))
mainExportPathDict["guid"] = new JsonValue("");
else
mainExportPathDict["guid"] = AssetDatabase.AssetPathToGUID(MainExportPath);
workflowDict["mainPath"] = mainExportPathDict;
var extraExportPathsList = new List<JsonValue>();
foreach (var path in ExtraExportPaths)
{
var pathDict = JsonValue.NewDict();
pathDict["path"] = path;
pathDict["guid"] = AssetDatabase.AssetPathToGUID(path);
extraExportPathsList.Add(pathDict);
}
workflowDict["extraPaths"] = extraExportPathsList;
return workflowDict;
}
protected bool DeserializeMainExportPath(JsonValue json, out string mainExportPath)
{
mainExportPath = string.Empty;
try
{
var mainPathDict = json["mainPath"];
if (!mainPathDict.ContainsKey("path") || !mainPathDict["path"].IsString())
return false;
mainExportPath = DeserializePath(mainPathDict);
return true;
}
catch
{
return false;
}
}
protected void DeserializeExtraExportPaths(JsonValue json, out List<string> extraExportPaths)
{
extraExportPaths = new List<string>();
try
{
var extraPathsList = json["extraPaths"].AsList();
extraExportPaths.AddRange(extraPathsList.Select(DeserializePath));
}
catch
{
ASDebug.LogWarning($"Deserializing extra export paths for {Name} failed");
extraExportPaths.Clear();
}
}
protected void DeserializeDependencies(JsonValue json, out List<string> dependencies)
{
dependencies = new List<string>();
try
{
var packageJsonList = json["dependenciesNames"].AsList();
dependencies.AddRange(packageJsonList.Select(package => package.AsString()));
}
catch
{
ASDebug.LogWarning($"Deserializing dependencies for {Name} failed");
dependencies.Clear();
}
}
protected void DeserializeDependenciesToggle(JsonValue json, out bool dependenciesBool)
{
bool includeDependencies;
try
{
includeDependencies = json["dependencies"].AsBool();
}
catch
{
ASDebug.LogWarning($"Deserializing dependencies toggle for {Name} failed");
includeDependencies = false;
}
dependenciesBool = includeDependencies;
}
private string DeserializePath(JsonValue pathDict)
{
// First pass - retrieve from GUID
var exportPath = AssetDatabase.GUIDToAssetPath(pathDict["guid"].AsString());
// Second pass - retrieve directly
if (string.IsNullOrEmpty(exportPath))
exportPath = pathDict["path"].AsString();
return exportPath;
}
public bool GetValidationSummary(out string validationSummary)
{
return ValidationElement.GetValidationSummary(out validationSummary);
}
public abstract void LoadSerializedWorkflow(JsonValue json, string lastUploadedPath, string lastUploadedGuid);
public abstract void LoadSerializedWorkflowFallback(string lastUploadedPath, string lastUploadedGuid);
protected abstract void BrowsePath();
public abstract Task<ExportResult> ExportPackage(string outputPath, bool isCompleteProject);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f84b47b78aca74c4db1e9b753d41422f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 342ae2f53a9d6714ca92eacbecb27801
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,128 @@
using AssetStoreTools.Utility.Json;
using System.IO;
using System.Text;
using UnityEngine;
namespace AssetStoreTools.Uploader.Utility
{
internal static class AssetStoreCache
{
public const string TempCachePath = "Temp/AssetStoreToolsCache";
public const string PersistentCachePath = "Library/AssetStoreToolsCache";
private const string PackageDataFile = "PackageMetadata.json";
private const string CategoryDataFile = "Categories.json";
private static void CreateFileInTempCache(string fileName, object content, bool overwrite)
{
CreateCacheFile(TempCachePath, fileName, content, overwrite);
}
private static void CreateFileInPersistentCache(string fileName, object content, bool overwrite)
{
CreateCacheFile(PersistentCachePath, fileName, content, overwrite);
}
private static void CreateCacheFile(string rootPath, string fileName, object content, bool overwrite)
{
if (!Directory.Exists(rootPath))
Directory.CreateDirectory(rootPath);
var fullPath = Path.Combine(rootPath, fileName);
if(File.Exists(fullPath))
{
if (overwrite)
File.Delete(fullPath);
else
return;
}
switch (content)
{
case byte[] bytes:
File.WriteAllBytes(fullPath, bytes);
break;
default:
File.WriteAllText(fullPath, content.ToString());
break;
}
}
public static void ClearTempCache()
{
if (!File.Exists(Path.Combine(TempCachePath, PackageDataFile)))
return;
// Cache consists of package data and package texture thumbnails. We don't clear
// texture thumbnails here since they are less likely to change. They are still
// deleted and redownloaded every project restart (because of being stored in the 'Temp' folder)
File.Delete(Path.Combine(TempCachePath, PackageDataFile));
}
public static void CacheCategories(JsonValue data)
{
CreateFileInTempCache(CategoryDataFile, data, true);
}
public static bool GetCachedCategories(out JsonValue data)
{
data = new JsonValue();
var path = Path.Combine(TempCachePath, CategoryDataFile);
if (!File.Exists(path))
return false;
data = JSONParser.SimpleParse(File.ReadAllText(path, Encoding.UTF8));
return true;
}
public static void CachePackageMetadata(JsonValue data)
{
CreateFileInTempCache(PackageDataFile, data.ToString(), true);
}
public static bool GetCachedPackageMetadata(out JsonValue data)
{
data = new JsonValue();
var path = Path.Combine(TempCachePath, PackageDataFile);
if (!File.Exists(path))
return false;
data = JSONParser.SimpleParse(File.ReadAllText(path, Encoding.UTF8));
return true;
}
public static void CacheTexture(string packageId, Texture2D texture)
{
CreateFileInTempCache($"{packageId}.png", texture.EncodeToPNG(), true);
}
public static bool GetCachedTexture(string packageId, out Texture2D texture)
{
texture = new Texture2D(1, 1);
var path = Path.Combine(TempCachePath, $"{packageId}.png");
if (!File.Exists(path))
return false;
texture.LoadImage(File.ReadAllBytes(path));
return true;
}
public static void CacheUploadSelections(string packageId, JsonValue json)
{
var fileName = $"{packageId}-uploadselection.asset";
CreateFileInPersistentCache(fileName, json.ToString(), true);
}
public static bool GetCachedUploadSelections(string packageId, out JsonValue json)
{
json = new JsonValue();
var path = Path.Combine(PersistentCachePath, $"{packageId}-uploadselection.asset");
if (!File.Exists(path))
return false;
json = JSONParser.SimpleParse(File.ReadAllText(path, Encoding.UTF8));
return true;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2e5fee0cad7655f458d9b600f4ae6d02
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,93 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using AssetStoreTools.Uploader.Data;
using AssetStoreTools.Utility;
using AssetStoreTools.Utility.Json;
namespace AssetStoreTools.Uploader.Utility
{
internal class PackageFetcher
{
public abstract class PackageFetcherResult
{
public bool Success;
public bool SilentFail;
public ASError Error;
public JsonValue Json;
}
public class PackageFetcherResultSingle : PackageFetcherResult
{
public PackageData Package;
}
public class PackageFetcherResultCollection : PackageFetcherResult
{
public ICollection<PackageData> Packages;
}
public async Task<PackageFetcherResultCollection> Fetch(bool useCached)
{
var result = await AssetStoreAPI.GetFullPackageDataAsync(useCached);
if (!result.Success)
return new PackageFetcherResultCollection() { Success = false, Error = result.Error, SilentFail = result.SilentFail };
if (result.Response.Equals(default(JsonValue)))
{
ASDebug.Log("No packages fetched");
return new PackageFetcherResultCollection() { Success = true, Packages = null, Json = result.Response };
}
var packages = ParsePackages(result.Response);
return new PackageFetcherResultCollection() { Success = true, Packages = packages, Json = result.Response };
}
public async Task<PackageFetcherResultSingle> FetchRefreshedPackage(string packageId)
{
var result = await AssetStoreAPI.GetRefreshedPackageData(packageId);
if (!result.Success)
{
ASDebug.LogError(result.Error.Message);
return new PackageFetcherResultSingle() { Success = false, Error = result.Error, SilentFail = result.SilentFail };
}
var package = ParseRefreshedPackage(packageId, result.Response);
return new PackageFetcherResultSingle() { Success = true, Package = package };
}
private ICollection<PackageData> ParsePackages(JsonValue json)
{
List<PackageData> packageList = new List<PackageData>();
var packageDict = json["packages"].AsDict(true);
ASDebug.Log($"All packages\n{json["packages"]}");
// Each package has an identifier and a bunch of data (current version id, name, etc.)
foreach (var p in packageDict)
{
var packageId = p.Key;
var package = ParseRefreshedPackage(packageId, p.Value);
packageList.Add(package);
}
return packageList;
}
private PackageData ParseRefreshedPackage(string packageId, JsonValue json)
{
var packageName = json["name"].AsString(true);
var versionId = json["id"].AsString(true);
var statusName = json["status"].AsString(true);
var isCompleteProject = json["is_complete_project"].AsBool(true);
var categoryName = json["extra_info"].Get("category_info").Get("name").AsString(true);
var lastUploadedPath = json["project_path"].IsString() ? json["project_path"].AsString() : string.Empty;
var lastUploadedGuid = json["root_guid"].IsString() ? json["root_guid"].AsString() : string.Empty;
var lastDate = json["extra_info"].Get("modified").AsString(true);
var lastSize = json["extra_info"].Get("size").AsString(true);
var package = new PackageData(packageId, packageName, versionId, statusName, categoryName, isCompleteProject, lastUploadedPath, lastUploadedGuid, lastDate, lastSize);
ASDebug.Log(package);
return package;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 24e1d75365cc42a09e5c5daec071813e
timeCreated: 1658918833

View File

@ -0,0 +1,31 @@
using AssetStoreTools.Uploader.Data;
using AssetStoreTools.Uploader.UIElements;
using System.Collections.Generic;
namespace AssetStoreTools.Uploader.Utility
{
internal static class PackageViewStorer
{
private static readonly Dictionary<string, PackageView> SavedPackages = new Dictionary<string, PackageView>();
public static PackageView GetPackage(PackageData packageData)
{
string versionId = packageData.VersionId;
if (SavedPackages.ContainsKey(versionId))
{
var savedPackage = SavedPackages[versionId];
savedPackage.UpdateDataValues(packageData);
return savedPackage;
}
var package = new PackageView(packageData);
SavedPackages.Add(package.VersionId, package);
return package;
}
public static void Reset()
{
SavedPackages.Clear();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 582639e8ed53f37499d12efcb4cde2c9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f9398c14296d30f479b9de5f3ec3b827
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b2d1d4985f5314246a7cb4ef749974af
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,37 @@
.primary-colors
{
/* Light - lighter */
background-color: rgb(220, 220, 220);
/* Light - middle */
background-color: rgb(200, 200, 200);
/* Light - darker */
background-color: rgb(180, 180, 180);
/* Dark - lighter */
background-color: rgb(78, 78, 78);
/* Dark - middle */
background-color: rgb(68, 68, 68);
/* Dark - darker */
background-color: rgb(58, 58, 58);
/* Border color - light */
border-color: rgb(200, 200, 200);
/* Border color - dark */
border-color: rgb(33, 33, 33);
}
.accent-color
{
border-color: rgb(58, 121, 187);
}
.empty-color
{
background-color: rgba(0, 0, 0, 0);
border-color: rgba(0, 0, 0, 0);
}
.root
{
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d00412342a1b6c943b91cc06edad1202
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@ -0,0 +1,37 @@
.primary-colors
{
/* Light - lighter */
background-color: rgb(220, 220, 220);
/* Light - middle */
background-color: rgb(200, 200, 200);
/* Light - darker */
background-color: rgb(180, 180, 180);
/* Dark - lighter */
background-color: rgb(50, 50, 50);
/* Dark - middle */
background-color: rgb(28, 28, 28);
/* Dark - darker */
background-color: rgb(0, 0, 0);
/* Border color - light */
border-color: rgb(200, 200, 200);
/* Border color - dark */
border-color: rgb(33, 33, 33);
}
.accent-color
{
border-color: rgb(58, 121, 187);
}
.empty-color
{
background-color: rgba(0, 0, 0, 0);
border-color: rgba(0, 0, 0, 0);
}
.root
{
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a9e3b7e7e1851a140b1a5c23270ded34
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@ -0,0 +1,15 @@
#MainScrollView > * > .unity-scroll-view__content-container
{
flex-grow: 1;
}
#MainScrollView > * > .unity-scroll-view__content-viewport
{
flex-shrink: 1;
}
#MainScrollView > * > * > .unity-scroll-view__content-container
{
flex-grow: 1;
flex-shrink: 1;
}

Some files were not shown because too many files have changed in this diff Show More