341 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
		
		
			
		
	
	
			341 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
|  | using System; | ||
|  | using System.Collections.Generic; | ||
|  | using Unity.Multiplayer.Center.Common; | ||
|  | using Unity.Multiplayer.Center.Recommendations; | ||
|  | using UnityEngine; | ||
|  | using static System.Int32; | ||
|  | 
 | ||
|  | namespace Unity.Multiplayer.Center.Questionnaire | ||
|  | { | ||
|  |     using AnswerMap = System.Collections.Generic.Dictionary<string, List<string>>; | ||
|  | 
 | ||
|  |     /// <summary> | ||
|  |     /// Question and answer logic manipulations. | ||
|  |     /// </summary> | ||
|  |     static internal class Logic | ||
|  |     { | ||
|  |         /// <summary> | ||
|  |         /// Checks if the answers make sense and correspond to the questions. | ||
|  |         /// </summary> | ||
|  |         /// <param name="questions">The reference questionnaire</param> | ||
|  |         /// <param name="currentAnswers">The validated answers.</param> | ||
|  |         /// <returns>A list of problems or an empty list.</returns> | ||
|  |         public static List<string> ValidateAnswers(QuestionnaireData questions, AnswerData currentAnswers) | ||
|  |         { | ||
|  |             if (questions == null || questions.Questions == null || questions.Questions.Length < 1) | ||
|  |                 return new List<string> {"No questions found in questionnaire"}; | ||
|  | 
 | ||
|  |             // TODO: check all question Id exist and are different | ||
|  |             // TODO: check the questions have at least two possible answers | ||
|  | 
 | ||
|  |             var errors = new List<string>(); | ||
|  |             for (int i = 0; i < currentAnswers.Answers.Count; ++i) | ||
|  |             { | ||
|  |                 var current = currentAnswers.Answers[i]; | ||
|  |                 if (string.IsNullOrEmpty(current.QuestionId)) | ||
|  |                     errors.Add($"AnswerData at index {i}: Question id is empty"); | ||
|  | 
 | ||
|  |                 if (!TryGetQuestionByQuestionId(questions, current.QuestionId, out var question)) | ||
|  |                     errors.Add($"AnswerData at index {i}: Question id {current.QuestionId} not found in questionnaire"); | ||
|  | 
 | ||
|  |                 if (current.Answers == null || current.Answers.Count < 1) // TODO: in the future we might have questions with nothing selected being a valid answer | ||
|  |                 { | ||
|  |                     errors.Add($"AnswerData at index {i}: No answers found (question {current.QuestionId})"); | ||
|  |                 } | ||
|  |                 else if (question != null) | ||
|  |                 { | ||
|  |                     switch (question.ViewType) | ||
|  |                     { | ||
|  |                         case ViewType.Toggle when current.Answers.Count > 1: | ||
|  |                         case ViewType.Radio when current.Answers.Count > 1: | ||
|  |                         case ViewType.DropDown when current.Answers.Count != 1: | ||
|  |                             errors.Add($"AnswerData at index {i}: Too many answers (question {current.QuestionId})"); | ||
|  |                             break; | ||
|  |                     } | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             return errors; | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Finds the first unanswered question index, aka the first question to display. | ||
|  |         /// If all questions have been answered, the index is the length of the questions array. | ||
|  |         /// </summary> | ||
|  |         /// <param name="questions">The questionnaire</param> | ||
|  |         /// <param name="currentAnswers">the answers so far</param> | ||
|  |         /// <returns>The index in the questions array or the size of the questions array</returns> | ||
|  |         public static int FindFirstUnansweredQuestion(QuestionnaireData questions, AnswerData currentAnswers) | ||
|  |         { | ||
|  |             if (currentAnswers.Answers.Count < 1) | ||
|  |                 return 0; | ||
|  | 
 | ||
|  |             var dico = AnswersToDictionary(questions, currentAnswers); // Assumes validation has already taken place. | ||
|  |             for (int i = 0; i < questions.Questions.Length; i++) | ||
|  |             { | ||
|  |                 var current = questions.Questions[i]; | ||
|  | 
 | ||
|  |                 bool isAnswered = dico.ContainsKey(current.Id); // Assumes validation has already taken place. | ||
|  |                 if (!isAnswered) | ||
|  |                     return i; | ||
|  |             } | ||
|  | 
 | ||
|  |             // all answered | ||
|  |             return questions.Questions.Length; | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Conversion to a dictionary for easier manipulation. | ||
|  |         /// </summary> | ||
|  |         /// <param name="questions">the questionnaire</param> | ||
|  |         /// <param name="currentAnswers">the answers so far</param> | ||
|  |         /// <returns>The dictionary containing (id, list of answer id) </returns> | ||
|  |         internal static AnswerMap AnswersToDictionary(QuestionnaireData questions, AnswerData currentAnswers) | ||
|  |         { | ||
|  |             var dico = new AnswerMap(); | ||
|  |             foreach (var answer in currentAnswers.Answers) | ||
|  |             { | ||
|  |                 dico.Add(answer.QuestionId, answer.Answers); | ||
|  |             } | ||
|  | 
 | ||
|  |             return dico; | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Checks if the question id exists in the questionnaire and return it. | ||
|  |         /// </summary> | ||
|  |         /// <param name="questions">The questionnaire</param> | ||
|  |         /// <param name="questionId">The id to find</param> | ||
|  |         /// <param name="foundQuestion">The found question or null</param> | ||
|  |         /// <returns>True if found, else false</returns> | ||
|  |         internal static bool TryGetQuestionByQuestionId(QuestionnaireData questions, string questionId, out Question foundQuestion) | ||
|  |         { | ||
|  |             foreach (var q in questions.Questions) | ||
|  |             { | ||
|  |                 if (q.Id == questionId) | ||
|  |                 { | ||
|  |                     foundQuestion = q; | ||
|  |                     return true; | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             foundQuestion = null; | ||
|  |             return false; | ||
|  |         } | ||
|  | 
 | ||
|  |         internal static bool TryGetAnswerByQuestionId(AnswerData answers, string questionId, out AnsweredQuestion foundAnswer) | ||
|  |         { | ||
|  |             return TryGetAnswerByQuestionId(answers.Answers, questionId, out foundAnswer); | ||
|  |         } | ||
|  | 
 | ||
|  |         internal static bool TryGetAnswerByQuestionId(IEnumerable<AnsweredQuestion> answerList, string questionId, out AnsweredQuestion foundAnswer) | ||
|  |         { | ||
|  |             foreach (var aq in answerList) | ||
|  |             { | ||
|  |                 if (aq.QuestionId == questionId) | ||
|  |                 { | ||
|  |                     foundAnswer = aq; | ||
|  |                     return true; | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             foundAnswer = null; | ||
|  |             return false; | ||
|  |         } | ||
|  | 
 | ||
|  |         internal static bool TryGetAnswerByAnswerId(Question question, string answerId, out Answer answer) | ||
|  |         { | ||
|  |             answer = null; | ||
|  | 
 | ||
|  |             foreach (var choice in question.Choices) | ||
|  |             { | ||
|  |                 if (answerId == choice.Id) | ||
|  |                 { | ||
|  |                     answer = choice; | ||
|  |                     return true; | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             return false; | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Applies the preset to the current answers and returns the new answers and the recommendation. | ||
|  |         /// It skips all the mandatory questions, which have to be answered separately. | ||
|  |         /// </summary> | ||
|  |         /// <param name="currentAnswers">The answers the user selected so far</param> | ||
|  |         /// <param name="preset">The preset to apply</param> | ||
|  |         /// <param name="questionnaire">All the available questions</param> | ||
|  |         /// <returns>The new answer data and the associated recommendation(might be null).</returns> | ||
|  |         public static (AnswerData, RecommendationViewData) ApplyPresetToAnswerData(AnswerData currentAnswers, Preset preset, | ||
|  |             QuestionnaireData questionnaire) | ||
|  |         { | ||
|  |             if (preset == Preset.None) return (new AnswerData(), null); | ||
|  | 
 | ||
|  |             var presetData = questionnaire.PresetData; | ||
|  |             var index = Array.IndexOf(presetData.Presets, preset); | ||
|  |             var presetAnswers = presetData.Answers[index]; | ||
|  |             var resultAnswerData = presetAnswers.Clone(); | ||
|  |             foreach (var question in questionnaire.Questions) | ||
|  |             { | ||
|  |                 if (!question.IsMandatory) continue; | ||
|  | 
 | ||
|  |                 if (TryGetAnswerByQuestionId(currentAnswers, question.Id, out var currentAnswer)) | ||
|  |                     Update(resultAnswerData, currentAnswer); | ||
|  |             } | ||
|  | 
 | ||
|  |             var recommendation = RecommenderSystem.GetRecommendation(questionnaire, resultAnswerData); | ||
|  |             return (resultAnswerData, recommendation); | ||
|  |         } | ||
|  |          | ||
|  |         public static void Update(AnswerData data, AnsweredQuestion a) | ||
|  |         { | ||
|  |             if (a.Answers == null || a.Answers.Count < 1) | ||
|  |             { | ||
|  |                 // TODO: this might need to change in the future | ||
|  |                 return; | ||
|  |             } | ||
|  |              | ||
|  |             if (data.Answers == null) | ||
|  |             { | ||
|  |                 data.Answers = new List<AnsweredQuestion>() {a}; | ||
|  |                 return; | ||
|  |             } | ||
|  |              | ||
|  |             for (int i = 0; i < data.Answers.Count; i++) | ||
|  |             { | ||
|  |                 if (data.Answers[i].QuestionId == a.QuestionId) | ||
|  |                 { | ||
|  |                     data.Answers[i] = a; | ||
|  |                     return; | ||
|  |                 } | ||
|  |             } | ||
|  |              | ||
|  |             data.Answers.Add(a); | ||
|  |         } | ||
|  |         public static bool AreMandatoryQuestionsFilled(QuestionnaireData questionnaire, AnswerData answers) | ||
|  |         { | ||
|  |             var mandatoryQuestions = new List<string>(); | ||
|  |             foreach (var question in questionnaire.Questions) | ||
|  |             { | ||
|  |                 if (question.IsMandatory) | ||
|  |                 { | ||
|  |                     mandatoryQuestions.Add(question.Id); | ||
|  |                 } | ||
|  |             } | ||
|  |              | ||
|  |             var foundAnswers = new bool[mandatoryQuestions.Count]; | ||
|  |             foreach (var answer in answers.Answers) | ||
|  |             { | ||
|  |                 for (var i = 0; i < mandatoryQuestions.Count; i++) | ||
|  |                 { | ||
|  |                     if (answer.QuestionId == mandatoryQuestions[i] && answer.Answers.Count > 0) | ||
|  |                     { | ||
|  |                         foundAnswers[i] = true; | ||
|  |                         break; | ||
|  |                     } | ||
|  |                 } | ||
|  |             } | ||
|  |              | ||
|  |             foreach (var answer in foundAnswers) | ||
|  |             { | ||
|  |                 if (!answer) | ||
|  |                 { | ||
|  |                     return false; | ||
|  |                 } | ||
|  |             } | ||
|  |              | ||
|  |             return true; | ||
|  |         } | ||
|  | 
 | ||
|  |         public static SelectedSolutionsData.HostingModel ConvertInfrastructure(RecommendedSolutionViewData serverArchitectureOption) | ||
|  |         { | ||
|  |             return serverArchitectureOption.Solution switch | ||
|  |             { | ||
|  |                 PossibleSolution.LS => SelectedSolutionsData.HostingModel.ClientHosted, | ||
|  |                 PossibleSolution.DS => SelectedSolutionsData.HostingModel.DedicatedServer, | ||
|  |                 PossibleSolution.CloudCode => SelectedSolutionsData.HostingModel.CloudCode, | ||
|  |                 PossibleSolution.DA => SelectedSolutionsData.HostingModel.DistributedAuthority, | ||
|  |                 _ => SelectedSolutionsData.HostingModel.None | ||
|  |             }; | ||
|  |         } | ||
|  | 
 | ||
|  |         public static SelectedSolutionsData.NetcodeSolution ConvertNetcodeSolution(RecommendedSolutionViewData netcodeOption) | ||
|  |         { | ||
|  |             return netcodeOption.Solution switch | ||
|  |             { | ||
|  |                 PossibleSolution.NGO => SelectedSolutionsData.NetcodeSolution.NGO, | ||
|  |                 PossibleSolution.N4E => SelectedSolutionsData.NetcodeSolution.N4E, | ||
|  |                 PossibleSolution.CustomNetcode => SelectedSolutionsData.NetcodeSolution.CustomNetcode, | ||
|  |                 PossibleSolution.NoNetcode => SelectedSolutionsData.NetcodeSolution.NoNetcode, | ||
|  |                 _ => SelectedSolutionsData.NetcodeSolution.None | ||
|  |             }; | ||
|  |         } | ||
|  | 
 | ||
|  |         public static void MigrateUserChoices(QuestionnaireData questionnaire, UserChoicesObject userChoices) | ||
|  |         { | ||
|  |             var versionBeforeMigration = string.IsNullOrEmpty(userChoices.QuestionnaireVersion) ? "1.0" : userChoices.QuestionnaireVersion; | ||
|  |              | ||
|  |             // first migration because field was not present | ||
|  |             if (questionnaire.Version is "1.2" && IsVersionLower(versionBeforeMigration, "1.2")) | ||
|  |             { | ||
|  |                 if (TryGetAnswerByQuestionId(userChoices.UserAnswers, "Competitiveness", out var competitiveQuestion)) | ||
|  |                 { | ||
|  |                     userChoices.UserAnswers.Answers.Remove(competitiveQuestion); | ||
|  |                 } | ||
|  |                  | ||
|  |                 // this will write the current version. | ||
|  |                 userChoices.Save(); | ||
|  |             } | ||
|  |              | ||
|  |             // Medium Pace Option was removed fall back to slow. | ||
|  |             if (questionnaire.Version is "1.3" && IsVersionLower(versionBeforeMigration, "1.3")) | ||
|  |             { | ||
|  |                 if (userChoices.UserAnswers.Answers != null && TryGetAnswerByQuestionId(userChoices.UserAnswers, "Pace", out var paceQuestion)) | ||
|  |                 { | ||
|  |                     if (paceQuestion.Answers.Contains("Medium")) | ||
|  |                     { | ||
|  |                         // Set the answer to slow, as in the sheet, we changed all medium to slow. | ||
|  |                         // So this is probably the best guess. | ||
|  |                         paceQuestion.Answers.Remove("Medium"); | ||
|  |                         paceQuestion.Answers.Add("Slow"); | ||
|  |                     } | ||
|  |                 } | ||
|  |                  | ||
|  |                 // this will write the current version. | ||
|  |                 userChoices.Save(); | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Compares two versions and returns true if the versionToTest is lower than the currentVersion. | ||
|  |         /// </summary> | ||
|  |         /// <param name="versionToTest">The version number that gets tested</param> | ||
|  |         /// <param name="currentVersion">The version number to test against</param> | ||
|  |         /// <returns>True if versionToTest is lower than currentVersion</returns> | ||
|  |         internal static bool IsVersionLower(string versionToTest, string currentVersion) | ||
|  |         { | ||
|  |             var versionToTestParts = versionToTest.Split('.'); | ||
|  |             var currentVersionParts = currentVersion.Split('.'); | ||
|  |          | ||
|  |             for (var i = 0; i < Math.Min(versionToTestParts.Length, currentVersionParts.Length); i++) | ||
|  |             { | ||
|  |                 var canParseCurrentVersion = TryParse(currentVersionParts[i], out var currentVersionPart); | ||
|  |                 var canParseVersionToTestVersion = TryParse(versionToTestParts[i], out var versionToTestPart); | ||
|  | 
 | ||
|  |                 if (canParseCurrentVersion == false || canParseVersionToTestVersion == false) | ||
|  |                 { | ||
|  |                     Debug.LogError("Version number is not in the correct format"); | ||
|  |                     return false;    | ||
|  |                 } | ||
|  |                  | ||
|  |                 if ( versionToTestPart != currentVersionPart) | ||
|  |                     return versionToTestPart < currentVersionPart; | ||
|  | 
 | ||
|  |             } | ||
|  | 
 | ||
|  |             return versionToTestParts.Length < currentVersionParts.Length; | ||
|  |         } | ||
|  |     } | ||
|  | } |