A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Reflection;
4using Unity.Multiplayer.Center.Analytics;
5using Unity.Multiplayer.Center.Common;
6using Unity.Multiplayer.Center.Onboarding;
7using Unity.Multiplayer.Center.Questionnaire;
8using Unity.Multiplayer.Center.Window.UI;
9using UnityEditor;
10using UnityEngine;
11using UnityEngine.UIElements;
12
13namespace Unity.Multiplayer.Center.Window
14{
15 [Serializable]
16 internal class QuickstartCategory
17 {
18 [SerializeField]
19 public OnboardingSectionCategory Category;
20
21 [SerializeReference]
22 public IOnboardingSection[] Sections;
23 }
24
25 /// <summary>
26 /// This is the main view for the Quickstart tab.
27 /// Note that in the code, the Quickstart tab is referred to as the Getting Started tab.
28 /// </summary>
29 [Serializable]
30 internal class GettingStartedTabView : ITabView
31 {
32 const string k_SectionUssClass = "onboarding-section-category-container";
33 const string k_CategoryButtonUssClass = "onboarding-category-button";
34 const string k_OnboardingCategoriesUssClass = "onboarding-categories";
35 const string k_OnboardingContentUssClass = "onboarding-content";
36
37 [field: SerializeField]
38 public string Name { get; private set; }
39
40 public bool IsEnabled => PackageManagement.IsAnyMultiplayerPackageInstalled();
41
42 public string ToolTip => IsEnabled ? "" : "Please install some multiplayer packages to access quickstart content.";
43
44 public VisualElement RootVisualElement { get; set; }
45
46 [SerializeField]
47 int m_SelectedCategory;
48
49 Dictionary<OnboardingSectionCategory, int> m_CategoryIndices;
50
51 VisualElement[] m_CategoryContainers;
52
53 [SerializeField]
54 QuickstartCategory[] m_SectionCategories;
55 /// <summary>
56 /// To find out if new section appeared, we need to keep track of the last section types we found.
57 /// </summary>
58 [SerializeField]
59 AvailableSectionTypes m_LastFoundSectionTypes;
60
61 public IMultiplayerCenterAnalytics MultiplayerCenterAnalytics { get; set; }
62
63 public GettingStartedTabView(string name = "Quickstart")
64 {
65 Name = name;
66 }
67
68 public void Refresh()
69 {
70 Debug.Assert(MultiplayerCenterAnalytics != null, "MultiplayerCenterAnalytics != null");
71 UserChoicesObject.instance.OnSolutionSelectionChanged -= NotifyChoicesChanged;
72 UserChoicesObject.instance.OnSolutionSelectionChanged += NotifyChoicesChanged;
73
74 var currentSectionTypes = SectionsFinder.FindSectionTypes();
75
76 if (m_SectionCategories == null || m_SectionCategories.Length == 0 || m_LastFoundSectionTypes.HaveTypesChanged(currentSectionTypes))
77 {
78 m_LastFoundSectionTypes = currentSectionTypes;
79 ConstructSectionInstances();
80 CreateViews();
81 }
82 else if(RootVisualElement == null || RootVisualElement.childCount == 0)
83 {
84 CreateViews();
85 }
86 }
87
88 public void Clear()
89 {
90 RootVisualElement?.Clear();
91 if (m_SectionCategories == null)
92 return;
93 foreach (var category in m_SectionCategories)
94 {
95 if(category == null) continue;
96 foreach (var section in category.Sections)
97 {
98 section?.Unload();
99 }
100 }
101
102 Array.Clear(m_SectionCategories, 0, m_SectionCategories.Length);
103 }
104
105 public void SetVisible(bool visible)
106 {
107 RootVisualElement.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
108 }
109
110 void ConstructSectionInstances()
111 {
112 var enumValues = Enum.GetValues(typeof(OnboardingSectionCategory));
113 var allCategories = new QuickstartCategory[enumValues.Length];
114 foreach (var categoryObject in enumValues)
115 {
116 var category = (OnboardingSectionCategory) categoryObject;
117 var categoryData = new QuickstartCategory {Category = category, Sections = Array.Empty<IOnboardingSection>()};
118 allCategories[(int) category] = categoryData;
119 if (!m_LastFoundSectionTypes.TryGetValue(category, out var sectionTypes))
120 {
121 continue; // no section for that category
122 }
123
124 categoryData.Sections = new IOnboardingSection[sectionTypes.Length];
125 for (var index = 0; index < sectionTypes.Length; index++)
126 {
127 var sectionType = sectionTypes[index];
128 var newSection = SectionFromType(sectionType);
129
130 // TODO: check what to do with null sections
131 if (newSection == null) continue;
132
133 categoryData.Sections[index] = newSection;
134 }
135 }
136
137 m_SectionCategories = allCategories;
138 }
139
140 void SetSelectedCategory(int categoryIndex)
141 {
142 m_SelectedCategory = categoryIndex;
143 for (var index = 0; index < m_CategoryContainers.Length; index++)
144 {
145 var categoryContainer = m_CategoryContainers[index];
146 if(categoryContainer != null)
147 categoryContainer.style.display = index == categoryIndex ? DisplayStyle.Flex : DisplayStyle.None;
148 }
149 }
150
151 void CreateViews()
152 {
153 RootVisualElement ??= new VisualElement();
154 RootVisualElement.Clear();
155
156 if (QuickstartIsMissingView.ShouldShow)
157 {
158 RootVisualElement.Add(new QuickstartIsMissingView().RootVisualElement);
159 }
160
161 m_CategoryIndices = new Dictionary<OnboardingSectionCategory, int>();
162 m_CategoryContainers = new VisualElement[m_SectionCategories.Length];
163
164 var horizontalContainer = new TwoPaneSplitView(0, 250, TwoPaneSplitViewOrientation.Horizontal);
165 RootVisualElement.Add(horizontalContainer);
166 horizontalContainer.AddToClassList(StyleClasses.MainSplitView);
167 var buttonGroup = new ToggleButtonGroup() { allowEmptySelection = false, isMultipleSelection = false};
168 buttonGroup.AddToClassList(k_OnboardingCategoriesUssClass);
169 buttonGroup.AddToClassList(StyleClasses.MainSplitViewLeft);
170 horizontalContainer.Add(buttonGroup);
171
172 var scrollView = new ScrollView(ScrollViewMode.Vertical) {horizontalScrollerVisibility = ScrollerVisibility.Hidden};
173 scrollView.AddToClassList(StyleClasses.MainSplitViewRight);
174 scrollView.AddToClassList(k_OnboardingContentUssClass);
175
176 horizontalContainer.Add(scrollView);
177
178 var index = -1;
179 foreach (var categoryData in m_SectionCategories)
180 {
181 if (categoryData == null || categoryData.Sections.Length == 0) continue;
182
183 ++index;
184 var category = categoryData.Category;
185 var currentContainer = StartNewSection(scrollView, category);
186 scrollView.Add(currentContainer);
187
188 m_CategoryIndices[category] = index;
189 m_CategoryContainers[index] = currentContainer;
190
191 var button = new Button { text = SectionCategoryToString(category)};
192 button.AddToClassList(k_CategoryButtonUssClass);
193 buttonGroup.Add(button);
194
195 CreateSectionViewsIn(currentContainer, categoryData);
196 }
197
198 // Hide the SplitView if we have nothing to show
199 var noContentToShow = index == -1;
200 horizontalContainer.style.display = noContentToShow ? DisplayStyle.None : DisplayStyle.Flex;
201
202 if (noContentToShow && !QuickstartIsMissingView.ShouldShow)
203 {
204 var noContentLabel = new Label("No content is available for the current selection in Netcode Solution and Hosting Model.");
205 noContentLabel.style.marginLeft = noContentLabel.style.marginRight = noContentLabel.style.marginTop = noContentLabel.style.marginBottom = 8;
206 RootVisualElement.Add(noContentLabel);
207 }
208
209 SetSelectedCategory(m_SelectedCategory);
210 ulong mask = (ulong) 1 << m_SelectedCategory;
211 buttonGroup.SetValueWithoutNotify(new ToggleButtonGroupState(mask, m_CategoryIndices.Count));
212
213 // MTT-8918 Block the callback on register as it will always return index 0,
214 // which can result in a mismatch between toggle group and selected category.
215 var onCreateFrame = EditorApplication.timeSinceStartup;
216 buttonGroup.RegisterValueChangedCallback(evt =>
217 {
218 if (Math.Abs(onCreateFrame - EditorApplication.timeSinceStartup) < 0.05f)
219 return;
220
221 var selectedIndex = evt.newValue.GetActiveOptions(stackalloc int[evt.newValue.length])[0];
222 SetSelectedCategory(selectedIndex);
223 });
224 NotifyChoicesChanged();
225 }
226
227 void CreateSectionViewsIn(VisualElement currentContainer, QuickstartCategory categoryData)
228 {
229 foreach (var section in categoryData.Sections)
230 {
231 try
232 {
233 if (section is ISectionWithAnalytics sectionWithAnalytics)
234 {
235 var attribute = section.GetType().GetCustomAttribute<OnboardingSectionAttribute>();
236 sectionWithAnalytics.AnalyticsProvider = new OnboardingSectionAnalyticsProvider(MultiplayerCenterAnalytics,
237 targetPackageId: attribute.TargetPackageId, sectionId: attribute.Id);
238 }
239
240 section.Load();
241 section.Root.name = section.GetType().Name;
242 currentContainer.Add(section.Root);
243 }
244 catch (Exception e)
245 {
246 Debug.LogWarning($"Could not load onboarding section {section?.GetType()}: {e}");
247 }
248 }
249 }
250
251 void NotifyChoicesChanged()
252 {
253 if (m_SectionCategories == null)
254 return;
255
256 foreach (var category in m_SectionCategories)
257 {
258 if (category == null) continue;
259 foreach (var section in category.Sections)
260 {
261 if (section is not ISectionDependingOnUserChoices dependentSection) continue;
262
263 try
264 {
265 dependentSection.HandleAnswerData(UserChoicesObject.instance.UserAnswers);
266 dependentSection.HandlePreset(UserChoicesObject.instance.Preset);
267 dependentSection.HandleUserSelectionData(UserChoicesObject.instance.SelectedSolutions);
268 }
269 catch (Exception e)
270 {
271 Debug.LogWarning($"Could not set data for onboarding section {section.GetType()}: {e}");
272 }
273 }
274 }
275 }
276
277 static VisualElement StartNewSection(VisualElement parent, OnboardingSectionCategory category)
278 {
279 var container = new VisualElement();
280 if (category != OnboardingSectionCategory.Intro)
281 {
282 var titleContainer = new VisualElement();
283 titleContainer.AddToClassList(StyleClasses.ViewHeadline);
284
285 var title = new Label(SectionCategoryToString(category));
286 titleContainer.Add(title);
287 container.Add(titleContainer);
288 }
289
290 container.AddToClassList(k_SectionUssClass);
291 parent.Add(container);
292 return container;
293 }
294
295 static IOnboardingSection SectionFromType(Type type)
296 {
297 var constructed = type.GetConstructor(Type.EmptyTypes)?.Invoke(null);
298 if (constructed is IOnboardingSection section) return section;
299
300 Debug.LogWarning($"Could not create onboarding section {type}");
301 return null;
302 }
303
304 static string SectionCategoryToString(OnboardingSectionCategory category)
305 {
306 return category switch
307 {
308 OnboardingSectionCategory.Intro => "Intro",
309 OnboardingSectionCategory.Netcode => "Netcode and Tools",
310 OnboardingSectionCategory.ConnectingPlayers => "Connecting Players",
311 OnboardingSectionCategory.ServerInfrastructure => "Hosting",
312 OnboardingSectionCategory.Other => "Other",
313 _ => category.ToString()
314 };
315 }
316 }
317}