A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using Unity.Multiplayer.Center.Common;
4using Unity.Multiplayer.Center.Questionnaire;
5using UnityEngine;
6
7namespace Unity.Multiplayer.Center.Recommendations
8{
9 using AnswerWithQuestion = Tuple<Question, Answer>;
10
11 /// <summary>
12 /// Builds recommendation views based on Questionnaire data and Answer data.
13 /// The recommendation is based on the scoring of the answers, which is controlled by the RecommenderSystemData.
14 /// </summary>
15 internal static class RecommenderSystem
16 {
17 /// <summary>
18 /// Main entry point for the recommender system: computes the recommendation based on the questionnaire data and
19 /// the answers.
20 /// If no answer has been given or the questionnaire does not match the answers, this returns null.
21 /// </summary>
22 /// <param name="questionnaireData">The questionnaire that the user filled.</param>
23 /// <param name="answerData">The answers the user gave.</param>
24 /// <returns>The recommendation view data.</returns>
25 public static RecommendationViewData GetRecommendation(QuestionnaireData questionnaireData, AnswerData answerData)
26 {
27 var answers = CollectAnswers(questionnaireData, answerData);
28
29 // Note: valid now only because we do not have multiple answers per question
30 if (answers.Count < questionnaireData.Questions.Length) return null;
31
32 var data = RecommenderSystemDataObject.instance.RecommenderSystemData;
33 var scoredSolutions = CalculateScore(data, answers);
34
35 return CreateRecommendation(data, scoredSolutions);
36 }
37
38 /// <summary>
39 /// Get the view data for all possible solution selections.
40 /// </summary>
41 /// <returns>The constructed set of views</returns>
42 public static SolutionsToRecommendedPackageViewData GetSolutionsToRecommendedPackageViewData()
43 {
44 var data = RecommenderSystemDataObject.instance.RecommenderSystemData;
45 var installedPackageDictionary = PackageManagement.InstalledPackageDictionary();
46 var selections = new SolutionSelection[16];
47 var packages = new RecommendedPackageViewData[16][];
48 PossibleSolution[] netcodes = { PossibleSolution.NGO, PossibleSolution.N4E, PossibleSolution.CustomNetcode, PossibleSolution.NoNetcode };
49 PossibleSolution[] hostings = { PossibleSolution.LS, PossibleSolution.DS, PossibleSolution.CloudCode, PossibleSolution.DA };
50
51 var index = 0;
52 foreach (var netcode in netcodes)
53 {
54 foreach (var hosting in hostings)
55 {
56 var selection = new SolutionSelection(netcode, hosting);
57 selections[index] = selection;
58 packages[index] = BuildRecommendationForSelection(data, selection, installedPackageDictionary);
59
60 ++index;
61 }
62 }
63
64 return new SolutionsToRecommendedPackageViewData(selections, packages);
65 }
66
67 public static void AdaptRecommendationToNetcodeSelection(RecommendationViewData recommendation)
68 {
69 RecommendationUtils.MarkIncompatibleHostingModels(recommendation);
70 var maxIndex = RecommendationUtils.IndexOfMaximumScore(recommendation.ServerArchitectureOptions);
71 recommendation.ServerArchitectureOptions[maxIndex].RecommendationType = RecommendationType.MainArchitectureChoice;
72 if (RecommendationUtils.GetSelectedHostingModel(recommendation) == null)
73 recommendation.ServerArchitectureOptions[maxIndex].Selected = true;
74 }
75
76 static List<AnswerWithQuestion> CollectAnswers(QuestionnaireData questionnaireData, AnswerData answerData)
77 {
78 if (questionnaireData?.Questions == null || questionnaireData.Questions.Length == 0)
79 throw new ArgumentException("Questionnaire data is null or empty", nameof(questionnaireData));
80
81 List<AnswerWithQuestion> givenAnswers = new();
82
83 var answers = answerData.Answers;
84
85 foreach (var answeredQuestion in answers)
86 {
87 // find question for the answer
88 if (!Logic.TryGetQuestionByQuestionId(questionnaireData, answeredQuestion.QuestionId, out var question))
89 continue;
90
91 // find answer object for the given answer id
92 foreach (var answerId in answeredQuestion.Answers)
93 {
94 if (!Logic.TryGetAnswerByAnswerId(question, answerId, out var choice))
95 continue;
96 givenAnswers.Add(Tuple.Create(question, choice));
97 }
98 }
99
100 return givenAnswers;
101 }
102
103 static Dictionary<PossibleSolution, Scoring> CalculateScore(RecommenderSystemData data, List<AnswerWithQuestion> answers)
104 {
105 var possibleSolutions = Enum.GetValues(typeof(PossibleSolution));
106 Dictionary<PossibleSolution, Scoring> scores = new(possibleSolutions.Length);
107
108 foreach (var solution in possibleSolutions)
109 {
110 var solutionObject = data.SolutionsByType[(PossibleSolution) solution];
111 scores.Add((PossibleSolution) solution, new Scoring(solutionObject.ShortDescription));
112 }
113
114 foreach (var (question, answer) in answers)
115 {
116 foreach (var scoreImpact in answer.ScoreImpacts)
117 {
118 scores[scoreImpact.Solution].AddScore(scoreImpact.Score * question.GlobalWeight, scoreImpact.Comment);
119 }
120 }
121
122 return scores;
123 }
124
125 static RecommendationViewData CreateRecommendation(RecommenderSystemData data, IReadOnlyDictionary<PossibleSolution, Scoring> scoredSolutions)
126 {
127 RecommendationViewData recommendation = new();
128 var installedPackageDictionary = PackageManagement.InstalledPackageDictionary();
129
130 recommendation.NetcodeOptions = BuildRecommendedSolutions(data, new [] {
131 (PossibleSolution.NGO, scoredSolutions[PossibleSolution.NGO]),
132 (PossibleSolution.N4E, scoredSolutions[PossibleSolution.N4E]),
133 (PossibleSolution.CustomNetcode, scoredSolutions[PossibleSolution.CustomNetcode]),
134 (PossibleSolution.NoNetcode, scoredSolutions[PossibleSolution.NoNetcode]) },
135 installedPackageDictionary);
136
137 recommendation.ServerArchitectureOptions = BuildRecommendedSolutions(data, new [] {
138 (PossibleSolution.LS, scoredSolutions[PossibleSolution.LS]),
139 (PossibleSolution.DS, scoredSolutions[PossibleSolution.DS]),
140 (PossibleSolution.CloudCode, scoredSolutions[PossibleSolution.CloudCode]),
141 (PossibleSolution.DA, scoredSolutions[PossibleSolution.DA]) },
142 installedPackageDictionary);
143
144 AdaptRecommendationToNetcodeSelection(recommendation);
145 return recommendation;
146 }
147
148 static RecommendedSolutionViewData[] BuildRecommendedSolutions(RecommenderSystemData data, (PossibleSolution, Scoring)[] scoredSolutions, Dictionary<string, string> installedPackageDictionary)
149 {
150 var recommendedSolution = RecommendationUtils.FindRecommendedSolution(scoredSolutions);
151 var result = new RecommendedSolutionViewData[scoredSolutions.Length];
152
153 for (var index = 0; index < scoredSolutions.Length; index++)
154 {
155 var scoredSolution = scoredSolutions[index];
156 var recoType = scoredSolution.Item1 == recommendedSolution ? RecommendationType.MainArchitectureChoice : RecommendationType.SecondArchitectureChoice;
157 var reco = new RecommendedSolutionViewData(data, data.SolutionsByType[scoredSolution.Item1], recoType, scoredSolution.Item2, installedPackageDictionary);
158 result[index] = reco;
159 }
160
161 return result;
162 }
163
164 static RecommendedPackageViewData[] BuildRecommendationForSelection(RecommenderSystemData data, SolutionSelection selection, Dictionary<string, string> installedPackageDictionary)
165 {
166 // Note: working on a copy that we modify
167 var netcodePackages = (RecommendedPackage[]) data.SolutionsByType[selection.Netcode].RecommendedPackages.Clone();
168 var hostingOverrides = data.SolutionsByType[selection.HostingModel].RecommendedPackages;
169 foreach (var package in hostingOverrides)
170 {
171 var existing = Array.FindIndex(netcodePackages, p => p.PackageId == package.PackageId);
172 if (existing == -1)
173 {
174 Debug.LogError($"Malformed data for hosting model {selection.HostingModel}: package {package.PackageId} not found in netcode packages of {selection.Netcode}.");
175 continue;
176 }
177
178 netcodePackages[existing] = package;
179 }
180
181 var result = new RecommendedPackageViewData[netcodePackages.Length];
182 for (var index = 0; index < netcodePackages.Length; index++)
183 {
184 var package = netcodePackages[index];
185 installedPackageDictionary.TryGetValue(package.PackageId, out var installedVersion);
186 result[index] = new RecommendedPackageViewData( data.PackageDetailsById[package.PackageId], package, installedVersion);
187 }
188
189 return result;
190 }
191 }
192}