using System;
using System.Collections.Generic;
using System.Reflection;
using Unity.Multiplayer.Center.Analytics;
using Unity.Multiplayer.Center.Common;
using Unity.Multiplayer.Center.Onboarding;
using Unity.Multiplayer.Center.Questionnaire;
using Unity.Multiplayer.Center.Window.UI;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Multiplayer.Center.Window
{
[Serializable]
internal class QuickstartCategory
{
[SerializeField]
public OnboardingSectionCategory Category;
[SerializeReference]
public IOnboardingSection[] Sections;
}
///
/// This is the main view for the Quickstart tab.
/// Note that in the code, the Quickstart tab is referred to as the Getting Started tab.
///
[Serializable]
internal class GettingStartedTabView : ITabView
{
const string k_SectionUssClass = "onboarding-section-category-container";
const string k_CategoryButtonUssClass = "onboarding-category-button";
const string k_OnboardingCategoriesUssClass = "onboarding-categories";
const string k_OnboardingContentUssClass = "onboarding-content";
[field: SerializeField]
public string Name { get; private set; }
public bool IsEnabled => PackageManagement.IsAnyMultiplayerPackageInstalled();
public string ToolTip => IsEnabled ? "" : "Please install some multiplayer packages to access quickstart content.";
public VisualElement RootVisualElement { get; set; }
[SerializeField]
int m_SelectedCategory;
Dictionary m_CategoryIndices;
VisualElement[] m_CategoryContainers;
[SerializeField]
QuickstartCategory[] m_SectionCategories;
///
/// To find out if new section appeared, we need to keep track of the last section types we found.
///
[SerializeField]
AvailableSectionTypes m_LastFoundSectionTypes;
public IMultiplayerCenterAnalytics MultiplayerCenterAnalytics { get; set; }
public GettingStartedTabView(string name = "Quickstart")
{
Name = name;
}
public void Refresh()
{
Debug.Assert(MultiplayerCenterAnalytics != null, "MultiplayerCenterAnalytics != null");
UserChoicesObject.instance.OnSolutionSelectionChanged -= NotifyChoicesChanged;
UserChoicesObject.instance.OnSolutionSelectionChanged += NotifyChoicesChanged;
var currentSectionTypes = SectionsFinder.FindSectionTypes();
if (m_SectionCategories == null || m_SectionCategories.Length == 0 || m_LastFoundSectionTypes.HaveTypesChanged(currentSectionTypes))
{
m_LastFoundSectionTypes = currentSectionTypes;
ConstructSectionInstances();
CreateViews();
}
else if(RootVisualElement == null || RootVisualElement.childCount == 0)
{
CreateViews();
}
}
public void Clear()
{
RootVisualElement?.Clear();
if (m_SectionCategories == null)
return;
foreach (var category in m_SectionCategories)
{
if(category == null) continue;
foreach (var section in category.Sections)
{
section?.Unload();
}
}
Array.Clear(m_SectionCategories, 0, m_SectionCategories.Length);
}
public void SetVisible(bool visible)
{
RootVisualElement.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
}
void ConstructSectionInstances()
{
var enumValues = Enum.GetValues(typeof(OnboardingSectionCategory));
var allCategories = new QuickstartCategory[enumValues.Length];
foreach (var categoryObject in enumValues)
{
var category = (OnboardingSectionCategory) categoryObject;
var categoryData = new QuickstartCategory {Category = category, Sections = Array.Empty()};
allCategories[(int) category] = categoryData;
if (!m_LastFoundSectionTypes.TryGetValue(category, out var sectionTypes))
{
continue; // no section for that category
}
categoryData.Sections = new IOnboardingSection[sectionTypes.Length];
for (var index = 0; index < sectionTypes.Length; index++)
{
var sectionType = sectionTypes[index];
var newSection = SectionFromType(sectionType);
// TODO: check what to do with null sections
if (newSection == null) continue;
categoryData.Sections[index] = newSection;
}
}
m_SectionCategories = allCategories;
}
void SetSelectedCategory(int categoryIndex)
{
m_SelectedCategory = categoryIndex;
for (var index = 0; index < m_CategoryContainers.Length; index++)
{
var categoryContainer = m_CategoryContainers[index];
if(categoryContainer != null)
categoryContainer.style.display = index == categoryIndex ? DisplayStyle.Flex : DisplayStyle.None;
}
}
void CreateViews()
{
RootVisualElement ??= new VisualElement();
RootVisualElement.Clear();
if (QuickstartIsMissingView.ShouldShow)
{
RootVisualElement.Add(new QuickstartIsMissingView().RootVisualElement);
}
m_CategoryIndices = new Dictionary();
m_CategoryContainers = new VisualElement[m_SectionCategories.Length];
var horizontalContainer = new TwoPaneSplitView(0, 250, TwoPaneSplitViewOrientation.Horizontal);
RootVisualElement.Add(horizontalContainer);
horizontalContainer.AddToClassList(StyleClasses.MainSplitView);
var buttonGroup = new ToggleButtonGroup() { allowEmptySelection = false, isMultipleSelection = false};
buttonGroup.AddToClassList(k_OnboardingCategoriesUssClass);
buttonGroup.AddToClassList(StyleClasses.MainSplitViewLeft);
horizontalContainer.Add(buttonGroup);
var scrollView = new ScrollView(ScrollViewMode.Vertical) {horizontalScrollerVisibility = ScrollerVisibility.Hidden};
scrollView.AddToClassList(StyleClasses.MainSplitViewRight);
scrollView.AddToClassList(k_OnboardingContentUssClass);
horizontalContainer.Add(scrollView);
var index = -1;
foreach (var categoryData in m_SectionCategories)
{
if (categoryData == null || categoryData.Sections.Length == 0) continue;
++index;
var category = categoryData.Category;
var currentContainer = StartNewSection(scrollView, category);
scrollView.Add(currentContainer);
m_CategoryIndices[category] = index;
m_CategoryContainers[index] = currentContainer;
var button = new Button { text = SectionCategoryToString(category)};
button.AddToClassList(k_CategoryButtonUssClass);
buttonGroup.Add(button);
CreateSectionViewsIn(currentContainer, categoryData);
}
// Hide the SplitView if we have nothing to show
var noContentToShow = index == -1;
horizontalContainer.style.display = noContentToShow ? DisplayStyle.None : DisplayStyle.Flex;
if (noContentToShow && !QuickstartIsMissingView.ShouldShow)
{
var noContentLabel = new Label("No content is available for the current selection in Netcode Solution and Hosting Model.");
noContentLabel.style.marginLeft = noContentLabel.style.marginRight = noContentLabel.style.marginTop = noContentLabel.style.marginBottom = 8;
RootVisualElement.Add(noContentLabel);
}
SetSelectedCategory(m_SelectedCategory);
ulong mask = (ulong) 1 << m_SelectedCategory;
buttonGroup.SetValueWithoutNotify(new ToggleButtonGroupState(mask, m_CategoryIndices.Count));
// MTT-8918 Block the callback on register as it will always return index 0,
// which can result in a mismatch between toggle group and selected category.
var onCreateFrame = EditorApplication.timeSinceStartup;
buttonGroup.RegisterValueChangedCallback(evt =>
{
if (Math.Abs(onCreateFrame - EditorApplication.timeSinceStartup) < 0.05f)
return;
var selectedIndex = evt.newValue.GetActiveOptions(stackalloc int[evt.newValue.length])[0];
SetSelectedCategory(selectedIndex);
});
NotifyChoicesChanged();
}
void CreateSectionViewsIn(VisualElement currentContainer, QuickstartCategory categoryData)
{
foreach (var section in categoryData.Sections)
{
try
{
if (section is ISectionWithAnalytics sectionWithAnalytics)
{
var attribute = section.GetType().GetCustomAttribute();
sectionWithAnalytics.AnalyticsProvider = new OnboardingSectionAnalyticsProvider(MultiplayerCenterAnalytics,
targetPackageId: attribute.TargetPackageId, sectionId: attribute.Id);
}
section.Load();
section.Root.name = section.GetType().Name;
currentContainer.Add(section.Root);
}
catch (Exception e)
{
Debug.LogWarning($"Could not load onboarding section {section?.GetType()}: {e}");
}
}
}
void NotifyChoicesChanged()
{
if (m_SectionCategories == null)
return;
foreach (var category in m_SectionCategories)
{
if (category == null) continue;
foreach (var section in category.Sections)
{
if (section is not ISectionDependingOnUserChoices dependentSection) continue;
try
{
dependentSection.HandleAnswerData(UserChoicesObject.instance.UserAnswers);
dependentSection.HandlePreset(UserChoicesObject.instance.Preset);
dependentSection.HandleUserSelectionData(UserChoicesObject.instance.SelectedSolutions);
}
catch (Exception e)
{
Debug.LogWarning($"Could not set data for onboarding section {section.GetType()}: {e}");
}
}
}
}
static VisualElement StartNewSection(VisualElement parent, OnboardingSectionCategory category)
{
var container = new VisualElement();
if (category != OnboardingSectionCategory.Intro)
{
var titleContainer = new VisualElement();
titleContainer.AddToClassList(StyleClasses.ViewHeadline);
var title = new Label(SectionCategoryToString(category));
titleContainer.Add(title);
container.Add(titleContainer);
}
container.AddToClassList(k_SectionUssClass);
parent.Add(container);
return container;
}
static IOnboardingSection SectionFromType(Type type)
{
var constructed = type.GetConstructor(Type.EmptyTypes)?.Invoke(null);
if (constructed is IOnboardingSection section) return section;
Debug.LogWarning($"Could not create onboarding section {type}");
return null;
}
static string SectionCategoryToString(OnboardingSectionCategory category)
{
return category switch
{
OnboardingSectionCategory.Intro => "Intro",
OnboardingSectionCategory.Netcode => "Netcode and Tools",
OnboardingSectionCategory.ConnectingPlayers => "Connecting Players",
OnboardingSectionCategory.ServerInfrastructure => "Hosting",
OnboardingSectionCategory.Other => "Other",
_ => category.ToString()
};
}
}
}