using AssortGenerator.Common.Helpers; using QuestValidator.Common; using QuestValidator.Common.Helpers; using QuestValidator.Common.Models; using QuestValidator.Helpers; using QuestValidator.Models; using QuestValidator.Models.Output; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Quest = QuestValidator.Models.Quest; namespace GenerateQuestFile { class Program { /// /// Generate a quests.json file in /output/ /// Uses every quest from the live quest dump file /// If any quests are missing, it will use the quests.json file to fill in the blanks /// /// static void Main(string[] args) { var inputPath = DiskHelpers.CreateWorkingFolders(); InputFileHelper.SetInputFiles(inputPath); // Read in quest files var questBlacklist = QuestHelper.GetQuestBlacklist(); var existingQuestData = QuestHelper.GetQuestData(); var liveQuestData = QuestHelper.GetLiveQuestData(); var mergedLiveData = QuestHelper.MergeLiveQuestFiles(liveQuestData, questBlacklist); OutputQuestRequirementsToConsole(mergedLiveData.data); JsonWriter.WriteJson(mergedLiveData, "output", Directory.GetCurrentDirectory(), "mergedlivejson"); // Find the quests that are missing from the live file from existing quests.json var missingQuests = GetMissingQuestsNotInLiveFile(existingQuestData, mergedLiveData, questBlacklist); // Create a list of quests to output // Use all quests in live file // Use quests from quests.json to fill in missing quests // Add live quests to collection to return later var questsToOutputToFile = new Dictionary(); foreach (var liveQuest in mergedLiveData.data) { questsToOutputToFile.Add(liveQuest._id, liveQuest); } // Add missing quests from existing quest data to fill in blanks from live data foreach (var missingQuest in missingQuests) { // Going from a pre-12.7.x version has problems, it doesnt have the new quest data format //CheckAndFixMissingProperties(missingQuest); questsToOutputToFile.Add(missingQuest._id, missingQuest); } // Now old + new quests have been merged, check quest list to see if any quests are missing foreach (var missingQuest in QuestNames.GetQuests()) { if (!questsToOutputToFile.Any(x => x.Key == missingQuest.Value)) { LoggingHelpers.LogWarning($" quest not found in new or old data: {missingQuest.Key}"); } } if (!questsToOutputToFile.ContainsKey("5e383a6386f77465910ce1f3")) // TextileP1Bear { // add textileP1Bear } if (!questsToOutputToFile.ContainsKey("5e4d515e86f77438b2195244")) // TextileP2Bear { // add TextileP2Bear } foreach (var quest in questsToOutputToFile) { AddQuestName(quest); var originalQuest = existingQuestData.FirstOrDefault(x => x.Key == quest.Key).Value; if (originalQuest is null) { LoggingHelpers.LogWarning($"Cant check for original start conditions. Unable to find original quest {quest.Key} {QuestHelper.GetQuestNameById(quest.Key)}, skipping."); continue; } AddMissingFields(quest); // Quest has start conditions, check to ensure they're carried over //if (originalQuest.conditions.AvailableForStart.Count > 0) //{ // AddMissingAvailableForStartConditions(originalQuest, quest); //} if (originalQuest.rewards.Fail.Count > 0) { AddMissingFailRewards(originalQuest, quest); } // To make diffs more sane, copy the random IDs from the existing quests.json if possible CopyExistingRandomIds(originalQuest, quest.Value); } // Iterate over quest objects a final time and add hard coded quest requirements if they dont already exist foreach (var quest in questsToOutputToFile) { var questRequirements = QuestRequirements.GetQuestRequirements(quest.Key); if (questRequirements is null || questRequirements.Count == 0) { LoggingHelpers.LogWarning($"Quest requirement not found for : {quest.Value.QuestName}, skipping."); continue; } foreach (var requirement in questRequirements) { if (requirement.PreReqType == PreRequisiteType.Quest) { // Does quest have requirement if (!quest.Value.conditions.AvailableForStart.Any(x => x._parent == "Quest" && x._props.target.ToString() == requirement.Quest.Id)) { LoggingHelpers.LogSuccess($"{quest.Value.QuestName} needs a prereq of quest {requirement.Quest.Name}, adding."); string hashData = quest.Value._id + requirement.Quest.Id; quest.Value.conditions.AvailableForStart.Add(new AvailableFor { _parent = "Quest", _props = new AvailableForProps { id = Sha256(hashData), index = GetNextIndex(quest.Value.conditions.AvailableForStart.LastOrDefault()?._props?.index), parentId = "", status = GetQuestStatus(requirement.QuestStatus), target = requirement.Quest.Id, visibilityConditions = new List(), availableAfter = 0 } } ); } else { if (questRequirements != null) { LoggingHelpers.LogInfo($"{quest.Value.QuestName} already has prereq of quest {requirement.Quest.Name}, skipping."); } } } if (requirement.PreReqType == PreRequisiteType.RemoveQuest) { if (quest.Value.conditions.AvailableForStart.RemoveAll(x => x._parent == "Quest" && x._props.target.ToString() == requirement.Quest.Id) > 0) { LoggingHelpers.LogSuccess($"{quest.Value.QuestName} required {requirement.Quest.Name}, removing."); } } if (requirement.PreReqType == PreRequisiteType.Level) { if (!quest.Value.conditions.AvailableForStart.Any(x => x._parent == "Level" && int.Parse(x._props.value.ToString()) == requirement.Level)) { LoggingHelpers.LogSuccess($"{quest.Value.QuestName} needs a prereq of level {requirement.Level}, adding."); string hashData = quest.Value._id + "Level"; quest.Value.conditions.AvailableForStart.Add(new AvailableFor { _parent = "Level", _props = new AvailableForProps { id = Sha256(hashData), index = GetNextIndex(quest.Value.conditions.AvailableForStart.LastOrDefault()?._props?.index), parentId = "", dynamicLocale = false, value = requirement.Level, compareMethod = ">=", visibilityConditions = new List() } } ); } } if (requirement.PreReqType == PreRequisiteType.RemoveLevel) { if (quest.Value.conditions.AvailableForStart.RemoveAll(x => x._parent == "Level") > 0) { LoggingHelpers.LogSuccess($"{quest.Value.QuestName} required level {requirement.Level}, removing."); } } } // To make diffs more sane, copy the random IDs from the existing quests.json if possible var originalQuest = existingQuestData.FirstOrDefault(x => x.Key == quest.Key).Value; if (originalQuest != null) { CopyExistingRandomIds(originalQuest, quest.Value); } } OutputQuestRequirementsToConsole2(questsToOutputToFile); JsonWriter.WriteJson(questsToOutputToFile, "output", Directory.GetCurrentDirectory(), "quests"); } private static void OutputQuestRequirementsToConsole(List quests) { var output = new List(); foreach (var quest in quests) { var questConditions = quest.conditions.AvailableForStart.Where(x => x._parent == "Quest"); if (questConditions != null) { foreach (var questCondition in questConditions) { var x = questCondition._props.target.ToString(); Console.WriteLine($"{QuestHelper.GetQuestNameById(quest._id)} needs {QuestHelper.GetQuestNameById(x)}"); } } } // JsonWriter.WriteJson(output, "output", Directory.GetCurrentDirectory(), "questRequirements"); } private static void OutputQuestRequirementsToConsole2(Dictionary quests) { var output = new List(); foreach (var quest in quests) { var questConditions = quest.Value.conditions.AvailableForStart.Where(x => x._parent == "Quest"); if (questConditions != null) { foreach (var questCondition in questConditions) { var x = questCondition._props.target.ToString(); Console.WriteLine($"{QuestHelper.GetQuestNameById(quest.Value._id)} needs {QuestHelper.GetQuestNameById(x)}"); } } } // JsonWriter.WriteJson(output, "output", Directory.GetCurrentDirectory(), "questRequirements"); } private static int[] GetQuestStatus(QuestStatus status) { switch (status) { case QuestStatus.Started: case QuestStatus.Success: case QuestStatus.Fail: return new int[] { (int)status }; case QuestStatus.StartedSuccess: return new int[] { (int)QuestStatus.Started, (int)QuestStatus.Success }; case QuestStatus.SuccessFail: return new int[] { (int)QuestStatus.Success, (int)QuestStatus.Fail }; } throw new Exception($"Unable to process quest status {status}"); } /// /// Latest version of eft has changed the quest json structure, this method adds missing fields /// Mega hack as we dont have a full dump as of 30/06/2022 /// /// quest to add missing fields to private static void AddMissingFields(KeyValuePair quest) { //side if (String.IsNullOrEmpty(quest.Value.side)) { quest.Value.side = "Pmc"; LoggingHelpers.LogInfo($"Updated quest {quest.Value.QuestName} to have a side of 'pmc'"); } //changeQuestMessageText if (String.IsNullOrEmpty(quest.Value.changeQuestMessageText)) { quest.Value.changeQuestMessageText = $"{quest.Value._id} changeQuestMessageText"; LoggingHelpers.LogInfo($"Updated quest {quest.Value.QuestName} to have a changeQuestMessageText value"); } // findInRaid foreach (var success in quest.Value.rewards.Success) { if (string.Equals(success.type, "item", StringComparison.OrdinalIgnoreCase) && success.findInRaid == null) { success.findInRaid = true; LoggingHelpers.LogInfo($"Updated quest {quest.Value.QuestName} to have a success item reward findInRaid value of 'true'"); } } } private static void AddMissingFailRewards(Quest originalQuest, KeyValuePair quest) { foreach (var originalFailReward in originalQuest.rewards.Fail) { // already has a fail reward of same type and target, skip if (quest.Value.rewards.Fail.Any(x => x.type == originalFailReward.type && x.target == originalFailReward.target)) { continue; } quest.Value.rewards.Fail.Add(originalFailReward); } } private static void CopyExistingRandomIds(Quest originalQuest, Quest quest) { CopyRewardRandomIds(originalQuest.rewards.Started, quest.rewards.Started); CopyRewardRandomIds(originalQuest.rewards.Success, quest.rewards.Success); CopyRewardRandomIds(originalQuest.rewards.Fail, quest.rewards.Fail); CopyConditionRandomIds(originalQuest.conditions.AvailableForStart, quest.conditions.AvailableForStart); } private static void CopyRewardRandomIds(List originalRewards, List rewards) { foreach (var reward in rewards) { var originalReward = originalRewards.FirstOrDefault(x => x.id == reward.id); if (originalReward == null) { LoggingHelpers.LogWarning($"Unable to find matching original reward for {reward.id}. Skipping."); continue; } reward.target = originalReward.target; if (reward.items != null) { foreach (var item in reward.items) { QuestRewardItem originalItem = originalReward.items.FirstOrDefault(x => x._tpl == item._tpl && x.slotId == item.slotId); if (originalItem == null) { LoggingHelpers.LogWarning($"Unable to find matching original reward item for {reward.id}-{item._tpl}. Skipping"); continue; } item._id = originalItem._id; item.parentId = originalItem.parentId; } // Above changes can cause the target and first items id to become mismatched if (reward.items.FirstOrDefault()._id != reward.target) { reward.target = reward.items.FirstOrDefault()._id; } } } } // Allow stripping all whitespace in a string, used for comparing _props.target, which may have differing whitespace but still match private static readonly Regex whitespace = new Regex(@"\s+"); private static string StripAllWhitespace(string input) { if (input == null) { return ""; } return whitespace.Replace(input, ""); } private static void CopyConditionRandomIds(List originalConditions, List conditions) { foreach (var condition in conditions) { var originalCondition = originalConditions.FirstOrDefault( x => x._parent == condition._parent && x._props.index == condition._props.index && StripAllWhitespace(x._props.target?.ToString()) == StripAllWhitespace(condition._props.target?.ToString()) && x._props.counter?.id == condition._props.counter?.id ); if (originalCondition == null) { LoggingHelpers.LogWarning($"Unable to find matching original condition for {condition._parent}-{StripAllWhitespace(condition._props.target?.ToString())}. Skipping."); continue; } condition._props.id = originalCondition._props.id; } } /// /// Check original quest for start conditions and if missing from new quest, add them /// /// /// quest to add to output json private static void AddMissingAvailableForStartConditions(Quest originalQuest, KeyValuePair questToUpdate) { // Iterate over quest requirements in existing quest file foreach (var questRequirementToAdd in originalQuest.conditions.AvailableForStart.ToList()) { //Exists already, skip //if (questToUpdate.Value.conditions.AvailableForStart.Any( // x => x._parent == questRequirementToAdd._parent // && x._props.target?.ToString() == questRequirementToAdd._props.target?.ToString()) // ) //{ // continue; //} if (questToUpdate.Value.conditions.AvailableForStart.Any(x => string.Equals(x._parent, "quest", StringComparison.CurrentCultureIgnoreCase))) { continue; } if (questRequirementToAdd._parent == "Quest") { LoggingHelpers.LogInfo($"Quest {questToUpdate.Value.QuestName} missing AvailableForStart quest requirement, adding prereq of {questRequirementToAdd._props.target} {QuestHelper.GetQuestNameById(questRequirementToAdd._props.target?.ToString())}"); if (!questRequirementToAdd._props.availableAfter.HasValue) { questRequirementToAdd._props.availableAfter = 0; } if (questRequirementToAdd._props.visibilityConditions == null || !questRequirementToAdd._props.visibilityConditions.Any()) { questRequirementToAdd._props.visibilityConditions = new List(); } questRequirementToAdd._props.index = GetNextIndex(questToUpdate.Value.conditions.AvailableForStart.LastOrDefault()?._props?.index); } // Already exists, skip if (questToUpdate.Value.conditions.AvailableForStart .Any(x => x._props.target?.ToString() == questRequirementToAdd._props.target?.ToString() && x._parent == questRequirementToAdd._parent)) { continue; } questToUpdate.Value.conditions.AvailableForStart.Add(questRequirementToAdd); } if (questToUpdate.Value.conditions.AvailableForStart.Count(x => x._parent == "Quest") > 1) { LoggingHelpers.LogWarning($"Quest {questToUpdate.Value.QuestName} has {questToUpdate.Value.conditions.AvailableForStart.Count(x => x._parent == "Quest")} quest prereqs, is this correct?"); } } /// /// Get a bsg happy guid, must be 24 chars long /// /// /// static string Sha256(string randomSalt) { var crypt = new System.Security.Cryptography.SHA256Managed(); var hash = new System.Text.StringBuilder(); byte[] crypto = crypt.ComputeHash(Encoding.UTF8.GetBytes(randomSalt)); foreach (byte theByte in crypto) { hash.Append(theByte.ToString("x2")); } return hash.ToString().Substring(0, 24); } /// /// Look up the quests name by guid and add human readable string to quest object /// /// private static void AddQuestName(KeyValuePair quest) { var questName = QuestHelper.GetQuestNameById(quest.Value._id); // special characters like ", brake the client when it parses it, gotta remove var rgx = new Regex("[^a-zA-Z0-9 -]"); quest.Value.QuestName = rgx.Replace(questName, ""); } /// /// Loop over live quests and use if it exists, otherwise use existing data /// private static List GetMissingQuestsNotInLiveFile(Dictionary existingQuests, QuestRoot liveQuestData, List blacklistedQuests) { var missingQuestsToReturn = new List(); foreach (var quest in existingQuests.Values) { var liveQuest = liveQuestData.data.Find(x => x._id == quest._id); if (liveQuest is null) { if (blacklistedQuests.Contains(quest._id)) { LoggingHelpers.LogInfo($"Skipping quest: {quest.QuestName}"); continue; } missingQuestsToReturn.Add(quest); LoggingHelpers.LogError($"ERROR Quest {quest._id} {QuestHelper.GetQuestNameById(quest._id)} missing in live file. Will use fallback quests.json"); } else { LoggingHelpers.LogSuccess($"SUCCESS Quest {quest._id} {QuestHelper.GetQuestNameById(quest._id)} found in live file."); } } return missingQuestsToReturn; } private static int GetNextIndex(int? previousIndex) { if (previousIndex == null) { return 0; } return previousIndex.Value + 1; } } }