using System; using System.Collections.Generic; using Unity.Multiplayer.Center.Common; using Unity.Multiplayer.Center.Questionnaire; using UnityEngine; namespace Unity.Multiplayer.Center.Recommendations { using AnswerWithQuestion = Tuple; /// /// Builds recommendation views based on Questionnaire data and Answer data. /// The recommendation is based on the scoring of the answers, which is controlled by the RecommenderSystemData. /// internal static class RecommenderSystem { /// /// Main entry point for the recommender system: computes the recommendation based on the questionnaire data and /// the answers. /// If no answer has been given or the questionnaire does not match the answers, this returns null. /// /// The questionnaire that the user filled. /// The answers the user gave. /// The recommendation view data. public static RecommendationViewData GetRecommendation(QuestionnaireData questionnaireData, AnswerData answerData) { var answers = CollectAnswers(questionnaireData, answerData); // Note: valid now only because we do not have multiple answers per question if (answers.Count < questionnaireData.Questions.Length) return null; var data = RecommenderSystemDataObject.instance.RecommenderSystemData; var scoredSolutions = CalculateScore(data, answers); return CreateRecommendation(data, scoredSolutions); } /// /// Get the view data for all possible solution selections. /// /// The constructed set of views public static SolutionsToRecommendedPackageViewData GetSolutionsToRecommendedPackageViewData() { var data = RecommenderSystemDataObject.instance.RecommenderSystemData; var installedPackageDictionary = PackageManagement.InstalledPackageDictionary(); var selections = new SolutionSelection[16]; var packages = new RecommendedPackageViewData[16][]; PossibleSolution[] netcodes = { PossibleSolution.NGO, PossibleSolution.N4E, PossibleSolution.CustomNetcode, PossibleSolution.NoNetcode }; PossibleSolution[] hostings = { PossibleSolution.LS, PossibleSolution.DS, PossibleSolution.CloudCode, PossibleSolution.DA }; var index = 0; foreach (var netcode in netcodes) { foreach (var hosting in hostings) { var selection = new SolutionSelection(netcode, hosting); selections[index] = selection; packages[index] = BuildRecommendationForSelection(data, selection, installedPackageDictionary); ++index; } } return new SolutionsToRecommendedPackageViewData(selections, packages); } public static void AdaptRecommendationToNetcodeSelection(RecommendationViewData recommendation) { RecommendationUtils.MarkIncompatibleHostingModels(recommendation); var maxIndex = RecommendationUtils.IndexOfMaximumScore(recommendation.ServerArchitectureOptions); recommendation.ServerArchitectureOptions[maxIndex].RecommendationType = RecommendationType.MainArchitectureChoice; if (RecommendationUtils.GetSelectedHostingModel(recommendation) == null) recommendation.ServerArchitectureOptions[maxIndex].Selected = true; } static List CollectAnswers(QuestionnaireData questionnaireData, AnswerData answerData) { if (questionnaireData?.Questions == null || questionnaireData.Questions.Length == 0) throw new ArgumentException("Questionnaire data is null or empty", nameof(questionnaireData)); List givenAnswers = new(); var answers = answerData.Answers; foreach (var answeredQuestion in answers) { // find question for the answer if (!Logic.TryGetQuestionByQuestionId(questionnaireData, answeredQuestion.QuestionId, out var question)) continue; // find answer object for the given answer id foreach (var answerId in answeredQuestion.Answers) { if (!Logic.TryGetAnswerByAnswerId(question, answerId, out var choice)) continue; givenAnswers.Add(Tuple.Create(question, choice)); } } return givenAnswers; } static Dictionary CalculateScore(RecommenderSystemData data, List answers) { var possibleSolutions = Enum.GetValues(typeof(PossibleSolution)); Dictionary scores = new(possibleSolutions.Length); foreach (var solution in possibleSolutions) { var solutionObject = data.SolutionsByType[(PossibleSolution) solution]; scores.Add((PossibleSolution) solution, new Scoring(solutionObject.ShortDescription)); } foreach (var (question, answer) in answers) { foreach (var scoreImpact in answer.ScoreImpacts) { scores[scoreImpact.Solution].AddScore(scoreImpact.Score * question.GlobalWeight, scoreImpact.Comment); } } return scores; } static RecommendationViewData CreateRecommendation(RecommenderSystemData data, IReadOnlyDictionary scoredSolutions) { RecommendationViewData recommendation = new(); var installedPackageDictionary = PackageManagement.InstalledPackageDictionary(); recommendation.NetcodeOptions = BuildRecommendedSolutions(data, new [] { (PossibleSolution.NGO, scoredSolutions[PossibleSolution.NGO]), (PossibleSolution.N4E, scoredSolutions[PossibleSolution.N4E]), (PossibleSolution.CustomNetcode, scoredSolutions[PossibleSolution.CustomNetcode]), (PossibleSolution.NoNetcode, scoredSolutions[PossibleSolution.NoNetcode]) }, installedPackageDictionary); recommendation.ServerArchitectureOptions = BuildRecommendedSolutions(data, new [] { (PossibleSolution.LS, scoredSolutions[PossibleSolution.LS]), (PossibleSolution.DS, scoredSolutions[PossibleSolution.DS]), (PossibleSolution.CloudCode, scoredSolutions[PossibleSolution.CloudCode]), (PossibleSolution.DA, scoredSolutions[PossibleSolution.DA]) }, installedPackageDictionary); AdaptRecommendationToNetcodeSelection(recommendation); return recommendation; } static RecommendedSolutionViewData[] BuildRecommendedSolutions(RecommenderSystemData data, (PossibleSolution, Scoring)[] scoredSolutions, Dictionary installedPackageDictionary) { var recommendedSolution = RecommendationUtils.FindRecommendedSolution(scoredSolutions); var result = new RecommendedSolutionViewData[scoredSolutions.Length]; for (var index = 0; index < scoredSolutions.Length; index++) { var scoredSolution = scoredSolutions[index]; var recoType = scoredSolution.Item1 == recommendedSolution ? RecommendationType.MainArchitectureChoice : RecommendationType.SecondArchitectureChoice; var reco = new RecommendedSolutionViewData(data, data.SolutionsByType[scoredSolution.Item1], recoType, scoredSolution.Item2, installedPackageDictionary); result[index] = reco; } return result; } static RecommendedPackageViewData[] BuildRecommendationForSelection(RecommenderSystemData data, SolutionSelection selection, Dictionary installedPackageDictionary) { // Note: working on a copy that we modify var netcodePackages = (RecommendedPackage[]) data.SolutionsByType[selection.Netcode].RecommendedPackages.Clone(); var hostingOverrides = data.SolutionsByType[selection.HostingModel].RecommendedPackages; foreach (var package in hostingOverrides) { var existing = Array.FindIndex(netcodePackages, p => p.PackageId == package.PackageId); if (existing == -1) { Debug.LogError($"Malformed data for hosting model {selection.HostingModel}: package {package.PackageId} not found in netcode packages of {selection.Netcode}."); continue; } netcodePackages[existing] = package; } var result = new RecommendedPackageViewData[netcodePackages.Length]; for (var index = 0; index < netcodePackages.Length; index++) { var package = netcodePackages[index]; installedPackageDictionary.TryGetValue(package.PackageId, out var installedVersion); result[index] = new RecommendedPackageViewData( data.PackageDetailsById[package.PackageId], package, installedVersion); } return result; } } }