UnityGame/Library/PackageCache/com.unity.multiplayer.center/Editor/Recommendations/RecommenderSystem.cs
2024-10-27 10:53:47 +03:00

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;
}
}
}