avatar/Assets/VRCSDK/Dependencies/VRChat/Editor/ControlPanel/VRCSdkControlPanelBuilder.cs
2022-09-27 20:47:45 -07:00

656 lines
26 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;
using VRC.SDKBase.Validation.Performance;
using Object = UnityEngine.Object;
using VRC.SDKBase.Editor;
public partial class VRCSdkControlPanel : EditorWindow
{
public static System.Action _EnableSpatialization = null; // assigned in AutoAddONSPAudioSourceComponents
public const string AVATAR_OPTIMIZATION_TIPS_URL = "https://docs.vrchat.com/docs/avatar-optimizing-tips";
public const string AVATAR_RIG_REQUIREMENTS_URL = "https://docs.vrchat.com/docs/rig-requirements";
const string kCantPublishContent = "Before you can upload avatars or worlds, you will need to spend some time in VRChat.";
const string kCantPublishAvatars = "Before you can upload avatars, you will need to spend some time in VRChat.";
const string kCantPublishWorlds = "Before you can upload worlds, you will need to spend some time in VRChat.";
private const string FIX_ISSUES_TO_BUILD_OR_TEST_WARNING_STRING = "You must address the above issues before you can build or test this content!";
static Texture _perfIcon_Excellent;
static Texture _perfIcon_Good;
static Texture _perfIcon_Medium;
static Texture _perfIcon_Poor;
static Texture _perfIcon_VeryPoor;
static Texture _bannerImage;
public void ResetIssues()
{
GUIErrors.Clear();
GUIInfos.Clear();
GUIWarnings.Clear();
GUILinks.Clear();
GUIStats.Clear();
CheckedForIssues = false;
}
public bool CheckedForIssues { get; set; } = false;
class Issue
{
public string issueText;
public System.Action showThisIssue;
public System.Action fixThisIssue;
public PerformanceRating performanceRating;
public Issue(string text, System.Action show, System.Action fix, PerformanceRating rating = PerformanceRating.None)
{
issueText = text;
showThisIssue = show;
fixThisIssue = fix;
performanceRating = rating;
}
public class Equality : IEqualityComparer<Issue>, IComparer<Issue>
{
public bool Equals(Issue b1, Issue b2)
{
return (b1.issueText == b2.issueText);
}
public int Compare(Issue b1, Issue b2)
{
return string.Compare(b1.issueText, b2.issueText);
}
public int GetHashCode(Issue bx)
{
return bx.issueText.GetHashCode();
}
}
}
Dictionary<Object, List<Issue>> GUIErrors = new Dictionary<Object, List<Issue>>();
Dictionary<Object, List<Issue>> GUIWarnings = new Dictionary<Object, List<Issue>>();
Dictionary<Object, List<Issue>> GUIInfos = new Dictionary<Object, List<Issue>>();
Dictionary<Object, List<Issue>> GUILinks = new Dictionary<Object, List<Issue>>();
Dictionary<Object, List<Issue>> GUIStats = new Dictionary<Object, List<Issue>>();
public bool NoGuiErrors()
{
return GUIErrors.Count == 0;
}
public bool NoGuiErrorsOrIssues()
{
return GUIErrors.Count == 0 && CheckedForIssues;
}
void AddToReport(Dictionary<Object, List<Issue>> report, Object subject, string output, System.Action show, System.Action fix)
{
if (subject == null)
subject = this;
if (!report.ContainsKey(subject))
report.Add(subject, new List<Issue>());
var issue = new Issue(output, show, fix);
if (!report[subject].Contains(issue, new Issue.Equality()))
{
report[subject].Add(issue);
report[subject].Sort(new Issue.Equality());
}
}
void BuilderAssemblyReload()
{
ResetIssues();
}
public void OnGUIError(Object subject, string output, System.Action show, System.Action fix)
{
AddToReport(GUIErrors, subject, output, show, fix);
}
public void OnGUIWarning(Object subject, string output, System.Action show, System.Action fix)
{
AddToReport(GUIWarnings, subject, output, show, fix);
}
public void OnGUIInformation(Object subject, string output)
{
AddToReport(GUIInfos, subject, output, null, null);
}
public void OnGUILink(Object subject, string output, string link)
{
AddToReport(GUILinks, subject, output + "\n" + link, null, null);
}
public void OnGUIStat(Object subject, string output, PerformanceRating rating, System.Action show, System.Action fix)
{
if (subject == null)
subject = this;
if (!GUIStats.ContainsKey(subject))
GUIStats.Add(subject, new List<Issue>());
GUIStats[subject].Add(new Issue(output, show, fix, rating));
}
public int triggerLineMode
{
get { return EditorPrefs.GetInt("VRC.SDKBase_triggerLineMode", 0); }
set { EditorPrefs.SetInt("VRC.SDKBase_triggerLineMode", value); }
}
private void ShowSettingsOptionsForBuilders()
{
if (_sdkBuilders == null)
{
PopulateSdkBuilders();
}
for (int i = 0; i < _sdkBuilders.Length; i++)
{
IVRCSdkControlPanelBuilder builder = _sdkBuilders[i];
builder.ShowSettingsOptions();
if (i < _sdkBuilders.Length - 1)
{
EditorGUILayout.Separator();
}
}
}
private IVRCSdkControlPanelBuilder[] _sdkBuilders;
private static List<Type> GetSdkBuilderTypesFromAttribute()
{
Type sdkBuilderInterfaceType = typeof(IVRCSdkControlPanelBuilder);
Type sdkBuilderAttributeType = typeof(VRCSdkControlPanelBuilderAttribute);
List<Type> moduleTypesFromAttribute = new List<Type>();
foreach(Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
VRCSdkControlPanelBuilderAttribute[] sdkBuilderAttributes;
try
{
sdkBuilderAttributes = (VRCSdkControlPanelBuilderAttribute[])assembly.GetCustomAttributes(sdkBuilderAttributeType, true);
}
catch
{
sdkBuilderAttributes = new VRCSdkControlPanelBuilderAttribute[0];
}
foreach(VRCSdkControlPanelBuilderAttribute udonWrapperModuleAttribute in sdkBuilderAttributes)
{
if(udonWrapperModuleAttribute == null)
{
continue;
}
if(!sdkBuilderInterfaceType.IsAssignableFrom(udonWrapperModuleAttribute.Type))
{
continue;
}
moduleTypesFromAttribute.Add(udonWrapperModuleAttribute.Type);
}
}
return moduleTypesFromAttribute;
}
private void PopulateSdkBuilders()
{
if (_sdkBuilders != null)
{
return;
}
List<IVRCSdkControlPanelBuilder> builders = new List<IVRCSdkControlPanelBuilder>();
foreach (Type type in GetSdkBuilderTypesFromAttribute())
{
IVRCSdkControlPanelBuilder builder = (IVRCSdkControlPanelBuilder)Activator.CreateInstance(type);
builder.RegisterBuilder(this);
builders.Add(builder);
}
_sdkBuilders = builders.ToArray();
}
void ShowBuilders()
{
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUILayout.BeginVertical();
if (VRC.Core.ConfigManager.RemoteConfig.IsInitialized())
{
string sdkUnityVersion = VRC.Core.ConfigManager.RemoteConfig.GetString("sdkUnityVersion");
if (Application.unityVersion != sdkUnityVersion)
{
OnGUIWarning(null, "You are not using the recommended Unity version for the VRChat SDK. Content built with this version may not work correctly. Please use Unity " + sdkUnityVersion,
null,
() => { Application.OpenURL("https://unity3d.com/get-unity/download/archive"); }
);
}
}
if (VRCSdk3Analysis.IsSdkDllActive(VRCSdk3Analysis.SdkVersion.VRCSDK2) && VRCSdk3Analysis.IsSdkDllActive(VRCSdk3Analysis.SdkVersion.VRCSDK3))
{
List<Component> sdk2Components = VRCSdk3Analysis.GetSDKInScene(VRCSdk3Analysis.SdkVersion.VRCSDK2);
List<Component> sdk3Components = VRCSdk3Analysis.GetSDKInScene(VRCSdk3Analysis.SdkVersion.VRCSDK3);
if (sdk2Components.Count > 0 && sdk3Components.Count > 0)
{
OnGUIError(null,
"This scene contains components from the VRChat SDK version 2 and version 3. Version two elements will have to be replaced with their version 3 counterparts to build with SDK3 and UDON.",
() => { Selection.objects = sdk2Components.ToArray(); },
null
);
}
}
if (Lightmapping.giWorkflowMode == Lightmapping.GIWorkflowMode.Iterative)
{
OnGUIWarning(null,
"Automatic lightmap generation is enabled, which may stall the Unity build process. Before building and uploading, consider turning off 'Auto Generate' at the bottom of the Lighting Window.",
() =>
{
EditorWindow lightingWindow = GetLightingWindow();
if (lightingWindow)
{
lightingWindow.Show();
lightingWindow.Focus();
}
},
() =>
{
Lightmapping.giWorkflowMode = Lightmapping.GIWorkflowMode.OnDemand;
EditorWindow lightingWindow = GetLightingWindow();
if (!lightingWindow) return;
lightingWindow.Repaint();
Focus();
}
);
}
PopulateSdkBuilders();
IVRCSdkControlPanelBuilder selectedBuilder = null;
string errorMessage = null;
foreach (IVRCSdkControlPanelBuilder sdkBuilder in _sdkBuilders)
{
if (!sdkBuilder.IsValidBuilder(out string message))
{
if (selectedBuilder == null)
{
errorMessage = message;
}
}
else
{
if (selectedBuilder == null)
{
selectedBuilder = sdkBuilder;
errorMessage = null;
}
else
{
errorMessage =
"A Unity scene cannot contain a VRChat Scene Descriptor and also contain VRChat Avatar Descriptors";
}
}
}
if (selectedBuilder == null)
{
string message = "";
#if VRC_SDK_VRCSDK2
message = "A VRC_SceneDescriptor or VRC_AvatarDescriptor\nis required to build VRChat SDK Content";
#elif UDON
message = "A VRCSceneDescriptor is required to build a World";
#elif VRC_SDK_VRCSDK3
message = "A VRCAvatarDescriptor is required to build an Avatar";
#else
message = "The SDK did not load properly. Try this - In the Project window, navigate to Assets/VRCSDK/Plugins. Select all the DLLs, then right click and choose 'Reimport'";
#endif
EditorGUILayout.LabelField(message, titleGuiStyle, GUILayout.Width(SdkWindowWidth));
}
else if (errorMessage != null)
{
OnGUIError(null,
errorMessage,
() => {
foreach (IVRCSdkControlPanelBuilder builder in _sdkBuilders)
{
builder.SelectAllComponents();
} },
null
);
OnGUIShowIssues();
}
else
{
selectedBuilder.ShowBuilder();
}
if (Event.current.type == EventType.Used) return;
GUILayout.EndVertical();
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
}
public bool showLayerHelp = false;
bool ShouldShowLightmapWarning
{
get
{
const string GraphicsSettingsAssetPath = "ProjectSettings/GraphicsSettings.asset";
SerializedObject graphicsManager = new SerializedObject(UnityEditor.AssetDatabase.LoadAllAssetsAtPath(GraphicsSettingsAssetPath)[0]);
SerializedProperty lightmapStripping = graphicsManager.FindProperty("m_LightmapStripping");
return lightmapStripping.enumValueIndex == 0;
}
}
bool ShouldShowFogWarning
{
get
{
const string GraphicsSettingsAssetPath = "ProjectSettings/GraphicsSettings.asset";
SerializedObject graphicsManager = new SerializedObject(UnityEditor.AssetDatabase.LoadAllAssetsAtPath(GraphicsSettingsAssetPath)[0]);
SerializedProperty lightmapStripping = graphicsManager.FindProperty("m_FogStripping");
return lightmapStripping.enumValueIndex == 0;
}
}
void DrawIssueBox(MessageType msgType, Texture icon, string message, System.Action show, System.Action fix)
{
bool haveButtons = ((show != null) || (fix != null));
GUIStyle style = new GUIStyle("HelpBox");
style.fixedWidth = (haveButtons ? (SdkWindowWidth - 90) : SdkWindowWidth);
float minHeight = 40;
try
{
EditorGUILayout.BeginHorizontal();
if (icon != null)
{
GUIContent c = new GUIContent(message, icon);
float height = style.CalcHeight(c, style.fixedWidth);
GUILayout.Box(c, style, GUILayout.MinHeight(Mathf.Max(minHeight, height)));
}
else
{
GUIContent c = new GUIContent(message);
float height = style.CalcHeight(c, style.fixedWidth);
Rect rt = GUILayoutUtility.GetRect(c, style, GUILayout.MinHeight(Mathf.Max(minHeight, height)));
EditorGUI.HelpBox(rt, message, msgType); // note: EditorGUILayout resulted in uneven button layout in this case
}
if (haveButtons)
{
EditorGUILayout.BeginVertical();
float buttonHeight = ((show == null || fix == null) ? minHeight : (minHeight * 0.5f));
if ((show != null) && GUILayout.Button("Select", GUILayout.Height(buttonHeight)))
show();
if ((fix != null) && GUILayout.Button("Auto Fix", GUILayout.Height(buttonHeight)))
{
fix();
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
CheckedForIssues = false;
Repaint();
}
EditorGUILayout.EndVertical();
}
EditorGUILayout.EndHorizontal();
}
catch
{
// mutes 'ArgumentException: Getting control 0's position in a group with only 0 controls when doing repaint'
}
}
public void OnGuiFixIssuesToBuildOrTest()
{
GUIStyle s = new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleCenter };
EditorGUILayout.Space();
GUILayout.BeginVertical(boxGuiStyle, GUILayout.Height(WARNING_ICON_SIZE), GUILayout.Width(SdkWindowWidth));
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal();
var textDimensions = s.CalcSize(new GUIContent(FIX_ISSUES_TO_BUILD_OR_TEST_WARNING_STRING));
GUILayout.Label(new GUIContent(warningIconGraphic), GUILayout.Width(WARNING_ICON_SIZE), GUILayout.Height(WARNING_ICON_SIZE));
EditorGUILayout.LabelField(FIX_ISSUES_TO_BUILD_OR_TEST_WARNING_STRING, s, GUILayout.Width(textDimensions.x), GUILayout.Height(WARNING_ICON_SIZE));
EditorGUILayout.EndHorizontal();
GUILayout.FlexibleSpace();
GUILayout.EndVertical();
}
public void OnGUIShowIssues(Object subject = null)
{
if (subject == null)
subject = this;
EditorGUI.BeginChangeCheck();
GUIStyle style = GUI.skin.GetStyle("HelpBox");
if (GUIErrors.ContainsKey(subject))
foreach (Issue error in GUIErrors[subject].Where(s => !string.IsNullOrEmpty(s.issueText)))
DrawIssueBox(MessageType.Error, null, error.issueText, error.showThisIssue, error.fixThisIssue);
if (GUIWarnings.ContainsKey(subject))
foreach (Issue error in GUIWarnings[subject].Where(s => !string.IsNullOrEmpty(s.issueText)))
DrawIssueBox(MessageType.Warning, null, error.issueText, error.showThisIssue, error.fixThisIssue);
if (GUIStats.ContainsKey(subject))
{
foreach (var kvp in GUIStats[subject].Where(k => k.performanceRating == PerformanceRating.VeryPoor))
DrawIssueBox(MessageType.Warning, GetPerformanceIconForRating(kvp.performanceRating), kvp.issueText, kvp.showThisIssue, kvp.fixThisIssue);
foreach (var kvp in GUIStats[subject].Where(k => k.performanceRating == PerformanceRating.Poor))
DrawIssueBox(MessageType.Warning, GetPerformanceIconForRating(kvp.performanceRating), kvp.issueText, kvp.showThisIssue, kvp.fixThisIssue);
foreach (var kvp in GUIStats[subject].Where(k => k.performanceRating == PerformanceRating.Medium))
DrawIssueBox(MessageType.Warning, GetPerformanceIconForRating(kvp.performanceRating), kvp.issueText, kvp.showThisIssue, kvp.fixThisIssue);
foreach (var kvp in GUIStats[subject].Where(k => k.performanceRating == PerformanceRating.Good || k.performanceRating == PerformanceRating.Excellent))
DrawIssueBox(MessageType.Warning, GetPerformanceIconForRating(kvp.performanceRating), kvp.issueText, kvp.showThisIssue, kvp.fixThisIssue);
}
if (GUIInfos.ContainsKey(subject))
foreach (Issue error in GUIInfos[subject].Where(s => !string.IsNullOrEmpty(s.issueText)))
EditorGUILayout.HelpBox(error.issueText, MessageType.Info);
if (GUILinks.ContainsKey(subject))
{
EditorGUILayout.BeginVertical(style);
foreach (Issue error in GUILinks[subject].Where(s => !string.IsNullOrEmpty(s.issueText)))
{
var s = error.issueText.Split('\n');
EditorGUILayout.BeginHorizontal();
GUILayout.Label(s[0]);
if (GUILayout.Button("Open Link", GUILayout.Width(100)))
Application.OpenURL(s[1]);
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
}
if (EditorGUI.EndChangeCheck())
{
EditorUtility.SetDirty(subject);
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene());
}
}
private Texture GetPerformanceIconForRating(PerformanceRating value)
{
if (_perfIcon_Excellent == null)
_perfIcon_Excellent = Resources.Load<Texture>("PerformanceIcons/Perf_Great_32");
if (_perfIcon_Good == null)
_perfIcon_Good = Resources.Load<Texture>("PerformanceIcons/Perf_Good_32");
if (_perfIcon_Medium == null)
_perfIcon_Medium = Resources.Load<Texture>("PerformanceIcons/Perf_Medium_32");
if (_perfIcon_Poor == null)
_perfIcon_Poor = Resources.Load<Texture>("PerformanceIcons/Perf_Poor_32");
if (_perfIcon_VeryPoor == null)
_perfIcon_VeryPoor = Resources.Load<Texture>("PerformanceIcons/Perf_Horrible_32");
switch (value)
{
case PerformanceRating.Excellent:
return _perfIcon_Excellent;
case PerformanceRating.Good:
return _perfIcon_Good;
case PerformanceRating.Medium:
return _perfIcon_Medium;
case PerformanceRating.Poor:
return _perfIcon_Poor;
case PerformanceRating.None:
case PerformanceRating.VeryPoor:
return _perfIcon_VeryPoor;
}
return _perfIcon_Excellent;
}
Texture2D CreateBackgroundColorImage(UnityEngine.Color color)
{
int w = 4, h = 4;
Texture2D back = new Texture2D(w, h);
UnityEngine.Color[] buffer = new UnityEngine.Color[w * h];
for (int i = 0; i < w; ++i)
for (int j = 0; j < h; ++j)
buffer[i + w * j] = color;
back.SetPixels(buffer);
back.Apply(false);
return back;
}
public static void DrawContentInfo(string name, string version, string description, string capacity, string releaseStatus, List<string> tags)
{
EditorGUILayout.LabelField("Name: " + name);
EditorGUILayout.LabelField("Version: " + version.ToString());
EditorGUILayout.LabelField("Description: " + description);
if (capacity != null)
EditorGUILayout.LabelField("Capacity: " + capacity);
EditorGUILayout.LabelField("Release: " + releaseStatus);
if (tags != null)
{
string tagString = "";
for (int i = 0; i < tags.Count; i++)
{
if (i != 0) tagString += ", ";
tagString += tags[i];
}
EditorGUILayout.LabelField("Tags: " + tagString);
}
}
public static void DrawContentPlatformSupport(VRC.Core.ApiModel m)
{
if (m.supportedPlatforms == VRC.Core.ApiModel.SupportedPlatforms.StandaloneWindows || m.supportedPlatforms == VRC.Core.ApiModel.SupportedPlatforms.All)
EditorGUILayout.LabelField("Windows Support: YES");
else
EditorGUILayout.LabelField("Windows Support: NO");
if (m.supportedPlatforms == VRC.Core.ApiModel.SupportedPlatforms.Android || m.supportedPlatforms == VRC.Core.ApiModel.SupportedPlatforms.All)
EditorGUILayout.LabelField("Android Support: YES");
else
EditorGUILayout.LabelField("Android Support: NO");
}
public static void DrawBuildTargetSwitcher()
{
EditorGUILayout.LabelField("Active Build Target: " + EditorUserBuildSettings.activeBuildTarget);
if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.StandaloneWindows || EditorUserBuildSettings.activeBuildTarget == BuildTarget.StandaloneWindows64 && GUILayout.Button("Switch Build Target to Android"))
{
if (EditorUtility.DisplayDialog("Build Target Switcher", "Are you sure you want to switch your build target to Android? This could take a while.", "Confirm", "Cancel"))
{
EditorUserBuildSettings.selectedBuildTargetGroup = BuildTargetGroup.Android;
EditorUserBuildSettings.SwitchActiveBuildTargetAsync(BuildTargetGroup.Android, BuildTarget.Android);
}
}
if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android && GUILayout.Button("Switch Build Target to Windows"))
{
if (EditorUtility.DisplayDialog("Build Target Switcher", "Are you sure you want to switch your build target to Windows? This could take a while.", "Confirm", "Cancel"))
{
EditorUserBuildSettings.selectedBuildTargetGroup = BuildTargetGroup.Standalone;
EditorUserBuildSettings.SwitchActiveBuildTargetAsync(BuildTargetGroup.Standalone, BuildTarget.StandaloneWindows64);
}
}
}
public static string GetBuildAndPublishButtonString()
{
string buildButtonString = "Build & Publish for UNSUPPORTED";
if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.StandaloneWindows || EditorUserBuildSettings.activeBuildTarget == BuildTarget.StandaloneWindows64)
buildButtonString = "Build & Publish for Windows";
if (EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android)
buildButtonString = "Build & Publish for Android";
return buildButtonString;
}
public static Object[] GetSubstanceObjects(GameObject obj = null, bool earlyOut = false)
{
// if 'obj' is null we check entire scene
// if 'earlyOut' is true we only return 1st object (to detect if substances are present)
List<Object> objects = new List<Object>();
if (obj == null) return objects.Count < 1 ? null : objects.ToArray();
Renderer[] renderers = obj ? obj.GetComponentsInChildren<Renderer>(true) : FindObjectsOfType<Renderer>();
if (renderers == null || renderers.Length < 1)
return null;
foreach (Renderer r in renderers)
{
if (r.sharedMaterials.Length < 1)
continue;
foreach (Material m in r.sharedMaterials)
{
if (!m)
continue;
string path = AssetDatabase.GetAssetPath(m);
if (string.IsNullOrEmpty(path))
continue;
if (path.EndsWith(".sbsar", true, System.Globalization.CultureInfo.InvariantCulture))
{
objects.Add(r.gameObject);
if (earlyOut)
return objects.ToArray();
}
}
}
return objects.Count < 1 ? null : objects.ToArray();
}
public static bool HasSubstances(GameObject obj = null)
{
return (GetSubstanceObjects(obj, true) != null);
}
EditorWindow GetLightingWindow()
{
var editorAsm = typeof(UnityEditor.Editor).Assembly;
return EditorWindow.GetWindow(editorAsm.GetType("UnityEditor.LightingWindow"));
}
public static void ShowContentPublishPermissionsDialog()
{
if (!VRC.Core.ConfigManager.RemoteConfig.IsInitialized())
{
VRC.Core.ConfigManager.RemoteConfig.Init(() => ShowContentPublishPermissionsDialog());
return;
}
string message = VRC.Core.ConfigManager.RemoteConfig.GetString("sdkNotAllowedToPublishMessage");
int result = UnityEditor.EditorUtility.DisplayDialogComplex("VRChat SDK", message, "Developer FAQ", "VRChat Discord", "OK");
if (result == 0)
{
ShowDeveloperFAQ();
}
if (result == 1)
{
ShowVRChatDiscord();
}
}
}