Added Unity project files
This commit is contained in:
@ -0,0 +1,36 @@
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
// Common interface used by all builder panels inside their respective packages
|
||||
|
||||
/// <summary>
|
||||
/// This interface is reserved for SDK use, refer to Interfaces inside the Public SDK API folder for public APIs
|
||||
/// </summary>
|
||||
public interface IVRCSdkControlPanelBuilder
|
||||
{
|
||||
void Initialize();
|
||||
void ShowSettingsOptions();
|
||||
bool IsValidBuilder(out string message);
|
||||
void CreateBuilderErrorGUI(VisualElement root);
|
||||
|
||||
void CreateValidationsGUI(VisualElement root);
|
||||
|
||||
EventHandler OnContentChanged { get; set; }
|
||||
EventHandler OnShouldRevalidate { get; set; }
|
||||
|
||||
void RegisterBuilder(VRCSdkControlPanel baseBuilder);
|
||||
void SelectAllComponents();
|
||||
void CreateContentInfoGUI(VisualElement root);
|
||||
void CreateBuildGUI(VisualElement root);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the image to show within the Builder tab. If no image is provided - the default image is used
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Texture2D GetHeaderImage()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 310a760e312f2984e85eece367bab19a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,511 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using JetBrains.Annotations;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using VRC.Core;
|
||||
using VRC.Editor;
|
||||
using VRC.SDKBase.Editor;
|
||||
|
||||
[assembly:InternalsVisibleTo("VRC.ExampleCentral.Editor")]
|
||||
|
||||
/// This class sets up the basic panel layout and draws the main tabs
|
||||
/// Implementation of each tab is handled within other files extending this partial class
|
||||
|
||||
|
||||
[ExecuteInEditMode]
|
||||
public partial class VRCSdkControlPanel : EditorWindow, IVRCSdkPanelApi
|
||||
{
|
||||
public static VRCSdkControlPanel window;
|
||||
|
||||
[MenuItem("VRChat SDK/Show Control Panel", false, 600)]
|
||||
internal static void ShowControlPanel()
|
||||
{
|
||||
if (!ConfigManager.RemoteConfig.IsInitialized())
|
||||
{
|
||||
VRC.Core.API.SetOnlineMode(true);
|
||||
ConfigManager.RemoteConfig.Init(() => ShowControlPanel());
|
||||
return;
|
||||
}
|
||||
|
||||
GetWindow(typeof(VRCSdkControlPanel));
|
||||
window.titleContent.text = "VRChat SDK";
|
||||
window.minSize = new Vector2(SdkWindowWidth + 4, 450);
|
||||
window.maxSize = new Vector2(SdkWindowWidth + 4, 2000);
|
||||
window.Init();
|
||||
window.Show();
|
||||
}
|
||||
|
||||
public VRCSdkControlPanel()
|
||||
{
|
||||
window = this;
|
||||
}
|
||||
|
||||
#region IMGUI Init
|
||||
|
||||
public static GUIStyle titleGuiStyle;
|
||||
public static GUIStyle boxGuiStyle;
|
||||
public static GUIStyle infoGuiStyle;
|
||||
public static GUIStyle listButtonStyleEven;
|
||||
public static GUIStyle listButtonStyleOdd;
|
||||
public static GUIStyle listButtonStyleSelected;
|
||||
public static GUIStyle scrollViewSeparatorStyle;
|
||||
public static GUIStyle searchBarStyle;
|
||||
public static GUIStyle accountWindowStyle;
|
||||
public static GUIStyle centeredLabelStyle;
|
||||
public static GUIStyle contentDescriptionStyle;
|
||||
public static GUIStyle contentTitleStyle;
|
||||
public static GUIStyle unityUpgradeBannerStyle;
|
||||
|
||||
void InitializeStyles()
|
||||
{
|
||||
titleGuiStyle = new GUIStyle();
|
||||
titleGuiStyle.fontSize = 15;
|
||||
titleGuiStyle.fontStyle = FontStyle.BoldAndItalic;
|
||||
titleGuiStyle.alignment = TextAnchor.MiddleCenter;
|
||||
titleGuiStyle.wordWrap = true;
|
||||
if (EditorGUIUtility.isProSkin)
|
||||
titleGuiStyle.normal.textColor = Color.white;
|
||||
else
|
||||
titleGuiStyle.normal.textColor = Color.black;
|
||||
|
||||
boxGuiStyle = new GUIStyle
|
||||
{
|
||||
padding = new RectOffset(5,5,5,5)
|
||||
};
|
||||
if (EditorGUIUtility.isProSkin)
|
||||
{
|
||||
boxGuiStyle.normal.background = CreateBackgroundColorImage(new Color(0.3f, 0.3f, 0.3f));
|
||||
boxGuiStyle.normal.textColor = Color.white;
|
||||
}
|
||||
else
|
||||
{
|
||||
boxGuiStyle.normal.background = CreateBackgroundColorImage(new Color(0.85f, 0.85f, 0.85f));
|
||||
boxGuiStyle.normal.textColor = Color.black;
|
||||
}
|
||||
|
||||
infoGuiStyle = new GUIStyle();
|
||||
infoGuiStyle.wordWrap = true;
|
||||
if (EditorGUIUtility.isProSkin)
|
||||
infoGuiStyle.normal.textColor = Color.white;
|
||||
else
|
||||
infoGuiStyle.normal.textColor = Color.black;
|
||||
infoGuiStyle.margin = new RectOffset(10, 10, 10, 10);
|
||||
|
||||
listButtonStyleEven = new GUIStyle();
|
||||
listButtonStyleEven.margin = new RectOffset(0, 0, 0, 0);
|
||||
listButtonStyleEven.border = new RectOffset(0, 0, 0, 0);
|
||||
if (EditorGUIUtility.isProSkin)
|
||||
{
|
||||
listButtonStyleEven.normal.textColor = new Color(0.8f, 0.8f, 0.8f);
|
||||
listButtonStyleEven.normal.background = CreateBackgroundColorImage(new Color(0.540f, 0.540f, 0.54f));
|
||||
}
|
||||
else
|
||||
{
|
||||
listButtonStyleEven.normal.textColor = Color.black;
|
||||
listButtonStyleEven.normal.background = CreateBackgroundColorImage(new Color(0.85f, 0.85f, 0.85f));
|
||||
}
|
||||
|
||||
listButtonStyleOdd = new GUIStyle();
|
||||
listButtonStyleOdd.margin = new RectOffset(0, 0, 0, 0);
|
||||
listButtonStyleOdd.border = new RectOffset(0, 0, 0, 0);
|
||||
if (EditorGUIUtility.isProSkin)
|
||||
{
|
||||
listButtonStyleOdd.normal.textColor = new Color(0.8f, 0.8f, 0.8f);
|
||||
//listButtonStyleOdd.normal.background = CreateBackgroundColorImage(new Color(0.50f, 0.50f, 0.50f));
|
||||
}
|
||||
else
|
||||
{
|
||||
listButtonStyleOdd.normal.textColor = Color.black;
|
||||
listButtonStyleOdd.normal.background = CreateBackgroundColorImage(new Color(0.90f, 0.90f, 0.90f));
|
||||
}
|
||||
|
||||
listButtonStyleSelected = new GUIStyle();
|
||||
listButtonStyleSelected.normal.textColor = Color.white;
|
||||
listButtonStyleSelected.margin = new RectOffset(0, 0, 0, 0);
|
||||
if (EditorGUIUtility.isProSkin)
|
||||
{
|
||||
listButtonStyleSelected.normal.textColor = new Color(0.8f, 0.8f, 0.8f);
|
||||
listButtonStyleSelected.normal.background = CreateBackgroundColorImage(new Color(0.4f, 0.4f, 0.4f));
|
||||
}
|
||||
else
|
||||
{
|
||||
listButtonStyleSelected.normal.textColor = Color.black;
|
||||
listButtonStyleSelected.normal.background = CreateBackgroundColorImage(new Color(0.75f, 0.75f, 0.75f));
|
||||
}
|
||||
|
||||
scrollViewSeparatorStyle = new GUIStyle("Toolbar");
|
||||
scrollViewSeparatorStyle.fixedWidth = SdkWindowWidth + 10;
|
||||
scrollViewSeparatorStyle.fixedHeight = 4;
|
||||
scrollViewSeparatorStyle.margin.top = 1;
|
||||
|
||||
searchBarStyle = new GUIStyle("Toolbar");
|
||||
searchBarStyle.fixedWidth = SdkWindowWidth - 8;
|
||||
searchBarStyle.fixedHeight = 23;
|
||||
searchBarStyle.padding.top = 3;
|
||||
|
||||
accountWindowStyle = new GUIStyle("window")
|
||||
{
|
||||
padding = new RectOffset(10, 10, 10, 10),
|
||||
margin = new RectOffset(0,0,30,30)
|
||||
};
|
||||
|
||||
centeredLabelStyle = new GUIStyle(EditorStyles.boldLabel)
|
||||
{
|
||||
alignment = TextAnchor.UpperCenter,
|
||||
margin = new RectOffset(0,0,0,10)
|
||||
};
|
||||
|
||||
contentDescriptionStyle = new GUIStyle(EditorStyles.wordWrappedLabel)
|
||||
{
|
||||
wordWrap = true
|
||||
};
|
||||
|
||||
contentTitleStyle = new GUIStyle(EditorStyles.boldLabel)
|
||||
{
|
||||
wordWrap = true
|
||||
};
|
||||
|
||||
unityUpgradeBannerStyle = new GUIStyle
|
||||
{
|
||||
normal = new GUIStyleState
|
||||
{
|
||||
background = Resources.Load<Texture2D>("vrcSdkMigrateTo2022Splash")
|
||||
},
|
||||
alignment = TextAnchor.LowerCenter,
|
||||
margin = new RectOffset(0,0,20,0),
|
||||
fixedWidth = 506,
|
||||
fixedHeight = 148
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void Init()
|
||||
{
|
||||
InitializeStyles();
|
||||
ResetIssues();
|
||||
InitAccount();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
OnEnableAccount();
|
||||
_stylesInitialized = false;
|
||||
OnPanelLoggedIn -= RestoreTab;
|
||||
OnPanelLoggedIn += RestoreTab;
|
||||
OnUserPlatformsFetched -= RefreshPlatformSwitcher;
|
||||
OnUserPlatformsFetched += RefreshPlatformSwitcher;
|
||||
AssemblyReloadEvents.afterAssemblyReload -= BuilderAssemblyReload;
|
||||
AssemblyReloadEvents.afterAssemblyReload += BuilderAssemblyReload;
|
||||
OnSdkPanelEnable?.Invoke(this, null);
|
||||
_panelState = SdkPanelState.Idle;
|
||||
OnSdkPanelStateChange?.Invoke(this, _panelState);
|
||||
TabsEnabled = true;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
OnPanelLoggedIn -= RestoreTab;
|
||||
OnUserPlatformsFetched -= RefreshPlatformSwitcher;
|
||||
AssemblyReloadEvents.afterAssemblyReload -= BuilderAssemblyReload;
|
||||
OnSdkPanelDisable?.Invoke(this, null);
|
||||
_panelState = SdkPanelState.Idle;
|
||||
OnSdkPanelStateChange?.Invoke(this, _panelState);
|
||||
}
|
||||
|
||||
private void RestoreTab(object sender, APIUser e)
|
||||
{
|
||||
rootVisualElement.schedule.Execute(() =>
|
||||
{
|
||||
SelectTab(PanelTab.Builder);
|
||||
}).ExecuteLater(200);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
AccountDestroy();
|
||||
}
|
||||
|
||||
public const int SdkWindowWidth = 512;
|
||||
|
||||
private readonly bool[] _toolbarOptionsLoggedIn = new bool[4] {true, true, true, true};
|
||||
private readonly bool[] _toolbarOptionsNotLoggedIn = new bool[4] {true, false, false, true};
|
||||
private bool _stylesInitialized;
|
||||
|
||||
private SdkPanelState _panelState = SdkPanelState.Idle;
|
||||
|
||||
private Button _authenticationTabBtn;
|
||||
private Button _buildTabBtn;
|
||||
private Button _contentManagerTabBtn;
|
||||
private Button _settingsTabBtn;
|
||||
private Button[] _tabButtons;
|
||||
private VisualElement _sdkPanel;
|
||||
private VisualElement _builderPanel;
|
||||
private VisualTreeAsset _builderPanelLayout;
|
||||
private StyleSheet _builderPanelStyles;
|
||||
private float _windowOpenTime;
|
||||
|
||||
private bool _tabsEnabled;
|
||||
internal bool TabsEnabled
|
||||
{
|
||||
get => _tabsEnabled;
|
||||
set
|
||||
{
|
||||
_tabsEnabled = value;
|
||||
if (_tabButtons != null)
|
||||
{
|
||||
foreach (var button in _tabButtons)
|
||||
{
|
||||
button.SetEnabled(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum PanelTab
|
||||
{
|
||||
Account,
|
||||
Builder,
|
||||
ContentManager,
|
||||
Settings
|
||||
}
|
||||
|
||||
private void CreateGUI()
|
||||
{
|
||||
if (window == null)
|
||||
{
|
||||
window = (VRCSdkControlPanel)EditorWindow.GetWindow(typeof(VRCSdkControlPanel));
|
||||
}
|
||||
|
||||
_windowOpenTime = Time.realtimeSinceStartup;
|
||||
|
||||
var visualTree = Resources.Load<VisualTreeAsset>("VRCSdkPanelLayout");
|
||||
visualTree.CloneTree(rootVisualElement);
|
||||
var styles = Resources.Load<StyleSheet>("VRCSdkPanelStyles");
|
||||
rootVisualElement.styleSheets.Add(styles);
|
||||
rootVisualElement.AddToClassList(EditorGUIUtility.isProSkin ? "dark" : "light");
|
||||
|
||||
_sdkPanel = rootVisualElement.Q("sdk-container");
|
||||
_builderPanel = rootVisualElement.Q("builder-panel");
|
||||
|
||||
CreateTabs();
|
||||
RenderTabs();
|
||||
|
||||
rootVisualElement.schedule.Execute(() =>
|
||||
{
|
||||
var currentPanel = VRCSettings.ActiveWindowPanel;
|
||||
if (EditorApplication.isPlaying && currentPanel != 0)
|
||||
{
|
||||
SelectTab(PanelTab.Account);
|
||||
return;
|
||||
}
|
||||
// Check that the tabs are enabled, if not - we must re-render tabs
|
||||
if (APIUser.IsLoggedIn && (!_tabButtons[1].enabledSelf || !_tabButtons[2].enabledSelf))
|
||||
{
|
||||
RenderTabs();
|
||||
return;
|
||||
}
|
||||
// When the user isn't logged in - we only allow Settings and Authentication tabs to be viewed
|
||||
if (APIUser.IsLoggedIn || currentPanel == 0 || currentPanel == 3) return;
|
||||
SelectTab(PanelTab.Account);
|
||||
}).Every(500);
|
||||
|
||||
var sdkContainer = rootVisualElement.Q("sdk-container");
|
||||
sdkContainer.Add(new IMGUIContainer(() =>
|
||||
{
|
||||
if (!_stylesInitialized)
|
||||
{
|
||||
InitializeStyles();
|
||||
_stylesInitialized = true;
|
||||
}
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.BeginVertical();
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
GUI.enabled = false;
|
||||
GUILayout.Space(20);
|
||||
EditorGUILayout.LabelField("Unity Application is running ...\nStop it to access the Control Panel", titleGuiStyle, GUILayout.Width(SdkWindowWidth));
|
||||
GUI.enabled = true;
|
||||
GUILayout.EndVertical();
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.EndHorizontal();
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
GUILayout.EndVertical();
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
switch ((PanelTab) VRCSettings.ActiveWindowPanel)
|
||||
{
|
||||
case PanelTab.Builder:
|
||||
break;
|
||||
case PanelTab.ContentManager:
|
||||
ShowContent();
|
||||
break;
|
||||
case PanelTab.Settings:
|
||||
ShowSettings();
|
||||
break;
|
||||
case PanelTab.Account:
|
||||
default:
|
||||
ShowAccount();
|
||||
break;
|
||||
}
|
||||
}));
|
||||
|
||||
EnvConfig.SetActiveSDKDefines();
|
||||
}
|
||||
|
||||
private void CreateTabs()
|
||||
{
|
||||
_authenticationTabBtn = rootVisualElement.Q<Button>("tab-authentication");
|
||||
_buildTabBtn = rootVisualElement.Q<Button>("tab-builder");
|
||||
_contentManagerTabBtn = rootVisualElement.Q<Button>("tab-content-manager");
|
||||
_settingsTabBtn = rootVisualElement.Q<Button>("tab-settings");
|
||||
|
||||
_tabButtons = new[]
|
||||
{
|
||||
_authenticationTabBtn,
|
||||
_buildTabBtn,
|
||||
_contentManagerTabBtn,
|
||||
_settingsTabBtn
|
||||
};
|
||||
|
||||
var currentPanel = VRCSettings.ActiveWindowPanel;
|
||||
|
||||
for (int i = 0; i < _tabButtons.Length; i++)
|
||||
{
|
||||
var btnIndex = i;
|
||||
_tabButtons[i].EnableInClassList("active", currentPanel == btnIndex);
|
||||
_tabButtons[i].SetEnabled(APIUser.IsLoggedIn ? _toolbarOptionsLoggedIn[i] : _toolbarOptionsNotLoggedIn[i]);
|
||||
|
||||
_tabButtons[i].clicked += () => SelectTab((PanelTab) btnIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectTab(PanelTab tab)
|
||||
{
|
||||
if (_tabButtons == null) return;
|
||||
if (EditorApplication.isPlaying) return;
|
||||
if (VRCSettings.ActiveWindowPanel == (int) tab)
|
||||
{
|
||||
return;
|
||||
}
|
||||
VRCSettings.ActiveWindowPanel = (int) tab;
|
||||
RenderTabs();
|
||||
}
|
||||
|
||||
internal PanelTab CurrentTab => (PanelTab) VRCSettings.ActiveWindowPanel;
|
||||
|
||||
private void RenderTabs()
|
||||
{
|
||||
var currentPanel = (PanelTab) VRCSettings.ActiveWindowPanel;
|
||||
|
||||
if (currentPanel != PanelTab.Builder)
|
||||
{
|
||||
if (_defaultHeaderImage == null)
|
||||
{
|
||||
_defaultHeaderImage = Resources.Load<Texture2D>("SDK_Panel_Banner");
|
||||
}
|
||||
rootVisualElement.Q("banner").style.backgroundImage = _defaultHeaderImage;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _tabButtons.Length; i++)
|
||||
{
|
||||
_tabButtons[i].EnableInClassList("active", currentPanel == (PanelTab) i);
|
||||
if (!TabsEnabled) continue;
|
||||
_tabButtons[i].SetEnabled(APIUser.IsLoggedIn ? _toolbarOptionsLoggedIn[i] : _toolbarOptionsNotLoggedIn[i]);
|
||||
}
|
||||
|
||||
if (currentPanel == PanelTab.Builder)
|
||||
{
|
||||
if (_builderPanel.childCount != 0) return;
|
||||
ShowBuilders();
|
||||
_builderPanel.RemoveFromClassList("d-none");
|
||||
_sdkPanel.AddToClassList("d-none");
|
||||
}
|
||||
else if (_builderPanel.childCount == 1)
|
||||
{
|
||||
_builderPanel.AddToClassList("d-none");
|
||||
_sdkPanel.RemoveFromClassList("d-none");
|
||||
_builderPanel.Remove(_builderPanel.Children().First());
|
||||
_builderPanel.styleSheets.Remove(_builderPanelStyles);
|
||||
}
|
||||
}
|
||||
|
||||
[UnityEditor.Callbacks.PostProcessScene]
|
||||
static void OnPostProcessScene()
|
||||
{
|
||||
if (window != null)
|
||||
window.Reset();
|
||||
}
|
||||
|
||||
private void OnFocus()
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
ResetIssues();
|
||||
// style backgrounds may be nulled on scene load. detect if so has happened
|
||||
if((boxGuiStyle != null) && (boxGuiStyle.normal.background == null))
|
||||
InitializeStyles();
|
||||
}
|
||||
|
||||
[UnityEditor.Callbacks.DidReloadScripts(int.MaxValue)]
|
||||
static void DidReloadScripts()
|
||||
{
|
||||
try
|
||||
{
|
||||
RefreshApiUrlSetting();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Unity's Mono is trash and randomly fails to assemblies types.
|
||||
Debug.LogException(e);
|
||||
}
|
||||
}
|
||||
|
||||
#region Internal API
|
||||
|
||||
[UsedImplicitly]
|
||||
internal void SetPanelIdle()
|
||||
{
|
||||
_panelState = SdkPanelState.Idle;
|
||||
OnSdkPanelStateChange?.Invoke(this, _panelState);
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
internal void SetPanelBuilding()
|
||||
{
|
||||
_panelState = SdkPanelState.Building;
|
||||
OnSdkPanelStateChange?.Invoke(this, _panelState);
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
internal void SetPanelUploading()
|
||||
{
|
||||
_panelState = SdkPanelState.Uploading;
|
||||
OnSdkPanelStateChange?.Invoke(this, _panelState);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
public static event EventHandler OnSdkPanelEnable;
|
||||
public static event EventHandler OnSdkPanelDisable;
|
||||
public static event EventHandler<SdkPanelState> OnSdkPanelStateChange;
|
||||
public SdkPanelState PanelState => _panelState;
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20b4cdbdda9655947aab6f8f2c90690f
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,686 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using VRC.Core;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine.UIElements;
|
||||
using VRC.SDKBase.Editor;
|
||||
|
||||
public enum TwoFactorType
|
||||
{
|
||||
None,
|
||||
TOTP,
|
||||
Email,
|
||||
}
|
||||
|
||||
// This file handles the Account tab of the SDK Panel
|
||||
|
||||
public partial class VRCSdkControlPanel : EditorWindow
|
||||
{
|
||||
static bool isInitialized = false;
|
||||
static string clientInstallPath;
|
||||
static bool signingIn = false;
|
||||
static double latestSignInTime = -1.0d;
|
||||
static string error = null;
|
||||
|
||||
private const string UNITY_UPGRADE_PROMPT_URL = "https://creators.vrchat.com/sdk/upgrade/unity-2022";
|
||||
private const double LogoutCooldownAfterLogin = 0.75d;
|
||||
|
||||
public static bool FutureProofPublishEnabled { get { return UnityEditor.EditorPrefs.GetBool("futureProofPublish", DefaultFutureProofPublishEnabled); } }
|
||||
//public static bool DefaultFutureProofPublishEnabled { get { return !SDKClientUtilities.IsInternalSDK(); } }
|
||||
public static bool DefaultFutureProofPublishEnabled { get { return false; } }
|
||||
|
||||
internal static event EventHandler<APIUser> OnPanelLoggedIn;
|
||||
internal static event EventHandler OnPanelLoggedOut;
|
||||
internal static event EventHandler<ApiUserPlatforms> OnUserPlatformsFetched;
|
||||
|
||||
static string storedUsername
|
||||
{
|
||||
get
|
||||
{
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
EditorPrefs.DeleteKey("sdk#username");
|
||||
}
|
||||
}
|
||||
|
||||
static string storedPassword
|
||||
{
|
||||
get
|
||||
{
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
EditorPrefs.DeleteKey("sdk#password");
|
||||
}
|
||||
}
|
||||
|
||||
static string username { get; set; } = null;
|
||||
static string password { get; set; } = null;
|
||||
|
||||
public static ApiServerEnvironment ApiEnvironment => serverEnvironment;
|
||||
static ApiServerEnvironment serverEnvironment
|
||||
{
|
||||
get
|
||||
{
|
||||
ApiServerEnvironment env = ApiServerEnvironment.Release;
|
||||
try
|
||||
{
|
||||
env = (ApiServerEnvironment)System.Enum.Parse(typeof(ApiServerEnvironment), UnityEditor.EditorPrefs.GetString("VRC_ApiServerEnvironment", env.ToString()));
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError("Invalid server environment name - " + e.ToString());
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
set
|
||||
{
|
||||
UnityEditor.EditorPrefs.SetString("VRC_ApiServerEnvironment", value.ToString());
|
||||
|
||||
API.SetApiUrlFromEnvironment(value);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnableAccount()
|
||||
{
|
||||
entered2faCodeIsInvalid = false;
|
||||
warningIconGraphic = Resources.Load("2FAIcons/SDK_Warning_Triangle_icon") as Texture2D;
|
||||
}
|
||||
|
||||
public static void RefreshApiUrlSetting()
|
||||
{
|
||||
// this forces the static api url variable to be reset from the server environment set in editor prefs.
|
||||
// needed because the static variable states get cleared when entering / exiting play mode
|
||||
ApiServerEnvironment env = serverEnvironment;
|
||||
serverEnvironment = env;
|
||||
}
|
||||
|
||||
public static void InitAccount()
|
||||
{
|
||||
if (isInitialized)
|
||||
return;
|
||||
|
||||
if (!APIUser.IsLoggedIn && ApiCredentials.Load())
|
||||
{
|
||||
APIUser.InitialFetchCurrentUser(c =>
|
||||
{
|
||||
window.rootVisualElement.Q<IMGUIContainer>().MarkDirtyRepaint();
|
||||
if (c.Model is not APIUser apiUser)
|
||||
{
|
||||
VRC.Core.Logger.LogError("Failed to load user information, please log in again");
|
||||
return;
|
||||
}
|
||||
AnalyticsSDK.LoggedInUserChanged(apiUser);
|
||||
|
||||
OnPanelLoggedIn?.Invoke(window, apiUser);
|
||||
ApiUserPlatforms.Fetch(apiUser.id, userPlatforms =>
|
||||
{
|
||||
OnUserPlatformsFetched?.Invoke(window, userPlatforms);
|
||||
}, null);
|
||||
}, null);
|
||||
}
|
||||
|
||||
// This code proceeds without waiting for the user fetch above to complete
|
||||
clientInstallPath = SDKClientUtilities.GetSavedVRCInstallPath();
|
||||
if (string.IsNullOrEmpty(clientInstallPath))
|
||||
clientInstallPath = SDKClientUtilities.LoadRegistryVRCInstallPath();
|
||||
|
||||
signingIn = false;
|
||||
isInitialized = true;
|
||||
|
||||
ClearContent();
|
||||
}
|
||||
|
||||
void OnAccountGUI()
|
||||
{
|
||||
using (new GUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
AccountWindowGUI();
|
||||
GUILayout.FlexibleSpace();
|
||||
}
|
||||
}
|
||||
|
||||
void AccountWindowGUI()
|
||||
{
|
||||
using (new EditorGUILayout.VerticalScope(accountWindowStyle, GUILayout.Width(340)))
|
||||
{
|
||||
EditorGUILayout.LabelField("Account", centeredLabelStyle);
|
||||
if (signingIn)
|
||||
{
|
||||
if (twoFactorAuthenticationEntryType == TwoFactorType.None)
|
||||
{
|
||||
EditorGUILayout.LabelField("Signing in as " + username + ".");
|
||||
}
|
||||
OnTwoFactorAuthenticationGUI(twoFactorAuthenticationEntryType);
|
||||
return;
|
||||
}
|
||||
|
||||
if (APIUser.IsLoggedIn)
|
||||
{
|
||||
if (Status != "Connected")
|
||||
{
|
||||
EditorGUILayout.LabelField(Status);
|
||||
}
|
||||
|
||||
OnCreatorStatusGUI();
|
||||
|
||||
// Add a space, pushing this away from the line that contained "Verify" on the previous page.
|
||||
EditorGUILayout.Space(EditorGUIUtility.singleLineHeight);
|
||||
|
||||
// Disable the logout button for a short amount of time after logging in.
|
||||
// This attempts to patch UX difficulties where users try to click "Verify" on the previous screen as
|
||||
// 2FA automatically submits so they end up accidentally clicking Logout instead.
|
||||
bool wasRecentLogin = latestSignInTime >= 0.0d && EditorApplication.timeSinceStartup < latestSignInTime + LogoutCooldownAfterLogin;
|
||||
using (new EditorGUI.DisabledScope(wasRecentLogin))
|
||||
{
|
||||
if (GUILayout.Button("Logout"))
|
||||
{
|
||||
storedUsername = username = null;
|
||||
storedPassword = password = null;
|
||||
|
||||
VRC.Tools.ClearCookies();
|
||||
APIUser.Logout();
|
||||
ClearContent();
|
||||
OnPanelLoggedOut?.Invoke(window, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
InitAccount();
|
||||
|
||||
ApiServerEnvironment newEnv = ApiServerEnvironment.Release;
|
||||
if (VRCSettings.DisplayAdvancedSettings)
|
||||
newEnv = (ApiServerEnvironment)EditorGUILayout.EnumPopup("Use API", serverEnvironment);
|
||||
if (serverEnvironment != newEnv)
|
||||
serverEnvironment = newEnv;
|
||||
|
||||
const string controlNameUser = "input_user";
|
||||
const string controlNamePass = "input_pass";
|
||||
|
||||
GUI.SetNextControlName(controlNameUser);
|
||||
username = EditorGUILayout.TextField("Username/Email", username);
|
||||
GUI.SetNextControlName(controlNamePass);
|
||||
password = EditorGUILayout.PasswordField("Password", password);
|
||||
|
||||
bool attemptKeyboardSignIn = false;
|
||||
if (
|
||||
Event.current != null &&
|
||||
Event.current.type == EventType.KeyUp &&
|
||||
(Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter))
|
||||
{
|
||||
switch (GUI.GetNameOfFocusedControl())
|
||||
{
|
||||
case controlNameUser:
|
||||
GUI.FocusControl(controlNamePass);
|
||||
break;
|
||||
case controlNamePass:
|
||||
attemptKeyboardSignIn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Sign In") || attemptKeyboardSignIn)
|
||||
{
|
||||
SignIn(true);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Sign up"))
|
||||
{
|
||||
Application.OpenURL("https://vrchat.com/register");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void OnCreatorStatusGUI()
|
||||
{
|
||||
EditorGUILayout.LabelField("Logged in as:", APIUser.CurrentUser.displayName);
|
||||
|
||||
//if (SDKClientUtilities.IsInternalSDK())
|
||||
// EditorGUILayout.LabelField("Developer Status: ", APIUser.CurrentUser.developerType.ToString());
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
EditorGUILayout.BeginVertical();
|
||||
EditorGUILayout.LabelField("World Creator Status: ", APIUser.CurrentUser.canPublishWorlds ? "Allowed to publish worlds" : "Not yet allowed to publish worlds");
|
||||
EditorGUILayout.LabelField("Avatar Creator Status: ", APIUser.CurrentUser.canPublishAvatars ? "Allowed to publish avatars" : "Not yet allowed to publish avatars");
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
if (!APIUser.CurrentUser.canPublishWorldsAndAvatars)
|
||||
{
|
||||
if (GUILayout.Button("More Info..."))
|
||||
{
|
||||
ShowContentPublishPermissionsDialog();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
void ShowAccount()
|
||||
{
|
||||
if (ConfigManager.RemoteConfig.IsInitialized())
|
||||
{
|
||||
if (ConfigManager.RemoteConfig.HasKey("sdkUnityVersion"))
|
||||
{
|
||||
string sdkUnityVersion = ConfigManager.RemoteConfig.GetString("sdkUnityVersion");
|
||||
if (string.IsNullOrEmpty(sdkUnityVersion))
|
||||
EditorGUILayout.LabelField("Could not fetch remote config.");
|
||||
else if (Application.unityVersion != sdkUnityVersion)
|
||||
{
|
||||
var currentVersion = UnityVersion.Parse(Application.unityVersion).major;
|
||||
var sdkVersion = UnityVersion.Parse(sdkUnityVersion).major;
|
||||
if (currentVersion < sdkVersion && sdkVersion == 2022)
|
||||
{
|
||||
using (new EditorGUILayout.VerticalScope(unityUpgradeBannerStyle))
|
||||
{
|
||||
EditorGUILayout.Space(115);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
if (GUILayout.Button("Learn More"))
|
||||
{
|
||||
Application.OpenURL(UNITY_UPGRADE_PROMPT_URL);
|
||||
}
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
EditorGUILayout.Space(5);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.LabelField("Unity Version", EditorStyles.boldLabel);
|
||||
EditorGUILayout.LabelField("Wrong Unity version. Please use " + sdkUnityVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
API.SetOnlineMode(true);
|
||||
ConfigManager.RemoteConfig.Init();
|
||||
}
|
||||
|
||||
OnAccountGUI();
|
||||
}
|
||||
|
||||
|
||||
private const string TWO_FACTOR_AUTHENTICATION_HELP_URL = "https://docs.vrchat.com/docs/setup-2fa";
|
||||
|
||||
private const string ENTER_2FA_CODE_TITLE_STRING = "Enter a numeric code from your authenticator app.";
|
||||
private const string ENTER_2FA_CODE_LABEL_STRING = "Code:";
|
||||
|
||||
private const string ENTER_2FA_CODE_GUI_EVENT = "Authentication Code Field";
|
||||
|
||||
private const string ENTER_EMAIL_2FA_CODE_TITLE_STRING = "Check your email for a numeric code.";
|
||||
|
||||
private const string CHECKING_2FA_CODE_STRING = "Checking code...";
|
||||
private const string ENTER_2FA_CODE_INVALID_CODE_STRING = "Oops, that code didn't work.\nTry again!";
|
||||
|
||||
private const string ENTER_2FA_CODE_VERIFY_STRING = "Verify";
|
||||
private const string ENTER_2FA_CODE_CANCEL_STRING = "Cancel";
|
||||
private const string ENTER_2FA_CODE_HELP_STRING = "Help";
|
||||
|
||||
private const int WARNING_ICON_SIZE = 60;
|
||||
private const int WARNING_FONT_HEIGHT = 14;
|
||||
|
||||
static private Texture2D warningIconGraphic;
|
||||
|
||||
static bool entered2faCodeIsInvalid;
|
||||
static bool authorizationCodeWasVerified;
|
||||
|
||||
static private int previousAuthenticationCodeLength = 0;
|
||||
static bool checkingCode;
|
||||
static string authenticationCode = "";
|
||||
static string lastCheckedAuthenticationCode = "";
|
||||
|
||||
static System.Action onAuthenticationVerifiedAction;
|
||||
|
||||
static TwoFactorType _twoFactorAuthenticationEntryType = TwoFactorType.None;
|
||||
|
||||
static TwoFactorType twoFactorAuthenticationEntryType
|
||||
{
|
||||
get
|
||||
{
|
||||
return _twoFactorAuthenticationEntryType;
|
||||
}
|
||||
set
|
||||
{
|
||||
_twoFactorAuthenticationEntryType = value;
|
||||
authenticationCode = "";
|
||||
lastCheckedAuthenticationCode = "";
|
||||
if (_twoFactorAuthenticationEntryType == TwoFactorType.None && !authorizationCodeWasVerified)
|
||||
Logout();
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsValidAuthenticationCodeFormat()
|
||||
{
|
||||
bool isValid2faAuthenticationCode = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(authenticationCode))
|
||||
{
|
||||
// check if the input is a valid 6-digit numberic code (ignoring spaces)
|
||||
Regex rx = new Regex(@"^(\s*\d\s*){6}$", RegexOptions.Compiled);
|
||||
MatchCollection matches6DigitCode = rx.Matches(authenticationCode);
|
||||
isValid2faAuthenticationCode = (matches6DigitCode.Count == 1);
|
||||
}
|
||||
|
||||
return isValid2faAuthenticationCode;
|
||||
}
|
||||
|
||||
static bool IsValidRecoveryCodeFormat()
|
||||
{
|
||||
bool isValid2faRecoveryCode = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(authenticationCode))
|
||||
{
|
||||
// check if the input is a valid 8-digit alpha-numberic code (format xxxx-xxxx) "-" is optional & ignore any spaces
|
||||
// OTP codes also exclude the letters i,l,o and the digit 1 to prevent any confusion
|
||||
Regex rx = new Regex(@"^(\s*[a-hj-km-np-zA-HJ-KM-NP-Z02-9]\s*){4}-?(\s*[a-hj-km-np-zA-HJ-KM-NP-Z02-9]\s*){4}$", RegexOptions.Compiled);
|
||||
MatchCollection matchesRecoveryCode = rx.Matches(authenticationCode);
|
||||
isValid2faRecoveryCode = (matchesRecoveryCode.Count == 1);
|
||||
}
|
||||
|
||||
return isValid2faRecoveryCode;
|
||||
}
|
||||
|
||||
static void OnTwoFactorAuthenticationGUI(TwoFactorType twoFactorType)
|
||||
{
|
||||
if (twoFactorType == TwoFactorType.None)
|
||||
return;
|
||||
|
||||
const int ENTER_2FA_CODE_BORDER_SIZE = 20;
|
||||
const int ENTER_2FA_CODE_BUTTON_WIDTH = 260;
|
||||
const int ENTER_2FA_CODE_VERIFY_BUTTON_WIDTH = ENTER_2FA_CODE_BUTTON_WIDTH / 2;
|
||||
const int ENTER_2FA_CODE_ENTRY_REGION_WIDTH = 130;
|
||||
const int ENTER_2FA_CODE_MIN_WINDOW_WIDTH = ENTER_2FA_CODE_VERIFY_BUTTON_WIDTH + ENTER_2FA_CODE_ENTRY_REGION_WIDTH + (ENTER_2FA_CODE_BORDER_SIZE * 3);
|
||||
|
||||
bool isValidAuthenticationCode = IsValidAuthenticationCodeFormat();
|
||||
|
||||
|
||||
// Invalid code text
|
||||
if (entered2faCodeIsInvalid)
|
||||
{
|
||||
GUIStyle s = new GUIStyle(EditorStyles.label)
|
||||
{
|
||||
normal =
|
||||
{
|
||||
textColor = new Color(1,0.3f,0.3f)
|
||||
},
|
||||
fontSize = WARNING_FONT_HEIGHT,
|
||||
fixedHeight = WARNING_ICON_SIZE
|
||||
};
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
using (new EditorGUILayout.VerticalScope())
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.Label(new GUIContent(warningIconGraphic), GUILayout.Width(WARNING_ICON_SIZE), GUILayout.Height(WARNING_ICON_SIZE));
|
||||
EditorGUILayout.LabelField(ENTER_2FA_CODE_INVALID_CODE_STRING, s);
|
||||
}
|
||||
}
|
||||
GUILayout.FlexibleSpace();
|
||||
}
|
||||
}
|
||||
else if (checkingCode)
|
||||
{
|
||||
// Display checking code message
|
||||
EditorGUILayout.BeginVertical();
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUIStyle s = new GUIStyle(EditorStyles.label);
|
||||
s.alignment = TextAnchor.MiddleCenter;
|
||||
s.fixedHeight = WARNING_ICON_SIZE;
|
||||
EditorGUILayout.LabelField(CHECKING_2FA_CODE_STRING, s, GUILayout.Height(WARNING_ICON_SIZE));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Space(ENTER_2FA_CODE_BORDER_SIZE);
|
||||
GUILayout.FlexibleSpace();
|
||||
GUIStyle titleStyle = new GUIStyle(EditorStyles.label);
|
||||
titleStyle.alignment = TextAnchor.MiddleCenter;
|
||||
titleStyle.wordWrap = true;
|
||||
string twofactorTitle = "Enter Code";
|
||||
switch (twoFactorType)
|
||||
{
|
||||
case TwoFactorType.TOTP:
|
||||
twofactorTitle = ENTER_2FA_CODE_TITLE_STRING;
|
||||
break;
|
||||
case TwoFactorType.Email:
|
||||
twofactorTitle = ENTER_EMAIL_2FA_CODE_TITLE_STRING;
|
||||
break;
|
||||
}
|
||||
EditorGUILayout.LabelField(twofactorTitle, titleStyle, GUILayout.Width(ENTER_2FA_CODE_MIN_WINDOW_WIDTH - (2 * ENTER_2FA_CODE_BORDER_SIZE)), GUILayout.Height(WARNING_ICON_SIZE), GUILayout.ExpandHeight(true));
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.Space(ENTER_2FA_CODE_BORDER_SIZE);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Space(ENTER_2FA_CODE_BORDER_SIZE);
|
||||
GUILayout.FlexibleSpace();
|
||||
Vector2 size = EditorStyles.boldLabel.CalcSize(new GUIContent(ENTER_2FA_CODE_LABEL_STRING));
|
||||
|
||||
EditorGUILayout.LabelField(ENTER_2FA_CODE_LABEL_STRING, EditorStyles.boldLabel, GUILayout.MaxWidth(size.x));
|
||||
authenticationCode = EditorGUILayout.TextField(authenticationCode);
|
||||
|
||||
|
||||
// Verify 2FA code button
|
||||
if (lastCheckedAuthenticationCode != authenticationCode && IsValidAuthenticationCodeFormat()
|
||||
|| GUILayout.Button(ENTER_2FA_CODE_VERIFY_STRING, GUILayout.Width(ENTER_2FA_CODE_VERIFY_BUTTON_WIDTH)))
|
||||
{
|
||||
lastCheckedAuthenticationCode = authenticationCode;
|
||||
checkingCode = true;
|
||||
string authCodeType = API2FA.TIME_BASED_ONE_TIME_PASSWORD_AUTHENTICATION;
|
||||
switch (twoFactorType)
|
||||
{
|
||||
case TwoFactorType.TOTP:
|
||||
authCodeType = API2FA.TIME_BASED_ONE_TIME_PASSWORD_AUTHENTICATION;
|
||||
break;
|
||||
case TwoFactorType.Email:
|
||||
authCodeType = API2FA.EMAIL_BASED_ONE_TIME_PASSWORD_AUTHENTICATION;
|
||||
break;
|
||||
}
|
||||
APIUser.VerifyTwoFactorAuthCode(authenticationCode, authCodeType, username, password,
|
||||
delegate
|
||||
{
|
||||
// valid 2FA code submitted
|
||||
entered2faCodeIsInvalid = false;
|
||||
authorizationCodeWasVerified = true;
|
||||
checkingCode = false;
|
||||
twoFactorAuthenticationEntryType = TwoFactorType.None;
|
||||
if (null != onAuthenticationVerifiedAction)
|
||||
onAuthenticationVerifiedAction();
|
||||
},
|
||||
delegate
|
||||
{
|
||||
entered2faCodeIsInvalid = true;
|
||||
checkingCode = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.Space(ENTER_2FA_CODE_BORDER_SIZE);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
// after user has entered an invalid code causing the invalid code message to be displayed,
|
||||
// edit the code will change it's length meaning it is invalid format, so we can clear the invalid code setting until they resubmit
|
||||
if (previousAuthenticationCodeLength != authenticationCode.Length)
|
||||
{
|
||||
previousAuthenticationCodeLength = authenticationCode.Length;
|
||||
entered2faCodeIsInvalid = false;
|
||||
}
|
||||
|
||||
GUI.enabled = true;
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.Space(ENTER_2FA_CODE_BORDER_SIZE);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
// Two-Factor Authentication Help button
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button(ENTER_2FA_CODE_HELP_STRING))
|
||||
{
|
||||
Application.OpenURL(TWO_FACTOR_AUTHENTICATION_HELP_URL);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
// Cancel button
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button(ENTER_2FA_CODE_CANCEL_STRING))
|
||||
{
|
||||
twoFactorAuthenticationEntryType = TwoFactorType.None;
|
||||
Logout();
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private static string Status
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!APIUser.IsLoggedIn)
|
||||
return error == null ? "Please log in." : "Error in authenticating: " + error;
|
||||
if (signingIn)
|
||||
return "Logging in.";
|
||||
else
|
||||
{
|
||||
if( serverEnvironment == ApiServerEnvironment.Dev )
|
||||
return "Connected to " + serverEnvironment.ToString();
|
||||
return "Connected";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnAuthenticationCompleted()
|
||||
{
|
||||
AttemptLogin();
|
||||
}
|
||||
|
||||
private static void AttemptLogin()
|
||||
{
|
||||
APIUser.Login(username, password,
|
||||
delegate (ApiModelContainer<APIUser> c)
|
||||
{
|
||||
APIUser user = c.Model as APIUser;
|
||||
if (c.Cookies.ContainsKey("twoFactorAuth"))
|
||||
ApiCredentials.Set(user.username, username, "vrchat", c.Cookies["auth"], c.Cookies["twoFactorAuth"]);
|
||||
else if (c.Cookies.ContainsKey("auth"))
|
||||
ApiCredentials.Set(user.username, username, "vrchat", c.Cookies["auth"]);
|
||||
else
|
||||
ApiCredentials.SetHumanName(user.username);
|
||||
signingIn = false;
|
||||
latestSignInTime = EditorApplication.timeSinceStartup;
|
||||
error = null;
|
||||
storedUsername = null;
|
||||
storedPassword = null;
|
||||
AnalyticsSDK.LoggedInUserChanged(user);
|
||||
|
||||
OnPanelLoggedIn?.Invoke(window, user);
|
||||
|
||||
if (!APIUser.CurrentUser.canPublishWorldsAndAvatars)
|
||||
{
|
||||
if (UnityEditor.SessionState.GetString("HasShownContentPublishPermissionsDialogForUser", "") != user.id)
|
||||
{
|
||||
UnityEditor.SessionState.SetString("HasShownContentPublishPermissionsDialogForUser", user.id);
|
||||
VRCSdkControlPanel.ShowContentPublishPermissionsDialog();
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch platforms that the user can publish to
|
||||
ApiUserPlatforms.Fetch(user.id, userPlatforms => {
|
||||
OnUserPlatformsFetched?.Invoke(window, userPlatforms);
|
||||
}, null);
|
||||
},
|
||||
delegate (ApiModelContainer<APIUser> c)
|
||||
{
|
||||
Logout();
|
||||
error = c.Error;
|
||||
EditorUtility.DisplayDialog("Error logging in", error, "OK");
|
||||
VRC.Core.Logger.Log("Error logging in: " + error);
|
||||
},
|
||||
delegate (ApiModelContainer<API2FA> c)
|
||||
{
|
||||
window.rootVisualElement.Q<IMGUIContainer>().MarkDirtyRepaint();
|
||||
if (c.Cookies.ContainsKey("auth"))
|
||||
ApiCredentials.Set(username, username, "vrchat", c.Cookies["auth"]);
|
||||
API2FA model2FA = c.Model as API2FA;
|
||||
if (model2FA.requiresTwoFactorAuth.Contains(API2FA.TIME_BASED_ONE_TIME_PASSWORD_AUTHENTICATION))
|
||||
twoFactorAuthenticationEntryType = TwoFactorType.TOTP;
|
||||
else if (model2FA.requiresTwoFactorAuth.Contains(API2FA.EMAIL_BASED_ONE_TIME_PASSWORD_AUTHENTICATION))
|
||||
twoFactorAuthenticationEntryType = TwoFactorType.Email;
|
||||
else
|
||||
twoFactorAuthenticationEntryType = TwoFactorType.None;
|
||||
onAuthenticationVerifiedAction = OnAuthenticationCompleted;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static object syncObject = new object();
|
||||
private void SignIn(bool explicitAttempt)
|
||||
{
|
||||
lock (syncObject)
|
||||
{
|
||||
if (signingIn
|
||||
|| APIUser.IsLoggedIn
|
||||
|| (!explicitAttempt && string.IsNullOrEmpty(storedUsername)))
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
||||
{
|
||||
EditorUtility.DisplayDialog("Error logging in", "Please enter a valid username and password.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
signingIn = true;
|
||||
}
|
||||
|
||||
InitAccount();
|
||||
|
||||
AttemptLogin();
|
||||
|
||||
// Show login status immediately without needing an automatic repaint (cursor movement).
|
||||
Repaint();
|
||||
}
|
||||
|
||||
public static void Logout()
|
||||
{
|
||||
signingIn = false;
|
||||
storedUsername = null;
|
||||
storedPassword = null;
|
||||
VRC.Tools.ClearCookies();
|
||||
APIUser.Logout();
|
||||
OnPanelLoggedOut?.Invoke(window, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void AccountDestroy()
|
||||
{
|
||||
signingIn = false;
|
||||
isInitialized = false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5066cd5c1cc208143a1253cac821714a
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c73e735ee0380241b186a8993fa56bf
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace VRC.SDKBase.Editor
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
[MeansImplicitUse]
|
||||
public class VRCSdkControlPanelBuilderAttribute : Attribute
|
||||
{
|
||||
public Type Type { get; }
|
||||
public VRCSdkControlPanelBuilderAttribute(Type type)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c768b42ca9a2f2b48afeb1fa03d5e1bf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,786 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using VRC.Core;
|
||||
using VRC.SDKBase.Editor;
|
||||
using VRC.SDKBase.Editor.Api;
|
||||
|
||||
// This file handles the Content tab of the SDK Panel
|
||||
|
||||
public partial class VRCSdkControlPanel : EditorWindow
|
||||
{
|
||||
const int PageLimit = 20;
|
||||
|
||||
static List<ApiAvatar> uploadedAvatars = null;
|
||||
static List<ApiWorld> uploadedWorlds = null;
|
||||
static List<ApiAvatar> testAvatars = null;
|
||||
|
||||
|
||||
|
||||
public static Dictionary<string, Texture2D> ImageCache = new Dictionary<string, Texture2D>();
|
||||
|
||||
static List<string> justDeletedContents;
|
||||
static List<ApiAvatar> justUpdatedAvatars;
|
||||
|
||||
static EditorCoroutine fetchingAvatars = null, fetchingWorlds = null;
|
||||
|
||||
|
||||
|
||||
private static string searchString = "";
|
||||
private static bool WorldsToggle = true;
|
||||
private static bool AvatarsToggle = true;
|
||||
private static bool TestAvatarsToggle = true;
|
||||
|
||||
|
||||
|
||||
const string WORLDS_WEB_URL = "https://vrchat.com/home/content/worlds";
|
||||
const string WORLD_WEB_URL = "https://vrchat.com/home/content/worlds/";
|
||||
const string WORLD_WEB_URL_SUFFIX = "/edit";
|
||||
const string AVATARS_WEB_URL = "https://vrchat.com/home/avatars";
|
||||
const string AVATAR_WEB_URL = "https://vrchat.com/home/avatar/";
|
||||
|
||||
const int SCROLLBAR_RESERVED_REGION_WIDTH = 50;
|
||||
const int OPEN_ON_WEB_BUTTON_WIDTH = 100;
|
||||
|
||||
const int WORLD_DESCRIPTION_FIELD_WIDTH = 140;
|
||||
const int WORLD_IMAGE_BUTTON_WIDTH = 100;
|
||||
const int WORLD_IMAGE_BUTTON_HEIGHT = 100;
|
||||
const int WORLD_RELEASE_STATUS_FIELD_WIDTH = 150;
|
||||
const int COPY_WORLD_ID_BUTTON_WIDTH = 75;
|
||||
const int DELETE_WORLD_BUTTON_WIDTH = 75;
|
||||
const int WORLD_ALL_INFORMATION_MAX_WIDTH = WORLD_DESCRIPTION_FIELD_WIDTH + WORLD_IMAGE_BUTTON_WIDTH + WORLD_RELEASE_STATUS_FIELD_WIDTH + COPY_WORLD_ID_BUTTON_WIDTH + DELETE_WORLD_BUTTON_WIDTH + OPEN_ON_WEB_BUTTON_WIDTH + SCROLLBAR_RESERVED_REGION_WIDTH;
|
||||
const int WORLD_REDUCED_INFORMATION_MAX_WIDTH = WORLD_DESCRIPTION_FIELD_WIDTH + WORLD_IMAGE_BUTTON_WIDTH + WORLD_RELEASE_STATUS_FIELD_WIDTH + SCROLLBAR_RESERVED_REGION_WIDTH;
|
||||
|
||||
const int AVATAR_DESCRIPTION_FIELD_WIDTH = 140;
|
||||
const int AVATAR_IMAGE_BUTTON_WIDTH = WORLD_IMAGE_BUTTON_WIDTH;
|
||||
const int AVATAR_IMAGE_BUTTON_HEIGHT = WORLD_IMAGE_BUTTON_HEIGHT;
|
||||
const int AVATAR_RELEASE_STATUS_FIELD_WIDTH = 150;
|
||||
const int SET_AVATAR_STATUS_BUTTON_WIDTH = 100;
|
||||
const int COPY_AVATAR_ID_BUTTON_WIDTH = COPY_WORLD_ID_BUTTON_WIDTH;
|
||||
const int DELETE_AVATAR_BUTTON_WIDTH = DELETE_WORLD_BUTTON_WIDTH;
|
||||
const int AVATAR_ALL_INFORMATION_MAX_WIDTH = AVATAR_DESCRIPTION_FIELD_WIDTH + AVATAR_IMAGE_BUTTON_WIDTH + AVATAR_RELEASE_STATUS_FIELD_WIDTH + SET_AVATAR_STATUS_BUTTON_WIDTH + COPY_AVATAR_ID_BUTTON_WIDTH + DELETE_AVATAR_BUTTON_WIDTH + OPEN_ON_WEB_BUTTON_WIDTH + SCROLLBAR_RESERVED_REGION_WIDTH;
|
||||
const int AVATAR_REDUCED_INFORMATION_MAX_WIDTH = AVATAR_DESCRIPTION_FIELD_WIDTH + AVATAR_IMAGE_BUTTON_WIDTH + AVATAR_RELEASE_STATUS_FIELD_WIDTH + SCROLLBAR_RESERVED_REGION_WIDTH;
|
||||
|
||||
const int MAX_ALL_INFORMATION_WIDTH = WORLD_ALL_INFORMATION_MAX_WIDTH > AVATAR_ALL_INFORMATION_MAX_WIDTH ? WORLD_ALL_INFORMATION_MAX_WIDTH : AVATAR_ALL_INFORMATION_MAX_WIDTH;
|
||||
const int MAX_REDUCED_INFORMATION_WIDTH = WORLD_REDUCED_INFORMATION_MAX_WIDTH > AVATAR_REDUCED_INFORMATION_MAX_WIDTH ? WORLD_REDUCED_INFORMATION_MAX_WIDTH : AVATAR_REDUCED_INFORMATION_MAX_WIDTH;
|
||||
|
||||
public static void ClearContent()
|
||||
{
|
||||
uploadedWorlds = null;
|
||||
uploadedAvatars = null;
|
||||
testAvatars = null;
|
||||
ImageCache.Clear();
|
||||
}
|
||||
|
||||
IEnumerator FetchUploadedData()
|
||||
{
|
||||
if (!ConfigManager.RemoteConfig.IsInitialized())
|
||||
ConfigManager.RemoteConfig.Init();
|
||||
|
||||
if (!APIUser.IsLoggedIn)
|
||||
yield break;
|
||||
|
||||
ApiCache.Clear();
|
||||
VRCCachedWebRequest.ClearOld();
|
||||
|
||||
if (fetchingAvatars == null)
|
||||
fetchingAvatars = EditorCoroutine.Start(() => FetchAvatars());
|
||||
if (fetchingWorlds == null)
|
||||
fetchingWorlds = EditorCoroutine.Start(() => FetchWorlds());
|
||||
FetchTestAvatars();
|
||||
}
|
||||
|
||||
private static void FetchAvatars(int offset = 0)
|
||||
{
|
||||
ApiAvatar.FetchList(
|
||||
delegate (IEnumerable<ApiAvatar> obj, bool _)
|
||||
{
|
||||
if (obj.FirstOrDefault() != null)
|
||||
fetchingAvatars = EditorCoroutine.Start(() =>
|
||||
{
|
||||
var l = obj.ToList();
|
||||
int count = l.Count;
|
||||
SetupAvatarData(l);
|
||||
FetchAvatars(offset + count);
|
||||
});
|
||||
else
|
||||
{
|
||||
fetchingAvatars = null;
|
||||
foreach (ApiAvatar a in uploadedAvatars)
|
||||
DownloadImage(a.id, a.thumbnailImageUrl);
|
||||
}
|
||||
},
|
||||
delegate (string obj)
|
||||
{
|
||||
Debug.LogError("Error fetching your uploaded avatars:\n" + obj);
|
||||
fetchingAvatars = null;
|
||||
},
|
||||
ApiAvatar.Owner.Mine,
|
||||
ApiAvatar.ReleaseStatus.All,
|
||||
null,
|
||||
PageLimit,
|
||||
offset,
|
||||
ApiAvatar.SortHeading.None,
|
||||
ApiAvatar.SortOrder.Descending,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
null,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private static void FetchTestAvatars()
|
||||
{
|
||||
#if VRC_SDK_VRCSDK3
|
||||
string sdkAvatarFolder = VRC.SDKBase.Editor.VRC_SdkBuilder.GetLocalLowPath() + "/VRChat/VRChat/Avatars/";
|
||||
string[] sdkavatars = Directory.GetFiles(sdkAvatarFolder);
|
||||
string filename = "";
|
||||
List<ApiAvatar> avatars = new List<ApiAvatar>();
|
||||
foreach (string sdkap in sdkavatars)
|
||||
{
|
||||
if (Path.GetExtension(sdkap) != ".vrca")
|
||||
continue;
|
||||
|
||||
filename = Path.GetFileNameWithoutExtension(sdkap);
|
||||
ApiAvatar sdka = API.FromCacheOrNew<ApiAvatar>("local:sdk_" + filename);
|
||||
sdka.assetUrl = sdkap;
|
||||
sdka.name = filename;
|
||||
sdka.releaseStatus = "public";
|
||||
ApiAvatar.AddLocal(sdka);
|
||||
avatars.Add(sdka);
|
||||
}
|
||||
|
||||
testAvatars = avatars;
|
||||
#else
|
||||
testAvatars = new List<ApiAvatar>();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static void FetchWorlds(int offset = 0)
|
||||
{
|
||||
ApiWorld.FetchList(
|
||||
delegate (IEnumerable<ApiWorld> obj)
|
||||
{
|
||||
if (obj.FirstOrDefault() != null)
|
||||
fetchingWorlds = EditorCoroutine.Start(() =>
|
||||
{
|
||||
var l = obj.ToList();
|
||||
int count = l.Count;
|
||||
SetupWorldData(l);
|
||||
FetchWorlds(offset + count);
|
||||
});
|
||||
else
|
||||
{
|
||||
fetchingWorlds = null;
|
||||
|
||||
foreach (ApiWorld w in uploadedWorlds)
|
||||
DownloadImage(w.id, w.thumbnailImageUrl);
|
||||
}
|
||||
},
|
||||
delegate (string obj)
|
||||
{
|
||||
Debug.LogError("Error fetching your uploaded worlds:\n" + obj);
|
||||
fetchingWorlds = null;
|
||||
},
|
||||
"updated",
|
||||
ApiWorld.SortOwnership.Mine,
|
||||
ApiWorld.SortOrder.Descending,
|
||||
offset,
|
||||
PageLimit,
|
||||
"",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"",
|
||||
ApiWorld.ReleaseStatus.All,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
false);
|
||||
}
|
||||
|
||||
static void SetupWorldData(List<ApiWorld> worlds)
|
||||
{
|
||||
if (worlds == null || uploadedWorlds == null)
|
||||
return;
|
||||
|
||||
worlds.RemoveAll(w => w == null || w.name == null || uploadedWorlds.Any(w2 => w2.id == w.id));
|
||||
|
||||
if (worlds.Count > 0)
|
||||
{
|
||||
uploadedWorlds.AddRange(worlds);
|
||||
uploadedWorlds.Sort((w1, w2) => -w1.updated_at.CompareTo(w2.updated_at));
|
||||
}
|
||||
}
|
||||
|
||||
static void SetupAvatarData(List<ApiAvatar> avatars)
|
||||
{
|
||||
if (avatars == null || uploadedAvatars == null)
|
||||
return;
|
||||
|
||||
avatars.RemoveAll(a => a == null || uploadedAvatars.Any(a2 => a2.id == a.id));
|
||||
foreach (var avatar in avatars)
|
||||
{
|
||||
if (string.IsNullOrEmpty(avatar.name))
|
||||
avatar.name = "(unnamed)";
|
||||
}
|
||||
|
||||
if (avatars.Count > 0)
|
||||
{
|
||||
uploadedAvatars.AddRange(avatars);
|
||||
uploadedAvatars.Sort((a1, a2) => -a1.updated_at.CompareTo(a2.updated_at));
|
||||
}
|
||||
}
|
||||
|
||||
private static void DownloadImage(string id, string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url))
|
||||
return;
|
||||
|
||||
if (ImageCache.ContainsKey(id) && ImageCache[id] != null)
|
||||
return;
|
||||
|
||||
EditorCoroutine.Start(VRCCachedWebRequest.Get(url, OnDone));
|
||||
void OnDone(Texture2D texture)
|
||||
{
|
||||
if (texture != null)
|
||||
ImageCache[id] = texture;
|
||||
else if (ImageCache.ContainsKey(id))
|
||||
ImageCache.Remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 contentScrollPos;
|
||||
|
||||
bool OnGUIUserInfo()
|
||||
{
|
||||
bool updatedContent = false;
|
||||
|
||||
if (!ConfigManager.RemoteConfig.IsInitialized())
|
||||
ConfigManager.RemoteConfig.Init();
|
||||
|
||||
if (APIUser.IsLoggedIn && uploadedWorlds != null && uploadedAvatars != null && testAvatars != null)
|
||||
{
|
||||
bool expandedLayout = false; // (position.width > MAX_ALL_INFORMATION_WIDTH); // uncomment for future wide layouts
|
||||
|
||||
if (!expandedLayout)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
}
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
|
||||
GUILayout.BeginVertical(searchBarStyle);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
float searchFieldShrinkOffset = 30f;
|
||||
GUILayoutOption layoutOption = (expandedLayout ? GUILayout.Width(position.width - searchFieldShrinkOffset) : GUILayout.Width(SdkWindowWidth - searchFieldShrinkOffset - 8));
|
||||
searchString = EditorGUILayout.TextField(searchString, GUI.skin.FindStyle("SearchTextField"), layoutOption);
|
||||
GUIStyle searchButtonStyle = searchString == string.Empty
|
||||
? GUI.skin.FindStyle("SearchCancelButtonEmpty")
|
||||
: GUI.skin.FindStyle("SearchCancelButton");
|
||||
if (GUILayout.Button(string.Empty, searchButtonStyle))
|
||||
{
|
||||
searchString = string.Empty;
|
||||
GUI.FocusControl(null);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if (!expandedLayout)
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
}
|
||||
|
||||
layoutOption = expandedLayout ? GUILayout.Width(position.width) : GUILayout.Width(SdkWindowWidth - 8);
|
||||
|
||||
using (var scroll = new EditorGUILayout.ScrollViewScope(contentScrollPos, layoutOption))
|
||||
{
|
||||
contentScrollPos = scroll.scrollPosition;
|
||||
|
||||
#if UDON
|
||||
if (uploadedWorlds.Count > 0)
|
||||
{
|
||||
WorldsListGUI(expandedLayout, ref updatedContent);
|
||||
}
|
||||
|
||||
if (uploadedAvatars.Count > 0)
|
||||
{
|
||||
AvatarsListGUI(expandedLayout, ref updatedContent);
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (testAvatars.Count > 0)
|
||||
{
|
||||
TestAvatarsListGUI(expandedLayout, ref updatedContent);
|
||||
}
|
||||
#else
|
||||
if (uploadedAvatars.Count > 0)
|
||||
{
|
||||
AvatarsListGUI(expandedLayout, ref updatedContent);
|
||||
}
|
||||
|
||||
|
||||
if (testAvatars.Count > 0)
|
||||
{
|
||||
TestAvatarsListGUI(expandedLayout, ref updatedContent);
|
||||
}
|
||||
|
||||
if (uploadedWorlds.Count > 0)
|
||||
{
|
||||
WorldsListGUI(expandedLayout, ref updatedContent);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (!expandedLayout)
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
if (updatedContent && (null != window)) window.Reset();
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void AvatarsListGUI(bool expandedLayout, ref bool updatedContent)
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField("Avatars", EditorStyles.boldLabel, GUILayout.ExpandWidth(false),
|
||||
GUILayout.Width(65));
|
||||
AvatarsToggle = EditorGUILayout.Foldout(AvatarsToggle, new GUIContent(""));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (AvatarsToggle)
|
||||
{
|
||||
List<ApiAvatar> tmpAvatars = new List<ApiAvatar>();
|
||||
|
||||
if (uploadedAvatars.Count > 0)
|
||||
tmpAvatars = new List<ApiAvatar>(uploadedAvatars);
|
||||
|
||||
if (justUpdatedAvatars != null)
|
||||
{
|
||||
foreach (ApiAvatar a in justUpdatedAvatars)
|
||||
{
|
||||
int index = tmpAvatars.FindIndex((av) => av.id == a.id);
|
||||
if (index != -1)
|
||||
tmpAvatars[index] = a;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ApiAvatar a in tmpAvatars)
|
||||
{
|
||||
if (justDeletedContents != null && justDeletedContents.Contains(a.id))
|
||||
{
|
||||
uploadedAvatars.Remove(a);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!a.name.ToLowerInvariant().Contains(searchString.ToLowerInvariant()))
|
||||
continue;
|
||||
|
||||
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
|
||||
|
||||
if (ImageCache.ContainsKey(a.id))
|
||||
{
|
||||
if (GUILayout.Button(ImageCache[a.id], GUILayout.Height(AVATAR_IMAGE_BUTTON_HEIGHT),
|
||||
GUILayout.Width(AVATAR_IMAGE_BUTTON_WIDTH)))
|
||||
Application.OpenURL(a.imageUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GUILayout.Button("", GUILayout.Height(AVATAR_IMAGE_BUTTON_HEIGHT),
|
||||
GUILayout.Width(AVATAR_IMAGE_BUTTON_WIDTH)))
|
||||
Application.OpenURL(a.imageUrl);
|
||||
}
|
||||
|
||||
if (expandedLayout)
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
else
|
||||
EditorGUILayout.BeginVertical();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField(a.name, contentTitleStyle);
|
||||
if (GUILayout.Button("Open on web", GUILayout.Width(OPEN_ON_WEB_BUTTON_WIDTH)))
|
||||
Application.OpenURL(AVATAR_WEB_URL + a.id);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.LabelField("Release Status: " + a.releaseStatus,
|
||||
GUILayout.Width(AVATAR_RELEASE_STATUS_FIELD_WIDTH));
|
||||
|
||||
string oppositeReleaseStatus = a.releaseStatus == "public" ? "private" : "public";
|
||||
if (GUILayout.Button("Make " + oppositeReleaseStatus,
|
||||
GUILayout.Width(SET_AVATAR_STATUS_BUTTON_WIDTH)))
|
||||
{
|
||||
a.releaseStatus = oppositeReleaseStatus;
|
||||
|
||||
a.SaveReleaseStatus((c) =>
|
||||
{
|
||||
ApiAvatar savedBP = (ApiAvatar)c.Model;
|
||||
|
||||
if (justUpdatedAvatars == null) justUpdatedAvatars = new List<ApiAvatar>();
|
||||
justUpdatedAvatars.Add(savedBP);
|
||||
},
|
||||
(c) =>
|
||||
{
|
||||
Debug.LogError(c.Error);
|
||||
EditorUtility.DisplayDialog("Avatar Updated",
|
||||
"Failed to change avatar release status", "OK");
|
||||
});
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Copy ID", GUILayout.Width(COPY_AVATAR_ID_BUTTON_WIDTH)))
|
||||
{
|
||||
TextEditor te = new TextEditor();
|
||||
te.text = a.id;
|
||||
te.SelectAll();
|
||||
te.Copy();
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Delete", GUILayout.Width(DELETE_AVATAR_BUTTON_WIDTH)))
|
||||
{
|
||||
if (EditorUtility.DisplayDialog("Delete " + a.name + "?",
|
||||
"Are you sure you want to delete " + a.name + "? This cannot be undone.", "Delete",
|
||||
"Cancel"))
|
||||
{
|
||||
foreach (VRC.Core.PipelineManager pm in FindObjectsByType<VRC.Core.PipelineManager>(FindObjectsSortMode.None)
|
||||
.Where(pm => pm.blueprintId == a.id))
|
||||
{
|
||||
pm.blueprintId = "";
|
||||
|
||||
EditorUtility.SetDirty(pm);
|
||||
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(pm.gameObject.scene);
|
||||
UnityEditor.SceneManagement.EditorSceneManager.SaveScene(pm.gameObject.scene);
|
||||
}
|
||||
|
||||
API.Delete<ApiAvatar>(a.id);
|
||||
uploadedAvatars.RemoveAll(avatar => avatar.id == a.id);
|
||||
if (ImageCache.ContainsKey(a.id))
|
||||
ImageCache.Remove(a.id);
|
||||
|
||||
if (justDeletedContents == null) justDeletedContents = new List<string>();
|
||||
justDeletedContents.Add(a.id);
|
||||
updatedContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (expandedLayout)
|
||||
EditorGUILayout.EndHorizontal();
|
||||
else
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void TestAvatarsListGUI(bool expandedLayout, ref bool updatedContent)
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField("Test Avatars", EditorStyles.boldLabel, GUILayout.ExpandWidth(false),
|
||||
GUILayout.Width(100));
|
||||
TestAvatarsToggle = EditorGUILayout.Foldout(TestAvatarsToggle, new GUIContent(""));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (TestAvatarsToggle)
|
||||
{
|
||||
List<ApiAvatar> tmpAvatars = new List<ApiAvatar>();
|
||||
|
||||
if (testAvatars.Count > 0)
|
||||
tmpAvatars = new List<ApiAvatar>(testAvatars);
|
||||
|
||||
foreach (ApiAvatar a in tmpAvatars)
|
||||
{
|
||||
if (!a.name.ToLowerInvariant().Contains(searchString.ToLowerInvariant()))
|
||||
continue;
|
||||
|
||||
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
|
||||
|
||||
if (expandedLayout)
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
else
|
||||
EditorGUILayout.BeginVertical();
|
||||
|
||||
EditorGUILayout.LabelField(a.name, contentDescriptionStyle,
|
||||
GUILayout.Width(expandedLayout
|
||||
? position.width - MAX_ALL_INFORMATION_WIDTH + AVATAR_DESCRIPTION_FIELD_WIDTH
|
||||
: AVATAR_DESCRIPTION_FIELD_WIDTH));
|
||||
|
||||
if (GUILayout.Button("Delete", GUILayout.Width(DELETE_AVATAR_BUTTON_WIDTH)))
|
||||
{
|
||||
if (EditorUtility.DisplayDialog("Delete " + a.name + "?",
|
||||
"Are you sure you want to delete " + a.name + "? This cannot be undone.", "Delete",
|
||||
"Cancel"))
|
||||
{
|
||||
API.Delete<ApiAvatar>(a.id);
|
||||
testAvatars.RemoveAll(avatar => avatar.id == a.id);
|
||||
File.Delete(a.assetUrl);
|
||||
|
||||
updatedContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (expandedLayout)
|
||||
EditorGUILayout.EndHorizontal();
|
||||
else
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WorldsListGUI(bool expandedLayout, ref bool updatedContent)
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField("Worlds", EditorStyles.boldLabel, GUILayout.ExpandWidth(false), GUILayout.Width(58));
|
||||
WorldsToggle = EditorGUILayout.Foldout(WorldsToggle, new GUIContent(""));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (WorldsToggle)
|
||||
{
|
||||
List<ApiWorld> tmpWorlds = new List<ApiWorld>();
|
||||
|
||||
if (uploadedWorlds.Count > 0)
|
||||
tmpWorlds = new List<ApiWorld>(uploadedWorlds);
|
||||
|
||||
foreach (ApiWorld w in tmpWorlds)
|
||||
{
|
||||
if (justDeletedContents != null && justDeletedContents.Contains(w.id))
|
||||
{
|
||||
uploadedWorlds.Remove(w);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!w.name.ToLowerInvariant().Contains(searchString.ToLowerInvariant()))
|
||||
continue;
|
||||
|
||||
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
|
||||
|
||||
if (ImageCache.ContainsKey(w.id))
|
||||
{
|
||||
if (GUILayout.Button(ImageCache[w.id], GUILayout.Height(WORLD_IMAGE_BUTTON_HEIGHT),
|
||||
GUILayout.Width(WORLD_IMAGE_BUTTON_WIDTH)))
|
||||
Application.OpenURL(w.imageUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GUILayout.Button("", GUILayout.Height(WORLD_IMAGE_BUTTON_HEIGHT),
|
||||
GUILayout.Width(WORLD_IMAGE_BUTTON_WIDTH)))
|
||||
Application.OpenURL(w.imageUrl);
|
||||
}
|
||||
|
||||
if (expandedLayout)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField(w.name, contentDescriptionStyle,
|
||||
GUILayout.Width(position.width - MAX_ALL_INFORMATION_WIDTH +
|
||||
WORLD_DESCRIPTION_FIELD_WIDTH));
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.BeginVertical();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
#if UDON
|
||||
if (w.id == _currentBlueprintId)
|
||||
{
|
||||
EditorGUILayout.LabelField(w.name + " (Current)", contentTitleStyle);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.LabelField(w.name, contentTitleStyle);
|
||||
}
|
||||
#else
|
||||
EditorGUILayout.LabelField(w.name, contentTitleStyle);
|
||||
#endif
|
||||
|
||||
if (GUILayout.Button("Open on web", GUILayout.Width(OPEN_ON_WEB_BUTTON_WIDTH)))
|
||||
Application.OpenURL(WORLD_WEB_URL + w.id + WORLD_WEB_URL_SUFFIX);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField("Release Status: " + w.releaseStatus,
|
||||
GUILayout.Width(WORLD_RELEASE_STATUS_FIELD_WIDTH));
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
using (new GUILayout.HorizontalScope())
|
||||
{
|
||||
#if UDON
|
||||
if (GUILayout.Button("Set Current", GUILayout.Width(COPY_WORLD_ID_BUTTON_WIDTH)))
|
||||
{
|
||||
var pM = FindFirstObjectByType<PipelineManager>();
|
||||
if (pM != null)
|
||||
{
|
||||
Undo.RecordObject(pM, "Set Current World");
|
||||
pM.blueprintId = w.id;
|
||||
_currentBlueprintId = w.id;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (GUILayout.Button("Copy ID", GUILayout.Width(COPY_WORLD_ID_BUTTON_WIDTH)))
|
||||
{
|
||||
TextEditor te = new TextEditor();
|
||||
te.text = w.id;
|
||||
te.SelectAll();
|
||||
te.Copy();
|
||||
}
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Delete", GUILayout.Width(DELETE_WORLD_BUTTON_WIDTH)))
|
||||
{
|
||||
if (EditorUtility.DisplayDialog("Delete " + w.name + "?",
|
||||
"Are you sure you want to delete " + w.name + "? This cannot be undone.", "Delete",
|
||||
"Cancel"))
|
||||
{
|
||||
foreach (VRC.Core.PipelineManager pm in FindObjectsByType<VRC.Core.PipelineManager>(FindObjectsSortMode.None)
|
||||
.Where(pm => pm.blueprintId == w.id))
|
||||
{
|
||||
pm.blueprintId = "";
|
||||
|
||||
EditorUtility.SetDirty(pm);
|
||||
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(pm.gameObject.scene);
|
||||
UnityEditor.SceneManagement.EditorSceneManager.SaveScene(pm.gameObject.scene);
|
||||
}
|
||||
|
||||
API.Delete<ApiWorld>(w.id);
|
||||
uploadedWorlds.RemoveAll(world => world.id == w.id);
|
||||
if (ImageCache.ContainsKey(w.id))
|
||||
ImageCache.Remove(w.id);
|
||||
|
||||
if (justDeletedContents == null) justDeletedContents = new List<string>();
|
||||
justDeletedContents.Add(w.id);
|
||||
updatedContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (expandedLayout)
|
||||
EditorGUILayout.EndHorizontal();
|
||||
else
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _currentBlueprintId;
|
||||
private void FetchCurrentBlueprintId()
|
||||
{
|
||||
#if UDON
|
||||
var pM = FindFirstObjectByType<PipelineManager>();
|
||||
_currentBlueprintId = pM != null ? pM.blueprintId : null;
|
||||
#endif
|
||||
}
|
||||
|
||||
void ShowContent()
|
||||
{
|
||||
GUIStyle centeredDescriptionStyle = new GUIStyle(EditorStyles.wordWrappedLabel);
|
||||
centeredDescriptionStyle.wordWrap = true;
|
||||
centeredDescriptionStyle.alignment = TextAnchor.MiddleCenter;
|
||||
|
||||
FetchCurrentBlueprintId();
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.BeginVertical();
|
||||
|
||||
GUILayout.BeginVertical(infoGuiStyle, GUILayout.Width(SdkWindowWidth));
|
||||
EditorGUILayout.LabelField("We recommend that you use the VRChat website to manage your content.", centeredDescriptionStyle);
|
||||
EditorGUILayout.Space();
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button("Worlds", GUILayout.Width(OPEN_ON_WEB_BUTTON_WIDTH)))
|
||||
Application.OpenURL(WORLDS_WEB_URL);
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button("Avatars", GUILayout.Width(OPEN_ON_WEB_BUTTON_WIDTH)))
|
||||
Application.OpenURL(AVATARS_WEB_URL);
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.EndVertical();
|
||||
|
||||
if (((PanelTab)VRCSettings.ActiveWindowPanel) == PanelTab.ContentManager)
|
||||
{
|
||||
if (uploadedWorlds == null || uploadedAvatars == null || testAvatars == null)
|
||||
{
|
||||
if (uploadedWorlds == null)
|
||||
uploadedWorlds = new List<ApiWorld>();
|
||||
if (uploadedAvatars == null)
|
||||
uploadedAvatars = new List<ApiAvatar>();
|
||||
if (testAvatars == null)
|
||||
testAvatars = new List<ApiAvatar>();
|
||||
|
||||
EditorCoroutine.Start(FetchUploadedData());
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
fetchingWorlds != null
|
||||
|| fetchingAvatars != null
|
||||
)
|
||||
{
|
||||
GUILayout.BeginVertical(boxGuiStyle, GUILayout.Width(SdkWindowWidth - 8));
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("Fetching Records", titleGuiStyle);
|
||||
EditorGUILayout.Space();
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
else
|
||||
{
|
||||
GUILayout.BeginVertical(boxGuiStyle, GUILayout.Width(SdkWindowWidth - 8));
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Fetch updated records from the VRChat server");
|
||||
if (GUILayout.Button("Fetch"))
|
||||
ClearContent();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.Space();
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
GUILayout.EndVertical();
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
OnGUIUserInfo();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7333cdb3df19724b84b4a1b05093fe0
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,64 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using VRC.Core;
|
||||
|
||||
/// This file handles the links inside VRChat SDK top bar menu
|
||||
|
||||
namespace VRC.SDKBase.Editor
|
||||
{
|
||||
public static class VRCSdkControlPanelHelp
|
||||
{
|
||||
public const string AVATAR_OPTIMIZATION_TIPS_URL = "https://creators.vrchat.com/avatars/avatar-optimizing-tips";
|
||||
public const string AVATAR_RIG_REQUIREMENTS_URL = "https://creators.vrchat.com/avatars/rig-requirements";
|
||||
public const string AVATAR_WRITE_DEFAULTS_ON_STATES_URL = "https://creators.vrchat.com/avatars/#write-defaults-on-states";
|
||||
public const string AVATAR_CUSTOM_HEAD_CHOP_URL = "https://creators.vrchat.com/avatars/avatar-dynamics/vrc-headchop";
|
||||
|
||||
[MenuItem("VRChat SDK/Help/Developer FAQ")]
|
||||
public static void ShowDeveloperFAQ()
|
||||
{
|
||||
if (!ConfigManager.RemoteConfig.IsInitialized())
|
||||
{
|
||||
ConfigManager.RemoteConfig.Init(() => ShowDeveloperFAQ());
|
||||
return;
|
||||
}
|
||||
|
||||
Application.OpenURL(ConfigManager.RemoteConfig.GetString("sdkDeveloperFaqUrl"));
|
||||
}
|
||||
|
||||
[MenuItem("VRChat SDK/Help/VRChat Discord")]
|
||||
public static void ShowVRChatDiscord()
|
||||
{
|
||||
if (!ConfigManager.RemoteConfig.IsInitialized())
|
||||
{
|
||||
ConfigManager.RemoteConfig.Init(() => ShowVRChatDiscord());
|
||||
return;
|
||||
}
|
||||
|
||||
Application.OpenURL(ConfigManager.RemoteConfig.GetString("sdkDiscordUrl"));
|
||||
}
|
||||
|
||||
[MenuItem("VRChat SDK/Help/Avatar Optimization Tips")]
|
||||
public static void ShowAvatarOptimizationTips()
|
||||
{
|
||||
if (!ConfigManager.RemoteConfig.IsInitialized())
|
||||
{
|
||||
ConfigManager.RemoteConfig.Init(() => ShowAvatarOptimizationTips());
|
||||
return;
|
||||
}
|
||||
|
||||
Application.OpenURL(AVATAR_OPTIMIZATION_TIPS_URL);
|
||||
}
|
||||
|
||||
[MenuItem("VRChat SDK/Help/Avatar Rig Requirements")]
|
||||
public static void ShowAvatarRigRequirements()
|
||||
{
|
||||
if (!ConfigManager.RemoteConfig.IsInitialized())
|
||||
{
|
||||
ConfigManager.RemoteConfig.Init(() => ShowAvatarRigRequirements());
|
||||
return;
|
||||
}
|
||||
|
||||
Application.OpenURL(AVATAR_RIG_REQUIREMENTS_URL);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f3507a74e4b8cfd469afac127fa5f4e5
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,225 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using VRC.Core;
|
||||
using VRC.SDKBase;
|
||||
using VRC.SDKBase.Editor;
|
||||
|
||||
// This file handles the Settings tab of the SDK Panel
|
||||
|
||||
public partial class VRCSdkControlPanel : EditorWindow
|
||||
{
|
||||
bool UseDevApi
|
||||
{
|
||||
get
|
||||
{
|
||||
return VRC.Core.API.GetApiUrl() == VRC.Core.API.devApiUrl;
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 settingsScroll;
|
||||
bool showLocalIpAddress;
|
||||
|
||||
void ShowSettings()
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.BeginVertical();
|
||||
|
||||
settingsScroll = EditorGUILayout.BeginScrollView(settingsScroll, GUILayout.Width(SdkWindowWidth - 8));
|
||||
|
||||
EditorGUILayout.BeginVertical(boxGuiStyle);
|
||||
EditorGUILayout.LabelField("Developer", EditorStyles.boldLabel);
|
||||
|
||||
VRCSettings.DisplayAdvancedSettings = EditorGUILayout.ToggleLeft("Show Extra Options on account page", VRCSettings.DisplayAdvancedSettings);
|
||||
|
||||
bool prevDisplayHelpBoxes = VRCSettings.DisplayHelpBoxes;
|
||||
VRCSettings.DisplayHelpBoxes = EditorGUILayout.ToggleLeft("Show Help Boxes on SDK components", VRCSettings.DisplayHelpBoxes);
|
||||
if (VRCSettings.DisplayHelpBoxes != prevDisplayHelpBoxes)
|
||||
{
|
||||
Editor[] editors = (Editor[])Resources.FindObjectsOfTypeAll<Editor>();
|
||||
for (int i = 0; i < editors.Length; i++)
|
||||
{
|
||||
editors[i].Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
// API logging
|
||||
{
|
||||
bool isLoggingEnabled = UnityEditor.EditorPrefs.GetBool("apiLoggingEnabled");
|
||||
bool enableLogging = EditorGUILayout.ToggleLeft("API Logging Enabled", isLoggingEnabled);
|
||||
if (enableLogging != isLoggingEnabled)
|
||||
{
|
||||
if (enableLogging)
|
||||
VRC.Core.Logger.EnableCategory(API.LOG_CATEGORY);
|
||||
else
|
||||
VRC.Core.Logger.DisableCategory(API.LOG_CATEGORY);
|
||||
|
||||
UnityEditor.EditorPrefs.SetBool("apiLoggingEnabled", enableLogging);
|
||||
}
|
||||
}
|
||||
|
||||
// Dry Builds
|
||||
if (APIUser.CurrentUser != null && APIUser.CurrentUser.hasSuperPowers) {
|
||||
var newDryRun = EditorGUILayout.ToggleLeft(new GUIContent("Dry Run Builds", "This will skip actual builds and uploads and instead pass as if they succeeded"), VRC_EditorTools.DryRunState);
|
||||
if (newDryRun != VRC_EditorTools.DryRunState)
|
||||
{
|
||||
VRC_EditorTools.DryRunState = newDryRun;
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// DPID based mipmap generation
|
||||
bool prevDpidMipmaps = VRCPackageSettings.Instance.dpidMipmaps;
|
||||
GUIContent dpidContent = new GUIContent("Override Kaiser mipmapping with Detail-Preserving Image Downscaling (BETA)",
|
||||
"Use a state of the art algorithm (DPID) for mipmap generation when Kaiser is selected. This can improve the quality of mipmaps.");
|
||||
VRCPackageSettings.Instance.dpidMipmaps = EditorGUILayout.ToggleLeft(dpidContent, VRCPackageSettings.Instance.dpidMipmaps);
|
||||
|
||||
bool prevDpidConservative = VRCPackageSettings.Instance.dpidConservative;
|
||||
GUIContent dpidConservativeContent = new GUIContent("Use conservative settings for DPID mipmapping",
|
||||
"Use conservative settings for DPID mipmapping. This can avoid issues with over-emphasis of details.");
|
||||
VRCPackageSettings.Instance.dpidConservative = EditorGUILayout.ToggleLeft(dpidConservativeContent, VRCPackageSettings.Instance.dpidConservative);
|
||||
|
||||
// When DPID setting changed, mark all textures as dirty
|
||||
if (VRCPackageSettings.Instance.dpidMipmaps != prevDpidMipmaps ||
|
||||
(VRCPackageSettings.Instance.dpidMipmaps && VRCPackageSettings.Instance.dpidConservative != prevDpidConservative))
|
||||
{
|
||||
VRC.Core.Logger.Log("DPID mipmaps setting changed, marking all textures as dirty");
|
||||
string[] guids = AssetDatabase.FindAssets("t:Texture");
|
||||
foreach (string guid in guids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter;
|
||||
if (importer != null && importer.mipmapFilter == TextureImporterMipFilter.KaiserFilter)
|
||||
{
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
}
|
||||
|
||||
VRCPackageSettings.Instance.Save();
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// Running VRChat constraints in edit mode
|
||||
bool prevVrcConstraintsInEditMode = VRCSettings.VrcConstraintsInEditMode;
|
||||
GUIContent vrcConstraintsInEditModeContent = new GUIContent("Execute VRChat Constraints in Edit Mode",
|
||||
"Allow VRChat Constraints to run while Unity is in Edit mode.");
|
||||
VRCSettings.VrcConstraintsInEditMode = EditorGUILayout.ToggleLeft(vrcConstraintsInEditModeContent, prevVrcConstraintsInEditMode);
|
||||
|
||||
if (VRCSettings.VrcConstraintsInEditMode != prevVrcConstraintsInEditMode)
|
||||
{
|
||||
VRC.Dynamics.VRCConstraintManager.CanExecuteConstraintJobsInEditMode = VRCSettings.VrcConstraintsInEditMode;
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
showLocalIpAddress = EditorGUILayout.Foldout(showLocalIpAddress, "Show Local IP Address", true);
|
||||
if (showLocalIpAddress)
|
||||
{
|
||||
string localIpAddress = GetLocalIPAddress();
|
||||
EditorGUILayout.HelpBox(localIpAddress, MessageType.None);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
EditorGUILayout.Separator();
|
||||
|
||||
ShowSettingsOptionsForBuilders();
|
||||
|
||||
// debugging
|
||||
if (APIUser.CurrentUser != null && APIUser.CurrentUser.hasSuperPowers)
|
||||
{
|
||||
EditorGUILayout.Separator();
|
||||
EditorGUILayout.BeginVertical(boxGuiStyle);
|
||||
|
||||
EditorGUILayout.LabelField("Logging", EditorStyles.boldLabel);
|
||||
|
||||
// All logging
|
||||
{
|
||||
bool isLoggingEnabled = UnityEditor.EditorPrefs.GetBool("allLoggingEnabled");
|
||||
bool enableLogging = EditorGUILayout.ToggleLeft("All Logging Enabled", isLoggingEnabled);
|
||||
if (enableLogging != isLoggingEnabled)
|
||||
{
|
||||
VRC.Core.Logger.SetTreatAllCategoriesAsEnabled(enableLogging);
|
||||
UnityEditor.EditorPrefs.SetBool("allLoggingEnabled", enableLogging);
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
else
|
||||
{
|
||||
// if (UnityEditor.EditorPrefs.GetBool("apiLoggingEnabled"))
|
||||
// UnityEditor.EditorPrefs.SetBool("apiLoggingEnabled", false);
|
||||
if (UnityEditor.EditorPrefs.GetBool("allLoggingEnabled"))
|
||||
UnityEditor.EditorPrefs.SetBool("allLoggingEnabled", false);
|
||||
}
|
||||
|
||||
|
||||
if (APIUser.CurrentUser != null)
|
||||
{
|
||||
EditorGUILayout.Separator();
|
||||
EditorGUILayout.BeginVertical(boxGuiStyle);
|
||||
|
||||
// custom vrchat install location
|
||||
OnVRCInstallPathGUI();
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
GUILayout.EndVertical();
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
static void OnVRCInstallPathGUI()
|
||||
{
|
||||
EditorGUILayout.LabelField("VRChat Client", EditorStyles.boldLabel);
|
||||
EditorGUILayout.LabelField("Installed Client Path: ", clientInstallPath);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Label("");
|
||||
if (GUILayout.Button("Edit"))
|
||||
{
|
||||
string initPath = "";
|
||||
if (!string.IsNullOrEmpty(clientInstallPath))
|
||||
initPath = clientInstallPath;
|
||||
|
||||
clientInstallPath = EditorUtility.OpenFilePanel("Choose VRC Client Exe", initPath, "exe");
|
||||
SDKClientUtilities.SetVRCInstallPath(clientInstallPath);
|
||||
}
|
||||
if (GUILayout.Button("Revert to Default"))
|
||||
{
|
||||
clientInstallPath = SDKClientUtilities.LoadRegistryVRCInstallPath();
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Separator();
|
||||
}
|
||||
|
||||
string GetLocalIPAddress()
|
||||
{
|
||||
// Note that there are usually many IP addresses on any particular machine (multiple ethernet ports, virtual machine IP addresses)
|
||||
// So will give you a whole list of them `Dns.GetHostEntry(Dns.GetHostName());`, but it's hard to say which one you care about.
|
||||
// https://stackoverflow.com/a/27376368
|
||||
// The following gives you exactly the address you care about by instead opening a UDP socket to get the address that would be used
|
||||
// As the post mentions, no real connection is established here.
|
||||
try
|
||||
{
|
||||
using Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0);
|
||||
socket.Connect("8.8.8.8", 65530);
|
||||
IPEndPoint endPoint = socket.LocalEndPoint as IPEndPoint;
|
||||
var localIP = endPoint.Address.ToString();
|
||||
return localIP;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogException(e);
|
||||
return "Unable to get local IP address";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8357b9b7ef2416946ae86f465a64c0e0
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user