using System; using System.Collections.Generic; using System.Reflection; using Unity.Multiplayer.Center.Common; using Unity.Multiplayer.Center.Questionnaire; using Unity.Multiplayer.Center.Recommendations; using UnityEditor; using UnityEngine; using DisplayCondition = Unity.Multiplayer.Center.Common.DisplayCondition; namespace Unity.Multiplayer.Center.Onboarding { using SectionCategoryToSectionIdToSectionType = Dictionary>; using SectionCategoryToSectionList = Dictionary; /// /// Stores the available section types in a serializable way, but for comparison purposes only. /// Only the assembly qualified names are serialized in a sorted array. /// [Serializable] internal class AvailableSectionTypes { readonly SectionCategoryToSectionList m_SectionMapping; [SerializeField] string[] m_SectionTypeNames; public IEnumerable SectionTypeNames => m_SectionTypeNames; public AvailableSectionTypes(SectionCategoryToSectionList sectionTypes) { m_SectionMapping = sectionTypes; m_SectionTypeNames = RecommendationUtils.GetSectionTypeNamesInOrder(m_SectionMapping).ToArray(); } public bool TryGetValue(OnboardingSectionCategory category, out Type[] sectionTypes) { return m_SectionMapping.TryGetValue(category, out sectionTypes); } public bool HaveTypesChanged(AvailableSectionTypes other) { return m_SectionTypeNames == null || !RecommendationUtils.AreArraysEqual(m_SectionTypeNames, other.m_SectionTypeNames); } } internal static class SectionsFinder { /// /// The target packages that are used to determine if "nothing is installed", which results in sections with /// the display condition "NoPackageInstalled" to be displayed. /// static readonly string[] k_TargetPackages = { "com.unity.services.core", // This is a dependency of all the services, so if any service is installed, this is installed "com.unity.netcode.gameobjects", "com.unity.netcode", // Netcodes }; public static AvailableSectionTypes FindSectionTypes() { var dico = GetSectionCategoryToSectionIdToSectionType(); return SortSectionsByOrder(dico); } static AvailableSectionTypes SortSectionsByOrder(SectionCategoryToSectionIdToSectionType dico) { var result = new SectionCategoryToSectionList(); foreach (var (category, idToTypeDictionary) in dico) { var typeAttributeList = GetOnboardingTypeAttributeList(idToTypeDictionary); typeAttributeList.Sort((typeA, typeB) => typeA.Attribute.Order.CompareTo(typeB.Attribute.Order)); result[category] = GetOnboardingTypeArray(typeAttributeList); } return new AvailableSectionTypes(result); } static Type[] GetOnboardingTypeArray(List<(Type Type, OnboardingSectionAttribute Attribute)> typeAttributeList) { var typeArray = new Type[typeAttributeList.Count]; for (var i = 0; i < typeAttributeList.Count; i++) { typeArray[i] = typeAttributeList[i].Type; } return typeArray; } static List<(Type Type, OnboardingSectionAttribute Attribute)> GetOnboardingTypeAttributeList(Dictionary idToTypeDictionary) { var typeAttributeList = new List<(Type Type, OnboardingSectionAttribute Attribute)>(idToTypeDictionary.Count); foreach (var (_, type) in idToTypeDictionary) { var attribute = type.GetCustomAttribute(); if (attribute != null) { typeAttributeList.Add((type, attribute)); } } return typeAttributeList; } static SectionCategoryToSectionIdToSectionType GetSectionCategoryToSectionIdToSectionType() { var dico = new Dictionary>(); foreach (var sectionType in TypeCache.GetTypesDerivedFrom()) { if (sectionType.IsAbstract) continue; // check if type has default constructor if (sectionType.GetConstructor(System.Type.EmptyTypes) == null) { Debug.LogWarning($"Onboarding section type {sectionType} does not have a default constructor and will be ignored."); continue; } var sectionAttribute = sectionType.GetCustomAttribute(); if(sectionAttribute == null) continue; if (!IsDisplayConditionFulfilledForSection(sectionAttribute)) continue; if (!IsSectionSupportedBySelectedInfrastructure(sectionAttribute)) continue; if (!IsSectionSupportedBySelectedNetcodeChoice(sectionAttribute)) continue; if (!dico.ContainsKey(sectionAttribute.Category)) dico[sectionAttribute.Category] = new Dictionary(); if (dico[sectionAttribute.Category].TryGetValue(sectionAttribute.Id, out var existing)) { var existingAttr = existing.GetCustomAttribute(); if (existingAttr!= null && existingAttr.Id == sectionAttribute.Id && sectionAttribute.Priority > existingAttr.Priority) { dico[sectionAttribute.Category][sectionAttribute.Id] = sectionType; } } else { dico[sectionAttribute.Category][sectionAttribute.Id] = sectionType; } } return dico; } static bool IsDisplayConditionFulfilledForSection(OnboardingSectionAttribute attribute) { return attribute.DisplayCondition switch { DisplayCondition.None => true, DisplayCondition.PackageInstalled when string.IsNullOrEmpty(attribute.TargetPackageId) => throw new ArgumentException("PackageInstalled condition requires a target package id"), DisplayCondition.PackageInstalled => PackageManagement.IsInstalled(attribute.TargetPackageId), DisplayCondition.NoPackageInstalled => NothingInstalled(), _ => throw new NotImplementedException($"Unknown display condition: {attribute.DisplayCondition}") }; } static bool IsSectionSupportedBySelectedInfrastructure(OnboardingSectionAttribute attribute) { if (UserChoicesObject.instance.SelectedSolutions == null) return true; var selectedInfrastructure = UserChoicesObject.instance.SelectedSolutions.SelectedHostingModel; return attribute.HostingModelDependency == SelectedSolutionsData.HostingModel.None || attribute.HostingModelDependency == selectedInfrastructure; } static bool IsSectionSupportedBySelectedNetcodeChoice(OnboardingSectionAttribute attribute) { if (UserChoicesObject.instance.SelectedSolutions == null) return true; var selectedNetcode = UserChoicesObject.instance.SelectedSolutions.SelectedNetcodeSolution; return attribute.NetcodeDependency == SelectedSolutionsData.NetcodeSolution.None || attribute.NetcodeDependency == selectedNetcode; } static bool NothingInstalled() { return !PackageManagement.IsAnyPackageInstalled(k_TargetPackages); } } }