using System; using System.Collections.Generic; using System.Text; using Unity.Multiplayer.Center.Common; using Unity.Multiplayer.Center.Onboarding; using Unity.Multiplayer.Center.Questionnaire; using UnityEngine; namespace Unity.Multiplayer.Center.Recommendations { internal static class RecommendationUtils { public static List PackagesToInstall(RecommendationViewData recommendation, SolutionsToRecommendedPackageViewData solutionToPackageData) { var packagesToInstall = new List(); // Can happen on first load if (recommendation?.NetcodeOptions == null) return packagesToInstall; var selectedNetcode = GetSelectedNetcode(recommendation); if (selectedNetcode == null) return packagesToInstall; // add features based on netcode if (selectedNetcode.MainPackage != null) packagesToInstall.Add(selectedNetcode.MainPackage); var selectedServerArchitecture = GetSelectedHostingModel(recommendation); if (selectedServerArchitecture.MainPackage != null) packagesToInstall.Add(selectedServerArchitecture.MainPackage); foreach (var package in solutionToPackageData.GetPackagesForSelection(selectedNetcode.Solution, selectedServerArchitecture.Solution)) { if (package.Selected) { packagesToInstall.Add(package); } } return packagesToInstall; } public static RecommendedSolutionViewData GetSelectedHostingModel(RecommendationViewData recommendation) { return GetSelectedSolution(recommendation.ServerArchitectureOptions); } public static RecommendedSolutionViewData GetSelectedNetcode(RecommendationViewData recommendation) { return GetSelectedSolution(recommendation.NetcodeOptions); } /// /// Finds the first selected solution in the input array. /// /// The available solutions. /// Returns the first selected solution. If no solution is selected, it returns null. public static RecommendedSolutionViewData GetSelectedSolution(RecommendedSolutionViewData[] availableSolutions) { foreach (var solution in availableSolutions) { if (solution.Selected) { return solution; } } return default; } public static PackageDetails GetPackageDetailForPackageId(string packageId) { var idToPackageDetailDict = RecommenderSystemDataObject.instance.RecommenderSystemData.PackageDetailsById; if (idToPackageDetailDict.TryGetValue(packageId, out var packageDetail)) return packageDetail; Debug.LogError("Trying to get package detail for package id that does not exist: " + packageId); return null; } /// /// Returns all the packages passed via packageIds and their informal dependencies (stored in AdditionalPackages) /// /// List of PackageDetails /// List of package id /// tooltip text public static void GetPackagesWithAdditionalPackages(List packages, out List ids, out List names, out string toolTip) { ids = new List(); names = new List(); var toolTipBuilder = new StringBuilder(); foreach (var package in packages) { var packageDetail = GetPackageDetailForPackageId(package.PackageId); var id = string.IsNullOrEmpty(package.PreReleaseVersion)? package.PackageId : $"{package.PackageId}@{package.PreReleaseVersion}"; var name = string.IsNullOrEmpty(package.PreReleaseVersion)? packageDetail.Name : $"{packageDetail.Name} {package.PreReleaseVersion}"; ids.Add(id); names.Add(name); toolTipBuilder.Append(name); if (packageDetail.AdditionalPackages is {Length: > 0}) { toolTipBuilder.Append(" + "); foreach (var additionalPackageId in packageDetail.AdditionalPackages) { var additionalPackage = GetPackageDetailForPackageId(additionalPackageId); ids.Add(additionalPackage.Id); names.Add(additionalPackage.Name); toolTipBuilder.Append(additionalPackage.Name); toolTipBuilder.Append(","); } } toolTipBuilder.Append("\n"); } // remove last newline toolTip = toolTipBuilder.ToString().TrimEnd('\n'); ids.Add(QuickstartIsMissingView.PackageId); } /// /// Reapplies the previous selection on the view data /// /// The recommendation view data to update /// The previous selection data public static void ApplyPreviousSelection(RecommendationViewData recommendation, SelectedSolutionsData data) { if (data == null || recommendation == null) return; if (data.SelectedNetcodeSolution != SelectedSolutionsData.NetcodeSolution.None) { foreach (var d in recommendation.NetcodeOptions) { d.Selected = Logic.ConvertNetcodeSolution(d) == data.SelectedNetcodeSolution; } } if (data.SelectedHostingModel != SelectedSolutionsData.HostingModel.None) { foreach (var view in recommendation.ServerArchitectureOptions) { view.Selected = Logic.ConvertInfrastructure(view) == data.SelectedHostingModel; } } } /// /// Returns the packages that are of the given recommendation type /// /// All the package view data as returned by the recommender system /// The target recommendation type /// The filtered list public static List FilterByType(IEnumerable packages, RecommendationType type) { var filteredPackages = new List(); foreach (var package in packages) { if (package.RecommendationType == type) { filteredPackages.Add(package); } } return filteredPackages; } public static int IndexOfMaximumScore(RecommendedSolutionViewData[] array) { var maxIndex = -1; var maxScore = float.MinValue; for (var index = 0; index < array.Length; index++) { var solution = array[index]; if (solution.RecommendationType == RecommendationType.Incompatible) continue; if (solution.Score > maxScore) { maxScore = solution.Score; maxIndex = index; } } return maxIndex; } /// /// Finds the recommended solution among the scored solutions. /// /// An array of tuples, where each tuple contains a PossibleSolution and its corresponding Scoring. /// Returns the recommended solution with the maximum total score. public static PossibleSolution FindRecommendedSolution((PossibleSolution, Scoring)[] scoredSolutions) { var maxScore = float.MinValue; PossibleSolution recommendedSolution = default; foreach (var (possibleSolution, scoring) in scoredSolutions) { if (scoring.TotalScore > maxScore) { maxScore = scoring.TotalScore; recommendedSolution = possibleSolution; } } return recommendedSolution; } /// /// Looks through the hosting models and marks them incompatible if necessary (deselected and recommendation /// type incompatible). /// Note that this might leave the recommendation in an invalid state (no hosting model selected) /// /// The recommendation data to modify. public static void MarkIncompatibleHostingModels(RecommendationViewData recommendation) { var netcode = GetSelectedNetcode(recommendation); for (var index = 0; index < recommendation.ServerArchitectureOptions.Length; index++) { var hosting = recommendation.ServerArchitectureOptions[index]; var isCompatible = RecommenderSystemDataObject.instance.RecommenderSystemData.IsHostingModelCompatibleWithNetcode(netcode.Solution, hosting.Solution, out _); if (!isCompatible) { hosting.RecommendationType = RecommendationType.Incompatible; hosting.Selected = false; } else { hosting.RecommendationType = RecommendationType.SecondArchitectureChoice; } } } /// /// Finds a recommended package view by its ID from a list. /// /// A list of Recommended Package ViewData representing the packages. /// The ID of the package to find. /// Returns the RecommendedPackageViewData object with the matching ID. If none is found, it returns null. public static RecommendedPackageViewData FindRecommendedPackageViewById( List packages, string id) { RecommendedPackageViewData featureToSet = default; foreach (var package in packages) { if (package.PackageId == id) { featureToSet = package; break; } } return featureToSet; } /// /// Checks if the specific question has been answered. /// /// The question to check. /// Returns true if the question has been answered by the user, otherwise returns false. public static bool IsQuestionAnswered(Question question) { return Logic.TryGetAnswerByQuestionId(UserChoicesObject.instance.UserAnswers, question.Id, out _); } /// /// Compares two arrays of type for equality. /// /// The type of the elements in the arrays. /// The first array to compare. /// The second array to compare. /// /// true if all elements are equal and they are in the same order, /// false otherwise. /// public static bool AreArraysEqual(T[] a, T[] b) { if (a.Length != b.Length) { return false; } for (var i = 0; i < a.Length; i++) { if (!EqualityComparer.Default.Equals(a[i], b[i])) { return false; } } return true; } /// /// Gets the section type names in order from the provided section mapping. /// /// A dictionary mapping OnboardingSectionCategory to SectionList types. /// A list of section type names sorted in ascending order. public static List GetSectionTypeNamesInOrder(Dictionary sectionMapping) { var sectionTypeNamesList = new List(); foreach (var sectionTypeArray in sectionMapping.Values) { foreach (var sectionType in sectionTypeArray) { sectionTypeNamesList.Add(sectionType.AssemblyQualifiedName); } } sectionTypeNamesList.Sort(StringComparer.InvariantCulture); return sectionTypeNamesList; } } }