193 lines
9.5 KiB
C#
193 lines
9.5 KiB
C#
|
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<Question, Answer>;
|
||
|
|
||
|
/// <summary>
|
||
|
/// 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.
|
||
|
/// </summary>
|
||
|
internal static class RecommenderSystem
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// 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.
|
||
|
/// </summary>
|
||
|
/// <param name="questionnaireData">The questionnaire that the user filled.</param>
|
||
|
/// <param name="answerData">The answers the user gave.</param>
|
||
|
/// <returns>The recommendation view data.</returns>
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Get the view data for all possible solution selections.
|
||
|
/// </summary>
|
||
|
/// <returns>The constructed set of views</returns>
|
||
|
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<AnswerWithQuestion> 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<AnswerWithQuestion> 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<PossibleSolution, Scoring> CalculateScore(RecommenderSystemData data, List<AnswerWithQuestion> answers)
|
||
|
{
|
||
|
var possibleSolutions = Enum.GetValues(typeof(PossibleSolution));
|
||
|
Dictionary<PossibleSolution, Scoring> 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<PossibleSolution, Scoring> 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<string, string> 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<string, string> 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;
|
||
|
}
|
||
|
}
|
||
|
}
|